Standard C and embedded programming

09.03.2013 15:14

Previous week I had a few discussions with my colleagues regarding some finer details of variable types in C and how code generated by GCC behaves in various edge cases. This led me to read a few chapters of the actual C standard and research this topic a bit.

It's surprising how much of embedded C code I see on a day to day basis depends on behaviors that are implementation-specific according to the standard. The most common cases seem to be:

  • Assuming how struct and union types are stored in memory - specifically how the struct elements are aligned in memory and how assigning one element in a union type affects other elements.
  • Assuming how integer types are stored - specifically byte order when casting between pointers to differently sized integers.
  • Assuming what the storage size for enum types is.

In the world of software for personal computers, it's more or less universally agreed (at least in free software community) that these offenses are bad. They lead to unportable software and angry package maintainers that have to deal with bugs that only appear when software is compiled on certain architectures.

With embedded software the situation is less clear. First of all, embedded software is by definition more closely tied with the hardware it is running on. It's impossible to make it independent of the architecture. A large fraction of firmwares are also developed over a relatively short-term: Compared to desktop software that can be maintained over years, once a firmware image is finished it is often shipped with the physical product and never updated (even if a possibility for that exists). That means that it's unlikely that the CPU architecture or the compiler will change during the development cycle.

In embedded software you often have to deal with serialization and deserialization of types. For instance, you might be sending structures or multi-byte integers over a bus that only processes bytes at a time. Just casting char buffer pointer to a complex type at the first glance produces shorter, simpler code than doing it the standard-compliant way with arithmetic operations and assignments to individual struct fields.

But is the code really simpler? The emphasis should always be on code that is easy to read, not easy to write. When making a cast from *char to *int you silently assume that the next pair of eyes knows the endianess of the architecture you are working on.

There's also the question of crossing the fine line between implementation specific and undefined behaviors. Former depend only on the architecture (and perhaps minor compiler version) and the latter can change under your feet in more subtle ways. For instance, I have seen a few cases where results of arithmetic operations that depended on undefined behaviors would change with changes to unrelated code. That leads to heisenbugs and other such beasts you most certainly do not want near your project. Granted these usually involve more cleverness from the developer's side than the memory storage assumptions I mentioned above. In fact since these seem to be depended on so often you could say that some of the struct and union storage details are de-facto standard these days.

So what's the verdict here? I admit I'm guilty of using some of these tricks myself. In the most extreme case they can save you hundreds of lines of boiler plate code that just shuffles bytes hence and forth. And down in the lonely trenches of proprietary firmware development code reusability can be such a low concern that it doesn't matter if your code even compiles the day after the dead-line.

Before starting a project it makes sense to take a step back and set a policy depending on the predicted life-time of the code. I do think though that sticking to portable code is a must if you want to publish your work under and open source license. With the ever growing open source hardware community, sharing drivers between platforms is getting more and more common. Even with proprietary projects, I'm sure the person down the line that will be debugging your code will be happier if she didn't have to needlessly delve into specifics of the processor architecture you are using.

Posted by Tomaž | Categories: Code

Comments

I've done a bunch of things in the sigrok project that I'm also unsure about regarding portability. But I find it rather hard to test this. I develop on your basic x86 machine of course, but other than compiling/running things on a small ARM9 board, I haven't tested much.

Do you have any hints on what would be a good platform to use to try and trigger bugs such as these? Do you know a big-endian platform even? These seem to have all but disappeared.

In C++11, there's a simple rule for this: if you can Static_assert it, it's safe to use it. If you reuse your code somewhere else and the behaviour is different, at least that will fail at compile time. This allows you to check sizeof for structs and enums, offsetof for struct fields, maybe the platform endianness.
Writing completely clean C is a pain. This is also one reason why a lot of "desktop" computer programs are moving to Java or other languages that have a well-defined behaviour in all cases. The drawback is this leaves less room for optimization to the compiler. On embedded platform, you can also use languages like Lua or TCL to solve the problem in a similar way. My preferred solution, however, is C++. It has not much overhead over C, and it allows to do the "right thing" (serializing a struct field-by-field, etc) but put it in a method of the object, to hide the mess. Even if you first implement your method the unsafe way, you can easily change it later on, and in a single place in the code. While the same can be done in C (you can do everything in C !) using private structs and .h files that only export accessor methods, it's a bit harder to make sure someone doesn't cheat.

Bert, from personal experience compiling and testing software on old PowerPC-based Macs used to be a very good test whether it's portable or not. Unfortunately they all switched to x86 now.

Both MIPS and ARM can be switched to big-endian mode, but all Linux distributions tend to choose little-endian (Debian big-endian armeb is more or less dead as far as I know).

Actually, it would be interesting to see if it's possible to compile and run software in big-endian ARM mode on an otherwise little-endian OS for testing purposes.

Posted by Tomaž

Add a new comment


(No HTML tags allowed. Separate paragraphs with a blank line.)