Zig on STM32: huge binary after update to 0.12

Hello, I’m experimenting with Zig in microcontroller projects.
In my current experiment, I build a simple program for a Nucleo-64 (STM32L476) :

  • Initialized by the STM32CubeMX software.
  • “build.zig” created to compile the sources (Zig / C / ASM)
  • Zig code (function “main_zig”) called from a C source file (main.c).

I managed to compile and run this code with version 0.11.0 of Zig. (even with an unknown linker error from lld)
When I have upgraded to 0.12.0, the compilation produces a binary that is too large.
I have no clue yet as to why this change occurred.

The example code is on GitHub:

Questions :

  • Updating from LLVM 16 to 17 could have broken the interpretation of my linker script (STM32L476RGTx_FLASH.ld) ?
  • Have the options for Zig passed to “lld” changed ?
  • Is it possible to pass linker options directly to lld via the “build.zig” file ?
  • Has a static library been added ? (math ?)
  • Other clues ?

Have you tried running an example of this? MicroZig

I’m on my phone so I haven’t seen the file sizes, but if the size difference is stark, you should submit an issue and someone will investigate this. @kubkon might be willing to help.

I need to look more into the MicroZig build system, however, from what I’ve gathered, MicroZig appears to implement all the software layers using the Zig programming language. In my experiment, I explore an approach that involves retaining the official drivers, such as HAL and LL drivers, in C, along with the code generated by the ST software and other components like FreeRTOS, also in C.

I will do it. Thank

You might start by seeing which sections have blown up:
$ objdump -h file.elf

If the text section has blown up, you can see where:
$ nm --print-size --size-sort file.elf

Hang tight for a full guide I’m making on porting existing STM32CubeMX projects to the Zig compiler, but a couple of things that may help you:

  • elf.setVerboseLink(true); I believe this is a bug that causes the linker to output to stderr, which is likely the “unknown” linker error you’re getting, see here:
    zig build --verbose-link shows errors on macOS · Issue #19410 · ziglang/zig · GitHub
  • const c_sources_compile_flags = [_][]const u8{ "-std=gnu17", "-DUSE_HAL_DRIVER", "-DSTM32L476xx", "-Wall", "-fdata-sections", "-ffunction-sections", "-mcpu=cortex-m4", "-mfpu=fpv4-sp-d16", "-mfloat-abi=hard", "-mthumb", "-lnosys", "--specs=nano.specs", "-Wl,--gc-sections" };
    • -lnosys doesn’t belong here! That’s a linker argument to include the library named “nosys”, or more specifically the “.a” file libnosys.a (more on that later), so you can remove that from this line
    • --specs=nano.specs also a linker argument, and unfortunately isn’t doing what you expect, this is actually specific to the arm-none-eabi-gcc compiler, and the TLDR is that uses the “nano” variant of newlib as the “standard c library” for the project. However, zig cc compiler doesn’t know about that spec sheet, so go ahead and remove that from this line, we’ll have to manually link those libraries in later
    • Wl,--gc-sections also a linker argument, see below how to do this without manually specifying the flag
  • Alright, so essentially arm-none-eabi-gcc comes with a bunch of pre-compiled libraries + objects. Annoyingly, a lot of these are snuck in without explicitly linking to them by default. So to replicate this behavior but using the zig compiler and linker, we have to manually reference these. I see you were already on the right track here, but I can give you some copy/paste that will do what you intend:
   // Manually including libraries bundled with arm-none-eabi-gcc
   elf.addLibraryPath(.{ .path = "[YOUR_ARM_COMPILER_PATH]arm-none-eabi/lib/thumb/v7e-m+dp/hard" });
   elf.addLibraryPath(.{ .path = "[YOUR_ARM_COMPILER_PATH]lib/gcc/arm-none-eabi/[YOUR_VERSION]/thumb/v7e-m+dp/hard" });
   elf.addSystemIncludePath(.{ .path = "[YOUR_ARM_COMPILER_PATH]arm-none-eabi/include" });
   elf.linkSystemLibrary("nosys");
   elf.linkSystemLibrary("c_nano");
   elf.linkSystemLibrary("m");
   elf.linkSystemLibrary("gcc");

   // Manually include C runtime objects bundled with arm-none-eabi-gcc
   elf.addObjectFile(.{ .path = "/[YOUR_ARM_COMPILER_PATH]/arm-none-eabi/lib/thumb/v7e-m+dp/hard/crt0.o" });
   elf.addObjectFile(.{ .path = "/[YOUR_ARM_COMPILER_PATH]/lib/gcc/arm-none-eabi/[YOUR_VERSION]/thumb/v7e-m+dp/hard/crti.o" });
   elf.addObjectFile(.{ .path = "/[YOUR_ARM_COMPILER_PATH]/lib/gcc/arm-none-eabi/[YOUR_VERSION]/thumb/v7e-m+dp/hard/crtbegin.o" });
   elf.addObjectFile(.{ .path = "/[YOUR_ARM_COMPILER_PATH]/lib/gcc/arm-none-eabi/[YOUR_VERSION]/thumb/v7e-m+dp/hard/crtend.o" });
   elf.addObjectFile(.{ .path = "/[YOUR_ARM_COMPILER_PATH]/lib/gcc/arm-none-eabi/[YOUR_VERSION]/thumb/v7e-m+dp/hard/crtn.o" });
  • elf.entry = .{ .symbol_name = "Reset_Handler" }; I don’t think this is necessary but also am not sure if it hurts anything or not… TBD on this while I investigate more on my own
  • The following will accomplish what you wanted with Wl,--gc-sections : elf.link_gc_sections = true;
  • As a bonus you can remove the manual specification of -ffunction-sections -fdata-sections flags and replace with:
    elf.link_data_sections = true;
    elf.link_function_sections = true;
  • Lastly, the giant .bin file is almost certainly because of differences in how zig (LLVM’s) linker interprets .ld files vs gcc. Look for areas to potentially add (NOLOAD) as I have a feeling it’s trying to “fill” the memory range between your flash and ram (despite there logically not being anything there in the chip)
5 Likes

Thank you for these insights, @haydenridd.
To resolve the issue, I have used the following instruction: elf.link_gc_sections = true;
Option summary:

elf.entry = .{ .symbol_name = "Reset_Handler" }; 
elf.want_lto = false; // -flto
elf.link_data_sections = true; // -fdata-sections
elf.link_function_sections = true; // -ffunction-sections
elf.link_gc_sections = true; // -Wl,--gc-sections

Notes:

  • The NOLOAD section is required for the _user_heap_stack symbol (See this topic for details.
  • I have corrected v7e-m+dp to v7e-m+fp (Upon verification, my component only has a single-precision FPU).
  • Regarding the Reset_Handler, I am uncertain as well. The linker script already sets it.

I have committed the fixes.

Bonus question:

  • How can I enable float parsing with the libc ( -u _printf_float linker option) ?
  • Is it possible to generate a .map file using lld ?
1 Like

How can I enable float parsing with the libc ( -u _printf_float linker option) ?

The symbol ‘_printf_float’ is present in the final binary, I suppose I don’t need to use this option. Float formatting still don’t work (tested with snprintf), I will investigate.

Is it possible to generate a .map file using lld ?

Apparently, this feature is not yet implemented : unsupported linker arg: -Map=skrouterd.map

Ahh yep, that’s actually because you’re using the “nano” variant of newlib, where float printf formatting isn’t included. One of the things that got axed to make the library tiny. You could link in the non “nano” version but that probably will bloat your size more than you want!

Enable Float formatting is possible :
See this topic for more details about porting: guide