[CodeView] LF_FUNC_ID deduplication causes incorrect source locations in inlinees
- Dominant language
- LLVM
- Stars
- 40.5k
- Forks
- 18.7k
- PR merge metrics
- PR metrics pending
Description
Consider this IR:
```llvm
; repro.ll
target datalayout = "e-m:w-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
target triple = "x86_64-unknown-windows-gnu"
declare void @print_my_address()
define void @repro() !dbg !5 {
Entry:
call void @print_my_address(), !dbg !7
call void @print_my_address(), !dbg !9
ret void
}
!llvm.dbg.cu = !{!2}
!llvm.module.flags = !{!0, !1}
!0 = !{i32 2, !"Debug Info Version", i32 3}
!1 = !{i32 2, !"CodeView", i32 1}
!2 = distinct !DICompileUnit(language: DW_LANG_C99, file: !3, producer: "zig 0.16.0", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug, enums: !{}, globals: !{}, splitDebugInlining: false)
!3 = !DIFile(filename: "a.zig", directory: "/home/mlugg/test")
!4 = !DIFile(filename: "b.zig", directory: "/home/mlugg/test")
!5 = distinct !DISubprogram(name: "repro", linkageName: "repro", scope: !3, file: !3, line: 1, type: !DISubroutineType(types: null), scopeLine: 1, flags: DIFlagStaticMember, spFlags: DISPFlagDefinition, unit: !2)
!6 = distinct !DISubprogram(name: "foo", linkageName: "a.foo", scope: !3, file: !3, line: 7, type: !DISubroutineType(types: null), scopeLine: 7, flags: DIFlagStaticMember, spFlags: DISPFlagDefinition, unit: !2)
!7 = !DILocation(line: 8, column: 21, scope: !6, inlinedAt: !DILocation(line: 2, column: 8, scope: !5))
!8 = distinct !DISubprogram(name: "foo", linkageName: "b.foo", scope: !4, file: !4, line: 2, type: !DISubroutineType(types: null), scopeLine: 2, flags: DIFlagStaticMember, spFlags: DISPFlagDefinition, unit: !2)
!9 = !DILocation(line: 3, column: 21, scope: !8, inlinedAt: !DILocation(line: 3, column: 25, scope: !5))
```
Note that the `!DILocation` annotations indicate that that these calls to `print_my_address` were inlined from two distinct functions, both of which have human-readable name `foo` but have distinct linkage names.
Compile it to an object file and dump its CodeView information:
```sh-session
$ llvm22/bin/llc --filetype=obj repro.ll -o repro.obj
$ llvm22/bin/llvm-readobj --codeview repro.obj
(irrelevant output omitted...)
FuncId (0x1002) {
TypeLeafKind: LF_FUNC_ID (0x1601)
ParentScope: 0x0
FunctionType: void () (0x1001)
Name: foo
}
(irrelevant output omitted...)
Subsection [
SubSectionType: InlineeLines (0xF6)
SubSectionSize: 0x1C
InlineeSourceLine {
Inlinee: foo (0x1002)
FileID: /home/mlugg/test/a.zig (0x0)
SourceLineNum: 7
}
InlineeSourceLine {
Inlinee: foo (0x1002)
FileID: /home/mlugg/test/b.zig (0x8)
SourceLineNum: 2
}
]
(irrelevant output omitted...)
```
Notice that in the `InlineeLines` subsection, both inlinees are identified by the same `LF_FUNC_ID`, with name `foo` and index 0x1002. This is invalid, because the `Inlinee` field is used as a unique identifier later. With this file, the `S_INLINEES`/`S_INLINESITE` associated with a function cannot know which of these `InlineeSourceLine` entries to consider, resulting in incorrect source locations.
To see the incorrect source locations, use this C source file:
```c
// main.c
#include
void print_my_address(void) {
const void *const ret_addr = __builtin_return_address(0);
// -1 to put us inside the "call" instruction
const void *const call_addr = (const char *)ret_addr - 1;
printf("caller address: %p\n", call_addr);
}
extern void repro(void);
int main(void) {
repro();
return 0;
}
```
Compile with Clang targeting Windows, and run the resulting binary (Wine is fine):
```sh-session
$ llvm22/bin/clang-cl -fuse-ld=lld -target x86_64-windows /vctoolsdir [...] /winsdkdir [...] repro.ll main.c -o main.exe -g
warning: overriding the module target triple with x86_64-unknown-windows-msvc19.33.0 [-Woverride-module]
1 warning generated.
$ wine main.exe
caller address: 0000000140001008
caller address: 000000014000100D
```
Finally, use `llvm-symbolizer` to look at those two addresses:
```sh-session
$ llvm22/bin/llvm-symbolizer --obj=main.exe -p 0x140001008 0x14000100D
foo at /home/mlugg/test/a.zig:8:0
(inlined by) repro at /home/mlugg/test/a.zig:2:8
foo at /home/mlugg/test/a.zig:8:0
(inlined by) repro at /home/mlugg/test/a.zig:3:25
```
As expected, the invalid CodeView information leads to the inlined locations being lost, so both addresses use a source location in `a.zig`, whereas the correct behavior would be for the latter to be in `b.zig`.
This can be a serious problem for languages with a "single compilation unit" model (or where compilation units are large). We first discovered this in Zig, and @MasonRemaley believes he has experienced a similar issue working with Rust.
---
While most common in languages with multi-file compilation units, is is also possible for C code to have distinct `!DISubprogram` nodes with identical `name` fields, by using LTO. Therefore, here is a reproduction which uses solely Clang:
```c
// a.c
extern void print_my_address(void);
__attribute__((always_inline)) static void foo(void) {
print_my_address();
}
void entry_a(void) {
foo();
}
```
```c
// b.c
extern void print_my_address(void);
__attribute__((always_inline)) static void foo(void) {
print_my_address();
}
void entry_b(void) {
foo();
}
```
```c
// main.c
#include
extern void entry_a(void);
extern void entry_b(void);
void print_my_address(void) {
const void *const ret_addr = __builtin_return_address(0);
// -1 to put us inside the "call" instruction
const void *const call_addr = (const char *)ret_addr - 1;
printf("caller address: %p\n", call_addr);
}
int main(void) {
entry_a();
entry_b();
return 0;
}
```
```sh-session
$ llvm22/bin/clang-cl -fuse-ld=lld -target x86_64-windows /vctoolsdir [...] /winsdkdir [...] a.c b.c main.c -o main.exe -g -flto
$ wine main.exe
caller address: 0000000140001008
caller address: 0000000140001018
$ llvm22/bin/llvm-symbolizer --obj=main.exe -p 0x140001008 0x140001018
foo at /home/mlugg/test/a.c:4:0
(inlined by) entry_a at /home/mlugg/test/a.c:7:0
foo at /home/mlugg/test/a.c:4:0
(inlined by) entry_b at /home/mlugg/test/b.c:7:0
```
Observe that both source locations are listed as `a.c`. (If `-flto` is not passed, the second address instead correctly maps to `b.c`.)
---
The deduplication of these CodeView records seems to happen in `llvm::codeview::GlobalTypeTableBuilder`, which creates an `llvm::codeview::GloballyHashedType` for the `LF_FUNC_ID` record and deduplicates it based on that:
https://github.com/llvm/llvm-project/blob/75ca0f71d209058d1f565d7529e6d1b117129911/llvm/lib/DebugInfo/CodeView/GlobalTypeTableBuilder.cpp#L86-L95
(with the `GlobalTypeTableBuilder` coming from the `TypeTable` field on `llvm::CodeViewDebug`.)
In isolation this deduplication seems fine (and makes obvious sense for actual types), but because `LF_FUNC_ID` is used as a unique "key" in the `InlineeLines` subsection, it may be important for otherwise-identical records of this type to remain distinct.
This issue was hit previously in the context of C++ lambda functions (https://github.com/llvm/llvm-project/issues/47776). In that instance, the issue was fixed by having Clang emit some extra type information to prevent the `LF_FUNC_ID` records from being identical.
However, I think this this should instead be considered an LLVM bug: it is perfectly valid for two functions in an LLVM module to have the same *human-readable* name and the same type, such as in the C LTO repro given above. Instead, I suspect that there should be special handling when emitting an inlined call to avoid `LF_FUNC_ID` record deduplication in that case.
Contributor guide
Research direction
Start in llvm/lib/DebugInfo/CodeView/GlobalTypeTableBuilder.cpp, especially the GlobalTypeTableBuilder handling of LF_FUNC_ID records, and trace how CodeViewDebug's TypeTable is used for inlinee emission. Reproduce the issue with the provided repro.ll and llvm-readobj or llvm-symbolizer commands, then add or update a regression test covering distinct inlinees with identical names and types. Done means the two inlinees retain distinct identifiers and symbolize to their respective source files.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- c, cpp
- Domain
- compilers
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 50/100