zig icon indicating copy to clipboard operation
zig copied to clipboard

Stage2 ARM Thumb Debug: LLD fails with "undefined symbol: __clzsi2"

Open bcrist opened this issue 2 years ago • 1 comments

Zig Version

0.10.0

Steps to Reproduce and Observed Behavior

When compiling embedded firmware targeting ARM Cortex M0+, using the stage2 compiler, in debug mode, the linker fails with this message:

LLD Link... ld.lld: error: undefined symbol: __clzsi2
>>> referenced by compiler_rt
>>>               C:\...\zig\o\abc2490849d1b9cc45a64ab5470e2f9d\libcompiler_rt.a.o:(__udivsi3) in archive C:\...\zig\o\abc2490849d1b9cc45a64ab5470e2f9d\libcompiler_rt.a
>>> referenced by compiler_rt
>>>               C:\...\zig\o\abc2490849d1b9cc45a64ab5470e2f9d\libcompiler_rt.a.o:(__udivsi3) in archive C:\...\zig\o\abc2490849d1b9cc45a64ab5470e2f9d\libcompiler_rt.a
>>> referenced by compiler_rt
>>>               C:\...\zig\o\abc2490849d1b9cc45a64ab5470e2f9d\libcompiler_rt.a.o:(__umodsi3) in archive C:\...\zig\o\abc2490849d1b9cc45a64ab5470e2f9d\libcompiler_rt.a
>>> referenced 1 more times
>>> did you mean: __ctzsi2
>>> defined in: C:\...\zig\o\abc2490849d1b9cc45a64ab5470e2f9d\libcompiler_rt.a(C:\...\zig\o\abc2490849d1b9cc45a64ab5470e2f9d\libcompiler_rt.a.o)

This doesn't happen when using any of the -Drelease-* modes, or when using the stage1 compiler. The reason it doesn't happen in release modes may just be that all the integer division and modulo operations are optimized out. I'm not exactly sure where those are coming from, even in debug; the only division I can think of should be comptime only.

It looks like lib/compiler_rt/count0bits.zig does have an implementation of __clzsi2 specifically for Thumb targets, but somehow the linker doesn't see it.

The specific CrossTarget being used is:

std.zig.CrossTarget{
    .cpu_arch = .thumb,
    .cpu_model = .{ .explicit = &std.Target.arm.cpu.cortex_m0plus },
    .os_tag = .freestanding,
    .abi = .none,
},

It can be reproduced by running zig build after checking out this commit: https://github.com/bcrist/microbe/tree/0b7404760350ef3504d95eb1200d6900c8b2a374

Running zig build again after the failure doesn't print the error again, it just triggers a FileNotFound error, but I think that's caused by a different issue: https://github.com/ziglang/zig/issues/13432

Deleting the zig-cache directory and building again shows the linker error again.

Expected Behavior

The linker should succeed and generate a valid ELF binary.

bcrist avatar Nov 06 '22 17:11 bcrist

After a little further poking, I wondered if this might be related to lib/compiler_rt/count0bits.zig declaring const __clzsi2 = switch(...) ... where sometimes, it resolves to fn (i32) callconv(.C) i32, but other times to fn (i32) callconv(.Naked) i32. I copied the switch statement into the comptime block at the top and changed it to export the __clzsi2_thumb1 and __clzsi2_arm32 functions directly, when necessary:

    switch (builtin.cpu.arch) {
        .arm, .armeb, .thumb, .thumbeb => {
            const use_thumb1 =
                (builtin.cpu.arch.isThumb() or
                std.Target.arm.featureSetHas(builtin.cpu.features, .noarm)) and
                !std.Target.arm.featureSetHas(builtin.cpu.features, .thumb2);

            if (use_thumb1) {
                @export(__clzsi2_thumb1, .{ .name = "__clzsi2", .linkage = common.linkage });
            }
            // From here on we're either targeting Thumb2 or ARM.
            else if (!builtin.cpu.arch.isThumb()) {
                @export(__clzsi2_arm32, .{ .name = "__clzsi2", .linkage = common.linkage });
            }
            // Use the generic implementation otherwise.
            else {
                @export(clzsi2_generic, .{ .name = "__clzsi2", .linkage = common.linkage });
            }
        },
        else => @export(clzsi2_generic, .{ .name = "__clzsi2", .linkage = common.linkage }),
    }

Obviously, this isn't very clean, but I am able to successfully link a working executable in debug mode with this change. I found the new libcompiler_rt.a.o and threw it into ghidra to verify that it contains the thumb1 version of __clzsi2 that uses a LUT. The old version just contained an extern thunk.

I don't know exactly what this means in terms of the root cause of this bug, but hopefully this helps.

On a side note, does anyone know why CPUs marked with the .thumb2 feature are excluded from using __clzsi2_thumb1? My understanding was that Thumb2 was a superset of Thumb1 -- technically the STM32G0 chip I'm using is a ~~Thumb2 core,~~ Edit: apparently it's not Thumb2, just Thumb1 with "Thumb-2 Technology"

bcrist avatar Nov 06 '22 19:11 bcrist

Based on your follow up I think this is caused by #13465

Vexu avatar Dec 05 '22 15:12 Vexu

that's the same number as this one?

nektro avatar Dec 05 '22 15:12 nektro

Meant #13706

Vexu avatar Dec 05 '22 15:12 Vexu