ziglang / zig

General-purpose programming language and toolchain for maintaining robust, optimal, and reusable software.
https://ziglang.org
MIT License
35.21k stars 2.57k forks source link

freestanding: zero-valued linker script variables when using PIE #22073

Open gballet opened 1 week ago

gballet commented 1 week ago

Zig Version

0.13.0

Steps to Reproduce and Observed Behavior

In a freestanding platform, I'm trying to set the global pointer to some memory location, which I can reuse later in code. This is the simplified version of my program, that reproduces the problem:

export fn _start() callconv(.Naked) void {
    asm volatile (
        \\.option push
        \\.option norelax
        \\la gp, __global_pointer
        \\la sp, __stack_start
        \\tail main
    );
}

pub export fn main() noreturn {
    while (true) {}
}

along with the linker script:

SECTIONS
{
  . = 0x10000100;
  .data : {
    *(.data)
  }
  .bss : { *(.bss) }

  .text : { *(.text) }

  __stack_start = 0x10000000;
  __global_pointer = .;
}

ASSERT(DEFINED(_start), "Error: _start is not defined.")
ENTRY(_start)

When compiling this in PIE mode, the value used in the relocation table is set to 0:

> zig build-exe -target riscv32-freestanding-none src/start.zig -fPIE -T linker.ld
> > riscv64-unknown-elf-objdump -s start | grep -F1 got
 100002e4 04000000 28010010 00000000 00000000  ....(...........
Contents of section .got:
 100002f4 84020010 00000000 00000010           ............ 

The 2nd value in the .got, which is the __global_pointer variable, is set to 0.

If I change the linker script to set __global_pointer to a constant value (e.g. 0x80000000), the variable is set:

linker script:

SECTIONS
{
  . = 0x10000100;
  .data : {
    *(.data)
  }
  .bss : { *(.bss) }

  .text : { *(.text) }

  __stack_start = 0x10000000;
  __global_pointer = 0x80000000;
}

ASSERT(DEFINED(_start), "Error: _start is not defined.")
ENTRY(_start)

executable content:

> zig build-exe -target riscv32-freestanding-none src/start.zig -fPIE -T linker.ld
> > riscv64-unknown-elf-objdump -s start | grep -F1 got
 100002dc 04000000 28010010 00000000 00000000  ....(...........
Contents of section .got:
 100002ec 7c020010 00000080 00000010           |........... 

Expected Behavior

The .got should contain the correct address for __global_pointer.

alexrp commented 4 days ago

I'm a bit confused at why you're even defining a GP symbol yourself. The linker will take care of that for you anyway. Also, the symbol is supposed to be named __global_pointer$ according to the RISC-V ABI. (You're also missing a .option pop directive in _start.)

Anyway, I don't see anything obviously wrong here?

❯ readelf --relocs test

Relocation section '.rela.dyn' at offset 0x113c contains 3 entries:
 Offset     Info    Type            Sym.Value  Sym. Name + Addend
10000350  00000003 R_RISCV_RELATIVE             100002f5
10000384  00000003 R_RISCV_RELATIVE             10000350
10000424  00000003 R_RISCV_RELATIVE             1000034e

The 3rd relocation applies to the GOT slot that points to __global_pointer. As expected, it fixes up the slot to point to the end of the .text section, which is where you defined __global_pointer to be.

I suspect you're looking at the wrong thing. Your la pseudo-instructions are likely the problem:

❯ objdump --disassemble=_start test

test:     file format elf32-littleriscv

Disassembly of section .text:

10000328 <_start>:
10000328:       00000197                auipc   gp,0x0
1000032c:       0fc1a183                lw      gp,252(gp) # 10000424 <_DYNAMIC+0x74>

10000330 <.Lpcrel_hi1>:
10000330:       00000117                auipc   sp,0x0
10000334:       0f812103                lw      sp,248(sp) # 10000428 <_DYNAMIC+0x78>
10000338:       00000317                auipc   t1,0x0
1000033c:       00a30067                jr      10(t1) # 10000342 <main>
        ...

This is obviously not going to work; gp is garbage at this point, so accessing the GOT through it is nonsensical. Bit of a chicken and egg situation.

Fixing it up to be lla:

❯ objdump --disassemble=_start test

test:     file format elf32-littleriscv

Disassembly of section .text:

10000320 <_start>:
10000320:       00000197                auipc   gp,0x0
10000324:       02618193                add     gp,gp,38 # 10000346 <__global_pointer>

10000328 <.Lpcrel_hi1>:
10000328:       00000117                auipc   sp,0x0
1000032c:       cd810113                add     sp,sp,-808 # 10000000 <__stack_start>
10000330:       00000317                auipc   t1,0x0
10000334:       00a30067                jr      10(t1) # 1000033a <main>
        ...

That looks a lot more sensible!

gballet commented 3 days ago

That's a lot of good points: indeed the symbol name is wrong, and I shouldn't be setting gp. That unblocks me, so thank you very much for that.

This being said, I think there is something wrong (at least, that I can't explain): if I keep the la (and therefore use the .got section), but use a0 instead of gp, the corresponding value is still 0. Sure, as you correctly pointed out, this won't work anyway because of the circular register use, Still, it seems to me that the correct value should be found in the .got - and not 0.

Interestingly, in spite of this error, the relocation dump resolves it correctly (it's not using .got, I assume):

Relocation section '.rela.dyn' at offset 0x20bc contains 34 entries:
 Offset     Info    Type            Sym.Value  Sym. Name + Addend
...
10000314  00000003 R_RISCV_RELATIVE             10001000

I'm just leaving this issue open in case someone wants to investigate/add some input. As far as I'm concerned, the matter is settled, though, so feel free to close this issue if this is not deemed interesting.

alexrp commented 3 days ago

This being said, I think there is something wrong (at least, that I can't explain): if I keep the la (and therefore use the .got section), but use a0 instead of gp, the corresponding value is still 0.

You mean la a0, __global_pointer? That does actually "work":

10000328 <_start>:
10000328:       00000517                auipc   a0,0x0
1000032c:       0fc52503                lw      a0,252(a0) # 10000424 <_DYNAMIC+0x74>

I put "work" in quotes here because of course it only works if the relocation on the GOT slot has actually been applied. Which means that:

This is obviously not going to work; gp is garbage at this point, so accessing the GOT through it is nonsensical. Bit of a chicken and egg situation.

I was actually wrong here. It is a chicken and egg situation, but not for the reason I wrote. la gp, __global_pointer would work fine too if the relocation was applied.

Anyway, in a freestanding PIE executable, it's your responsibility to make sure that relocations are applied one way or another. Here's how we do it in Zig for statically-linked executables w/o libc:

https://github.com/ziglang/zig/blob/8594f179f9d70fbc3bf39111c4e1f147d4a6dc3c/lib/std/start.zig#L483-L488 https://github.com/ziglang/zig/blob/8594f179f9d70fbc3bf39111c4e1f147d4a6dc3c/lib/std/os/linux/pie.zig#L202-L304

So you might do it that way or have your executable loader do it, or something else entirely.

alexrp commented 3 days ago

That's a lot of good points: indeed the symbol name is wrong, and I shouldn't be setting gp. That unblocks me, so thank you very much for that.

You probably do want to be setting gp unless some other code is doing it for you. The linker only takes care of creating the __global_pointer$ symbol for you; it doesn't actually initialize the gp register. Here's what we do in Zig:

https://github.com/ziglang/zig/blob/8594f179f9d70fbc3bf39111c4e1f147d4a6dc3c/lib/std/start.zig#L233-L241