…for Embedded and Low-Level Systems Development
C provides the convenience of learning one language while retaining the ability to target a variety of platforms including modern operating systems (Linux, Windows, Mac), real-time operating systems, systems-on-a-chip, and a host of microcontrollers for embedded development. And if you have to “mov” the bits around yourself (device drivers, DMA controllers), you can do that too. This is a significant efficiency over assembly languages which are essentially chip-specific control codes and therefore require understanding the architecture of the chip in each target platform.
The C Language for Systems Development
The C language can be thought of as a machine independent, “macro assembler” with a small library of useful core subroutines provided so that you don’t have to re-invent the wheel every time you write low-level code.
Why choose C (over Assembly) for Systems Development?
So you’ve tried assembly language? That’s good. It brings the pedal to the metal when it comes to understanding toolchains, binary files, object code, disassemblers, libraries, linkers, stacks, function calling conventions and all the myriad complexities that one has to understand in order to even step onto the mat of assembly language programming on and for a computer platform with an OS like Windows or Linux.
But when you’re coding in assembly, what grabs you straight away? You’ll notice the fact that the most basic things are not there. There is NO support for what is quite commonly needed for even low-level applications: input, output, file access, and all the rest.
Sure, you say, that’s why there is a market for Operating Systems; the operating system handles all that for you. Perhaps, IF you are working on a platform for which there IS an operating system. But even here, interfacing assembly language with the Operating System’s API directly means spending more time dealing with the details of the OS API than coding your application. Every call needs to be handled by manually placing its various parameters on the call stack and restoring the registers upon each return. And there are quite literally thousands of OS API functions on a modern, fine-grained OS.
And for many embedded systems applications (and certainly for operating system development itself), there is no operating system.
There must be a better way — and there is.
Origins of the C Language
In the 1970s, before deregulation of the telephone industry, when Bell Labs was one of the most powerful companies in the U.S., Dennis Ritchie and Ken Thompson developed the C language at Bell Labs. Since then, C has become a universal standard, with a standardized library of functions and standards for compiler designers etc. In a nutshell, the C language is now a UNIVERSAL interface to assembly language for all those systems for which there exists a C compiler that targets it.
The Strengths of C
C was designed from the ground up to be not one hair less powerful than assembler. Everything you can do in assembler, you can do in C. And if you want to drop down INTO assembler, you can do that as well: inline assembly (also called embedded assembly). If you want to link in modules or functions written purely in assembly and assembled into object files, you can do that also. And if you want to write in assembler and pull in (extend) assembly with the C functions, you can do that too.
What you get is the convenience of learning one language (a significant efficiency over assembly language), while retaining the ability to target a variety of platforms — from modern operating systems (Linux, Windows, Mac) to real-time operating systems, (PC/104 stacks), systems-on-a-chip (Analog Devices Blackfin, Microchip’s 32-bit PIC chips), and a host of microcontrollers for embedded development (Microchip’s 8-bit PIC chips, Atmel AVR, Motorola (Freescale) 68k family, Intel 8051 family). And whenever you have to “mov” the bits around yourself (device drivers, DMA controllers), well, you can do that too, using inline assembly.
Bottom line: for most things that even system level programmers want to do, C is the preferred level of work. This can be seen by looking at the source code for the Linux OS and Windows OS. Both are done primarily in C.
“Noise-free” programming of Algorithms and Data Structure
There is a pragmatic advantage when coding algorithms and data structures in C vs. assembly. Working in assembly means that the essence of data structures and algorithms is often hidden in the noise of mov’s and pushes. In C, algorithmic details stand out more clearly, so the patterns can be more easily understood, and programs more easily modified and maintained.
Bare Metal Programming
Like assembly, in C you are forced to work directly with memory, pointers, and the underlying arrangement of the bits and bytes that make up your data structures and their layout (packed structures, byte ordering, data types and conversions, etc.) No garbage collection, no forgiveness for buffer overruns, no regular expressions. C is for bare metal programming — which typically require safe coding practices, tight program design, and a good understanding of the toolchain and the target environment.
But when you want the underlying elements of the data structures to be visible and your algorithms to be working with these bare metal details, C is a great choice for turning out programs that interact with hardware, networks, sensors, and peripherals, and that are small (compiled size), fast (execution time), and have a small footprint (few dependencies, low loading overhead).
With the aid of bare bones compilation modes like gcc -S %1.c -fno-exceptions -s -Os, and of working syntax converters like A2I, whenever you wish to see the kind the assembly code that C is generating for you, you can.
By using C for systems level development, one retains the power of assembly but the efficiency of a language that abstracts the specific innards of the target chip.
Three Plus Three Further Reasons to Code In C (over Assembly)
There are three good reasons why C is a more compelling choice than assembly for systems development:
- it more easily supports a myriad of simple as well as complex data structures that are the bedrock of algorithms and therefore implementations
- it comes with a wide variety of standard facilities (the C Standard Library)
- it is independent of particular computing architectures — the essential details posed by specific computing architectures are handled by the target specific compiler. (Note! In theory, C as a language is independent of Operating System, but in practice, there is usually an Operating System dependence — see Practical Matters, below.)
Even where the use of assembly is compelling, there are three reasons why C might yet be a better choice:
- C retains all of the power and fine-grained control that assembly has; C can be treated as an assembly language for assembly
- C plays well with assembly, allowing inline and embedded assembly, as well as the linking in of modules written entirely in assembly
- modern optimizing compilers can create both simple and straightforward assembly, as well as handle the complexities of modern architectures and their mind-boggling array of advanced capabilities (e.g. pipelining, caching, etc.) much better than an individual can, so that the C compiler can be viewed as an automatic assembly code generating machine. Indeed, C compilers exist that target most embedded platforms, chips, and environments.
Practical Matters: Designing Complex Programs in C
The C standard libraries are designed to make available the facilities most commonly needed, and make them available in a manner that is independent of platform and operating system.
In practice, however, there are typically facilities that you will desire for your programs that are not part of the C standard library, and that will be supplied to you by the Operating System. At this point, your C program will need headers and object code provided by the Operating System. The form of the functions you call will then often be Operating System dependent.
Thus, one way to think of C is that while it enables hardware independence, it does NOT enable operating system independence.
Complex programs need their own, coarser-grained building blocks. When working in C, programmers typically create their own application specific libraries. This allows program designers to break down their designs into modules: abstract, general, re-usable blocks of code. These modules are then woven into applications that work within the same problem space, and thus, that share the fundamental elements of application logic and program design.
Encapsulation of implementation
The function is the fundamental unit of modular programming in C (versus the class in object oriented programming).
One of the goals of a library is to provide building blocks whose interfaces and functionality is well documented but whose implementation is a black box and hence free to be modified without affecting any user code (code using the library). For this be accomplished, it is important to be able to separate user code from library code.
C’s method of separation is by convention: .h header files containing the user interface; .c code files containing the implementation. However, this gets blurred when one-line inline definitions or macro definitions are used in the header files (bad style).
Perhaps the best way of addressing this problem is to have pre-compiled libraries of object code (.a, .dll, .o) that are linked into each new design. This requires documentation external from the object code (since object code is not human readable), and it also introduces the possibility of dll hell: the mismatch and maintenance of versions of the object code, since these typically then proliferate and are copied and re-copied, etc.
Object-Oriented C vs. Higher Level Languages
Object-oriented design is certainly possible in C — it is just that the programmer must code in a way that respects the fundamental principles of object orientation, namely information hiding, and the encapsulation of both data structures AND methods into abstract blocks — objects.
It is these, and other facilities that are addressed and improved upon in higher level languages than C: Objective-C, C++, Java, .Net along one language stream, and Perl, Python, and Ruby along another.
I’ll take up higher level languages (Objective-C, C++, Java, .Net, Perl, Python, Ruby) and their strengths for applications development in upcoming posts.
I’ll also discuss a functional open-source development environment for systems and embedded development in C.
If you enjoyed this article, subscribe to our RSS feed — don’t miss the next article in the series.
>> Browse Computing Toolkits.