Mixing some C into the Baking Pi course

My post last week left me wondering how hard it would be to mix a few C functions into the assembly-language “operating system” I’ve been following in Alex Chadwick’s Baking Pi tutorial. The answer, it turns out, is “not very hard” – but it does highlight a few elements of the compilation process that many developers (maybe excluding those doing a lot of embedded stuff) tend to take for granted.

The most common way of mixing C and assembly is probably to include inline assembly in C or C++ code. The idea is that C is doing most of the work, and the assembly is added in for the occasional special purpose. Here, I’m doing the opposite. I’ve got about 1300 lines of ARM assembly that already builds and works together. I want to add a C function that a) I can call from assembly, and b) that can itself call other assembly language routines. To do this, I need four things:

1. A C compiler

Conveniently, I’ve already got that. The Baking Pi course recommends a project called YAGARTO (Yet Another GNU Arm Toolchain) – basically, it’s the GCC compiler built as an ARM cross-compiler running on Windows. I suppose the other option would be to just use GCC under Raspbian on the Pi itself, but I’m doing enough swapping of SD cards as it is.

Baking Pi itself is all assembly, so it uses the YAGARTO assembler (arm-none-eabi-as) and linker (arm-none-eabi-ld), but the C compiler is also there, and just waiting to be used. The one thing I’m missing is a big chunk of the standard C library. A lot of the library depends on the underlying OS (think about File I/O, or something like printf() ) – and I don’t really have one yet.

2. A Makefile

Next, we need to teach the project’s makefile how to compile the C parts of the program. It’s been a while since I manually edited a makefile, but in this case I was able to squeak by with some minor changes to the default Baking Pi makefile. First, by teaching it that “.c” files should also produce corresponding object files:

3. A Linker script

It turns out that’s not quite enough to get my C code to build. Item three on the to-do list is to adjust the linker script, and this is the part I had the least experience with. Before this, I’ve had to manually edit linker scripts twice in my life. The first was actually for the Baking Pi course, to adjust the provided script to deal with changes in the Raspberry Pi bootloader. The second time was to teach CC65 to link code correctly for my Project:65 breadboard computer.

The linker script tells the linker how to put all the bits and pieces of the object files together into an executable. The object files (and the original assembly) split code into different sections. For example, “.text” contains the program code, and “.data” contains initial values for global variables. If you want to see the default linker script used by GCC under Linux, try to compile a simple program with the argument “-Wl,-verbose”. It’s… long. Happily, the Baking Pi script skips most of that complexity. There’s an “.init” section that’s hardcoded to 0x8000, “.text” starting at 0x8080 (my adjustment from last time), and a “.data” section following “.text”.

My C function required two other sections, “.rodata” and “COMMON”, which require a little explanation. “.rodata” stands for “read-only data”, which in this case was mostly string constants. When I was working on Project:65’s kernel, that was an important distinction, because anything in “.rodata” could stay in ROM, whereas “.data” needed to be allocated and initialized in RAM. In this case the entire program is running out of RAM, so it’s not important, and I just put the “.rodata” section after the regular “.data”.

Finally, there’s the “COMMON” section, about which I don’t know a lot. GCC apparently uses it for uninitialized global variables that can appear in more than one file. It’s a backward-compatibility thing, and if you turn it off this data would probably go into the “.bss” section instead.

4. Some source code written in C

Finally, I needed some test code. I decided to write a little test function that did some string manipulation and printing using the assembly-language FormatString and DrawString methods from Baking Pi’s Screen03 and Screen04 tutorials. FormatString is a poor man’s sprintf(), while DrawString() actually draws a string into the screen buffer.

The only real trick here is that I needed to create C function declarations for my assembly language routines, so the C compiler would know how to pass data back and forth. The assembly-language routines in the Baking Pi course all use the same calling conventions as GCC, which makes this easier. The first four arguments for a function are in registers R0 through R3, and any subsequent arguments go on the stack. The return value of a function is placed in R0. My function declarations looked like:

That Tag structure needs some explanation. FindTag is an assembly-language routine that searches through a set of tag objects containing startup information. The one we care about is the command-line arguments passed to our kernel. The function returns a pointer to a structure in memory, so I created an equivalent C struct, with one minor cheat. The real tags include their data inline, so they have variable length. In this case, I’m using a phony one-character array that I can pass to the string functions.

Finally, here’s my actual C function. It’s got local variables, uses the stack, calls assembly language routines, and so on. All it’s really doing is printing a few lines of text, but it’s a good example of how a little C can make for a friendlier experience without sacrificing too much performance.

Calling myfunc() from assembly is also simple; just use the command “bl myfunc”. One advantage of using plain old C (instead of C++) on ARM is that there’s no name-mangling required, as there would be for C++. Instead, function and variable names in C can be used as label names in assembly, and vice versa.

It didn’t take very long to figure out how to get these two languages to play nice with each other, and I’m glad I did it. Using C for the “connective tissue” between assembly routines makes development a lot simpler, and it may encourage me to spend some more time expanding this pseudo-operating-system to see how much it can really do.