Trimming cost and size: The software side of the story

Benjamin M. Brosgol

April 02, 2012

Benjamin M. BrosgolApril 02, 2012

Reducing cost and size for embedded hardware involves issues with circuit design, fabrication and similar topics. Software, however, raises a different set of concerns. Though development costs need to be taken into account, a much more significant expense comes from software errors, which can lead to delays, product recalls, possible lawsuits and damage to a company’s reputation.  

A key to cost reduction is to prevent errors from getting into the code in the first place or, if that fails, to detect and remove the errors before the software gets fielded. The size issue is a bit subtler. Some of the features that have been introduced into programming languages in the interest of reliability—for example, exception handling and high-level concurrency support—require run-time libraries that might be too large for certain kinds of embedded systems.

Developers need some way to tailor the run-time libraries to remove unneeded functionality—in effect to scale the size of the executable code based on the language features actually used. A new approach that couples a reliable language with customizable and specialized run-time libraries can reduce both cost and footprint size for embedded systems.

For several reasons, buggy software is especially acute in embedded applications. First, an embedded system typically monitors or controls some external device, so in critical applications an error or vulnerability could compromise safety, security or both; lives could be lost, and substantial assets could be at risk.

Second, debugging embedded software is harder than debugging native software. For example, embedded systems generally involve concurrency and real-time constraints, which introduce opportunities for errors such as deadlock, missed deadlines and corrupted data. Because some of these bugs are timing dependent and not easily reproduced, they could go undetected until the software is fielded. With perhaps thousands or even millions of systems in use, even a bug with low probability of being triggered will occur eventually.

Third, correcting a defect in an embedded product already in service is complex and expensive. Solutions that involve wireless uploads raise serious security issues that are just beginning to be addressed in equipment ranging from medical devices to automotive systems.

Testing can catch some errors. For example, RTCA Inc. commercial-avionics software safety standard DO-178C [1] specifies extensive tests to demonstrate that software meets all requirements and that these tests fully cover the source-code structure. Though testing is an ongoing process for real-world systems, use of programming language and tool technologies that automate error detection can bolster confidence in code correctness as well as demonstrate the code’s safety and security properties.

Ideally, the language can express the program’s intent so that automated error detection takes place at compile time. If that isn’t possible, then automated error detection can occur at run-time with a well-defined effect.  

This idea isn’t new; features such as strong type checking have been in some languages for decades. What is new, or at least becoming more widely adopted, is the ability to specify stronger program properties, or contracts, that can either be proved through formal methods or checked at run-time.

The Spark language [2] illustrates the first approach. Spark is an Ada subset augmented with contracts that specify intermodule data and information flow and that capture logical assertions concerning program state (subprogram pre- and postconditions, invariants). The Spark tools check that the source program conforms to its annotations and can automate the process of proving that the contracts are correct. Spark has been used in practice to demonstrate correctness properties (for example, the absence of run-time exceptions) on a range of safety-critical and high-security systems.

Other language technologies treat contracts as run-time constructs. A recent example is Ada 2012 [3], in which contracts take the form of Boolean conditions that are supplied in contexts such as invariants for types and preconditions and/or postconditions for subprograms. The programmer can control whether the software generates code to check these conditions at run-time; a failed check raises an exception. In this way, the specified contracts can be used either as formal comments that document the program’s intent or as run-time checks that are part of a testing regimen (Figure 1).

Figure 1. Contract-based programming in Ada 2012.

The underlying methodology, sometimes known as correctness by construction [4], design by contract [5] or contract-based programming [6,] strengthens the concept of an interface to include a behavioral specification and can help detect many kinds of coding errors early and inexpensively.



KISS for programmers: Keep it small and simple

Despite the general advances in hardware capacity, limited processor power and memory space constrain many kinds of embedded systems; size does matter. That presents a dilemma. In the past several decades, many of the new high-level-language features that improve reliability and maintainability—input/output (I/O) libraries, memory management, exception handling, concurrency control—require run-time-support libraries.

