Observations from setting up an embedded zig project

I’m brand new to zig. I just started using it this past weekend and decided to jump straight into embedded with it. Overall if you’re experienced with setting up an embedded build with with make or cmake then setting up an embedded zig project is pretty straight forward. I’d never used clang before this so I had a bit of a learning curve there. The linker files that ST provide are for gcc so there are some small modifications needed.

My setup is a build targeting an stm32 using ST’s hal libraries for the drivers and zig 0.12.0 for the application. Currently it just blinks an led so the code is nothing to write home about but I encountered some oddities in setting up the build so I thought I’d share and see if anyone else has had similar experiences.

The first, is that zig gives me a warning from ld.lld that my elf and an object file, ld-temp.o, are of different triples. I don’t know what ld-temp.o contains, but I assume based on the name that it has something to do with the linker. Zig marks the warning as a error, but still produces a working elf file.

ld.lld: warning: Linking two modules of different target triples: '/home/git/GPS-Tracking-Device/zig/zig-cache/o/400208bfbe7179686ede919aa975077a/gps.elf.o' is 'thumb-unknown-unknown-eabi' whereas 'ld-temp.o' is 'thumbv7m-unknown-unknown-eabi'

I’m guessing that this is zig and ld.lld/clang just using a slightly different name for the same triple, but I don’t know enough about either compiler to be sure. I tried to give clang flags that matched my resolveTargetQuery parameters.

    //stm32f103
    const target = b.resolveTargetQuery(.{
        .cpu_arch = .thumb,
        .cpu_model = .{ .explicit = &std.Target.arm.cpu.cortex_m3 },
        .abi = .eabi,
        .os_tag = .freestanding,
    });

    const c_flags = [_][]const u8{
        "-g3", //max debug symbols
        //"-O1", //minor optimizations
        "-Wall",
        "-Wextra",
        "-mthumb",
        "-mlittle-endian",
        "-specs=nosys.specs", //don't link to libc
        "-mcpu=cortex-m3", 
        "-mfloat-abi=soft", //Software floating point
        "-ffreestanding",
        //"-Wl,--verbose,-Map=bin/gps.map", //not giving me a map file for some reason
        "-ffunction-sections",
        "-fdata-sections",
        "-nostdlib",
        "-nostdinc",
    };

The second oddity is the contents of my binary depending on my compiler optimization level.

Debug - This gives me a binary that is so large that it over flows my my flash memory of 64k. I think is makes sense since Debug specifically states that it creates a larger binary. I’m wondering how much value I would get out of using Debug during development if I wrote a custom panic handler that writes to uart or SWD.

install
└─ install gps.elf
   └─ zig build-exe gps.elf Debug thumb-freestanding-eabi 4 errors
error: ld.lld: section '.text' will not fit in region 'FLASH': overflowed by 336648 bytes
error: ld.lld: section '.rodata' will not fit in region 'FLASH': overflowed by 353920 bytes
error: ld.lld: section '.ARM' will not fit in region 'FLASH': overflowed by 353936 bytes
error: ld.lld: section '.data' will not fit in region 'FLASH': overflowed by 353972 bytes
error: the following command failed with 4 compilation errors:

Another interesting thing about this is that if I give clang the -O1 flag it optimizes enough of the binary for it to just barely fit in flash memory.

ReleaseSafe - Since Debug gave me a huge binary I expected ReleaseSafe to give me a smaller but still large binary. I thought that a lot of the size for a Debug build was due to the safety checks. However, the resulting binary using ReleaseSafe and no clang optimizations was just under 6k. This does seem on the large side for a blinky + hal but at least its not 400k. Giving clan -O1 this time only shrinks it by 0.4k. I’m still a little foggy on how zig does safety checks and what that means exactly. What happens if zig finds a safety issue during runtime? Panic? Crash? Is ReleaseSafe supposed to be a smaller/faster alternative to Debug during development?

ReleaseFast - The binary was the exact same size as ReleaseSafe. This is probably because c currently makes up 95% of my program. I only have a handful of zig lines written.

ReleaseSmall - From the information about embedded zig I gathered it seemed like ReleaseSmall is the preferred optimization for embedded systems. That makes a lot of sense given the limited flash sizes. However, when I build with ReleaseSmall the entire content of my symbol table is missing. ReleaseSmall might be just a tad to small.

That’s pretty much it. I’m not sure how much of this are zig bugs/issues and how much of this is just me not knowing what I’m doing. I’m looking forward to this project and seeing how zig compares to c for embedded work.

5 Likes

Hi thanks for your observations, if I can try to answer you about the release safe, From what I remember ReleaseSafe, keeps most of the safety features of Zig, so unreachable branches will trigger a panic, the equivalent of having __builtin_trap(); in an else condition in C. But from my own testing, it seems to be equivalent and even better than -fsanitize=memory, -fsanitize=integer, -fstrict-overflow, -fsanitize=address. Idk the specifics but i’d be interested in knowing too.

I may have an answer for your size issue as I just went through this excercise on an ST chip myself! It looks like you’re using -ffunction-sections and -fdata-sections flags, do you have the corresponding linker flag (-Wl,--gc-sections) which is what actually strips out all those unused symbols? With the zig build system you can use:
your_exe_object.link_gc_sections = true;
To get this behavior. I’m assuming you’re linking in the pre-built c std lib binaries bundled with arm-none-eabi-gcc? Without stripping out unused symbols in the linking stage it indeed can’t fit in 64K flash (my chip has the same internal flash size).

1 Like

Hey there, I was missing the linker flag. I added elf.link_gc_sections = true; like you suggested and it does shrink my Debug binary down enough that it fits in flash. But its still 55k for a debug release, and 7.8k for ReleaseSafe. How big is your binary?

I’m not linking to the c std lib binaries. When I added elf.linkLibC(); zig tells me that lib c isn’t available: error: libc not available. Since everything builds without it I haven’t circled back around to dig into it more. Is there a way to point zig’s to the arm-none-eabi-gcc’s lib c?

edit: Just read a couple of your other posts. Makes sense that you have to manually point zig to arm-none-eabi-gcc’s objects and include paths. Is there a downside to not linking to any lib c? It doesn’t seem like ST’s HAL uses it for anything.

edit 2: Adding -O1 flag to the compiler plus link_gc_sections gets my Debug binary down to 22k.

It’s a fair question! Really just depends on whether you need any of the features offered by the C std library. For instance you could forgo linking in the “math” library (libm.a) if you aren’t using anything from the <math.h> header.

That being said the __libc_init_array symbol is supplied by libc_nano.a which you want since the default generated assembly script uses that symbol to initialize all your default valued variables.

TLDR, you almost certainly want to link in the standard library, but math library is up to you. It also looks like you may not need to link in libgcc.a if you look at this post with a response from Andrew: