Skip to content

Latest commit

 

History

History

02_drop_in_compiler

zig cc as a Drop-in Compiler Replacement

The easisest possible way to try Zig, is to replace arm-none-eabi-gcc with Zig's own command line compiler zig cc.

Information Gathering

Before we even touch Zig's compiler, it's useful to gather information on what exactly our current compiler is doing when we build. Examing the Makefile generated by ST, I want to highlight a couple of things:

Compiler setup:

PREFIX = arm-none-eabi-
# The gcc compiler bin path can be either defined in make command via GCC_PATH variable (> make GCC_PATH=xxx)
# either it can be added to the PATH environment variable.
ifdef GCC_PATH
CC = $(GCC_PATH)/$(PREFIX)gcc
AS = $(GCC_PATH)/$(PREFIX)gcc -x assembler-with-cpp
CP = $(GCC_PATH)/$(PREFIX)objcopy
SZ = $(GCC_PATH)/$(PREFIX)size
else
CC = $(PREFIX)gcc
AS = $(PREFIX)gcc -x assembler-with-cpp
CP = $(PREFIX)objcopy
SZ = $(PREFIX)size
endif
HEX = $(CP) -O ihex
BIN = $(CP) -O binary -S

This sets up what utility actually gets called at various steps in the compilation process. This isn't a tutorial on compilation, but one interesting thing of note is:

AS = $(PREFIX)gcc -x assembler-with-cpp

The compiler used to compile assembly files (.s/.S) is just the normal arm-none-eabi-gcc with the arg -x assembler-with-cpp which roughly means: "The language I'm compiling is assembly code, but it also has C pre-processor commands (#defines, etc.) so please support those".

Some more critical lines to pay attention to in the order they appear:

# cpu
CPU = -mcpu=cortex-m7

# fpu
FPU = -mfpu=fpv5-sp-d16

# float-abi
FLOAT-ABI = -mfloat-abi=hard

# mcu
MCU = $(CPU) -mthumb $(FPU) $(FLOAT-ABI)

This tells the compiler what architecture/cpu/floating-point hardware configuration to actually compile code for, we'll have to do the same equivalent setup with zig cc later (spoiler: the arguments are almost identical).

LIBS = -lc -lm -lnosys 

Links in the C standard library, math library, and "nosys" library in that order. The "nosys" library provides non-functional stubs for all the normal "OS" level functions (fwrite(), fopen(), exit(), etc.) that aren't implemented due to not having an OS when running "bare metal". Confusingly, ST also auto-generates syscalls.c which also does this. That will come up later.

-specs=nano.specs

You can find more information on spec files here, however this particular one does the following:

  • Any time -lc (the C standard library) is linked in, actually use -lc_nano, which is the "nano" variant of the newlib standard C library.

Newlib, and newlib-nano, are bundled with arm-none-eabi-gcc as pre-compiled binaries. The compiler knows which ones to choose automatically based on -mcpu, -mfpu, -mfloat-abi and -mthumb compiler args. zig cc will not know which one to choose, or even where to find it by default, which we'll get to later.

Now we're almost ready to get started with porting, but there's one more extremely helpful thing we can do. If the linker argument Wl,--verbose is added to the linker args via LDFLAGS variable, it can give us some easy information about what precisely is getting linked into our code. Running make | grep succeeded produces the following:

