Skip to content

Commit 60e5867

Browse files
committed
pkg/cover/backend: extract PC ranges from Rust DWARF
Rust compilation units are different from C in that a single compilation unit includes multiple source files, but we still need to tell which PC range belong to which source file. Infer that information from the LineEntry structures. Cc #6000.
1 parent 77908e5 commit 60e5867

File tree

1 file changed

+110
-25
lines changed

1 file changed

+110
-25
lines changed

pkg/cover/backend/dwarf.go

Lines changed: 110 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,36 @@ type pcFixFn = (func([2]uint64) ([2]uint64, bool))
351351
func readTextRanges(debugInfo *dwarf.Data, module *vminfo.KernelModule, pcFix pcFixFn) (
352352
[]pcRange, []*CompileUnit, error) {
353353
var ranges []pcRange
354-
var units []*CompileUnit
354+
unitMap := map[string]*CompileUnit{}
355+
addRange := func(r [2]uint64, unitName string) {
356+
if pcFix != nil {
357+
var filtered bool
358+
r, filtered = pcFix(r)
359+
if filtered {
360+
return
361+
}
362+
}
363+
// Rust compilation units have a special naming scheme.
364+
if strings.Contains(unitName, "/@/") {
365+
unitName, _, _ = strings.Cut(unitName, "/@/")
366+
}
367+
unit, ok := unitMap[unitName]
368+
if !ok {
369+
unit = &CompileUnit{
370+
ObjectUnit: ObjectUnit{
371+
Name: unitName,
372+
},
373+
Module: module,
374+
}
375+
unitMap[unitName] = unit
376+
}
377+
if module.Name == "" {
378+
ranges = append(ranges, pcRange{r[0], r[1], unit})
379+
} else {
380+
ranges = append(ranges, pcRange{r[0] + module.Addr, r[1] + module.Addr, unit})
381+
}
382+
}
383+
355384
for r := debugInfo.Reader(); ; {
356385
ent, err := r.Next()
357386
if err != nil {
@@ -363,41 +392,97 @@ func readTextRanges(debugInfo *dwarf.Data, module *vminfo.KernelModule, pcFix pc
363392
if ent.Tag != dwarf.TagCompileUnit {
364393
return nil, nil, fmt.Errorf("found unexpected tag %v on top level", ent.Tag)
365394
}
366-
attrName := ent.Val(dwarf.AttrName)
367-
if attrName == nil {
395+
attrName, ok := ent.Val(dwarf.AttrName).(string)
396+
if !ok {
368397
continue
369398
}
370-
unit := &CompileUnit{
371-
ObjectUnit: ObjectUnit{
372-
Name: attrName.(string),
373-
},
374-
Module: module,
375-
}
376-
units = append(units, unit)
377-
ranges1, err := debugInfo.Ranges(ent)
378-
if err != nil {
379-
return nil, nil, err
380-
}
399+
attrCompDir, _ := ent.Val(dwarf.AttrCompDir).(string)
400+
// Compile unit names are relative to the compilation dir, while per-line info isn't.
401+
// Let's stick to the common approach.
402+
unitName := attrCompDir + attrName
381403

382-
var filtered bool
383-
for _, r := range ranges1 {
384-
if pcFix != nil {
385-
r, filtered = pcFix(r)
386-
if filtered {
387-
continue
388-
}
404+
const languageRust = 28
405+
406+
language := ent.Val(dwarf.AttrLanguage)
407+
if language != nil && language.(int64) == languageRust {
408+
rawRanges, err := rustRanges(debugInfo, ent)
409+
if err != nil {
410+
return nil, nil, fmt.Errorf("failed to query Rust PC ranges: %w", err)
389411
}
390-
if module.Name == "" {
391-
ranges = append(ranges, pcRange{r[0], r[1], unit})
392-
} else {
393-
ranges = append(ranges, pcRange{r[0] + module.Addr, r[1] + module.Addr, unit})
412+
for _, r := range rawRanges {
413+
addRange([2]uint64{r.start, r.end}, r.file)
414+
}
415+
} else {
416+
ranges1, err := debugInfo.Ranges(ent)
417+
if err != nil {
418+
return nil, nil, err
419+
}
420+
for _, r := range ranges1 {
421+
addRange(r, unitName)
394422
}
395423
}
396424
r.SkipChildren()
397425
}
426+
var units []*CompileUnit
427+
for _, unit := range unitMap {
428+
units = append(units, unit)
429+
}
398430
return ranges, units, nil
399431
}
400432

433+
type rustRange struct {
434+
start uint64
435+
end uint64
436+
file string
437+
}
438+
439+
func rustRanges(debugInfo *dwarf.Data, ent *dwarf.Entry) ([]rustRange, error) {
440+
// For Rust, a single compilation unit may comprise all .rs files that belong to the crate.
441+
// To properly render the coverage, we need to somehow infer the ranges that belong to
442+
// those individual .rs files.
443+
// For simplicity, let's create fake ranges by looking at the DWARF line information.
444+
var ret []rustRange
445+
lr, err := debugInfo.LineReader(ent)
446+
if err != nil {
447+
return nil, fmt.Errorf("failed to query line reader: %w", err)
448+
}
449+
var startPC uint64
450+
var files []string
451+
for {
452+
var entry dwarf.LineEntry
453+
if err = lr.Next(&entry); err != nil {
454+
if err == io.EOF {
455+
break
456+
}
457+
return nil, fmt.Errorf("failed to parse next line entry: %w", err)
458+
}
459+
if startPC == 0 || entry.Address != startPC {
460+
for _, file := range files {
461+
ret = append(ret, rustRange{
462+
start: startPC,
463+
end: entry.Address - 1,
464+
file: file,
465+
})
466+
}
467+
files = files[:0]
468+
startPC = entry.Address
469+
}
470+
// Keep on collecting file names that are covered by the range.
471+
files = append(files, entry.File.Name)
472+
}
473+
if startPC != 0 {
474+
// We don't know the end PC for these, but let's still add them to the ranges.
475+
for _, file := range files {
476+
ret = append(ret, rustRange{
477+
start: startPC,
478+
end: startPC,
479+
file: file,
480+
})
481+
}
482+
}
483+
return ret, nil
484+
}
485+
401486
func symbolizeModule(target *targets.Target, interner *symbolizer.Interner, kernelDirs *mgrconfig.KernelDirs,
402487
splitBuildDelimiters []string, mod *vminfo.KernelModule, pcs []uint64) ([]*Frame, error) {
403488
procs := min(runtime.GOMAXPROCS(0)/2, len(pcs)/1000)

0 commit comments

Comments
 (0)