Skip to content

rustc -Clink-dead-code causes MSVC linker to produce invalid binaries when LLVM InstrProf counters are enabled #76038

Open
@richkadel

Description

@richkadel

This reports a bug with the -C link-dead-code option is enabled, when targeting MSVC and when LLVM InstrProf counter reports are enabled. The bug is probably not specific to InstrProf, but InstrProf sets the conditions for exposing the bug.

I hope someone with better familiarity with the -C link-dead-code option, and the MSVC linker options it enables and/or interacts with, can help determine the root cause of the issue, and provide a fix or better workaround that allows InstrProf and -Z instrument-coverage to work with -C link-dead-code on MSVC.

Brief summary of error: When running a program compiled with -Z instrument-coverage and -C link-dead-code, the program should generate an InstrProf counter report upon program exit, but instead it generates a zero-length file and almost always crashes with a Segmentation fault.

Background: The InstrProf counter report (a file generated at program exit, called default.profraw unless overridden by setting LLVM_PROFILE_FILE) is generated by the LLVM compiler-rt source that is compiled into a rust library by rust-lang's rust/library/profiler-builtins/.

The -Z instrument-coverage flag injects LLVM intrinsics that increment global counter variables during program execution, and the profiler-builtins library sets up atexit() callbacks that gather the final counter values and write them to the default.profraw file.

I've documented my tests and observations, in case they are helpful to someone with more knowledge in this area.

I compared working InstrProf-enabled binaries in a C program compiled by Clang to the failing Rust-based binaries, stepping through the counter report generation code of both programs using Visual Studio debugger, the debugger's disassembled view, and by inspecting the instructions and call graphs in IDA Pro for both.

I observed that the Rust binary had some notable (and clearly incorrect) differences, such as:

  • Some jmp instructions appear to be jumping to the wrong offset (4 bytes off)
  • Some instructions and variables (such as instructions initializing fields of a Header structure for the counter report) were simply not present in the Rust binary. They are present in the C binary, and map to sequential assignments in the compiler-rt source, but they are not shown in the disassembled views. The debugger skips over the source for these statements, and the variables involved cannot be evaluated. The debugger complains that certain variables were "optimized out".

My best guess is the MSVC linker is first miscalculating the jmp addresses for some functions, and then, when the miscalculated addresses don't line up to known functions, an optimization pass may be deciding that some variables are not used (because their functions don't appear to be called), so code that updates those variables is optimized out.

Note that PR #76002 overcomes what was originally thought to be a bug related to the -Zinstrument-coverage implementation. Prior to PR #76002, the -Zinstrument-coverage option always automatically enabled -Clink-dead-code by default, to ensure dead code was still counted, as zero executions, in coverage reports. PR #76002 works around the problem by only enabling -Clink-dead-code by default for non-MSVC platforms.

Since PR #76002, the bug can still be reproduced on MSVC by enabling -Clink-dead-code explicitly (as shown in the Code section below).

Code

fn testfunc() {                                                                 
}                                                                               
                                                                                
fn main() {                                                                     
    let mut i = 0;                                                              
    loop {                                                                      
        if i >= 10 {                                                            
            break;                                                              
        }                                                                       
        testfunc();                                                             
        i += 1;                                                                 
    }                                                                           
}    

Meta

rustc --version --verbose:

binary: rustc                                                                   
commit-hash: unknown                                                            
commit-date: unknown                                                            
host: x86_64-pc-windows-msvc                                                    
release: 1.47.0-dev                                                             
LLVM version: 11.0 

Error output

The following example explicitly enables or disables -Clink-dead-code. The default value was to enable this setting, prior to PR #76002, and as of this PR the default is now to not enable this setting.

From an msys2 terminal:

$ build/x86_64-pc-windows-msvc/stage1/bin/rustc -Zinstrument-coverage -Clink-dead-code=no basic.rs
$ ./basic
Hello world
$ ls -l default.profraw
-rw-r--r-- 1 richkadel Domain Users 168 Aug 28 10:43 default.profraw

$ build/x86_64-pc-windows-msvc/stage1/bin/rustc -Zinstrument-coverage -Clink-dead-code basic.rs
$ ./basic
Hello world
Segmentation fault

and also generates an empty default.profraw file.

Metadata

Metadata

Assignees

No one assigned

    Labels

    -Clink-dead-codeLinkage option: -Clink-dead-codeC-bugCategory: This is a bug.I-ICEIssue: The compiler panicked, giving an Internal Compilation Error (ICE) ❄️O-windows-msvcToolchain: MSVC, Operating system: WindowsT-compilerRelevant to the compiler team, which will review and decide on the PR/issue.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions