Bare Bones Programming: The C Language

Spread the love

…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 target chip.


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 computing toolchains: binary files, object code, assemblers and disassemblers, libraries, linkers, stacks, function calling conventions and other complexities.

When you’re coding in assembly, what is immediately obvious is that you’re on your own: even the most basic support routines are not there. All you have are the target chip and its capabilities, and that’s it. Everything else is considered higher level and there is no built-in support for it. Files? Forget it. Serial? TCP/IP? Web? Nope. If you need the functionality you have to build it yourself.

On the one hand, that is why there was a market for Operating Systems; the operating system handles all that for you. And these were indeed originally coded in assembly. But allowing for an operating system does not reduce the complexity of working in Assembly. You would need to interface directly with the Operating System’s API, e.g. by manually placing parameters on the call stack ahead of every OS call and restoring the registers upon each return. There are literally thousands of OS API functions on a modern, fine-grained OS.

On the other, for many embedded systems applications (and certainly for operating system development itself), there is no operating system, i.e. bare metal programming.

For these situations, there are two languages — C, which we will cover here, and Forth, covered separately.

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 as a way to speed up the porting of their newly programmed UNIX operating system between PDP minicomputers. This approach was wildly successful, and put the C language as the mainstream OS development language. 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 Bones 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 bones 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 bones 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:

  1. it more easily supports a myriad of simple as well as complex data structures that are the bedrock of algorithms and therefore implementations
  2. it comes with a wide variety of standard facilities (the C Standard Library)
  3. 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:

  1. C retains all of the power and fine-grained control that assembly has; C can be treated as an assembly language for assembly
  2. C plays well with assembly, allowing inline and embedded assembly, as well as the linking in of modules written entirely in assembly

Controversies around Optimizing Compilers

A end-to-end compiler does 4 things to go from source code to executable: pre-process, compile, assemble, and link. Here’s how you can setup GCC to see the intermediate results of each step.

  1. Non-optimizing compilers such as TCC can create simple/straightforward assembly code and be used as an efficient assembler (see Assembly Part III). C compilers exist that target most embedded platforms, chips, and environments.
  2. Modern optimizing compilers such as GCC are no longer portable, universal assemblers (their original vision). While they handle the complexities of modern processor architectures, their mission has shifted and they have become repositories of compiler optimization capabilities (e.g. pipelining, caching, etc.) making the C compiler into automatic code generator, but at the same time taking liberties with C code which now becomes a suggestion to the compiler rather than a specification for a machine.

For a more detailed discussion of the controvery, see Assembly Part I, Appendix: The Sacred and the Profane.

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.

Stay tuned.


If you enjoyed this article, subscribe to our RSS feed — don’t miss the next article in the series.


Further Reading

>> Browse Computing Toolkits.

  1. Reference:
  2. Safety:
  3. Style:

Related Articles

6 comments to Bare Bones Programming: The C Language

Leave a Reply

You can use these HTML tags

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

  

  

  

Your comments are valued! (Please indulge the gatekeeping question as spam-bots cannot (yet) do simple arithmetic...) - required

Optionally add an image (JPEG only)

 

Stats: 1,066,417 article views since 2010 (March update)

Dear Readers:

Welcome to the conversation!  We publish long-form pieces as well as a curated collection of spotlighted articles covering a broader range of topics.   Notifications for new long-form articles are through the feeds (you can join below).  We love hearing from you.  Feel free to leave your thoughts in comments, or use the contact information to reach us!

Reading List…

Looking for the best long-form articles on this site? Below is a curated list by the main topics covered.

Mathematics-History & Philosophy

  1. What is Mathematics?
  2. Prehistoric Origins of Mathematics
  3. The Mathematics of Uruk & Susa (3500-3000 BCE)
  4. How Algebra Became Abstract: George Peacock & the Birth of Modern Algebra (England, 1830)
  5. The Rise of Mathematical Logic: from Laws of Thoughts to Foundations for Mathematics
  6. Mathematical Finance and The Rise of the Modern Financial Marketplace
  7. A Course in the Philosophy and Foundations of Mathematics
  8. The Development of Mathematics
  9. Catalysts in the Development of Mathematics
  10. Characteristics of Modern Mathematics

Electronic & Software Engineering

  1. Electronics in the Junior School - Gateway to Technology
  2. Coding for Pre-Schoolers - A Turtle Logo in Forth
  3. Experimenting with Microcontrollers - an Arduino development kit for under £12
  4. Making Sensors Talk for under £5, and Voice Controlled Hardware
  5. Computer Programming: A brief survey from the 1940s to the present
  6. Forth, Lisp, & Ruby: languages that make it easy to write your own domain specific language (DSL)
  7. Programming Microcontrollers: Low Power, Small Footprints & Fast Prototypes
  8. Building a 13-key pure analog electronic piano.
  9. TinyPhoto: Embedded Graphics and Low-Fat Computing
  10. Computing / Software Toolkits
  11. Assembly Language programming (Part 1 | Part 2 | Part 3)
  12. Bare Bones Programming: The C Language

Pure & Applied Mathematics

  1. Fuzzy Classifiers & Quantile Statistics Techniques in Continuous Data Monitoring
  2. LOGIC in a Nutshell: Theory & Applications (including a FORTH simulator and digital circuit design)
  3. Finite Summation of Integer Powers: (Part 1 | Part 2 | Part 3)
  4. The Mathematics of Duelling
  5. A Radar Tracking Approach to Data Mining
  6. Analysis of Visitor Statistics: Data Mining in-the-Small
  7. Why Zero Raised to the Zero Power IS One

Technology: Sensors & Intelligent Systems

  1. Knowledge Engineering & the Emerging Technologies of the Next Decade
  2. Sensors and Systems
  3. Unmanned Autonomous Systems & Networks of Sensors
  4. The Advance of Marine Micro-ROVs

Math Education

  1. Teaching Enriched Mathematics, Part 1
  2. Teaching Enriched Mathematics, Part 2: Levelling Student Success Factors
  3. A Course in the Philosophy and Foundations of Mathematics
  4. Logic, Proof, and Professional Communication: five reflections
  5. Good mathematical technique and the case for mathematical insight

Explore…

Timeline