Exported symbols removed unexpectedly in release mode

zig version: 0.14.0-dev.2435+7575f2121
target: riscv32

linkscript:

    . = ALIGN(4);
    _octopus_init_begin = .;
    KEEP(*(.octopus.init));
    _octopus_init_end = .;
    . = ALIGN(4);

code

fn MakeSymbols(comptime name: []const u8, comptime initfn: *const fn () callconv(.C) void) void {
    const foo = octopus.initm.OctopusInitElem{ .name = @ptrCast(name), .init = initfn };
    @export(&foo, .{
        .name = name,
        .section = ".octopus.init",
    });
}

comptime {
    MakeSymbols("A", _board_init);
    MakeSymbols("B", _board_init);
    MakeSymbols("C", _board_init);
}

When build in Debug mode, it works fine, which can be seen from the ‘readelf-a.out’:

 45451: 8004b6a0     0 NOTYPE  GLOBAL DEFAULT    2 _octopus_init_begin
 45452: 8004b6b8     0 NOTYPE  GLOBAL DEFAULT    2 _octopus_init_end
 45453: 8004b6a0     8 OBJECT  GLOBAL DEFAULT    2 A
 45454: 8004b6a8     8 OBJECT  GLOBAL DEFAULT    2 B
 45455: 8004b6b0     8 OBJECT  GLOBAL DEFAULT    2 C

But if try to build in release mode (ReleaseSafe), there are no symbols:

 10419: 80005ecc     0 NOTYPE  GLOBAL DEFAULT    2 _octopus_init_begin
 10420: 80005ecc     0 NOTYPE  GLOBAL DEFAULT    2 _octopus_init_end
 10421: 80000178    32 FUNC    WEAK   DEFAULT    2 memset

Why and how to solve ?

My guess would be because of this issue. You can try disabling LTO in your build script, or you can reference the symbols and force the compiler to include them as I’ve done in some of my bare metal code:

asm volatile("" :: [a] "s" (&stage2_data), [b] "s" (&vector_table));
4 Likes

You are right. After disabling LTO (by exe.want_lto = false;) symbols kept. Thanks!

I’m getting the same issue with want_lto disabled in zig 0.14.1. In my linker script I have:

    .text : {
        __logical_binary_start = .;
        KEEP (*(.vtable))
        KEEP (*(._reset_handler))

		*(.text*)
		*(.rodata*)
    } > FLASH

And in main.zig

export fn _reset_handler() linksection(".text") callconv(.c) noreturn {
    asm volatile (""
        :
        : [a] "s" (&vtable),
    );
    const led = hal.gpio.init(25, .output);
    led.put(true);
    var counter: u32 = 0;
    while (true) : (counter += 1) {
        if (counter < 75_000_000) {
            led.toggle();
            counter = 0;
        }
    }
}

export const vtable: u32 = 0xdeadbeef;

Inspecting the symbol table with objdump shows no sign of vtable in release builds even though it’s there in debug builds. When I read the emitted binary I can see no sign of the value of vtable, even though it should be at the start (Actually, I’m compiling for the RPI Pico, so the first 256 bytes are reserved for stage2 boot, but the vtable should be the first address after that). Interestingly, the _reset_handler is placed where the vtable should be, as if KEEP (*(.vtable)) wasn’t there (I know it’s the reset handler both because of objdump and because I have spent way too much time looking at the emitted binary, so I’m now familiar with it).

In the emitted assembly I can see this:

vtable:
	.long	3735928559
	.size	vtable, 4

	.section	".note.GNU-stack","",%progbits
	.eabi_attribute	30, 4

But that seems to get lost by the time it gets to linking, because if I Assert(vtable) in the linker script it tells me that it doesn’t exist.

Not sure if this is helpful, because I am struggling to find documentation or good examples, but I was also just trying to get a magic number as first bytes on my PinePhone binary. I had the same issues with exported symbols being removed on ReleaseSmall. The only work-around I found so far was to use LONG(symbol); in my .ld file. Let me know if you find a better solution please.

Welcome to Ziggit! You’ll still need to set the linker section of the vtable declaration.

const vtable: u32 linksection(".vtable") = 0xdeadbeef;

I’m also using size based asserts to catch this issue at link time:

.boot : {
    section_stage2_start = .;
    KEEP(*(.boot.stage2))
    ASSERT((. - section_stage2_start) == 0x100, "Detected invalid stage 2 bootloader")
    section_vtor_start = .;
    KEEP(*(.boot.vtor))
    ASSERT((. - section_vtor_start) == 0xA8, "Detected invalid vector table")
    KEEP(*(.boot))
} > flash

Thank you, that was it. I thought I had already tried it, but I must have used the wrong section, I can now see the value in the place I needed it.