Keeping safe at C: How to neutralize some of the inherent dangers of the language

This article will take a look at the issues involved in using C in the development of systems with safety-critical functionality.

Despite the fact that the language is full of undefined behavior, hardware dependencies and other pitfalls, it is still a widely used and popular language in the field of safety-critical development. With some forethought and planning, we can turn a potential problem into an advantage.

Move your feet

Back in 1991, the magazine Developer’s Insight published an entertaining article titled How to Shoot Yourself In the Foot that started out: “The proliferation of modern programming languages (all of which seem to have stolen countless features from one another) sometimes makes it difficult to remember what language you're currently using. This guide is offered as a public service to help programmers who find themselves in such dilemmas.”

The list of languages starts with C and simply states:

  • C - You shoot yourself in the foot.

This verdict might seem harsh, but holds more than just a grain of truth. However, even if alternative programming languages might be cursed with far less problems in terms of, for example, type safety and undefined behavior, they often lack features for programming close to the metal. If we stick with C we need to strike a balance—navigating between its obvious and not so obvious pitfalls while making the best use of its features. We can look at C for development of safety-critical functionality from two different perspectives:

  • Which external requirements are there in a safety-critical project, with respect to the choice of programming language?
  • What can you do to remedy some of the more blatant issues with C, also when working with legacy code?

Standard reply

If the products you are working on have a place in for example automotive, industrial control, medical devices, or railway, there’s a fair chance that they are subject to formal functional safety requirements. Such requirements can boil down to a very specific requirement on the tolerated failure rate of the product, or the allowed failure rate of some specific functionality in the product. It can also be a general requirement of the product being developed in accordance with a specific functional safety standard, like IEC61508 (electric and electronic programmable devices), ISO26262 (automotive), or EN 50126x (railway). For at least 10 years, a clear trend has been that implementation of safety functionality is moving away from pure mechanics or PLC controlled automation and into the microcontroller world, thus the requirements are spilling into the software domain.

Since the software requirements in the various standards are similar in intent, we will use IEC61508 as an example. This standard provides the foundation for many sector specific standards; requirements that are valid for IEC61508 will to a large extent be valid also for ISO26262 for example.

These standards will heavily influence the way you work and document your work: all the way from requirements gathering, to how you plan for deployment and decommissioning of the product at customer sites. It is not you and your project stakeholders alone that decide if you have succeeded in achieving the objectives in a chosen standard–you must also convince a third party assessor from an accredited body, or someone in your organization acting the part.

Most of these standards use variations of the safety integrity level concept. So depending on the classification of your product, there will be some variation in how you apply the appropriate standard.

Standards apply?

Let’s get to the interesting question: What does all this have to do with my choice of programming language? Quite a lot, it turns out. The table below gives advice on how to select a suitable programming language depending on the safety integrity level of your application or safety function.

HR is short for Highly recommended, which in standards lingo it is actually a very strong indication that you should follow the recommendation or have a very good reason not to, i.e. a justification that you can back up 100 percent.

Table_from_IEC61508

Table 1: Table from IEC61508 part 3 appendix A

Say what?

As you can see in the table it is highly recommended to use a suitable programming language, which does not really make a lot of sense as far as recommendations go, does it? But the C appendix referenced in the table gives the following definition of a suitable programming language:

The language should be fully and unambiguously defined. The language should be user- or problem-orientated rather than processor/platform machine-orientated. Widely used languages or their subsets are preferred to special purpose languages. The language should encourage the use of small and manageable software modules; restriction of access to data in specific software modules; definition of variable sub-ranges; and any other type of error-limiting constructs.

Let’s look at the various parts of the above definition and how C stacks up to them:

  • The language should be fully and unambiguously defined: Well… Depending on how you count, it can be argued that C99 contains at least 190 undefined behaviors.
  • The language should be user- or problem-orientated rather than processor/platform machine-orientated: Hmm, given that C was originally created to be a system development language for the PDP-11 architecture; and given that a specific C implementation for a specific target is bound to be different from the implementation for another target and sometimes even different to an alternative implementation for the same target, we cannot really claim that C is compliant with this part of the definition..
  • Widely used languages or their subsets are preferred to special purpose languages: At last, something we can claim is true for C! There are a huge number of developers out there that knows C or any of its cousins like C++.
  • The language should encourage the use of small and manageable software modules; restriction of access to data in specific software modules; definition of variable sub-ranges; and any other type of error-limiting constructs: Although C does not explicitly forbid the creation of abstractions that support these concepts it can be said outright that the language in itself does absolutely nothing to support them either. In fact, it can even be argued that the exact opposite is true.

C does not really live up to the expectations in the standard. What we can do about it?

Actually, the answer is quite simple, at least as long as we are only reading the standard. If we read on, we will find a table with judgments on specific languages and this is what it says for C:

Table_from_IEC61508

Table 2: Table from IEC61508, part 7, appendix C

Even though C is not a recommended language, C with a suitable subset is even a highly recommended language if used together with a coding standard and static analysis tools. But what does subset and coding standard mean in this context?

Substandard

The aim of a language subset in this context is to reduce the probability of programming errors and to increase the likelihood of finding such errors that has crept into the code base anyway. For C, this means eliminating the use of as many as possible of the undefined or implementation defined behaviors. There are a number of such language subsets available but the most widely known is probably MISRA-C. The MISRA-C rule set started out as an initiative by the Motor Industry Software Reliability Association in the UK and was aimed solely at automotive software. Over the years, the MISRA-C rules has spread over the world and into other industry segments, and the rule set is now the most widely used C subset in the embedded industry.

