zig

fork of https://codeberg.org/ziglang/zig
Log | Files | Refs | README | LICENSE

commit c841f52fcbb5b5505c9702934db6364b1177ee7f (tree)
parent bcc2fb66f377745747b2394a01ec0bb3acee9341
Author: Motiejus Jakštys <motiejus@uber.com>
Date:   Fri, 16 Dec 2022 09:29:09 +0200

Elf: switch link order of libcompiler_rt and libc

Given `main.go`:

    package main
    import  _ "os/user"
    func main() {}

Compiling it to linux/arm64:

    $ CGO_CFLAGS='-O0' GOOS=linux GOARCH=arm64 CGO_ENABLED=1 CC="zig cc -target aarch64-linux-gnu.2.28" go build main.go

Results in this error:

    runtime/cgo(.text): unknown symbol memset in callarm64
    runtime/cgo(.text): unknown symbol memset in callarm64
    runtime/cgo(.text): relocation target memset not defined

In the midst of intermediate compilations files we can see this commmand:

    ld.lld -o _cgo_.o <...> /tmp/go-build206961058/b043/_x009.o <...> ~/.cache/zig/.../libcompiler_rt.a <...> ~/.cache/.../libc.so.6

`_x009.o` needs memset:

    $ readelf -Ws ./b043/_x009.o | grep memset
        22: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND memset

Both `libcompiler_rt.a` and `libc.so.6` provide it:

    $ readelf -Ws ~/.cache/zig/.../libcompiler_rt.a | grep memset
       870: 0000000000000000   318 FUNC    WEAK   DEFAULT  519 memset

    $ readelf -Ws ~/.cache/zig/.../libc.so.6 | grep -w memset
       476: 000000000001d34c     0 FUNC    GLOBAL DEFAULT    7 memset@@GLIBC_2.2.5

Since `libcompiler_rt.a` comes before libc in the linker line, the
resulting `_cgo_.o` still links to a weak, unversioned memset:

    $ readelf -Ws ./b043/_cgo_.o | grep -w memset
        40: 000000000022c07c   160 FUNC    WEAK   DEFAULT   14 memset
       719: 000000000022c07c   160 FUNC    WEAK   DEFAULT   14 memset

Since the final linking step is done by Golang's linker, it does not
know of `libcompiler_rt.a`, and fails to link with the error message
above. However, Go linker does recognize memset from glibc. If we
specify an `-lc` equivalent before the `libcompiler_rt.a`, it will link
to memset from libc:

    $ readelf -Wa ./b043/_x009.o |grep memset
        14: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND memset@GLIBC_2.17 (2)
       157: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND memset@GLIBC_2.17

... and then `main.go` will compile+link successfully.

Why doesn't Go linker take memset from glibc? An educated guess: Go
determines whether to link with glibc from what the program asks (I
presume `.dynsym`). Since `memset` is no longer attributed to glibc, Go
skips linking to glibc altogether.

Bonus question: curious why `-O0` is necessary? Because when
optimizations are enabled (the default), the C compiler replaces
`memset` function call with plain `stp` instructions (on aarch64).

Diffstat:
Msrc/link/Elf.zig | 12+++++++-----
1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/link/Elf.zig b/src/link/Elf.zig @@ -1698,11 +1698,6 @@ fn linkWithLLD(self: *Elf, comp: *Compilation, prog_node: *std.Progress.Node) !v try argv.append(ssp.full_object_path); } - // compiler-rt - if (compiler_rt_path) |p| { - try argv.append(p); - } - // Shared libraries. if (is_exe_or_dyn_lib) { const system_libs = self.base.options.system_libs.keys(); @@ -1781,6 +1776,13 @@ fn linkWithLLD(self: *Elf, comp: *Compilation, prog_node: *std.Progress.Node) !v } } + // compiler-rt. Since compiler_rt exports symbols like `memset`, it needs + // to be after the shared libraries, so they are picked up from the shared + // libraries, not libcompiler_rt. + if (compiler_rt_path) |p| { + try argv.append(p); + } + // crt postlude if (csu.crtend) |v| try argv.append(v); if (csu.crtn) |v| try argv.append(v);