Seventeen steps to safer C code

Thomas Honold

April 27, 2011

Thomas HonoldApril 27, 2011

17 tips for writing safety-critical C code using methods adapted from C++ and Ada.

Click image to go to digital edition.
In embedded systems design, many of us tend to write our software in C the way our “grandfathers" did, which was appropriate before we had to worry about ubiquitous connectivity and its security implications. Today, the programming methods of the past must be adapted to a world in which safety-critical design is required not only in military/aerospace applications but in ordinary commercial applications as well. The C language is definitely not type safe, and only by applying many good practices and self-imposed rules can it be made a viable choice for safety-critical software development.

I learned these rules and best practices working for companies that were moving to programming paradigms more amenable to safety-critical software development. For example, at one company we were developing Internet banking and chip-card terminal applications, using C++ on Windows PCs. At the time, we believed we were doing object-oriented programming, but I now believe that what we were writing was really C with C++ syntax. Having seen it often enough, I theorize that embedded systems developers naturally fall into this type of hybrid coding when migrating from procedural C programming to the object-oriented C++ paradigm.

Later I moved on to what was, at the time, a new domain of software engineering: safety-critical embedded systems design. My first project required me to learn Ada. At the end of the project, I understood that new hardware in the embedded systems area also means new software and firmware, and I learned from Ada what type safety really means.

What is type safety
I generally define type safety as Wikipedia defines it: “In computer science, type safety is the extent to which a programming language discourages or prevents type errors. A type error is erroneous or undesirable program behaviour caused by a discrepancy between differing data types.”* If you define a type, this means nothing more than saying there are a certain number of bits that represent a predefined data type. For instance, uint32_t number_of_bytes defines 32 bits, which hold an unsigned scalar value ranging from 0 to 4.294.967.295. If you would assign a negative value to the number_of_bytes variable, a type-safe language would raise an exception during run time or a compile-time error at compile time. The Ada language does this, but C does not. In C, you could also assign a floating-point value like 3.456 to the variable, which makes some compilers complain at compile time and produces undefined behavior during run time.

*Wikipedia entry on type safety, from April 20, 2011. http://en.wikipedia.org/wiki/Type_safety

From this and other experiences, I've come up with a set of 17 tips summarizing the lessons I've learned as a software engineer in the embedded systems environment, particularly as they relate to C programming, as a way to help others avoid the same potholes I encountered. In the process, I had a lot of help, particularly with the many tips on safe C in articles at EmbeddedGurus.com and Embedded.com.

Tip #1—Follow the rules you've read a hundred times

There are three things you must do each time you start writing your code. You've read these rules many times before and resolved to do them the next time you started code development. This time, do them—they will help you to avoid many long hours of debugging:

  • Initialize variables before use.
  • Do not ignore compiler warnings.
  • Check return values.
Accessing objects before they have a defined state can lead to strange effects. Not only does avoiding these effects require that you make sure you've set all your ints and floats to a defined state, you also have to make sure that your complex type functions, such as typedefed structs, are initialized first.

For instance, declare an object like the one in Listing 1 in the header. In Listing 1, the object has the typical init flag and two function pointers for reading and updating data.


Click on image to enlarge.


Then, in the C module, initialize the object to a level for first usage. In this case, the init flag was set to false. The variable is static here, since I have only one instance of it:


static symbol_model_t 
g_symbol_model =
{
  false,/* is initialized */
  NULL, /* update function */
  NULL  /* read function */
};

Later on during construction of the object, you can check if things were not initialized, such as shown in Listing 2.


Click on image to enlarge.


Even though they finally compile the code, modern compilers are always complaining about strange constructs. Do not ignore these complaints. More often than not, they are right. Also do not ignore return values since they indicate the first time something has gone wrong. If you ignore such warnings, you'll have a ticking time bomb in your system that will explode at a later point.

If you follow these procedures, you'll have more time left at the end of the project. After all, the end of the project is the point at which money and time are running out and people are overstressed, especially if the product isn't working and is shipping late. Now you'll have time to help them set things right.

Tip #2—Use enums as error types

Every module should have a specific error return type that explains what the problem is in detail, at the time it occurs. Often you receive error codes like “-1" or “an error occurred." If there is a run-time error detected and you know exactly what it is, document this for your later reference and for those who maintain the software.

For example, consider the code in Listing 3.


Click on image to enlarge.


Such an error type already has been decoded and the cause determined. The last entry, called <XYZ>_LAST_ERROR, makes it possible to iterate over the content of the enum. That means you only have to know what the first element in the chain is. No matter how many more errors you add between the first and the last, all you have to do is check the range or iterate. Also, this last enum value gives you the total number of entries. More on this later.

Tip #3—Expect to fail
Failures happen. Often. So plan for it and use it to your advantage. It's good practice to set the default return value of an operation to something like UNNOWN_ERROR. Only in the case of a good result should you set it to SUCCESS, for instance, as in Listing 4.


Click on image to enlarge.


This pessimistic approach is safer than expecting all things to go well and setting the default to _SUCCESS. In programming, it's safer to assume that the failure is not the exception in a string of successes but exactly the opposite: The good case is the only exception in a string of more commonly occurring error cases.

Tip #4—Check input values: never trust a stranger

If your modules expect input data from other modules, you should never trust a stranger. That is, at the outmost layer of your software architecture, check all input values for consistency. The check has to be at the outmost layer since it must be detected as soon as possible. Otherwise you could, for instance, dereference an invalid pointer given to you at one of your lower layers. The result: The crash dump reports that it was your software's problem, but later, after many hours of debugging, you find out that someone has given you invalid input.

Here is an example using an enum error type mapped to a string representation for trace output to a console window, shown in Listing 5.


Click on image to enlarge.


The lookup table representing the strings is defined as shown in Listing 6. Safe access to the string map that does not allow any out of bounds access is shown in Listing 7.


Click on image to enlarge.




Click on image to enlarge.


Unfortunately enums in C are integers. That means you could hand over any value of integer to the interface accessing the array, an error that can be avoided.

By the way, if you define the lookup table with the _LAST enum as a size parameter, it will have the right size and keep you from indexing out of bounds. Also getting the string out of the string array is a very simple offset addressing operation, which is really fast in C.

So, that was range checking. You should also check for NULL pointers if someone gives you an address value. You cannot check pointers for anything other than the NULL value, but this is better than nothing.

Tip #5—Write once, read many times
When we read other people's code, we're thankful for any good line of comment or more readable code; most of the time, however, the original coder hasn't been so kind to us. If the variables are called i, j, and k, you'll soon have a mental break down. Often the longest variable name is pbuf. What can happen when code is difficult to decipher is that even though the next programmer should only slightly change the software, he or she says, “I can't understand this hacker's code. It will be faster to rewrite it." The rewrite results in extra work and possibly new bugs.

So what can you do? First of all, if you write code, write it to be as readable as a newspaper. Well-written code requires only a few lines of comments. Also consider that although code is nothing for compilers, it needs to be readable by human beings.

Don't be lazy at typing new variable names and, if required, add the unit to the name. For example, do not call parameters Size, Length, Temperature, or Angle. Instead, since all those parameters have a unit, call them:
  • number_of_bytes
  • length_in_meters
  • temperature_in_celsius
  • angle_in_radians

There are famous examples of errors coming from wrong unit conversions, such as the loss of a Mars climate orbiter (see http://mars.jpl.nasa.gov/msp98/news/mco990930.html). Not using units in your application programming interface's definitions can also cause major design failures. See How To Design A Good API and Why it Matters (Joshua Bloch's Google TechTalks video from 2007) at www.youtube.com/watch?v=aAb7hSCtvGw.

If you've written code that requires some renaming, I recommend the use of the open-source Eclipse Development Environment (www.eclipse.org). It has a great feature called Refactoring that renames any kind of object everywhere in the code. For instance if you want to change a function parameter's name from number_of_bytes to number_of_floats, just mark it, press ALT-SHIFT-R, and change the name.

Documenting the source code is helpful not only for your future reference but for those who come after you. For instance, if you're working on an embedded system, you need to have a memory map indicating where all the memory-mapped devices can be found. Listing 8 shows an example of a memory map.


Click on image to enlarge.


It's useful to have diagrams of all the software layers in your application as well as diagrams of the overall software architecture, preparing them in a format that allows you to simply cut and paste them to a word processing program. Remember that if you write it down, you don't have to keep it in mind.

Tip #6—When in doubt, leave it out

I've already mentioned the API design tutorial from Joshua Bloch, a guru in the Java community. He brings up a good point in his API-design tutorial on YouTube (URL mentioned earlier).

And that is: If you design an API that is nothing other than the external interface of your modules, consider the need of an operation. If you are not sure anyone will ever need an operation, leave it out. If someone does use your API and you later remove an operation, you'll break his code. So Josh says, “When in doubt, leave it out. You can always add, but you can never remove."

< Previous
Page 1 of 2
Next >

Loading comments...

Most Commented

  • Currently no items

Parts Search Datasheets.com

KNOWLEDGE CENTER