attempt to open /mygccarmpath/bin/../lib/gcc/arm-none-eabi/10.3.1/thumb/v7e-m+fp/hard/crti.o succeeded
attempt to open /mygccarmpath/bin/../lib/gcc/arm-none-eabi/10.3.1/thumb/v7e-m+fp/hard/crtbegin.o succeeded
attempt to open /mygccarmpath/bin/../lib/gcc/arm-none-eabi/10.3.1/../../../../arm-none-eabi/lib/thumb/v7e-m+fp/hard/crt0.o succeeded
attempt to open build/main.o succeeded
attempt to open build/gpio.o succeeded
attempt to open build/stm32f7xx_it.o succeeded
attempt to open build/stm32f7xx_hal_msp.o succeeded
attempt to open build/stm32f7xx_hal_cortex.o succeeded
attempt to open build/stm32f7xx_hal_rcc.o succeeded
attempt to open build/stm32f7xx_hal_rcc_ex.o succeeded
attempt to open build/stm32f7xx_hal_flash.o succeeded
attempt to open build/stm32f7xx_hal_flash_ex.o succeeded
attempt to open build/stm32f7xx_hal_gpio.o succeeded
attempt to open build/stm32f7xx_hal_dma.o succeeded
attempt to open build/stm32f7xx_hal_dma_ex.o succeeded
attempt to open build/stm32f7xx_hal_pwr.o succeeded
attempt to open build/stm32f7xx_hal_pwr_ex.o succeeded
attempt to open build/stm32f7xx_hal.o succeeded
attempt to open build/stm32f7xx_hal_i2c.o succeeded
attempt to open build/stm32f7xx_hal_i2c_ex.o succeeded
attempt to open build/stm32f7xx_hal_exti.o succeeded
attempt to open build/stm32f7xx_hal_tim.o succeeded
attempt to open build/stm32f7xx_hal_tim_ex.o succeeded
attempt to open build/system_stm32f7xx.o succeeded
attempt to open build/sysmem.o succeeded
attempt to open build/syscalls.o succeeded
attempt to open build/startup_stm32f750xx.o succeeded
attempt to open /mygccarmpath/bin/../lib/gcc/arm-none-eabi/10.3.1/../../../../arm-none-eabi/lib/thumb/v7e-m+fp/hard/libc_nano.a succeeded
attempt to open /mygccarmpath/bin/../lib/gcc/arm-none-eabi/10.3.1/../../../../arm-none-eabi/lib/thumb/v7e-m+fp/hard/libm.a succeeded
attempt to open /mygccarmpath/bin/../lib/gcc/arm-none-eabi/10.3.1/../../../../arm-none-eabi/lib/thumb/v7e-m+fp/hard/libnosys.a succeeded
attempt to open /mygccarmpath/bin/../lib/gcc/arm-none-eabi/10.3.1/thumb/v7e-m+fp/hard/libgcc.a succeeded
attempt to open /mygccarmpath/bin/../lib/gcc/arm-none-eabi/10.3.1/../../../../arm-none-eabi/lib/thumb/v7e-m+fp/hard/libc_nano.a succeeded
attempt to open /mygccarmpath/bin/../lib/gcc/arm-none-eabi/10.3.1/thumb/v7e-m+fp/hard/libgcc.a succeeded
attempt to open /mygccarmpath/bin/../lib/gcc/arm-none-eabi/10.3.1/../../../../arm-none-eabi/lib/thumb/v7e-m+fp/hard/libc_nano.a succeeded
attempt to open /mygccarmpath/bin/../lib/gcc/arm-none-eabi/10.3.1/thumb/v7e-m+fp/hard/crtend.o succeeded
attempt to open /mygccarmpath/bin/../lib/gcc/arm-none-eabi/10.3.1/thumb/v7e-m+fp/hard/crtn.o succeeded
attempt to open /mygccarmpath/bin/../lib/gcc/arm-none-eabi/10.3.1/../../../../arm-none-eabi/lib/thumb/v7e-m+fp/hard/libc.a succeeded
attempt to open /mygccarmpath/bin/../lib/gcc/arm-none-eabi/10.3.1/../../../../arm-none-eabi/lib/thumb/v7e-m+fp/hard/libm.a succeeded
attempt to open /mygccarmpath/bin/../lib/gcc/arm-none-eabi/10.3.1/thumb/v7e-m+fp/hard/libgcc.a succeeded

Interesting! Putting the object files generated from our source code aside, we can see we linked in all sorts of stuff! Also note how the paths are tuned to our specific architecture: .../thumb/v7e-m+fp/hard/.... Try messing around with floating point compile flags to see how the paths change.

The following files are responsible for initializing the C runtime on your device. You can read more about them here but generally speaking they are what's responsible for gracefully starting/ending your C program, calling C "constructors" and "destructors", etc.

crt0.o
crti.o
crtbegin.o
crtend.o
crtn.o

And here are the actual pre-compiled library files corresponding to our -l... library includes seen earlier, with one additional one, libgcc.a. An explanation of that one can be found here;

libc_nano.a
libm.a
libnosys.a
libgcc.a

Modifying our Linker Script

If you try to compile at this point, you would notice an error like so:

ld.lld: error: STM32F750N8Hx_FLASH.ld:56: memory region not defined: RAM
>>> _estack = ORIGIN(RAM) + LENGTH(RAM);    /* end of RAM */

Zig uses LLVM's linker ld.lld to link. Linker scripts are sadly not standardized across compilers, so this linker interprets our linker script slightly differently than arm-none-eabi-ld. Fortunately, this is quite easy to fix, we just need to move the declaration for _estack to after the region RAM is defined.

There's one other change want to make as well. ST's default script defines a section:

/* User_heap_stack section, used to check that there is enough RAM left */
  ._user_heap_stack :
  {
    . = ALIGN(8);
    PROVIDE ( end = . );
    PROVIDE ( _end = . );
    . = . + _Min_Heap_Size;
    . = . + _Min_Stack_Size;
    . = ALIGN(8);
  } >RAM

This section is in RAM (and thus wont' be flashed to the device), so it really should be marked with (NOLOAD) like so:

/* User_heap_stack section, used to check that there is enough RAM left */
  ._user_heap_stack (NOLOAD) :
  {
    . = ALIGN(8);
    PROVIDE ( end = . );
    PROVIDE ( _end = . );
    . = . + _Min_Heap_Size;
    . = . + _Min_Stack_Size;
    . = ALIGN(8);
  } >RAM

Diff the linker script from this project with the base project to see all changes

Modifying our Makefile

Armed with our previously gathered information, we can make a new Makefile to use zig cc instead of arm-none-eabi-gcc. You can diff our new Makefile with the Makefile from the base project to see what changed. I've marked + commented changed lines with CHANGE NOTE: so all explanations are contained within the modified Makefile. An overview of the steps taken:

  • We switch our compiler to use zig cc
  • We change some compiler arguments to match what zig cc expects
  • We add an additional system include path using -isystem to point at our arm-none-eabi-gcc installation so we can manually link in pre-compiled libraries With these modifications
  • We ditch linking in -lgcc and -lnosys, see Makefile for a more detailed explanation

And that's it! We (or at least I) now have a blinky program that blinks as expected when compiled with arm-none-eabi-gcc OR zig cc.