Skip to content

Compute the load bias with dl_iterate_phdr, not base - offset - #293

Closed
emeryberger wants to merge 1 commit into
masterfrom
fix-load-bias
Closed

Compute the load bias with dl_iterate_phdr, not base - offset#293
emeryberger wants to merge 1 commit into
masterfrom
fix-load-bias

Conversation

@emeryberger

Copy link
Copy Markdown
Member

get_loaded_files() parsed /proc/self/maps and took base - offset of each executable mapping as that 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, so the bug hid. It does not for rustc's output:

binary exec PT_LOAD vaddr - offset
benchmarks/toy (g++) off=0x1000 vaddr=0x1000 0 ✓
rust/examples/toy off=0x14f80 vaddr=0x15f80 0x1000

So 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:

samples matched hottest line reported
before 39049 / 43506 (89.8%) rust/examples/toy.rs:5 — the thread::spawn call
after 43492 / 43513 (99.95%) rust/src/lib.rs:164 — the atomic increment

Line 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_phdr reports dlpi_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 empty dlpi_name, so it's resolved via /proc/self/exe. Objects without an executable PT_LOAD can'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)

  • Rust toy: 89.8% → 99.95% of samples matched, attribution moves to the line that actually runs.
  • benchmarks/toy: 107488 / 107525 matched, toy.cpp:18 slope 0.504 — unchanged.
  • macOS is untouched; it has its own get_loaded_files() built on dyld.

This should also turn the Rust bindings (ubuntu-latest) job in #286 green.

🤖 Generated with Claude Code

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>
@emeryberger

Copy link
Copy Markdown
Member Author

Superseded: this change landed on master via #286 (commit 4e53882, cherry-picked onto rust-support-verify). The branch content is now byte-identical to master.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant