Skip to Content
Course content

209: Cross-Compiling C for Different Architectures

Click on the "Edit" button in the top corner of the screen to edit your slide content.

You’ve likely spent most of your time in this course writing code on your laptop and running it on that same laptop. In that world, your gcc or clang is a "native compiler." It looks at your CPU—probably an x86_64 or an Apple Silicon M-series chip—and generates machine code specifically for that architecture. But in the real world, especially if you're moving into embedded systems or kernel development, you'll often find yourself in a situation where the machine you're coding on (the host) is completely different from the machine the code will actually run on (the target).

The "Just Copy the Binary" Delusion

When I first started working with embedded ARM boards, I did something incredibly naive: I wrote a simple program to toggle a GPIO pin, compiled it with my standard system gcc, and tried to scp the resulting binary onto the target board. It didn't work. Of course it didn't work. My laptop was shouting instructions in x86_64, and the ARM chip was expecting ARMv7-M. It's like trying to read a book written in Japanese when you only speak Portuguese; the symbols are there, but the meaning is completely lost.

Some developers try to solve this by installing a C compiler directly on the target device. If you're targeting a Raspberry Pi, this is easy. But if you're targeting a microcontroller with 128KB of RAM and no operating system, you can't exactly apt-get install build-essential. Even on larger targets, compiling on the device is painfully slow and wears out the flash storage. You don't want your production environment to be your build environment.

The Toolchain Triplet

The professional way to handle this is cross-compiling. This means using a compiler that runs on your host but generates code for your target. To do this, you need a cross-compiler toolchain. You'll notice these compilers usually have strange, hyphenated names, like arm-none-eabi-gcc. This isn't just random naming; it's a "triplet" that tells you exactly what the tool is for.

  • arm: The target architecture.
  • none: The vendor (in this case, "none" usually means there is no specific vendor-locked OS).
  • eabi: The Application Binary Interface (ABI). This tells the compiler how to handle function calls, stack alignment, and data types so the resulting binary is compatible with the target's runtime environment.

Instead of running gcc main.c -o main, you'll run something like this:

arm-none-eabi-gcc -mcpu=cortex-m4 -mthumb -c main.c -o main.o

Notice the extra flags. I'm explicitly telling the compiler that I'm targeting a Cortex-M4 and that I want "Thumb" instructions (a denser instruction set used by ARM). If you forget these, the compiler might default to a generic ARM profile that the hardware doesn't actually support, and you'll spend three hours debugging a "HardFault" exception that was actually just a compiler mismatch.

The Hidden Cost: The Linker Script

Cross-compiling isn't just about swapping the compiler; it's about the memory map. On your laptop, the OS handles where your program sits in RAM. On a bare-metal target, there is no OS. You have to tell the linker exactly where the Flash memory starts and where the RAM resides.

This is where the "naive" approach usually breaks down. If you just compile and link, the compiler assumes a standard layout. But your target chip might have Flash at 0x08000000 and RAM at 0x20000000. To fix this, you use a linker script (usually a .ld file). I've seen plenty of engineers struggle with "strange" crashes only to realize their code was being linked to a memory address that didn't even exist on the chip.

/* A snippet of a linker script */
MEMORY
{
    FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 256K
    RAM (rwx)  : ORIGIN = 0x20000000, LENGTH = 64K
}

By passing this to the linker via -T layout.ld, you bridge the gap between your powerful development workstation and the constrained reality of your target hardware. It's a bit of a setup headache, but it allows you to iterate in seconds rather than waiting for a slow on-device build.




📋 Practical Task

Exercise: Cross-Compiling a Bare-Metal UART Heartbeat

Your goal is to simulate a cross-compilation workflow for an ARM Cortex-M4 microcontroller. You aren't required to have the hardware, but you must produce a binary that is architecturally correct for the target.

  1. The Code: Create a file named heartbeat.c. Write a program that simulates writing a "Heartbeat" message (e.g., "System Alive\r\n") to a memory-mapped UART register. (Assume the UART data register is at address 0x4000C000).
  2. The Toolchain: Use the arm-none-eabi-gcc toolchain. If you don't have it installed, install it via your package manager (e.g., sudo apt install gcc-arm-none-eabi).
  3. The Compilation: Compile the code using the -mcpu=cortex-m4 and -mthumb flags.
  4. The Linker: Create a simple linker script target.ld that defines a FLASH region starting at 0x08000000 and a RAM region at 0x20000000.
  5. The Final Binary: Link the object file using your linker script.
  6. Verification: Use arm-none-eabi-objdump -d heartbeat.elf to inspect the assembly. Verify that the instructions are ARM Thumb instructions and that the memory addresses referenced match your linker script and UART register address.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.