Compute the load bias with dl_iterate_phdr, not base - offset - #293
Closed
emeryberger wants to merge 1 commit into
Closed
Compute the load bias with dl_iterate_phdr, not base - offset#293emeryberger wants to merge 1 commit into
emeryberger wants to merge 1 commit into
Conversation
get_loaded_files() parsed /proc/self/maps and took `base - offset` of each
executable mapping as the object's load bias. That is only the bias when the
executable PT_LOAD segment satisfies p_vaddr == p_offset.
It usually does for a simple C program -- benchmarks/toy has off=0x1000 against
vaddr=0x1000 -- so the bug stayed hidden. rustc's output does not: the Rust toy
has off=0x14f80 against vaddr=0x15f80, and the computed bias came out 0x1000 too
large. Every line range was then shifted by a page.
The consequence is worse than a crash, because nothing looks wrong. Most samples
still land inside *some* line, so coz reports a confident, plausible profile
attributed to the wrong source lines. Measured on x86_64 with the Rust toy:
samples matched hottest line reported
before 39049 / 43506 (89.8%) rust/examples/toy.rs:5 (thread::spawn)
after 43492 / 43513 (99.95%) rust/src/lib.rs:164 (the atomic add)
Line 5 is where the toy spawns its threads; it burns no time there. Line 164 is
the counter increment the toy actually runs 4.4e9 times.
On some layouts the shift lands outside every range and nothing matches at all,
which is what CI hit: the Rust toy produced 10242 perf samples and matched zero
of them, while the C toy in the same job matched 23300 of 23300.
dl_iterate_phdr reports dlpi_addr, which *is* the bias, for the main executable
and every shared object. Use it. The main executable comes back with an empty
dlpi_name, so resolve it through /proc/self/exe; objects with no executable
PT_LOAD cannot contain a sampled PC and are skipped, as are synthetic ones like
the vDSO that have no file to read DWARF from.
benchmarks/toy is unchanged: 107488/107525 samples matched, toy.cpp:18 at slope
0.504. macOS is untouched -- it has its own get_loaded_files() using dyld.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Member
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
get_loaded_files()parsed/proc/self/mapsand tookbase - offsetof each executable mapping as that object's load bias. That is only the bias when the executablePT_LOADsegment satisfiesp_vaddr == p_offset.It usually does for a simple C program, so the bug hid. It does not for rustc's output:
PT_LOADvaddr - offsetbenchmarks/toy(g++)off=0x1000 vaddr=0x1000rust/examples/toyoff=0x14f80 vaddr=0x15f80So the computed bias was a page too large, and every line range was shifted by 4 KB.
Why this is worse than a crash
Nothing looks wrong. Most samples still land inside some line, so coz produces a confident, plausible profile — attributed to the wrong source lines. Measured on x86_64 with the Rust toy:
rust/examples/toy.rs:5— thethread::spawncallrust/src/lib.rs:164— the atomic incrementLine 5 is where the toy spawns its threads and burns no time. Line 164 is the counter increment it runs 4.4e9 times. The old answer was not noise; it was a specific, wrong, believable answer.
On some layouts the shift lands outside every range and nothing matches. That is what CI was hitting: the Rust toy produced 10242 perf samples and matched zero, while the C toy in the same job matched 23300 / 23300. I chased that from an empty profile →
sigprof=1023, samples=10242, matched=0→ the program headers.Fix
dl_iterate_phdrreportsdlpi_addr, which is the bias, for the main executable and every shared object — no header arithmetic, no maps parsing. The main executable comes back with an emptydlpi_name, so it's resolved via/proc/self/exe. Objects without an executablePT_LOADcan't contain a sampled PC and are skipped, as are synthetic ones (vDSO) with no file to read DWARF from.Verification (x86_64 Ubuntu, real hardware)
benchmarks/toy: 107488 / 107525 matched,toy.cpp:18slope 0.504 — unchanged.get_loaded_files()built on dyld.This should also turn the
Rust bindings (ubuntu-latest)job in #286 green.🤖 Generated with Claude Code