Potential Bug in Zig Build Linker Call for Freestanding Embedded Target

I have a relatively simple build.zig file for a freestanding (bare metal) 32 bit ARM target here:

const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.resolveTargetQuery(.{
        .cpu_arch = .thumb,
        .os_tag = .freestanding,
        .abi = .eabihf,
        .cpu_model = std.zig.CrossTarget.CpuModel{ .explicit = &std.Target.arm.cpu.cortex_m7 },
        .cpu_features_add = std.Target.arm.featureSet(&[_]std.Target.arm.Feature{std.Target.arm.Feature.fp_armv8d16sp}),

    const arm_gcc_version = "13.2.1";
    const project_name = "blinky";

    b.verbose_cc = true;
    b.verbose_link = true;
    const optimize = b.standardOptimizeOption(.{});
    const blinky_exe = b.addExecutable(.{
        .name = project_name ++ ".elf",
        .target = target,
        .optimize = optimize,
        .link_libc = false,
        .linkage = .static,
        .single_threaded = true,

    // Manually including libraries bundled with arm-none-eabi-gcc
    const arm_gcc_path = b.option([]const u8, "armgcc", "Path to arm-none-eabi-gcc compiler") orelse unreachable;
    blinky_exe.addLibraryPath(.{ .path = b.fmt("{s}/arm-none-eabi/lib/thumb/v7e-m+fp/hard", .{arm_gcc_path}) });
    blinky_exe.addLibraryPath(.{ .path = b.fmt("{s}/lib/gcc/arm-none-eabi/" ++ arm_gcc_version ++ "/thumb/v7e-m+fp/hard", .{arm_gcc_path}) });
    blinky_exe.addSystemIncludePath(.{ .path = b.fmt("{s}/arm-none-eabi/include", .{arm_gcc_path}) });

    // Manually include C runtime objects bundled with arm-none-eabi-gcc
    blinky_exe.addObjectFile(.{ .path = b.fmt("{s}/arm-none-eabi/lib/thumb/v7e-m+fp/hard/crt0.o", .{arm_gcc_path}) });
    blinky_exe.addObjectFile(.{ .path = b.fmt("{s}/lib/gcc/arm-none-eabi/" ++ arm_gcc_version ++ "/thumb/v7e-m+fp/hard/crti.o", .{arm_gcc_path}) });
    blinky_exe.addObjectFile(.{ .path = b.fmt("{s}/lib/gcc/arm-none-eabi/" ++ arm_gcc_version ++ "/thumb/v7e-m+fp/hard/crtbegin.o", .{arm_gcc_path}) });
    blinky_exe.addObjectFile(.{ .path = b.fmt("{s}/lib/gcc/arm-none-eabi/" ++ arm_gcc_version ++ "/thumb/v7e-m+fp/hard/crtend.o", .{arm_gcc_path}) });
    blinky_exe.addObjectFile(.{ .path = b.fmt("{s}/lib/gcc/arm-none-eabi/" ++ arm_gcc_version ++ "/thumb/v7e-m+fp/hard/crtn.o", .{arm_gcc_path}) });

    // Startup file

    // Source files
        .files = &.{
        .flags = &.{ "-Og", "-std=c11", "-DUSE_HAL_DRIVER", "-DSTM32F750xx" },

    blinky_exe.link_gc_sections = true;
    blinky_exe.link_data_sections = true;
    blinky_exe.link_function_sections = true;
    blinky_exe.setLinkerScriptPath(.{ .path = "./STM32F750N8Hx_FLASH.ld" });


I was examining the linking calls to ld.lld for a different reason, but disconcertingly noticed that despite setting everything up correctly as a freestanding target (as far as I’m aware), zig build ends up passing the arg -m armelf_linux_eabi (“Set target emulation”) to its ld.lld call. I believe this is incorrect right? Shouldn’t it be something to the effect of -m armelf_freestanding_eabi given this is a freestanding target?

For what it’s worth, some test applications compiled with this “work” on chip, so this may be nothing but wanted some clarification.

Listing all emulations for my arm toolchain:

❯ arm-none-eabi-ld -V
GNU ld (2.40-2+18+b1) 2.40
  Supported emulations:

see: Linker emulation selection

Zig pass the -m flag as is to llvm, with the following values:
Zig Linker Emulations

Innnnteresting… For lack of a better way to describe it, is _linux_ a bit of a misnomer then with this flag? It appears any arm target using the thumb instruction set defaults to this linker emulation flag independent of whether or not this is going to be a freestanding target. I guess I’m just concerned that it appears the linker is using an emulation setting that assumes Linux as the OS.

It is my understanding that armelf_linux_eabi is the standard linker emulation for arm32. Yes, linux in it looks like a misnomer.

Cool, potential minor issue to file on the ziglang Github? It’s trivial, but I feel like armelf or armelf_eabi similar to the original arg to arm-none-eabi-gcc would be better.