Static linking newlibc as libc, freestanding on ARM, zig + c libraries

Hi I’m trying to build a C project with build.zig
The project is for an embedded platform on ARMv7-M (thumb) specifically for an STM32 CPU. The vendor provides hardware abstraction layer (HAL c-sources ) libraries, startup-code (assembly) and main file.
I successfully included paths to header and sources (c+asm) for the HAL and linker script.
The constraint is that I have to link against newlib as the libc standard library. I added the references to the newlib library object files for my target (ARM embedded cpus have multiple pre-build binaries compiled by the arm-none-eabi-gcc project like crt0.o crtbegin.o, newlibc.o etc…) but I’m unable to compile with zig build because it fails with no significant messages on the linking step. I suspect that it has something to do with how I link to newlib.

I read the startup assembly script for my CPU, and it is the first code that runs after a cpu reset. It does most of the initialization that a normal crt0.s code does a part from calling _start. In fact later on it calls __libc_init_array that calls __init an probably later __start but the call to main is done in the startup assembly (startup_stm32f446xx.s).
I don’t know how to debug my linking error and how to generate a build.
build.zig

const std = @import("std");

// Although this function looks imperative, note that its job is to
// declaratively construct a build graph that will be executed by an external
// runner.
pub fn build(b: *std.Build) void {
    //b.verbose_cc = true;
    b.verbose_link = true;
    //b.verbose_air = true;
    //b.verbose = true;

    const prj_name = "test";

    const target = .{
        .cpu_arch = .thumb, // ARMv7
        .cpu_model = .{ .explicit = &std.Target.arm.cpu.cortex_m4 }, // STM32F446RE
        .os_tag = .freestanding, // running in bare metal
        .abi = .eabihf, // no libc (noneabi) with hardware floating point (hf)
    };
    const asm_sources = [_][]const u8{"startup_stm32f446xx.s"};

    //const asm_flags = [_][]const u8{
    //    "-fdata-sections",
    //    "-ffunction-sections",
    //    "-Wall",
    //    "-Wextra",
    //    "-Werror",
    //    "-pedantic",
    //    "-fstack-usage"
    //};

    const c_includes = [_][]const u8{
        "./Core/Inc",
        "./Drivers/STM32F4xx_HAL_Driver/Inc",
        "./Drivers/STM32F4xx_HAL_Driver/Inc/Legacy",
        "./Drivers/CMSIS/Device/ST/STM32F4xx/Include",
        "./Drivers/CMSIS/Include",
    };

    const c_sources_drivers = [_][]const u8{
        "Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_tim.c",
        "Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_tim_ex.c",
        "Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_uart.c",
        "Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_rcc.c",
        "Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_rcc_ex.c",
        "Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_flash.c",
        "Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_flash_ex.c",
        "Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_flash_ramfunc.c",
        "Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_gpio.c",
        "Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_dma_ex.c",
        "Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_dma.c",
        "Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_pwr.c",
        "Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_pwr_ex.c",
        "Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_cortex.c",
        "Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal.c",
        "Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_exti.c",
    };
    // "-Wno-unused-parameter",
    const c_sources_drivers_compile_flags = [_][]const u8{
        "-std=gnu11",
        "-DUSE_HAL_DRIVER",
        "-DSTM32F446xx",
        "-ffunction-sections",
        "-fdata-sections",
        "-nostdlib",
        "-nostdinc",
        "-Wall",
        //"-Werror",
        "-Wextra",
        "-pedantic",
        "-fstack-usage",
        "-mthumb",
    };

    const c_sources_core = [_][]const u8{
        "./Core/Src/system_stm32f4xx.c",
        "./Core/Src/stm32f4xx_it.c",
        "./Core/Src/stm32f4xx_hal_msp.c",
    };
    const c_sources_app = [_][]const u8{
        "./Core/Src/main.c",
        "./Core/Src/MedianFilter.c",
    };

    //const c_compile_flags = [_][]const u8{ "-DUSE_HAL_DRIVER", "-DSTM32F446xx", "-std=gnu11", "-Wall", "-Werror", "-Wextra", "-pedantic", "-fstack-usage", "-fdata-sections", "-ffunction-sections" };
    const c_compile_flags = [_][]const u8{ "-DUSE_HAL_DRIVER", "-DSTM32F446xx", "-std=gnu11", "-Wall", "-Wextra", "-pedantic", "-fstack-usage", "-fdata-sections", "-ffunction-sections" };
    //const c_compile_flags = [_][]const u8{ "-DUSE_HAL_DRIVER", "-DSTM32F446xx", "-fstack-usage", "-fdata-sections", "-ffunction-sections" };

    // Standard optimization options allow the person running `zig build` to select
    // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not
    // set a preferred release mode, allowing the user to decide how to optimize.
    const optimize = b.standardOptimizeOption(.{});

    const elf = b.addExecutable(.{
        .name = prj_name ++ ".elf",
        .target = target,
        .optimize = optimize, // use optimization given by user
        //.strip = false, // do not strip debug symbols
        .linkage = .static, // static linking
        .link_libc = false, // will link against newlib_nano
        .single_threaded = true, // single core cpu
    });

    // Add necessary libraries and link to them
    elf.addIncludePath(.{ .path = "/usr/arm-none-eabi/include" });
    elf.addIncludePath(.{ .path = "/usr/arm-none-eabi/include/newlib-nano" });
    elf.addObjectFile(.{ .path = "/usr/arm-none-eabi/lib/thumb/v7e-m+fp/hard/libnosys.a" });
    elf.addObjectFile(.{ .path = "/usr/arm-none-eabi/lib/thumb/v7e-m+fp/hard/libc_nano.a" });
    elf.addObjectFile(.{ .path = "/usr/arm-none-eabi/lib/thumb/v7e-m+fp/hard/libm.a" });
    //elf.addObjectFile(.{ .path = "/usr/arm-none-eabi/lib/thumb/v7e-m+fp/hard/libgcc.a" });
    //elf.addObjectFile(.{ .path = "/usr/arm-none-eabi/lib/thumb/v7e-m+fp/hard/crti.o" });
    //elf.addObjectFile(.{ .path = "/usr/arm-none-eabi/lib/thumb/v7e-m+fp/hard/crtbegin.o" });

    // C runtime 0 contains _start function, initializes stack and libc
    // runtime, then calls _main
    // Cruntime 0: This object is expected to contain the _start symbol which
    // takes care of bootstrapping the initial execution of the program.
    // this object initializes very early ABI requirements
    // (like the stack or frame pointer), setting up the argc/argv/env values, and
    // then passing pointers to the init/fini/main funcs to the internal libc main
    // which in turn does more general bootstrapping before finally calling the real
    // main function.
    elf.addObjectFile(.{ .path = "/usr/arm-none-eabi/lib/thumb/v7e-m+fp/hard/crt0.o" });

    // crti (cruntime init) Defines the function prologs for the .init and
    // .fini sections (with the _init and _fini symbols respectively).  This
    // way they can be called directly. They contain constructors and destructors
    // for global structs
    elf.addObjectFile(.{ .path = "/usr/lib/gcc/arm-none-eabi/13.2.0/thumb/v7e-m+fp/hard/crti.o" });
    //  Defines the function epilogs for the .init/.fini sections
    //elf.addObjectFile(.{ .path = "/usr/lib/gcc/arm-none-eabi/13.2.0/thumb/v7e-m+fp/hard/crtn.o" });

    // GCC uses this to find the start of the constructors
    elf.addObjectFile(.{ .path = "/usr/lib/gcc/arm-none-eabi/13.2.0/thumb/v7e-m+fp/hard/crtbegin.o" });
    // GCC uses this to find the start of the destructors.
    //elf.addObjectFile(.{ .path = "/usr/lib/gcc/arm-none-eabi/13.2.0/thumb/v7e-m+fp/hard/crtend.o" });

    // Add C source files
    elf.addCSourceFiles(&c_sources_drivers, &c_sources_drivers_compile_flags);
    elf.addCSourceFiles(&c_sources_core, &c_compile_flags);
    elf.addCSourceFiles(&c_sources_app, &c_compile_flags);

    // Add Assembly sources
    for (asm_sources) |path| {
        elf.addAssemblyFile(.{ .path = path });
    }
    // Add C headers include dirs
    for (c_includes) |path| {
        elf.addIncludePath(.{ .path = path });
    }

    // Set Entry Point of the firmware
    elf.entry_symbol_name = "Reset_Handler";

    // Set linker script file
    elf.setLinkerScriptPath(.{ .path = "./STM32F446RETx_FLASH.ld" });
    elf.setVerboseLink(true);

    b.default_step.dependOn(&elf.step);
    b.installArtifact(elf);
}

Here my linking error result after zig build:

zig build-exe test.elf Debug thumb-freestanding-eabihf: error: ld.lld --error-limit=0 -O0 --entry Reset_Handler -z stack-size=16777216 -T /home/simone/Documents/test/stm32f446re/STM32F446RETx_FLASH.ld --gc-sections -znow -m armelf_linux_eabi -Bstatic -o /home/simone/Documents/test/stm32f446re/zig-cache/o/2926eb9bebe0b6086adb9408f974c605/test.elf /usr/arm-none-eabi/lib/thumb/v7e-m+fp/hard/libnosys.a /usr/arm-none-eabi/lib/thumb/v7e-m+fp/hard/libc_nano.a /usr/arm-none-eabi/lib/thumb/v7e-m+fp/hard/libm.a /usr/arm-none-eabi/lib/thumb/v7e-m+fp/hard/crt0.o /usr/lib/gcc/arm-none-eabi/13.2.0/thumb/v7e-m+fp/hard/crti.o /usr/lib/gcc/arm-none-eabi/13.2.0/thumb/v7e-m+fp/hard/crtbegin.o /home/simone/Documents/test/stm32f446re/zig-cache/o/99288ce3e59a0a256c3a994803be2af0/stm32f4xx_hal_tim.o /home/simone/Documents/test/stm32f446re/zig-cache/o/3392d5d156564ae16d92e6e1e61287dd/stm32f4xx_hal_tim_ex.o /home/simone/Documents/test/stm32f446re/zig-cache/o/f45f1e22a18dfeb915af7d502fe65480/stm32f4xx_hal_uart.o /home/simone/Documents/test/stm32f446re/zig-cache/o/dfc9ef3cafd45a08e1748e5daf7d89c1/stm32f4xx_hal_rcc.o /home/simone/Documents/test/stm32f446re/zig-cache/o/dd46e027c458622b9f55f8559f13606c/stm32f4xx_hal_rcc_ex.o /home/simone/Documents/test/stm32f446re/zig-cache/o/7ec7a26337c091e4b5b68b21fca20066/stm32f4xx_hal_flash.o /home/simone/Documents/test/stm32f446re/zig-cache/o/efd3655fa5f8371e61870ae4070e0039/stm32f4xx_hal_flash_ex.o /home/simone/Documents/test/stm32f446re/zig-cache/o/e575702641443b72ac5f71bcd076bb7e/stm32f4xx_hal_flash_ramfunc.o /home/simone/Documents/test/stm32f446re/zig-cache/o/8bd73bf66e49c02b4de785fa40745d3d/stm32f4xx_hal_gpio.o /home/simone/Documents/test/stm32f446re/zig-cache/o/320697216849f57288f06ee30b883927/stm32f4xx_hal_dma_ex.o /home/simone/Documents/test/stm32f446re/zig-cache/o/d8005e7182f54a18ef4f67f4611b352e/stm32f4xx_hal_dma.o /home/simone/Documents/test/stm32f446re/zig-cache/o/b47ab7db75a3fa4f15862b8bf641f383/stm32f4xx_hal_pwr.o /home/simone/Documents/test/stm32f446re/zig-cache/o/01e7e5906a3cfce5eb6706ad53b571b8/stm32f4xx_hal_pwr_ex.o /home/simone/Documents/test/stm32f446re/zig-cache/o/e5b65b2fa6b1256cab359bf320787ca0/stm32f4xx_hal_cortex.o /home/simone/Documents/test/stm32f446re/zig-cache/o/1696de14f92d2c979677719783df1848/stm32f4xx_hal.o /home/simone/Documents/test/stm32f446re/zig-cache/o/47c1c46543fadda6325e01461f19c71c/stm32f4xx_hal_exti.o /home/simone/Documents/test/stm32f446re/zig-cache/o/458200b72dc662e8df51c6bf2e572a28/system_stm32f4xx.o /home/simone/Documents/test/stm32f446re/zig-cache/o/8556142a4e8cb3a64acad0a21c600e20/stm32f4xx_it.o /home/simone/Documents/test/stm32f446re/zig-cache/o/88e82eefb964b9cc17f8339c485391dd/stm32f4xx_hal_msp.o /home/simone/Documents/test/stm32f446re/zig-cache/o/9e04bf4154200a9ad229a98adf1ef133/main.o /home/simone/Documents/test/stm32f446re/zig-cache/o/608588b52ad424e39fbe7b754bc2d13b/MedianFilter.o /home/simone/Documents/test/stm32f446re/zig-cache/o/34deb1aa406cbf5da07069ba8d251c04/startup_stm32f446xx.o /home/simone/.cache/zig/o/c0e0857fbf17c71bbc9eeea5e5debd97/libc.a --as-needed /home/simone/.cache/zig/o/5201866f2b3579a8a2d793a3128633bc/libcompiler_rt.a --allow-shlib-undefined

You can find the repo and other files here GitHub - simoneruffini/test
The linking of newlib is hardcoded because provided by the arm-none-eabi-newlib package in archlinux, but other linux distributions have the same package.
The project works with the provided makefile that I used as the base to create the build.zig. You can see the verbose output of the compilation with makefile in this file for understanding how and which libraries are linked: test/make_v.log at master · simoneruffini/test · GitHub
Thank you, I don’t know how to proceed more then this.

For whatever reason even with that error from ld.lld the binary output is still generated but its size is Huge with resepect to the gcc build one… I suspect the linker adds stuff I’m not using (for example _malloc_r: my codes doesn’t use mallocs anywhere), but I don’t know how to compare correctly the elf files since the symbols are saved in different memory addresses. How can I proceed?

Hi,
I’m starting to learn Zig (0.11.0) on a STM32L4. While reading the linker script generated by CubeMX (STM32L476RGTx_FLASH.ld), I found a symbol that is positioned in RAM:

  ._user_heap_stack :
  {
    . = ALIGN(8);
    PROVIDE ( end = . );
    PROVIDE ( _end = . );
    . = . + _Min_Heap_Size;
    . = . + _Min_Stack_Size;
    . = ALIGN(8);
  } >RAM 

Output : 385M zig-out/bin/output.bin
By placing it directly in VMA (Virtual Memory Address). The binary is okay and works on my board :

  ._user_heap_stack :
  {
    . = ALIGN(8);
    PROVIDE ( end = . );
    PROVIDE ( _end = . );
    . = . + _Min_Heap_Size;
    . = . + _Min_Stack_Size;
    . = ALIGN(8);
  } >RAM AT> FLASH

Output : 51K zig-out/bin/output.bin
I think GCC discard the symbol because it’s not used in the program, but not ld.lld

Despite the operation, “zig build” still shows an error with the linker:

zig build-exe stm32l4.elf Debug thumb-freestanding-eabihf: error: ld.lld --error-limit=0 -O0 --entry Reset_Handler -z stack-size=16777216 -T /zig_project/zig_stm32/src/STM32L476RGTx_FLASH.ld --gc-sections -znow -m armelf_linux_eabi -Bstatic -o /zig_project/zig_stm32/zig-cache/o/ce5dbe3f3e71910af06efddeb4d5c229/stm32l4.elf /opt/dev/xpack-arm-none-eabi-gcc-12.3.1-1.2/arm-none-eabi/lib/thumb/v7e-m+dp/hard/libc.a /zig_project/zig_stm32/zig-cache/o/74ee626e2a3d3d6977d2616366679352/startup_stm32l476xx.o /zig_project/zig_stm32/zig-cache/o/67d47904970becb84133bf8f0e14c39e/stm32l4xx_hal_tim.o /zig_project/zig_stm32/zig-cache/o/f41a9aee6939d4f97607a64a1425e77b/stm32l4xx_hal_tim_ex.o /zig_project/zig_stm32/zig-cache/o/6c7362fe52a5a51b0a4f3a0c92fced81/stm32l4xx_hal_uart.o /zig_project/zig_stm32/zig-cache/o/a8c77495c65c656a3674aa10f23524d2/stm32l4xx_hal_uart_ex.o /zig_project/zig_stm32/zig-cache/o/16f60863f7e49ada14a016382434d8bd/stm32l4xx_hal.o /zig_project/zig_stm32/zig-cache/o/3240cf16329199a3c4264a3beaa74793/stm32l4xx_hal_rcc.o /zig_project/zig_stm32/zig-cache/o/c869ed4ed81489cf5af7803400e73881/stm32l4xx_hal_rcc_ex.o /zig_project/zig_stm32/zig-cache/o/58ceab9c4c8000e51bc7d3b52cbf5345/stm32l4xx_hal_flash.o /zig_project/zig_stm32/zig-cache/o/70a79bcfafab7b3d01726435ded8a95f/stm32l4xx_hal_flash_ex.o /zig_project/zig_stm32/zig-cache/o/e51706e377113ea2c260b0c2c2a9b437/stm32l4xx_hal_flash_ramfunc.o /zig_project/zig_stm32/zig-cache/o/b5c9cf80e9409e9a21ab2a76afcc9c2c/stm32l4xx_hal_gpio.o /zig_project/zig_stm32/zig-cache/o/b550e66960d58fa3ba3881ee70722d87/stm32l4xx_hal_i2c.o /zig_project/zig_stm32/zig-cache/o/53b42b58944cdf5fe5e2b0c0d0f7f6ca/stm32l4xx_hal_i2c_ex.o /zig_project/zig_stm32/zig-cache/o/2747753cd7ebca5d413d7df0b89ef5cd/stm32l4xx_hal_dma.o /zig_project/zig_stm32/zig-cache/o/6c74762ece7a04b0461a291529ee0d54/stm32l4xx_hal_dma_ex.o /zig_project/zig_stm32/zig-cache/o/38c6fb854dd05593776630ddf9d36d55/stm32l4xx_hal_pwr.o /zig_project/zig_stm32/zig-cache/o/71541a52a6821544af4229d410b1e830/stm32l4xx_hal_pwr_ex.o /zig_project/zig_stm32/zig-cache/o/cc6d0421d04547108c9a26be763dfc84/stm32l4xx_hal_cortex.o /zig_project/zig_stm32/zig-cache/o/ffbf2bcf49f67b4595822b880a0f1fa7/stm32l4xx_hal_exti.o /zig_project/zig_stm32/zig-cache/o/43cdf702171eac3d16ef13ae4daa9e53/main.o /zig_project/zig_stm32/zig-cache/o/264f7080b869ad24492eb2277e00fb3c/gpio.o /zig_project/zig_stm32/zig-cache/o/730a963accbfa3e88a36ac8026593bc4/usart.o /zig_project/zig_stm32/zig-cache/o/da35801154f672a888699f8c8aa2db00/stm32l4xx_it.o /zig_project/zig_stm32/zig-cache/o/cc84d6817c68602ff5d73282517d19b9/stm32l4xx_hal_msp.o /zig_project/zig_stm32/zig-cache/o/75f89ba08afb6283d66ec952e0a89b29/system_stm32l4xx.o /zig_project/zig_stm32/zig-cache/o/ce5dbe3f3e71910af06efddeb4d5c229/stm32l4.elf.o ~/.cache/zig/o/c0e0857fbf17c71bbc9eeea5e5debd97/libc.a --as-needed ~/.cache/zig/o/5201866f2b3579a8a2d793a3128633bc/libcompiler_rt.a --allow-shlib-undefined

I still don’t understand why.

I think I have the same linker error.

Why do you put that section in VMA?

Why do you put that section in VMA?

From my understanding of linker script:

  • LMA or Load Memory Address. Specifies where the section and associated data shall be loaded into memory.
  • VMA or Virtual Memory Address. Specifies the virtual address for the section, which will be used during runtime by the program to access the symbols in it.

Source : tutorial link

We don’t want to place the “_user_heap_stack” symbol directly at the RAM address (0x20000000). The linker will directly put it in this position in the final binary
Using the VMA, it will be placed as a runtime instead. This doesn’t seem like the best solution

After digging:

Output command : arm-none-eabi-readelf -S
Zig build (lld):

[11] ._user_heap_stack PROGBITS        2000009c 02009c 000604 00  WA  0   0  1

Gcc build (ld):

[11] ._user_heap_stack NOBITS          200000b8 0040b8 000600 00  WA  0   0  1

The symbol is not of the same type depending on the linker. LLD and LD don’t behave the same way when it interprets the script. The `(NOLOAD)’ directive will mark a section to not be loaded at run time
The updated version of the section :

  ._user_heap_stack (NOLOAD):
  {
    . = ALIGN(8);
    PROVIDE ( end = . );
    PROVIDE ( _end = . );
    . = . + _Min_Heap_Size;
    . = . + _Min_Stack_Size;
    . = ALIGN(8);
  } >RAM

The section is used only to control the remaining memory. It will not be used in the final binary.

Solution: link