Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rustc adds line-number information for unhittable panic handlers #55352

Open
bossmc opened this issue Oct 25, 2018 · 4 comments
Open

Rustc adds line-number information for unhittable panic handlers #55352

bossmc opened this issue Oct 25, 2018 · 4 comments
Assignees
Labels
A-debuginfo Area: Debugging information in compiled programs (DWARF, PDB, etc.) T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.

Comments

@bossmc
Copy link
Contributor

bossmc commented Oct 25, 2018

First off, root issue I was investigating was kcov producing bad coverage information for Rust binaries. I've worked out why kcov is producing the results it is and they're "correct" given what Rustc is doing. I'm not sure where the fix (if any) needs to be made, but I'm starting with Rustc as kcov's strategy looks sound.

Consider the following code:

struct Person {
  name: String,
  age: u32,
}

fn get_age() -> u32 {
  42
}

fn create_bob() -> Person {
  Person {
    name: "Bob".to_string(),
    age: get_age(),
  }  // Uncovered
}

fn main() {
  let b = create_bob();
}

Kcov will mark the "Uncovered" line as unhit. This is surprising, as that line was definitely passed during the execution of the program. Lines can be omitted from kcov coverage (with // KCOV_EXCL_LINE) but this leads to hundreds of those markers scattered around the code, potentially hiding real coverage lapses/lowering maintainability of the codebase.


Kcov determines coverage by looking at the .debug_lines section of the binary, which contains a mapping from address in the binary to line of code. It then sets a break point at every listed address before running the program. Each time a break point is hit, kcov marks the associated line of code as hit, and clears the breakpoint (as hitting it again tells us nothing, and breakpoints are slow). After the process completes, any lines remaining were not hit.

This means that kcov's "was this line hit" logic is pretty solid, assuming the .debug_lines section is accurate.


When Rust generates a block that calls any function after creating any binding to a type with a Drop implementation, it also generates a unwind-cleanup block to call that drop() in the event of the function panic-ing. In the above case, the generated code is something like (assuming Rust had exceptions):

fn create_bob() -> Person {
  let name = "Bob".to_string();
  try {
    let age = get_age();
  } catch e {
    drop(name);
    throw e;
  }
  Person {
    name,
    age,
  }
}

This makes sense, if there is a panic (which might get handled up the stack somewhere) we should delete bindings that are memory-safe (yay Rust!) to free up memory that's not going to be accessible any more.


Unfortunately:

  • The cleanup code is associated with the "end of block" marker (the } line marked above) so unless get_age panics, this code won't be hit.
  • The marked line is not associated with any other generated machine code, so nothing else will cause kcov to consider it hit
  • get_age never panics so this cleanup code can't be hit (without changing the get_age() function at least)

In a release build, the cleanup handler is stripped because LLVM notices that there's no way for anything in the try block to panic, so the handler is not needed. In a debug build, it doesn't do this optimisation and leaves genuinely unhittable code in the binary, but associates it with a line of code, causing many false positives in kcov's output (especially as kcov uses debug builds to prevent dead-code elimination removing untested code).


What to do differently? I'm not sure, but here are some ideas:

  • Associate the cleanup of a binding with the line that created the binding, rather than the end of the block. Might be confusing if you are actually debugging a panic cleanup.
  • Don't associate the cleanup code with any lines of code. Definitely unhelpful in panic debugging.
  • Enhance Kcov to ignore cleanup landing pads. Isn't really fair, as in a language like C++ exceptions are fairly common and cleanup is important. Also, landing pads are language-specific (see eh_personality and friends) so it's tricky to get this right.
  • Somehow perform minimal dead-code elimination even in debug builds (or test builds more accurately) that only removes unnecessary cleanup code. Not sure how plausible this is, nor how likely it is to create false positives.
  • Something else...?
@tromey tromey added the A-debuginfo Area: Debugging information in compiled programs (DWARF, PDB, etc.) label Oct 25, 2018
@bossmc
Copy link
Contributor Author

bossmc commented Oct 25, 2018

Simple repro in https://github.com/bossmc/kcov-rust-issue.

Reproduce with cargo kcov (from https://github.com/kennytm/cargo-kcov). Note that the closing brace is marked as uncovered unless the second test is enabled.

@pnkfelix
Copy link
Member

This still reproduces.

I want to dig in with a little private investigation to understand the scenarios where the cleanup path is marked as unreached, and to know how one should be expected to avoid such marking in cases like this (e.g. should we require a #[no_panic] attribute on the function signature of fn get_age to signal that tools can assume it doesn't panic?)

I also want to understand the scenarios where people want to know about code paths that were silently injected by the compiler. I do believe there are cases where people want to know about that, but I also think the majority of programmers are trying to avoid actively thinking about cleanup paths, and for the most part we want to encourage that...

@bossmc
Copy link
Contributor Author

bossmc commented Sep 16, 2022

There are two "markings" at play here:

  1. The compiler marking addresses with debug information (mainly intended for consumption by gdb/lldb/etc.
  2. KCov marking lines of source code as hit/unhit

Which are you asking about? Are you exploring if Rustc can detect the unhittable landing pads and not emit debug information for them? Or if kcov can tell if a given debug info line corresponds to a landing pad and therefore not report it?

@pnkfelix
Copy link
Member

pnkfelix commented Sep 19, 2022

I'm asking about the expected/desired experience for an end-developer. I expect some people are going to want to test the control-flow paths that correspond to panics for certain library routines. But I also expect some set of people to treat some panic paths as something they do not attempt to test (because, for example, they are truly unreachable -- as is the case in the situation outlined in this issue).

What is the right way to serve both groups? Is it to ask people to add certain attributes to their source code (and then rustc incorporates those attributes into the metadata that it generates for kcov)? Or should we expect the source code in both of these scenarios to remain unchanged?

(To be clear: @bossmc 's original description included bullet points that explore this question with ideas of paths to follow. I just want to start from a higher level: To ask what the ideal experience should be in the two scenarios I outlined, and then try to work backwards from there.)

@Nilstrieb Nilstrieb added the T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. label Apr 5, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-debuginfo Area: Debugging information in compiled programs (DWARF, PDB, etc.) T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.
Projects
None yet
Development

No branches or pull requests

4 participants