IEC61508 has quite a lot to say on the coding standard issue as well. The following are some examples of topics that should be considered in addition to the MISRA-C rules:

  • How to guard accesses to shared resources, like global variables.
  • Use of stack and heap memory for object allocation.
  • Recursion, allowed or not?
  • Complexity limits, like limits to the allowed cyclomatic complexity of functions.
  • How to waive e.g. MISRA-C rules that are not applicable in a certain context.
  • How to use compiler specific functionality, like intrinsic functions or language extensions.
  • How to use range checking, assertions and pre- and post-conditions and similar constructs to catch errors.
  • Interface organization and access between modules.
  • Documentation requirements.

In essence, a coding standard should give advice on how to deal with issues that affect code quality and integrity but are not explicitly addressed by the language or the subset.

Practice makes perfect

We will now touch some topics arising from the previous section and how to approach them in a project.

MISRA-C

If you plan to use MISRA-C, the approach can be slightly different depending on if you start out with a clean slate or if you are reusing legacy code. For newly developed code, consider the following advice:

  • Don’t go out of your way to blindly support each and every rule! For parts of your code, you probably cannot comply with one or more rules. This is especially true for code that interfaces to hardware. Instead, make an informed decision to deviate from the rule and document the decision. Discuss up-front with project stake holders and your external assessor if you should aim to support all rules or if there are rules that can be ignored on a project level.
  • At all times, strive to be compliant with rules that deal with the basic types, and arithmetic and conversion on such types! This area is riddled with pitfalls and code can seem to function perfectly on one platform, but break down on another.
  • If you find yourself using your deviation procedure for the same rule over and over on similar pieces of code you should consider it as a warning signal:
    • Are you interpreting the rule correctly?
    • Is the code pattern really needed? If it is, consider factoring out the offending code as an isolated function or set of functions.
    • Use a static checker that can check your compliance interactively during development.

 If you are applying a set of MISRA-C rules to legacy code it can be beneficial to:

  • Work one rule at a time.
  • Select easy rules first, like the rule to embrace also single statements forming the body of a conditional construct with ‘{‘ and ‘}’. (MISRA-C:2004 rule 14.8)
  • Look carefully at the rules stating that plain types like shortint and char should not be used and consider changing one module at a time to use explicitly sized types like uint16_t.
  • After practicing a bit on some easy modules, start off with modules that are considered to be buggy or difficult to maintain.

Synchronize it!

We will now change focus to one of the more widely misunderstood areas of the C language: the volatile keyword. No matter who you ask, misuse of this keyword will go very high on the list of things that make embedded systems crash and burn.

The main reason to declare an object as volatile is to inform the compiler that the value of the object can change in ways unknown to the compiler and thus all accesses to the object must be preserved. There are three typical scenarios creating the need for volatile objects:

  • Shared access; the object is shared between several tasks in a multitasking environment or is accessed both from a single thread of execution and one or more interrupt service routines.
  • Trigger access; as for a memory-mapped hardware device where the fact that an access occurs has an effect on the device
  • Modified access; where the contents of the object can change in ways not known to the compiler

So, what kind of guarantees do you get from the compiler if you apply the volatile keyword to an object declaration? Essentially the following: All read and write accesses are preserved. That’s it!

Depending on the target architecture you might also get all accesses complete, performed in the order given in the abstract machine and, if applicable, atomic.

Compilation_of_volatile

Figure 1: Compilation of volatile for an ARM/THUMB target

Is the code in figure 1 thread-safe and interrupt-safe, given that the volatile object can be accessed from different execution contexts? Both the load from memory and the store to memory of the value of vol are atomic since this is for a 32-bit load/store architecture. But the source statement is not atomic! We can still be hit by a context switch or interrupt somewhere in between the three instructions making up the vol++ statement.

How to cope

  • Never assume that volatile means atomic except for certain memory accesses!
  • Make sure code that does more than just an atomic read or write to a volatile object is guarded by proper serialization primitives, like a mutex, or by interrupt disabling, if the object can be accessed from different execution contexts.
  • Cover the proper usage of the volatile keyword and serialization primitives in your coding standard.
  • Consider reviewing usage of all global objects, including file-scoped static objects in existing code.

Stacking up?

Is your stack small and big enough? This is a perpetual conundrum for developers. If the stack is unnecessarily big, it might mean a part with more on-board RAM has to be used which drives costs. If the stack is too small, we might go down in flames, which for a product under safety-critical requirements is not good at all, to say the least. Here is a checklist of possible actions to consider when determining the stack size:

  • Use the run-time stack check functionality of your debugger, if such functionality is available.
  • Fill memory above and/or below the stack with a magic pattern that can be checked periodically in runtime by a dedicated check routine. Due to requirements in the IEC 60730 standard for household appliances, your MCU supplier might actually already have this functionality and other MCU self-check functions available in a special purpose library.
  • Perform a call tree analysis to determine worst case stack depth, including stack used by interrupt handlers. If you review code and examine linker map files by hand, remember that optimizations will affect stack usage. You can also buy or develop tool support, or make sure you are using a build tool chain that can assist in the call tree analysis and stack depth analysis.

Take it away!

To sum up in a few important pointers, I would like to leave you with the following:

  • Familiarize yourself with the software development requirements defined by the applicable standard before you start
  • Use MISRA-C as basis for a coding standard
  • Review your usage of volatile
  • Implement a test and analysis strategy for stack allocation

We do no longer support Internet Explorer. To get the best experience of iar.com, we recommend upgrading to a modern browser such as Chrome or Edge.