-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathixdiff.go
More file actions
259 lines (233 loc) · 6.97 KB
/
Copy pathixdiff.go
File metadata and controls
259 lines (233 loc) · 6.97 KB
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
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
// Package ixdiff compares the assembly of two compiled binaries.
//
// It loads ELF, Mach-O, PE, wasm, and Go compile-archive files, pairs
// their functions by name (detecting likely renames), and classifies
// every pair: byte-identical, changed, differing only by relocation,
// added, or removed. Changed pairs carry a normalized instruction-level
// edit script whose labels and moved-address operands are stable across
// layout shifts, so the diff shows real code generation changes rather
// than linker noise.
//
// Typical use:
//
// old, err := ixdiff.Open("app.v1")
// new, err := ixdiff.Open("app.v2")
// diff, err := ixdiff.Compare(old, new, nil)
// for _, p := range diff.Pairs() {
// if p.State == ixdiff.Changed {
// lines, err := diff.Lines(p)
// ...
// }
// }
package ixdiff
import (
"cmp"
"slices"
"sync"
"github.com/loov/ixdiff/internal/disasm"
"github.com/loov/ixdiff/internal/objfile"
)
// Binary is a loaded binary, backed by a read-only memory mapping of
// the file. Close releases the mapping; Func.Code slices become
// invalid afterwards.
type Binary struct {
path string
obj *objfile.Binary
// once guards the lazily built function views below.
once sync.Once
funcs []*Func
byName map[string]*Func
lookup disasm.SymLookup
}
// Open maps the binary at path and parses it. The file format is
// detected from its magic bytes; ELF, Mach-O, PE, wasm, and Go compile
// archives are recognized.
func Open(path string) (*Binary, error) {
obj, err := objfile.Open(path)
if err != nil {
return nil, err
}
return &Binary{path: path, obj: obj}, nil
}
// Close releases the binary's file mapping.
func (b *Binary) Close() error { return b.obj.Close() }
// Path returns the path the binary was opened from.
func (b *Binary) Path() string { return b.path }
// Arch returns the Go name of the binary's instruction set, such as
// "amd64" or "arm64".
func (b *Binary) Arch() string { return b.obj.Arch.String() }
// init builds the function views and the symbol lookup once.
func (b *Binary) init() {
b.once.Do(func() {
b.byName = make(map[string]*Func, len(b.obj.Funcs))
b.funcs = make([]*Func, 0, len(b.obj.Funcs))
for name, fn := range b.obj.Funcs {
f := &Func{
Name: name,
Package: pkgOf(name),
Addr: fn.Addr,
Size: int64(fn.Size),
obj: fn,
bin: b,
}
b.byName[name] = f
b.funcs = append(b.funcs, f)
}
// Ties broken by name so aliased symbols at the same address
// order deterministically.
slices.SortFunc(b.funcs, func(x, y *Func) int {
if x.Addr != y.Addr {
return cmp.Compare(x.Addr, y.Addr)
}
return cmp.Compare(x.Name, y.Name)
})
b.lookup = disasm.Lookup(b.obj)
})
}
// symLookup returns the memoized symbol lookup for decoding.
func (b *Binary) symLookup() disasm.SymLookup {
b.init()
return b.lookup
}
// Funcs returns every function of the binary in address order. The
// returned slice is shared and must not be modified.
func (b *Binary) Funcs() []*Func {
b.init()
return b.funcs
}
// Func returns the function with the exact symbol name.
func (b *Binary) Func(name string) (*Func, bool) {
b.init()
f, ok := b.byName[name]
return f, ok
}
// TextBytes returns the total size of all functions in bytes.
func (b *Binary) TextBytes() int64 {
var total int64
for _, f := range b.Funcs() {
total += f.Size
}
return total
}
// Padding is the space between functions, split by gap size: Align
// holds gaps of at most 64 bytes (ordinary function alignment) and
// Large the rest, which usually indicates non-symbol code or data
// interleaved with the text section.
type Padding struct {
Align, Large int64
}
// Padding sums the gaps between the address-sorted functions of the
// binary. Go compile archives have no memory layout, so their padding
// is zero.
func (b *Binary) Padding() Padding {
var p Padding
if b.obj.NoLayout {
return p
}
end := uint64(0)
first := true
for _, f := range b.Funcs() {
if !first && f.Addr > end {
if gap := int64(f.Addr - end); gap <= 64 {
p.Align += gap
} else {
p.Large += gap
}
}
first = false
if e := f.Addr + uint64(f.Size); e > end {
end = e
}
}
return p
}
// Func is a single function inside a binary.
type Func struct {
Name string
Package string // the package part of Name, e.g. "net/url"
Addr uint64 // virtual address; wasm: function index; archives: file offset
Size int64 // size of the function body in bytes
obj *objfile.Func
bin *Binary
}
// Code returns the machine code of the function. The returned slice
// aliases the binary's file mapping and must not be modified.
func (f *Func) Code() []byte { return f.obj.Code() }
// Inst is a single decoded instruction.
type Inst struct {
Addr uint64 // virtual address of the instruction
Text string // Go-syntax rendering, e.g. "CALL runtime.mallocgc(SB)"
}
// Text disassembles the function, resolving call and data targets to
// symbol names.
func (f *Func) Text() ([]Inst, error) {
insts, err := f.decode()
if err != nil {
return nil, err
}
out := make([]Inst, len(insts))
for i, in := range insts {
out[i] = Inst{Addr: in.Addr, Text: in.Text}
}
return out, nil
}
// Ops disassembles the function and counts its instruction mnemonics.
// BYTE pseudo-instructions (padding and undecodable bytes) are
// excluded: they are not code.
func (f *Func) Ops() (OpCount, error) {
insts, err := f.decode()
if err != nil {
return nil, err
}
return countOps(ops(insts)), nil
}
// Spills disassembles the function and counts the registers its stack
// accesses move: register spills and reloads, but also stack-passed
// call arguments and register saves, which use the same addressing.
// Accesses made through a scratch register holding a stack address
// are included. See countSpills.
func (f *Func) Spills() (int, error) {
insts, err := f.decode()
if err != nil {
return 0, err
}
spills, _ := countSpills(f.bin.obj.Arch, insts)
return spills, nil
}
// StackSlots disassembles the function and counts the 8-byte stack
// slots its stack accesses touch. Unlike [Func.Spills] it measures
// memory traffic, so pair/vector/scalar lowering conversions with the
// same traffic count the same. See countSpills.
func (f *Func) StackSlots() (int, error) {
insts, err := f.decode()
if err != nil {
return 0, err
}
_, slots := countSpills(f.bin.obj.Arch, insts)
return slots, nil
}
// decode disassembles the function with the binary's memoized lookup.
func (f *Func) decode() ([]disasm.Inst, error) {
return disasm.Decode(f.bin.obj.Arch, f.Code(), f.Addr, f.bin.symLookup())
}
// ops extracts the mnemonics of insts, skipping BYTE pseudo-
// instructions: padding is not code and would pollute the statistics.
func ops(insts []disasm.Inst) []string {
out := make([]string, 0, len(insts))
for _, in := range insts {
if in.Op != "BYTE" {
out = append(out, in.Op)
}
}
return out
}
// countInsts counts real instructions, excluding BYTE padding.
func countInsts(insts []disasm.Inst) int {
n := 0
for _, in := range insts {
if in.Op != "BYTE" {
n++
}
}
return n
}