Seventeen steps to safer C code
Tip #7—Use the right tools
Everyone has a favorite editor, debugger, and compiler. But sometimes it's worth looking for something new since “the better is the enemy of the good." Here is what I use (many of which you may already use):
- Eclipse: Has a good editor, is good at refactoring code, also good for prototyping architectures on the PC (for example, with Cygwin on Windows). www.eclipse.org.
- Astyle: Artistic Style 2.01 is a great code formatter that can be configured in many ways to beautify the code. http://astyle.sourceforge.net/.
- Cygwin: For PC-based prototypes and for architectural studies, you can use the GNU tool chain of cygwin. Make sure you install the make, binutils, and gcc from the development package. www.cygwin.com/.
- GNU tool suite: Many embedded systems tool chains use this set of tools. Even if you don't have hardware at the beginning of the project (your hardware developers may not have finished their work), you can start writing prototypes for your architecture. Eclipse together with Cygwin using the GNU tools is worth trying. www.gnu.org.
- Tortoise SVN: This is a nice add-on for Windows Explorer to access the subversion versioning system. http://tortoisesvn.tigris.org/.
All these software packages are available for free.
Tip #8—Define the software requirements first
Defining the requirements for the software you write is the first step for a successful product. I mean the software requirements for the final product, not those for the quick hacked throwaway prototype you're working on as a first step to the final product. And this, of course, requires defining the goal to be reached.
If you don't define the requirements, you can't test your final software properly—you'll have nothing to use to define a useful test case. In other words, how can you determine if you've finished the development? Here's a helpful multiple-choice software quiz along these lines:
How can you determine if you have finished the development?
- There is no more money left;
- There is no more time left; or
- All the requirements are implemented and tested successfully.
To properly finish development (you should all know which answer is the correct one above), I define the following on every project:
- Requirements for the OK case—that is, what is required to fulfill the main functionality.
- Requirements for the ERROR case, important for the safety-critical design since you also need to define what has to be done if things go wrong. Remember that the good case is often the exception in a string of more commonly occurring error cases.
- Tests that check if the above defined requirements are implemented correctly.
If you do testing on the code base directly (white-box testing), you may tend to test what the code does and not what it's supposed to do. Here's where requirement-based testing can improve your end product: You're forced to do black-box testing.
Tip #9—During boot phase, dump all available versions
If you're the one who implements the boot loader on new hardware, you would normally do the following:
- Initialize the hardware according to the required memory map.
- Execute a hardware self test.
- Start booting the application.
Nothing new here: That is what your PC typically does every time you boot up.
But in embedded systems development in the era of FPGAs and CPLDs, the hardware is as modifiable and subject to change as the software. Dump all programmable logic devices version registers onto a console window or file before starting the application. This step is important since hardware developers nowadays use programmable devices to quickly change the behavior of their hardware, with the result that VHDL code can be changed as fast as software code can be changed.
To circumvent such messages as “this error is only on your system" or “we cannot reproduce the problem," you should dump at least all the version registers of the hardware devices. Also your software should say what version it is. For a real product, there must be a matrix telling you which hardware works with what software version.
If you do this, you'll find out that often people are operating illegal combinations that can cause some super-strange effects. Such versioning information is very helpful for your product hotline or test staff, as well as to production people who can use it to check if what they've produced is the right configuration.
Tip #10—Use a software version string for every release
If you've finished development of a particular stage in a project in order to do tests on it or to release a software version, be sure you take the following steps in exactly the order written:
- Update the version string and date.
- Check the software version in to your versioning system.
- Update the version string right after check-in for the next version.
- Test the software.
- Fix the bugs.
- Continue developing the next version.
The most important step is 3.
I know of several instances where developers have given their software to testers or customers without incrementing the software version string. The result: Several software versions were out there with the same version string. It can take days before you realize this inconsistency and resolve it. (By the way, in the era of programmable devices, this also applies to the hardware engineers. So, if you meet some of them in the breakroom, remind them).
Tip #11—Design for reuse: use standards
Don't try to reinvent the wheel, believing your wheel will be better than all the millions that have already been invented. I've read so many times things like Listing 9.
Click on image to enlarge.
Since the C99 language standard has defined the stdbool.h and stdint.h headers, things have become portable and there is absolutely no need to define your own int or boolean types.
Tip #12—Expose only what is needed
When I read other programmers' code, I wonder if they've ever heard about “information hiding." I find many externally declared variables that can be accessed from several modules. The practice is both pointless and sometimes dangerous.
Module internal operations and variables are often not declared static, which allows them to be accessible from other modules. This accessibility results in a design that is not modular because when operations and variables are not declared static, they're interdependent and not modular (since one thing cannot live without the other).
Also C doesn't have any syntax for anything like namespaces, common in other object-oriented languages. Or to be more precise, C knows only one, the global namespace. This means that all nonstatic operations or variables are visible globally unless you hide them. This global visibility could result—and often does—in a name clash detected at linking the software. As long as you have all the source code for the project, you can easily resolve this issue. If you have only a library in binary format and some function headers, the situation is more complicated.
Another related topic: Parameters have to be declared as const if the implementer of the interface doesn't want this object to be changed. The difference between C and C++ is that in C++ const means constant, whereas C defines constant to be interpreted as read-only. To illustrate this, Listing 10 shows a declaration of a read operation reading data into a specified buffer called display_data, which is at a constant address.
A write operation that is creating constant data located at a constant buffer address requires const two times, shown in Listing 11.
Click on image to enlarge.
If you later try to modify constant objects, your compiler will correct you.
You should let your compiler help you develop your software in a safe way. What you need to do is to provide the compiler the information on how the objects are to be treated.
Tip #13—Make sure you've used “volatile" correctly
In embedded software development you sometimes have to do things that your host-based colleagues are often not concerned about. One of those things is declaring variables to be volatile, which keeps the compiler from optimizing read or write operations for this variable. How to do this is well described by Michael Barr's blog posting “Firmware-Specific Bug #3: Missing Volatile Keyword" found at http://embeddedgurus.com/barr-code/2010/02/firmware-specific-bug-3-missing-volatile-keyword/
Tip #14—Don't start with optimization as the goal
Some developers are intent on writing “fast code," even though they cannot define what “fast" means in the context of their application. It sounds good as an objective, but what I've seen is that often under the cover of writing fast code, they want to move beyond the existing system definition and move to a nonexisting architecture.
To account for this and other system-redefining goals, you should think seriously about developing a flexible architecture capable of adapting to various exigencies. First, this means developing a set of software requirements for the product being planned. Then you should assume that once developed, your software architecture will no doubt be extended, so consider how you can design it to be flexible enough to incorporate new features into the existing platform without scrapping the code base you've already developed.
If you're concerned about performance, wait until your project's first integration phase, at which point you can determine how fast the system is. This doesn't have to be the last milestone in the project. As proof of concept, you can plan to produce several interim “proof of concept" implementations and measure performance. Working from that known value, consider what you need to do to achieve the necessary performance goals.
If you then detect that your code is not fast enough, you have to check which parts are responsible for the main time consumption. Then you profile the software and determine what loops and routines are consuming all the time.
The rule with optimization is that you first have to know where you are before considering what you need to optimize in order to get where you want to be. And don't forget that the software must still be maintainable.
Tip #15—Don't write complex code
Complex code is error-prone code. I think most of you already know this. But the question is, what does complex really mean in the context of your particular design?
A good metric for defining this is the McCabe or cyclomatic complexity algorithm. See also this well-written article by Jack Ganssle (“Taming software complexity," Embedded.com, 2008) at www.eetimes.com/4007519.
My opinion is that if you write safety-critical code, the best rules are:
• Max. cyclomatic complexity per function: 10
• In a few exceptions, such as use of switch-case constructs: 15
Many good tools exist for measuring the complexity of your code. “Understand for C," at www.scitools.com is a good tool, but many others are available.
You shouldn't just think about writing code during the initial software development phase. You need to think about all the stages of the code's life. The code of a product will be changed and extended many times during the product's life cycle. And the people who have to do this are pretty often not the ones who have written the code initially. In other words, do not just think about your own needs: many others are coming after you.
Tip #16—Use a static code checker
If you write safety-critical code, you surely have a coding guideline. Even if your guideline contains only 10 rules, you must have a tool to help you check those rules. If your team doesn't have a tool for checking, you can be sure that things won't be checked.
Many tools, such as PC-Lint, are available to accomplish this task. You should check your code at every significant milestone in your project to be sure that the code quality is good.
Remember, software testing is a multistage process, of which static code checking is one part. The other stages include:
• Functional tests.
• Requirements-based tests.
• Coverage tests (such as MC/DC).
All these tests have one general purpose: to reduce the number of bugs in your code.
No software code base exists on this planet that can be considered to be error-free. But there are many good software code-base products that do what they're supposed to do. Unfortunately, there are also many that do not.
Tip #17—Myths and sagas
Many myths and sagas persist in the world of safety-critical embedded systems. One of the most common is that dynamic memory allocation is forbidden. This myth, however, is only half of the truth. Every application has an initialization phase. This phase is followed by the operational one. It's no risk at all to do dynamic memory allocation during the initialization phase. But to avoid memory fragmentation, during the operational phase, you aren't allowed to change those allocations.
Avoid the potholes
With this article, I've identified some potholes on the road to safety-critical software development and how you can avoid them. When you come to the part of your job where you tell a computer what you want it to do, I hope these tips will be helpful. At that time, remember this one sentence summary I came across once about our common job domain: “Computers have the strange habit of doing what you say, not what you mean."
Thomas Honold is a software architecture designer, specializing in safety-critical DO-178B software development in the defense/aerospace industry. He has a master in electronic engineering and has worked 15 years on software architectures and design for banking software, Internet banking, chip-card readers, avionics, and bootloader driver software.