For a rich-featured language, these libraries can add tens, perhaps hundreds, of kilobytes to the size of an executable code. This raises two issues:

• The executable code containing these libraries may be too large to fit into the available memory.
• When compliance with a safety or security standard is required, the complexity of the libraries makes it harder to meet the certification objectives.

Embedded-systems developers sometimes address these issues by choosing a lower-level language such as C, with modest run-time-support requirements. But C is notorious for vulnerabilities such as buffer overflow, raising the risk of introducing bugs and their associated detection and removal expenses.

An alternative approach is to start with a language that more directly supports the development of reliable software, and then to eliminate those features whose run-time-support libraries are too large and/or too complex. This can be accomplished in an ad hoc fashion—for example, with the programmer adhering to set stylistic restrictions and supplying linker directives to avoid linking in unwanted libraries. This approach, however, is indirect and nonportable.

In contrast, one language that makes it possible to tailor run-time requirements in a standard way is Ada. It does so through a compiler directive known as pragma Restrictions.

With this pragma, the programmer can specify which features are not being used. Some common restrictions include no tasking, no exception propagation and no local allocators. The Ada compiler verifies that the program adheres to the specified restrictions; when the executable is built, the unneeded libraries can be omitted. Some vendors also supply several prepackaged run-time libraries, called profiles, corresponding to common sets of restrictions.

An example is AdaCore’s GNAT Pro High-Integrity Edition development environment, which includes profiles for minimal or no run-time libraries (zero footprint), for simple libraries compliant with DO-178B certification guidance, for libraries augmented by the tasking features permitted by the Ravenscar [7] restrictions (see Figure 2) and for the full Ada language.


Figure 2. Ravenscar tasking profile.


Using either the à la carte mechanism provided by pragma Restrictions or one of the preconfigured run-time libraries provided by the Ada vendor, the programmer can choose features that are expressive enough to do the job but simple enough to accommodate their run-time support in the available memory (and, if applicable, comply with the relevant certification objectives).

Making the software fit

Two techniques can be used to make embedded development less expensive and more reliable, while meeting the hardware memory constraints:

• First, eliminate or reduce the cost of tracking and correcting bugs. Make sure the program does what it is supposed to do and doesn’t do what it is not supposed to do. Modern language features can help; in particular, the ability to specify contracts that express behavioral properties for program entities encourages the developer to think about the program logic in advance and to build in quality.

• Second, scale the program’s run-time functionality to fit in the available memory space. Configurable run-time-support libraries that reflect the features actually used will meet this goal. Create these using appropriate language features (such as Ada’s pragma Restrictions) or specialized profiles supplied by the implementation.

References


1. RTCA DO-178C—Software Considerations in Airborne Systems and Equipment Certification. December 2011.

2. J.G.P. Barnes. High Integrity Software, The SPARK Approach to Safety and Security. Addison-Wesley, 2003.

3. SO/IEC SC22/JTC1/WG9. Ada Rapporteur Group. Ada 2012 Language Reference Manual. http://bit.ly/9imSYX

4. Altran Praxis. “Correctness by Construction.” http://bit.ly/GD1iqv

5. Eiffel Software. “The Power of Design by Contract.” http://bit.ly/16tBo7

6. C. Comar, J. Kanig and Y. Moy. “Integrating Formal Program Verification with Testing.” http://bit.ly/GBdnyZ

7. A. Burns, B. Dobbing and G. Romanski. “The Ravenscar Profile for High Integrity Real Time Programs,” in Reliable Software Technologies—Ada Europe ’98, Springer
Verlag Lecture Notes in Computer Science, Vol. 1411.

About the author

Benjamin Brosgol
, a senior member of AdaCore’s technical staff, has worked with programming languages and technology for more than 30 years, concentrating on high-integrity systems. He holds a BA in mathematics from Amherst College and an MS and PhD in applied mathematics from Harvard University.

Loading comments...