-
Notifications
You must be signed in to change notification settings - Fork 244
/
dwarf.go
226 lines (202 loc) · 7.36 KB
/
dwarf.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
package wasmdebug
import (
"debug/dwarf"
"errors"
"fmt"
"io"
"sort"
"strings"
"sync"
)
// DWARFLines is used to retrieve source code line information from the DWARF data.
type DWARFLines struct {
// d is created by DWARF custom sections.
d *dwarf.Data
// linesPerEntry maps dwarf.Offset for dwarf.Entry to the list of lines contained by the entry.
// The value is sorted in the increasing order by the address.
linesPerEntry map[dwarf.Offset][]line
mux sync.Mutex
}
type line struct {
addr uint64
pos dwarf.LineReaderPos
}
// NewDWARFLines returns DWARFLines for the given *dwarf.Data.
func NewDWARFLines(d *dwarf.Data) *DWARFLines {
if d == nil {
return nil
}
return &DWARFLines{d: d, linesPerEntry: map[dwarf.Offset][]line{}}
}
// isTombstoneAddr returns true if the given address is invalid a.k.a tombstone address which was made no longer valid
// by linker. According to the DWARF spec[1], the value is encoded as 0xffffffff for Wasm (as 32-bit target),
// but some tools encode it either in -1, -2 [2] or 1<<32 (This might not be by tools, but by debug/dwarf package's bug).
//
// [1] https://dwarfstd.org/issues/200609.1.html
// [2] https://github.com/WebAssembly/binaryen/blob/97178d08d4a20d2a5e3a6be813fc6a7079ef86e1/src/wasm/wasm-debug.cpp#L651-L660
// [3] https://reviews.llvm.org/D81784
func isTombstoneAddr(addr uint64) bool {
addr32 := int32(addr)
return addr32 == -1 || addr32 == -2 ||
addr32 == 0 // This covers 1 <<32.
}
// Line returns the line information for the given instructionOffset which is an offset in
// the code section of the original Wasm binary. Returns empty string if the info is not found.
func (d *DWARFLines) Line(instructionOffset uint64) (ret []string) {
if d == nil {
return
}
// DWARFLines is created per Wasm binary, so there's a possibility that multiple instances
// created from a same binary face runtime error at the same time, and that results in
// concurrent access to this function.
d.mux.Lock()
defer d.mux.Unlock()
r := d.d.Reader()
var inlinedRoutines []*dwarf.Entry
var cu *dwarf.Entry
var inlinedDone bool
entry:
for {
ent, err := r.Next()
if err != nil || ent == nil {
break
}
// If we already found the compilation unit and relevant inlined routines, we can stop searching entries.
if cu != nil && inlinedDone {
break
}
switch ent.Tag {
case dwarf.TagCompileUnit, dwarf.TagInlinedSubroutine:
default:
// Only CompileUnit and InlinedSubroutines are relevant.
continue
}
// Check if the entry spans the range which contains the target instruction.
ranges, err := d.d.Ranges(ent)
if err != nil {
continue
}
for _, pcs := range ranges {
start, end := pcs[0], pcs[1]
if isTombstoneAddr(start) || isTombstoneAddr(end) {
continue
}
if start <= instructionOffset && instructionOffset < end {
switch ent.Tag {
case dwarf.TagCompileUnit:
cu = ent
case dwarf.TagInlinedSubroutine:
inlinedRoutines = append(inlinedRoutines, ent)
// Search inlined subroutines until all the children.
inlinedDone = !ent.Children
// Not that "children" in the DWARF spec is defined as the next entry to this entry.
// See "2.3 Relationship of Debugging Information Entries" in https://dwarfstd.org/doc/DWARF4.pdf
}
continue entry
}
}
}
// If the relevant compilation unit is not found, nothing we can do with this DWARF info.
if cu == nil {
return
}
lineReader, err := d.d.LineReader(cu)
if err != nil || lineReader == nil {
return
}
var lines []line
var ok bool
var le dwarf.LineEntry
// Get the lines inside the entry.
if lines, ok = d.linesPerEntry[cu.Offset]; !ok {
// If not found, we create the list of lines by reading all the LineEntries in the Entry.
//
// Note that the dwarf.LineEntry.SeekPC API shouldn't be used because the Go's dwarf package assumes that
// all the line entries in an Entry are sorted in increasing order which *might not* be true
// for some languages. Such order requirement is not a part of DWARF specification,
// and in fact Zig language tends to emit interleaved line information.
//
// Thus, here we read all line entries here, and sort them in the increasing order wrt addresses.
for {
pos := lineReader.Tell()
err = lineReader.Next(&le)
if errors.Is(err, io.EOF) {
break
} else if err != nil {
return
}
// TODO: Maybe we should ignore tombstone addresses by using isTombstoneAddr,
// but not sure if that would be an issue in practice.
lines = append(lines, line{addr: le.Address, pos: pos})
}
sort.Slice(lines, func(i, j int) bool { return lines[i].addr < lines[j].addr })
d.linesPerEntry[cu.Offset] = lines // Caches for the future inquiries for the same Entry.
}
// Now we have the lines for this entry. We can find the corresponding source line for instructionOffset
// via binary search on the list.
n := len(lines)
index := sort.Search(n, func(i int) bool { return lines[i].addr >= instructionOffset })
if index == n { // This case the address is not found. See the doc sort.Search.
return
}
ln := lines[index]
if ln.addr != instructionOffset {
// If the address doesn't match exactly, the previous entry is the one that contains the instruction.
// That can happen anytime as the DWARF spec allows it, and other tools can handle it in this way conventionally
// https://github.com/gimli-rs/addr2line/blob/3a2dbaf84551a06a429f26e9c96071bb409b371f/src/lib.rs#L236-L242
// https://github.com/kateinoigakukun/wasminspect/blob/f29f052f1b03104da9f702508ac0c1bbc3530ae4/crates/debugger/src/dwarf/mod.rs#L453-L459
if index-1 < 0 {
return
}
ln = lines[index-1]
}
// Advance the line reader for the found position.
lineReader.Seek(ln.pos)
err = lineReader.Next(&le)
if err != nil {
// If we reach this block, that means there's a bug in the []line creation logic above.
panic("BUG: stored dwarf.LineReaderPos is invalid")
}
// In the inlined case, the line info is the innermost inlined function call.
inlined := len(inlinedRoutines) != 0
prefix := fmt.Sprintf("%#x: ", instructionOffset)
ret = append(ret, formatLine(prefix, le.File.Name, int64(le.Line), int64(le.Column), inlined))
if inlined {
prefix = strings.Repeat(" ", len(prefix))
files := lineReader.Files()
// inlinedRoutines contain the inlined call information in the reverse order (children is higher than parent),
// so we traverse the reverse order and emit the inlined calls.
for i := len(inlinedRoutines) - 1; i >= 0; i-- {
inlined := inlinedRoutines[i]
fileIndex, ok := inlined.Val(dwarf.AttrCallFile).(int64)
if !ok {
return
} else if fileIndex >= int64(len(files)) {
// This in theory shouldn't happen according to the spec, but guard against ill-formed DWARF info.
return
}
fileName := files[fileIndex]
line, _ := inlined.Val(dwarf.AttrCallLine).(int64)
col, _ := inlined.Val(dwarf.AttrCallColumn).(int64)
ret = append(ret, formatLine(prefix, fileName.Name, line, col,
// Last one is the origin of the inlined function calls.
i != 0))
}
}
return
}
func formatLine(prefix, fileName string, line, col int64, inlined bool) string {
builder := strings.Builder{}
builder.WriteString(prefix)
builder.WriteString(fileName)
if line != 0 {
builder.WriteString(fmt.Sprintf(":%d", line))
if col != 0 {
builder.WriteString(fmt.Sprintf(":%d", col))
}
}
if inlined {
builder.WriteString(" (inlined)")
}
return builder.String()
}