Description
TLDR: when profiling CGo programs compiled with zig cc
, we are missing the profiles of pure C functions as observed with go tool pprof
.
I work with the C++ toolchain at Uber and we recently switched to using zig cc
to compile our CGo programs. I have not yet heard complains internally of
missing CGo profiles, but I suspect it's a matter of time. :)
Also, this seems to be the last suite of tests that fail when running CC="zig cc" ./all.bash
on Linux amd64.
Go version: master (8d68b38)
Zig version: 0.10.1 and master (ziglang/zig@8853005)
Steps to reproduce
Compile testprogcgo
with zig cc
and clang-15
:
$ cd src/runtime/testdata/testprogcgo
$ CC="/code/zig-linux-x86_64-0.10.1/zig cc" ../../../../bin/go build -o testprogcgo-zig-0.10.1
$ CC=clang-15 ../../../../bin/go build -o testprogcgo-clang-15
Run both resulting binaries:
$ ./testprogcgo-zig-0.10.1 CgoPprof
/tmp/prof1336230719
$ ./testprogcgo-clang-15 CgoPprof
/tmp/prof2489818717
Compare the profiles:
$ go tool pprof -traces ./testprogcgo-clang-15 /tmp/prof2489818717
File: testprogcgo-clang-15
Build ID: 57e147c5c3713dc48115b4856c5c9a568154a3cc
Type: cpu
Time: Apr 6, 2023 at 2:58pm (EEST)
Duration: 1.10s, Total samples = 1s (90.71%)
-----------+-------------------------------------------------------
990ms cpuHog
cpuHog2
runtime.cgocall
main._Cfunc_cpuHog
main.CgoPprof
main.main
runtime.main
-----------+-------------------------------------------------------
10ms runtime.futex
runtime.futexwakeup
runtime.notewakeup
runtime.exitsyscallfast_pidle
runtime.exitsyscallfast.func1
runtime.systemstack
runtime.exitsyscallfast
runtime.exitsyscall
runtime.cgocall
main._Cfunc_cpuHog
main.CgoPprof
main.main
runtime.main
-----------+-------------------------------------------------------
$ go tool pprof -traces ./testprogcgo-zig-0.10.1 /tmp/prof1336230719
File: testprogcgo-zig-0.10.1
Type: cpu
Time: Apr 6, 2023 at 2:56pm (EEST)
Duration: 1.10s, Total samples = 1s (90.70%)
-----------+-------------------------------------------------------
990ms pprofCgoTraceback
pprofCgoTraceback
runtime.cgocallbackg
main._Cfunc_foo2
main.CgoPprofCallback
main._Cfunc_CheckM
runtime.forcegchelper
-----------+-------------------------------------------------------
10ms runtime.malg
runtime.exitsyscall0
runtime.unspillArgs
runtime.exitsyscallfast_pidle
syscall.runtime_BeforeFork
runtime.cgocallbackg1
main._Cfunc_foo2
main.CgoPprofCallback
main._Cfunc_CheckM
runtime.forcegchelper
-----------+-------------------------------------------------------
Note that cpuHog
and cpuHog2
are not present in the profile of the program that was compiled with zig cc
. This is what the unit test TestCgoPprof
asserts.
Background / steps taken
I have observed and compared the resulting compiler and linker flags from both builds did not observe anything standing out.
I would appreciate some help debugging this. I am somewhat comfortable looking at disassembly and the low-level compiler flags, but, in this case, I don't know where to look.
Activity
motiejus commentedon Apr 6, 2023
Attaching both binaries for quick reference (this is x86_64 ELF, mind you)
testprogcgo-zig-0.10.1.pptx
testprogcgo-clang-15.pptx
[-]missing CGo profiles with CC="zig cc"[/-][+]missing profile info for CGo code when compiled with with CC="zig cc"[/+][-]missing profile info for CGo code when compiled with with CC="zig cc"[/-][+]missing profile info for CGo code when compiled with CC="zig cc"[/+][-]missing profile info for CGo code when compiled with CC="zig cc"[/-][+]runtime/pprof: missing profile info for CGo code when compiled with CC="zig cc"[/+]mknyszek commentedon Apr 6, 2023
CC @golang/runtime
cherrymui commentedon Apr 6, 2023
It looks like the profile you got from zig is running
testprogcgo CgoPprofCallback
whereas the clang one is fromtestprogcgo CgoPprof
? (The command line you pasted looks like both are runningCgoPprof
, but the second profile hasmain.CgoPprofCallback
)prattmic commentedon Apr 6, 2023
The second profile looks like it has symbolization wrong. e.g., this doesn't make any sense:
Are you sure that the binary passed to pprof matches the binary used for the profile? In fact, you don't need to pass the binary to pprof at all, as the profile contains the symbol information. Do you get the same results if you drop the binary from the pprof cli?
motiejus commentedon Apr 6, 2023
I may have messed something up. A fresh honest copy-paste now, single command, no previous state:
zig
clang-15
testprogcgo-zig-0.10.1.pptx
testprogcgo-clang-15.pptx
@prattmic
I get an interactive pprof shell:
cherrymui commentedon Apr 6, 2023
In the new one the call stack also doesn't make much sense. Maybe the symbolization is completely broken. And you probably run
CgoPprof
correctly but as the symbolization is wrong it showsmain.CgoPprofCallback
instead...Do you know how zig's linker is implemented? Does it wrap a C linker, or it has its own implementation? Is there anything special?
(I sent CL https://golang.org/cl/482975 to fix the C compiler warning.)
motiejus commentedon Apr 6, 2023
On linux it wraps clang for compiling and ld.lld for linking, adds a few flags.
One can see the exact lld invocations with
ZIG_VERBOSE_LINK=1 zig cc <...>
. I can paste the output here, but there is a lot of linking going on, so tell me which files to narrow it down to.cc @andrewrk
Thanks!
cherrymui commentedon Apr 6, 2023
Hmmm, the symbolization definitely is wrong. I ran the zig binary you pasted above. I got a profile, which contains a sample like
But that PC is actually corresponding to
main.CgoPprof
:which looks correct.
So it seems the symbolization collected at run time by the profiler is wrong.
I wonder if you use zig to link a cgo program that just panics, does it print the right panic stack trace?
The zig linking is definitely somewhat unusual. For a quick look, it puts the
text
section after therodata
section, instead of before like other linkers usually do (this probably doesn't matter, I just noted this is something unusual I saw). I'll see if there is anything differs that could matter.cherrymui commentedon Apr 6, 2023
Well, this is interesting: with the profile I have, I got the similar incorrect symbolization
Rename the binary, so the pprof command cannot find it from the memory mapping, then we get the correct symbolization! (It cannot symbolize C functions because it cannot find the binary's symbol table, but at least the Go functions are correct.)
So the mapping and the binary actually confused the pprof command. (The symbol information collected at run time is actually fine.)
Looking at the binary, this is the
readelf -l
output:Note that the 4th segment,
It has alignment 0x1000, so the address and offset should be aligned to 0x1000. But it doesn't. This leads to this interesting memory mapping at run time: (from GDB)
The second one is the text segment. The start address of the segment in the binary is 0x337220, but the mapping starts at 0x337000 due to misalignment. I think this confused the pprof command, causing a shift of 0x220 in address. In fact, if we shift the PCs by 0x220, we get the wrong symbolization:
The second one is the first one +0x220. The profile incorrectly shows
main.CgoPprofCallback /code/go/src/runtime/testdata/testprogcgo/pprof_callback.go:64
, which matches the shifted address.gopherbot commentedon Apr 7, 2023
Change https://go.dev/cl/483035 mentions this issue:
cmd/internal/objfile: align the load address of ELF binary
motiejus commentedon Apr 7, 2023
Wow, thanks for the investigation! I was able to replicate the zig's issue with clang+lld too:
I can confirm your fix fixed the original issue, with both
zig cc
andclang-15+lld
. Yay!Now looking at the different linker:
This is what lld does. Here is a quick example:
cgo.go
lld
gnu ld
As you can observe above,
text
androdata
sections are flipped.I will create two follow-up issues soon:
cherrymui commentedon Apr 7, 2023
Thanks!
Filed #59482 for using LLD on builder. (@prattmic and I were thinking about this the other day)