From ffdfa3da65d677531fbe092d9de37236b8a75f5a Mon Sep 17 00:00:00 2001 From: Patricio Whittingslow Date: Wed, 23 Sep 2026 19:45:40 -0300 Subject: [PATCH 1/2] xwasm: llm generate xwasm based on xelf and our API patterns --- build/xwasm/code.go | 182 +++++++ build/xwasm/data.go | 179 +++++++ build/xwasm/file.go | 244 ++++++++++ build/xwasm/fuzz_test.go | 96 ++++ build/xwasm/leb.go | 69 +++ build/xwasm/names.go | 124 +++++ build/xwasm/safeio.go | 54 +++ build/xwasm/stream.go | 218 +++++++++ build/xwasm/xwasm.go | 239 +++++++++ build/xwasm/xwasm_test.go | 539 +++++++++++++++++++++ cmd/bindiff/dwarf.go | 25 +- cmd/bindiff/main.go | 8 +- cmd/bindiff/symbols.go | 138 +++--- cmd/bindiff/testdata/src/gen.go | 5 + cmd/bindiff/testdata/src/hellowasm/main.go | 7 + cmd/bindiff/wasm.go | 223 +++++++++ cmd/bindiff/wasm_test.go | 210 ++++++++ testdata/hello-nodebug.wasm | Bin 0 -> 20581 bytes testdata/hello.wasm | Bin 0 -> 118060 bytes 19 files changed, 2500 insertions(+), 60 deletions(-) create mode 100644 build/xwasm/code.go create mode 100644 build/xwasm/data.go create mode 100644 build/xwasm/file.go create mode 100644 build/xwasm/fuzz_test.go create mode 100644 build/xwasm/leb.go create mode 100644 build/xwasm/names.go create mode 100644 build/xwasm/safeio.go create mode 100644 build/xwasm/stream.go create mode 100644 build/xwasm/xwasm.go create mode 100644 build/xwasm/xwasm_test.go create mode 100644 cmd/bindiff/testdata/src/hellowasm/main.go create mode 100644 cmd/bindiff/wasm.go create mode 100644 cmd/bindiff/wasm_test.go create mode 100644 testdata/hello-nodebug.wasm create mode 100644 testdata/hello.wasm diff --git a/build/xwasm/code.go b/build/xwasm/code.go new file mode 100644 index 0000000..270e0ee --- /dev/null +++ b/build/xwasm/code.go @@ -0,0 +1,182 @@ +package xwasm + +import ( + "errors" + "io" + "math" +) + +// Func locates one function body in the Code section. +type Func struct { + // Index is the function's index in the module's function index space, + // where imported functions come first: the body's ordinal plus + // [File.NumImportedFuncs]. The name section keys names by this index. + Index uint32 + // Offset is the position of the body's size prefix, measured from the start + // of the Code section's contents. DWARF addresses in a WebAssembly module + // are measured from the same point, but a subprogram's DW_AT_low_pc names + // the body proper: [Func.BodyOffset]. + Offset uint64 + // PrefixSize is the length of the size prefix, 1 to 5 bytes. + PrefixSize uint8 + // Size is the body's size in bytes, excluding the prefix. + Size uint64 +} + +// BodyOffset returns the offset of the body proper -- its local declarations, +// then its code -- from the start of the Code section's contents. +func (fn Func) BodyOffset() uint64 { return fn.Offset + uint64(fn.PrefixSize) } + +// End returns the offset one past the body's last byte, from the start of the +// Code section's contents. +func (fn Func) End() uint64 { return fn.BodyOffset() + fn.Size } + +// CodeReader walks the function bodies of a module's Code section through a +// caller-owned buffer, without holding anything per function. +// +// A CodeReader is reusable: [CodeReader.Reset] points it at another module and +// keeps the buffer. The buffer belongs to the reader from Reset until the next +// Reset; lending it to another reader in between corrupts both walks. +type CodeReader struct { + c cursor + start int64 // File offset of the Code section's contents. + count uint32 + imported uint32 + err error +} + +// Reset points cr at f's Code section, reading through buf. buf only needs to +// hold a few bytes at a time: bodies are skipped, never read. Larger buffers +// mean fewer reads. A module with no Code section yields no functions. +func (cr *CodeReader) Reset(f *File, buf []byte) error { + if len(buf) < maxSectionFrame { + return BufferTooSmallError{Need: maxSectionFrame, Have: len(buf)} + } + imported, err := f.NumImportedFuncs() + if err != nil { + return err + } + *cr = CodeReader{c: cr.c, imported: imported} + sec, err := f.SectionByID(SecCode) + if err != nil { + cr.c.reset(f.r, buf, 0, 0) + return nil + } + sh := sec.SectionHeader() + cr.start = int64(sh.Offset) + cr.c.reset(f.r, buf, cr.start, int64(sh.End())) + // Every body takes at least its one-byte size prefix and one byte of local + // declaration count. + cr.count = cr.c.vecLen(2) + if cr.c.err != nil { + return cr.c.err + } + if uint64(imported)+uint64(cr.count) > math.MaxUint32 { + return makeFormatErr(sh.Offset, "function index space overflows uint32", cr.count) + } + return nil +} + +// NumFuncs returns the number of function bodies in the Code section. +func (cr *CodeReader) NumFuncs() uint32 { return cr.count } + +// NumImportedFuncs returns the number of imported functions, which precede the +// module's own in the function index space. +func (cr *CodeReader) NumImportedFuncs() uint32 { return cr.imported } + +// Err returns why the last [CodeReader.Funcs] walk stopped early, or nil if it +// ran to completion or the caller stopped it. +func (cr *CodeReader) Err() error { return cr.err } + +// Funcs yields each function body in order of index. It may be called again to +// walk the bodies anew. +func (cr *CodeReader) Funcs(yield func(Func) bool) { + c := &cr.c + cr.err = nil + if cr.count == 0 { + return + } + c.err = nil + c.seek(cr.start) + c.u32() // Count, validated by Reset. + for i := uint32(0); i < cr.count; i++ { + prefix := c.pos() + size := c.u32() + if c.err != nil { + break + } + fn := Func{ + Index: cr.imported + i, + Offset: uint64(prefix - cr.start), + PrefixSize: uint8(c.pos() - prefix), + Size: uint64(size), + } + c.skip(fn.Size) + if c.err != nil { + break + } + if !yield(fn) { + return + } + } + if c.err == nil && c.pos() != c.end { + c.fail(makeFormatErr(uint64(c.pos()), "trailing bytes after function bodies", c.end-c.pos())) + } + cr.err = c.err +} + +// Import descriptor kinds. +const ( + importFunc = 0x00 + importTable = 0x01 + importMemory = 0x02 + importGlobal = 0x03 + importTag = 0x04 +) + +// NumImportedFuncs walks the Import section and counts the imported functions. +// A module with no Import section imports none. +func (f *File) NumImportedFuncs() (uint32, error) { + sec, err := f.SectionByID(SecImport) + if errors.Is(err, errNoSection) { + return 0, nil + } + sh := sec.SectionHeader() + var c cursor + c.reset(f.r, f.buf[:], int64(sh.Offset), int64(sh.End())) + // An import is at least two empty names and a kind byte with a one-byte + // operand. + count := c.vecLen(4) + var funcs uint32 + for i := uint32(0); i < count && c.err == nil; i++ { + c.skipName() // Module. + c.skipName() // Field. + switch kind := c.u8(); kind { + case importFunc: + c.u32() // Type index. + funcs++ + case importTable: + c.valType() + c.limits() + case importMemory: + c.limits() + case importGlobal: + c.valType() + c.u8() // Mutability. + case importTag: + c.u8() // Attribute. + c.u32() // Type index. + default: + if c.err == nil { + c.fail(makeFormatErr(uint64(c.pos()-1), "unknown import kind", kind)) + } + } + } + if c.err == nil && c.pos() != c.end { + c.fail(makeFormatErr(uint64(c.pos()), "trailing bytes after imports", c.end-c.pos())) + } + if c.err == io.EOF { + c.err = io.ErrUnexpectedEOF + } + return funcs, c.err +} diff --git a/build/xwasm/data.go b/build/xwasm/data.go new file mode 100644 index 0000000..f09bf4a --- /dev/null +++ b/build/xwasm/data.go @@ -0,0 +1,179 @@ +package xwasm + +// DataSegment locates one segment of the Data section. +type DataSegment struct { + // Index is the segment's index, which the name section keys names by. + Index uint32 + // Offset is the position of the segment's first byte -- its flags -- + // measured from the start of the Data section's contents. + Offset uint64 + // HeaderSize is the bytes the segment spends before its initializer: flags, + // memory index, offset expression and byte count. + HeaderSize uint32 + // Size is the length of the initializer bytes. + Size uint64 + // Passive marks a segment copied into memory only by memory.init, which + // has no address of its own. + Passive bool + // Addr is the linear-memory address an active segment is placed at, valid + // when AddrKnown is set: the offset expression is a single constant. An + // expression reading a global is only resolved at instantiation. + Addr uint64 + AddrKnown bool +} + +// DataOffset returns the offset of the initializer bytes from the start of the +// Data section's contents. +func (s DataSegment) DataOffset() uint64 { return s.Offset + uint64(s.HeaderSize) } + +// End returns the offset one past the segment's last byte, from the start of +// the Data section's contents. +func (s DataSegment) End() uint64 { return s.DataOffset() + s.Size } + +// DataReader walks the segments of a module's Data section through a +// caller-owned buffer, without holding anything per segment. +// +// A DataReader is reusable: [DataReader.Reset] points it at another module and +// keeps the buffer. The buffer belongs to the reader from Reset until the next +// Reset; lending it to another reader in between corrupts both walks. +type DataReader struct { + c cursor + start int64 + count uint32 + err error +} + +// Reset points dr at f's Data section, reading through buf. As with +// [CodeReader], segment contents are skipped rather than read, so buf needs to +// hold only a few bytes. A module with no Data section yields no segments. +func (dr *DataReader) Reset(f *File, buf []byte) error { + if len(buf) < maxSectionFrame { + return BufferTooSmallError{Need: maxSectionFrame, Have: len(buf)} + } + *dr = DataReader{c: dr.c} + sec, err := f.SectionByID(SecData) + if err != nil { + dr.c.reset(f.r, buf, 0, 0) + return nil + } + sh := sec.SectionHeader() + dr.start = int64(sh.Offset) + dr.c.reset(f.r, buf, dr.start, int64(sh.End())) + // The smallest segment is passive and empty: flags and a zero count. + dr.count = dr.c.vecLen(2) + return dr.c.err +} + +// NumSegments returns the number of segments in the Data section. +func (dr *DataReader) NumSegments() uint32 { return dr.count } + +// Err returns why the last [DataReader.Segments] walk stopped early, or nil if +// it ran to completion or the caller stopped it. +func (dr *DataReader) Err() error { return dr.err } + +// Segments yields each data segment in order of index. It may be called again +// to walk the segments anew. +func (dr *DataReader) Segments(yield func(DataSegment) bool) { + c := &dr.c + dr.err = nil + if dr.count == 0 { + return + } + c.err = nil + c.seek(dr.start) + c.u32() // Count, validated by Reset. + for i := uint32(0); i < dr.count; i++ { + first := c.pos() + seg := DataSegment{Index: i, Offset: uint64(first - dr.start)} + switch flags := c.u32(); flags { + case 0: + seg.Addr, seg.AddrKnown = c.constExpr() + case 1: + seg.Passive = true + case 2: + c.u32() // Memory index. + seg.Addr, seg.AddrKnown = c.constExpr() + default: + if c.err == nil { + c.fail(makeFormatErr(uint64(first), "invalid data segment flags", flags)) + } + } + seg.Size = uint64(c.u32()) + if c.err != nil { + break + } + seg.HeaderSize = uint32(c.pos() - first) + c.skip(seg.Size) + if c.err != nil { + break + } + if !yield(seg) { + return + } + } + if c.err == nil && c.pos() != c.end { + c.fail(makeFormatErr(uint64(c.pos()), "trailing bytes after data segments", c.end-c.pos())) + } + dr.err = c.err +} + +// Opcodes that may appear in a constant expression, with the extended-const +// proposal's integer arithmetic. +const ( + opEnd = 0x0b + opGlobalGet = 0x23 + opI32Const = 0x41 + opI64Const = 0x42 + opF32Const = 0x43 + opF64Const = 0x44 + opI32Add = 0x6a + opI32Sub = 0x6b + opI32Mul = 0x6c + opI64Add = 0x7c + opI64Sub = 0x7d + opI64Mul = 0x7e + opRefNull = 0xd0 + opRefFunc = 0xd2 + opPrefixSIMD = 0xfd + simdV128Const = 12 +) + +// constExpr skips a constant expression through its end opcode. It reports the +// expression's value when the expression is a lone integer constant, which is +// how a linker places a data segment. +func (c *cursor) constExpr() (v uint64, known bool) { + start := c.pos() + for n := 0; c.err == nil; n++ { + op := c.u8() + switch op { + case opEnd: + if !known || n != 1 { + return 0, false // Not a lone constant: resolved at instantiation. + } + return v, true + case opI32Const: + v, known = uint64(uint32(c.sleb(32))), true + case opI64Const: + v, known = uint64(c.sleb(64)), true + case opF32Const: + c.skip(4) + case opF64Const: + c.skip(8) + case opGlobalGet, opRefFunc: + c.u32() + case opRefNull: + c.sleb(33) // Heap type. + case opI32Add, opI32Sub, opI32Mul, opI64Add, opI64Sub, opI64Mul: + case opPrefixSIMD: + if sub := c.u32(); sub != simdV128Const && c.err == nil { + c.fail(makeFormatErr(uint64(start), "non-constant SIMD instruction in constant expression", sub)) + } + c.skip(16) + default: + if c.err == nil { + c.fail(makeFormatErr(uint64(c.pos()-1), "non-constant instruction in constant expression", op)) + } + } + } + return 0, false +} diff --git a/build/xwasm/file.go b/build/xwasm/file.go new file mode 100644 index 0000000..f2ba1a5 --- /dev/null +++ b/build/xwasm/file.go @@ -0,0 +1,244 @@ +package xwasm + +import ( + "errors" + "fmt" + "io" +) + +// File is a decoded WebAssembly module: its header and the location of each +// section. Section contents are read on demand through the [io.ReaderAt] given +// to [File.Read], which must outlive the File. +type File struct { + hdr Header + sections []section + r io.ReaderAt + + // buf is scratch for decoding section framing and comparing names. + buf [fileBufSize]byte +} + +// section is deliberately just the header: a module may declare thousands of +// tiny custom sections, and nothing per section is worth holding resident but +// where it is. +type section struct { + SectionHeader +} + +// Read parses the module in r, recording where each section lies. It reads the +// header and the framing of each section, never section contents. The module +// is expected to start at offset 0 and end at the end of r. +// +// Calling Read again on the same File reuses its section table, so parsing a +// sequence of modules allocates only when one has more sections than any +// before it. +func (f *File) Read(r io.ReaderAt) error { + buf := f.buf[:] + n, err := r.ReadAt(buf[:headerSize], 0) + if n < headerSize { + if err == nil || err == io.EOF { + err = io.ErrUnexpectedEOF + } + return err + } + header, _, err := DecodeHeader(buf) + if err != nil { + return err + } + sections := f.sections[:0] + // Leave f describing nothing unless the whole module parses: a half-read + // section table is worse than none. + f.hdr, f.sections, f.r = Header{}, sections, nil + + var lastRank uint8 + for off := uint64(headerSize); ; { + n, err := r.ReadAt(buf[:maxSectionFrame], int64(off)) + if n == 0 { + if err == io.EOF || err == nil { + break // Clean end of module on a section boundary. + } + return err + } else if err != nil && err != io.EOF { + return err + } + sh, _, err := DecodeSectionHeader(buf[:n], off) + if err != nil { + return err + } + if sh.ID != SecCustom { + rank := sectionRank[sh.ID] + if rank <= lastRank { + return makeFormatErr(off, "section out of order or duplicated", sh.ID) + } + lastRank = rank + } + end := sh.End() + if end > 1<<63-1 { + return makeFormatErr(off, errSectionPastEOF.Error(), sh.Size) + } + // Probe the last byte: a section claiming more than the file holds is + // caught here rather than by whichever reader first reaches its tail. + if n, _ := r.ReadAt(buf[:1], int64(end)-1); n != 1 { + return makeFormatErr(off, errSectionPastEOF.Error(), sh.Size) + } + if len(sections) >= maxSections { + return makeFormatErr(off, errTooManySecs.Error(), len(sections)) + } + sections = append(sections, section{SectionHeader: sh}) + off = end + } + f.hdr, f.sections, f.r = header, sections, r + return nil +} + +// Header returns the module header. +func (f *File) Header() Header { return f.hdr } + +// NumSections returns the number of sections in the module, custom ones +// included. +func (f *File) NumSections() int { return len(f.sections) } + +// Section returns the section at sectionIdx, in file order. +func (f *File) Section(sectionIdx int) (FileSection, error) { + if sectionIdx >= len(f.sections) || sectionIdx < 0 { + return FileSection{}, errors.New("OOB/negative section index") + } + return FileSection{f: f, sindex: sectionIdx}, nil +} + +// SectionByID returns the first section with the given id. For any id but +// [SecCustom] that is the only one. +func (f *File) SectionByID(id SectionID) (FileSection, error) { + for i := range f.sections { + if f.sections[i].ID == id { + return FileSection{f: f, sindex: i}, nil + } + } + return FileSection{}, fmt.Errorf("%w: %s", errNoSection, id) +} + +// SectionByName returns the first custom section with the given name, or the +// standard section whose id's [SectionID.String] is name ("code", "data", ...). +func (f *File) SectionByName(name string) (FileSection, error) { + for i := range f.sections { + s := FileSection{f: f, sindex: i} + sh := &f.sections[i] + if sh.ID != SecCustom { + if sh.ID.String() == name { + return s, nil + } + continue + } + ok, err := s.nameEquals(name) + if err != nil { + return FileSection{}, err + } else if ok { + return s, nil + } + } + return FileSection{}, fmt.Errorf("%w: %q", errNoSection, name) +} + +// FileSection is the handle to a module's section. +type FileSection struct { + f *File + sindex int +} + +func (fs FileSection) ptr() *section { return &fs.f.sections[fs.sindex] } + +// Index returns the index of the section within the module, in file order. +func (fs FileSection) Index() int { return fs.sindex } + +// SectionHeader returns the section's header. +func (fs FileSection) SectionHeader() SectionHeader { return fs.ptr().SectionHeader } + +// Size returns the size of the section's contents in bytes, which for a custom +// section excludes its name. +func (fs FileSection) Size() int64 { return int64(fs.ptr().Size) } + +// Open returns a new [io.SectionReader] reading the section's contents. +func (fs FileSection) Open() *io.SectionReader { + s := fs.ptr() + return io.NewSectionReader(fs.f.r, int64(s.Offset), int64(s.Size)) +} + +// AppendData appends the section's contents to dst and returns the result. +func (fs FileSection) AppendData(dst []byte) ([]byte, error) { + s := fs.ptr() + return appendAt(dst, fs.f.r, s.Offset, s.Size) +} + +// AppendName appends the section's name to dst: a custom section's own name, +// or for a standard section its id's [SectionID.String]. +func (fs FileSection) AppendName(dst []byte) ([]byte, error) { + s := fs.ptr() + if s.ID != SecCustom { + return append(dst, s.ID.String()...), nil + } + return appendAt(dst, fs.f.r, s.NameOff, uint64(s.NameLen)) +} + +// Name returns the section's name. See [FileSection.AppendName]. +func (fs FileSection) Name() (string, error) { + s := fs.ptr() + if s.ID != SecCustom { + return s.ID.String(), nil + } + b, err := fs.AppendName(nil) + return string(b), err +} + +// nameEquals compares a custom section's name against name, reading it through +// the File's scratch buffer so the comparison allocates nothing. +func (fs FileSection) nameEquals(name string) (bool, error) { + s := fs.ptr() + if uint64(s.NameLen) != uint64(len(name)) { + return false, nil + } + buf := fs.f.buf[:] + for off := 0; off < len(name); { + chunk := buf[:min(len(buf), len(name)-off)] + n, err := fs.f.r.ReadAt(chunk, int64(s.NameOff)+int64(off)) + if n < len(chunk) { + if err == nil || err == io.EOF { + err = io.ErrUnexpectedEOF + } + return false, err + } + if string(chunk) != name[off:off+n] { // Compiles to a comparison, no copy. + return false, nil + } + off += n + } + return true, nil +} + +func (fs FileSection) String() string { + if fs.f == nil { + return "" + } + name, _ := fs.Name() + sh := fs.SectionHeader() + return fmt.Sprintf("%s, %s off=%#x size=%d", name, sh.ID.String(), sh.Offset, sh.Size) +} + +// appendAt appends the size bytes at off in r to dst. +func appendAt(dst []byte, r io.ReaderAt, off, size uint64) ([]byte, error) { + if size == 0 { + return dst, nil + } + if sliceCapWithSize(1, size) < 0 || size > uint64(1<<63-1-len(dst)) { + return dst, errors.New("section too large") + } + dst = slicesGrow(dst, int(size)) + toRead := dst[len(dst) : len(dst)+int(size)] + n, err := r.ReadAt(toRead, int64(off)) + if n < len(toRead) { + if err == nil || err == io.EOF { + err = io.ErrUnexpectedEOF + } + return dst, err + } + return dst[:len(dst)+int(size)], nil +} diff --git a/build/xwasm/fuzz_test.go b/build/xwasm/fuzz_test.go new file mode 100644 index 0000000..3acefe9 --- /dev/null +++ b/build/xwasm/fuzz_test.go @@ -0,0 +1,96 @@ +package xwasm + +import ( + "bytes" + "os" + "testing" +) + +// fuzzState is everything a fuzz iteration touches, allocated once. The fuzz +// engine calls the target sequentially within a worker process, so one set +// serves every iteration: an iteration allocates nothing but what a module +// with more sections than any before it grows the section table by. +type fuzzState struct { + r bytes.Reader + f File + cr CodeReader + nr NameReader + dr DataReader + cbuf [64]byte + dbuf [64]byte + nbuf [256]byte +} + +func (s *fuzzState) run(data []byte) { + s.r.Reset(data) + if s.f.Read(&s.r) != nil { + return + } + for i := 0; i < s.f.NumSections(); i++ { + sec, _ := s.f.Section(i) + sh := sec.SectionHeader() + if sh.End() > uint64(len(data)) || sh.Offset < sh.Start { + panic("section outside module") + } + } + s.f.SectionByName(".debug_line") + var size uint64 // A module without a Code section yields no functions. + if code, err := s.f.SectionByID(SecCode); err == nil { + size = code.SectionHeader().Size + } + if s.cr.Reset(&s.f, s.cbuf[:]) == nil { + for fn := range s.cr.Funcs { + if fn.End() > size { + panic("function body outside code section") + } + } + } + if s.nr.Reset(&s.f, s.nbuf[:]) == nil { + for range s.nr.FuncNames { + } + for range s.nr.DataNames { + } + } + size = 0 + if data, err := s.f.SectionByID(SecData); err == nil { + size = data.SectionHeader().Size + } + if s.dr.Reset(&s.f, s.dbuf[:]) == nil { + for seg := range s.dr.Segments { + if seg.End() > size { + panic("data segment outside data section") + } + } + } +} + +func FuzzRead(f *testing.F) { + for _, name := range fixtures { + data, err := os.ReadFile(name) + if err != nil { + f.Fatal(err) + } + f.Add(data) + } + f.Add([]byte("\x00asm\x01\x00\x00\x00")) + var s fuzzState + f.Fuzz(func(t *testing.T, data []byte) { + s.run(data) + }) +} + +// BenchmarkFuzzIteration guards the fuzz target's allocation behavior: B/op +// must stay at zero, since the fuzzer runs it flat out on every core. +func BenchmarkFuzzIteration(b *testing.B) { + data, err := os.ReadFile(fixtures[0]) + if err != nil { + b.Fatal(err) + } + var s fuzzState + s.run(data) + b.ReportAllocs() + b.SetBytes(int64(len(data))) + for b.Loop() { + s.run(data) + } +} diff --git a/build/xwasm/leb.go b/build/xwasm/leb.go new file mode 100644 index 0000000..22fd9b3 --- /dev/null +++ b/build/xwasm/leb.go @@ -0,0 +1,69 @@ +package xwasm + +import "io" + +// DecodeULEB128 decodes an unsigned LEB128 value of at most bits bits (1 to +// 64), returning the value and the number of bytes consumed. +// +// WebAssembly is stricter than DWARF about LEB128: an encoding may use at most +// ceil(bits/7) bytes, and the bits of the last byte past the type's width must +// be zero. A violation is an error, not a truncation; in a module it means the +// decoder has lost its place. +func DecodeULEB128(b []byte, bits int) (v uint64, n int, err error) { + maxBytes := (bits + 6) / 7 + var shift uint + for n < len(b) { + c := b[n] + n++ + if n == maxBytes { + if rem := bits - int(shift); rem < 7 && c>>rem != 0 { + return 0, n, errLEBOverflow // Also catches a continuation bit. + } + } + v |= uint64(c&0x7f) << shift + if c&0x80 == 0 { + return v, n, nil + } + if n == maxBytes { + return 0, n, errLEBTooLong + } + shift += 7 + } + return 0, n, io.ErrUnexpectedEOF +} + +// DecodeSLEB128 decodes a signed LEB128 value of at most bits bits (1 to 64) +// under the same rules as [DecodeULEB128]: at most ceil(bits/7) bytes, and the +// unused bits of the last byte must repeat the sign bit. +func DecodeSLEB128(b []byte, bits int) (v int64, n int, err error) { + maxBytes := (bits + 6) / 7 + var shift uint + for n < len(b) { + c := b[n] + n++ + if n == maxBytes { + if c&0x80 != 0 { + return 0, n, errLEBTooLong + } + // The payload's sign bit and every payload bit above the type's + // width must agree: all clear or all set. + rem := bits - int(shift) + top := (c & 0x7f) >> (rem - 1) + if top != 0 && top != 0x7f>>(rem-1) { + return 0, n, errLEBOverflow + } + } + v |= int64(c&0x7f) << shift + shift += 7 + if c&0x80 == 0 { + if shift < 64 && c&0x40 != 0 { + v |= -1 << shift + } + return v, n, nil + } + if n == maxBytes { + return 0, n, errLEBTooLong + } + } + return 0, n, io.ErrUnexpectedEOF +} diff --git a/build/xwasm/names.go b/build/xwasm/names.go new file mode 100644 index 0000000..24509f4 --- /dev/null +++ b/build/xwasm/names.go @@ -0,0 +1,124 @@ +package xwasm + +// Subsection ids of the "name" custom section, from the extended name section +// proposal. Each is optional and at most one of each appears, in order. +const ( + nameSubModule = 0 + nameSubFunction = 1 + nameSubGlobal = 7 + nameSubData = 9 +) + +// NameReader walks the name maps in a module's "name" custom section +// through a caller-owned buffer. Names are presented in place, borrowed from +// the buffer, so a walk allocates nothing. +// +// A NameReader is reusable: [NameReader.Reset] points it at another module and +// keeps the buffer. The buffer belongs to the reader from Reset until the next +// Reset; lending it to another reader in between corrupts both walks. +type NameReader struct { + c cursor + start int64 + ok bool // Module has a name section. + err error +} + +// Reset points nr at f's "name" section, reading through buf. The longest +// function name must fit in buf; a longer one stops the walk with a +// [BufferTooSmallError], and the caller can grow buf and walk again. A module +// with no name section yields no names. +func (nr *NameReader) Reset(f *File, buf []byte) error { + if len(buf) < maxSectionFrame { + return BufferTooSmallError{Need: maxSectionFrame, Have: len(buf)} + } + *nr = NameReader{c: nr.c} + sec, err := f.SectionByName("name") + if err != nil || sec.SectionHeader().ID != SecCustom { + nr.c.reset(f.r, buf, 0, 0) + return nil + } + sh := sec.SectionHeader() + nr.start = int64(sh.Offset) + nr.ok = true + nr.c.reset(f.r, buf, nr.start, int64(sh.End())) + return nil +} + +// Err returns why the last walk stopped early, or nil if it ran to completion +// or the caller stopped it. +func (nr *NameReader) Err() error { return nr.err } + +// FuncNames yields each named function's index and name, in increasing order +// of index as the format requires. name is valid only until the next yield. +// Functions the section does not name are skipped. +func (nr *NameReader) FuncNames(yield func(idx uint32, name []byte) bool) { + nr.nameMap(nameSubFunction, yield) +} + +// DataNames yields each named data segment's index and name, as FuncNames +// does for functions. wasm-ld names segments after the output section they +// came from: ".rodata", ".data". +func (nr *NameReader) DataNames(yield func(idx uint32, name []byte) bool) { + nr.nameMap(nameSubData, yield) +} + +// GlobalNames yields each named global's index and name, as FuncNames does for +// functions. +func (nr *NameReader) GlobalNames(yield func(idx uint32, name []byte) bool) { + nr.nameMap(nameSubGlobal, yield) +} + +// nameMap walks the name map in subsection sub. +func (nr *NameReader) nameMap(sub uint8, yield func(idx uint32, name []byte) bool) { + nr.err = nil + if !nr.ok { + return + } + c := &nr.c + c.err = nil + c.seek(nr.start) + for c.err == nil && c.pos() < c.end { + id := c.u8() + size := c.u32() + subEnd := c.pos() + int64(size) + if c.err != nil { + break + } else if subEnd > c.end { + c.fail(makeFormatErr(uint64(c.pos()), "name subsection exceeds section", size)) + break + } + if id != sub { + c.seek(subEnd) + continue + } + saved := c.end + c.end = subEnd // Bound the name map by its subsection. + // An entry is at least a one-byte index and a one-byte empty name. + count := c.vecLen(2) + var last uint32 + for i := uint32(0); i < count && c.err == nil; i++ { + idx := c.u32() + name := c.view(uint64(c.u32())) + if c.err != nil { + break + } + if i > 0 && idx <= last { + c.fail(makeFormatErr(uint64(c.pos()), "names out of order", idx)) + break + } + last = idx + if !yield(idx, name) { + // Lift the bound here too, or the next walk sees only this + // subsection. + c.end = saved + return + } + } + if c.err == nil && c.pos() != subEnd { + c.fail(makeFormatErr(uint64(c.pos()), "trailing bytes in name map", subEnd-c.pos())) + } + c.end = saved + break // At most one subsection of each kind. + } + nr.err = c.err +} diff --git a/build/xwasm/safeio.go b/build/xwasm/safeio.go new file mode 100644 index 0000000..3b3b4bb --- /dev/null +++ b/build/xwasm/safeio.go @@ -0,0 +1,54 @@ +package xwasm + +import "unsafe" + +// safechunk is an arbitrary limit on how much memory we are willing +// to allocate without concern. +const safechunk = 10 << 20 // 10M + +// sliceCapWithSize returns the capacity to use when allocating a slice. +// After the slice is allocated with the capacity, it should be +// built using append. This will avoid allocating too much memory +// if the capacity is large and incorrect. +// +// A negative result means that the value is always too big. +func sliceCapWithSize(size, c uint64) int { + if int64(c) < 0 || c != uint64(int(c)) { + return -1 + } + if size > 0 && c > (1<<64-1)/size { + return -1 + } + if c*size > safechunk { + c = safechunk / size + if c == 0 { + c = 1 + } + } + return int(c) +} + +// sliceCap is like SliceCapWithSize but using generics. +func sliceCap[E any](c uint64) int { + var v E + size := uint64(unsafe.Sizeof(v)) + return sliceCapWithSize(size, c) +} + +// Grow increases the slice's capacity, if necessary, to guarantee space for +// another n elements. After Grow(n), at least n elements can be appended +// to the slice without another allocation. If n is negative or too large to +// allocate the memory, Grow panics. +func slicesGrow[S ~[]E, E any](s S, n int) S { + if n < 0 { + panic("cannot be negative") + } + if n -= cap(s) - len(s); n > 0 { + s = append(s[:cap(s)], make([]E, n)...)[:len(s)] + } + return s +} + +func aliases[T ~int64 | ~uint64 | ~int](start0, end0, start1, end1 T) bool { + return start0 < end1 && end0 > start1 +} diff --git a/build/xwasm/stream.go b/build/xwasm/stream.go new file mode 100644 index 0000000..9c974f1 --- /dev/null +++ b/build/xwasm/stream.go @@ -0,0 +1,218 @@ +package xwasm + +import ( + "io" + + "github.com/soypat/lexorg" +) + +// cursor reads a section's contents forward through a caller-owned window, +// refusing to run past the section's end. It records the first error it hits +// and then reports zero values, so a decoder can read a whole structure and +// check for failure once at the end. +// +// It is xdwarf's streamCursor adapted to WebAssembly's width-bounded LEB128. +type cursor struct { + wr lexorg.WindowReader + bufLen int // Fill size: the largest span view can present whole. + end int64 // Absolute offset one past the last byte the cursor may read. + err error +} + +// reset binds the cursor to [start, end) of r. It always discards what the +// window holds: the same reader and buffer may now describe different bytes, +// since a File can be re-read from a reader whose contents changed, and a +// caller may have lent the buffer to another reader in between. +func (c *cursor) reset(r io.ReaderAt, buf []byte, start, end int64) { + c.wr.Reset(r, buf, start) + c.wr.Drop() + c.bufLen, c.end, c.err = len(buf), end, nil +} + +// pos reports the absolute offset the next read starts at. +func (c *cursor) pos() int64 { return c.wr.Offset() } + +func (c *cursor) fail(err error) { + if c.err == nil { + c.err = err + } +} + +// seek moves the cursor to off, which costs no read when off is resident. +func (c *cursor) seek(off int64) { + if c.err != nil { + return + } + if off > c.end || off < 0 { + c.fail(makeFormatErr(uint64(c.pos()), "seek past end of section", off)) + return + } + c.wr.Reset(c.wr.ReaderAt(), nil, off) +} + +// skip advances the cursor by n bytes without reading them. +func (c *cursor) skip(n uint64) { + if c.err != nil { + return + } + if n > uint64(c.end-c.pos()) { + c.fail(makeFormatErr(uint64(c.pos()), "skip past end of section", n)) + return + } + c.seek(c.pos() + int64(n)) +} + +func (c *cursor) u8() uint8 { + if c.err != nil { + return 0 + } + if c.pos() >= c.end { + c.fail(makeFormatErr(uint64(c.pos()), "read past end of section", 1)) + return 0 + } + b, err := c.wr.ReadByte() + if err != nil { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + c.fail(err) + return 0 + } + return b +} + +// uleb reads an unsigned LEB128 of at most bits bits, under the rules of +// [DecodeULEB128]. +func (c *cursor) uleb(bits int) uint64 { + maxBytes := (bits + 6) / 7 + start := c.pos() + var v uint64 + var shift uint + for n := 1; ; n++ { + b := c.u8() + if c.err != nil { + return 0 + } + if n == maxBytes { + if b&0x80 != 0 { + c.fail(makeFormatErr(uint64(start), errLEBTooLong.Error(), bits)) + return 0 + } + if rem := bits - int(shift); rem < 7 && b>>rem != 0 { + c.fail(makeFormatErr(uint64(start), errLEBOverflow.Error(), bits)) + return 0 + } + } + v |= uint64(b&0x7f) << shift + if b&0x80 == 0 { + return v + } + shift += 7 + } +} + +func (c *cursor) u32() uint32 { return uint32(c.uleb(32)) } + +// sleb reads a signed LEB128 of at most bits bits, under the rules of +// [DecodeSLEB128]. +func (c *cursor) sleb(bits int) int64 { + maxBytes := (bits + 6) / 7 + start := c.pos() + var v int64 + var shift uint + for n := 1; ; n++ { + b := c.u8() + if c.err != nil { + return 0 + } + if n == maxBytes { + if b&0x80 != 0 { + c.fail(makeFormatErr(uint64(start), errLEBTooLong.Error(), bits)) + return 0 + } + rem := bits - int(shift) + if top := (b & 0x7f) >> (rem - 1); top != 0 && top != 0x7f>>(rem-1) { + c.fail(makeFormatErr(uint64(start), errLEBOverflow.Error(), bits)) + return 0 + } + } + v |= int64(b&0x7f) << shift + shift += 7 + if b&0x80 == 0 { + if shift < 64 && b&0x40 != 0 { + v |= -1 << shift + } + return v + } + } +} + +// view returns the next n bytes as a subslice of the window, valid until the +// cursor next moves. A span longer than the window fails with a +// [BufferTooSmallError] naming the size that would have held it. +func (c *cursor) view(n uint64) []byte { + if c.err != nil { + return nil + } + if n > uint64(c.end-c.pos()) { + c.fail(makeFormatErr(uint64(c.pos()), "read past end of section", n)) + return nil + } + if n > uint64(c.bufLen) { + c.fail(BufferTooSmallError{Need: int(n), Have: c.bufLen}) + return nil + } + b, err := c.wr.ReadView(int(n)) + if err != nil { + c.fail(err) + return nil + } + return b +} + +// vecLen reads a vector's element count and rejects one that cannot fit in the +// rest of the section when every element takes at least minElem bytes. Counts +// come straight from the file, so this is what stops a loop over a forged count +// from spinning for billions of iterations before it notices the section ended. +func (c *cursor) vecLen(minElem uint64) uint32 { + n := c.u32() + if c.err == nil && uint64(n)*minElem > uint64(c.end-c.pos()) { + c.fail(makeFormatErr(uint64(c.pos()), "vector count exceeds section", n)) + return 0 + } + return n +} + +// skipName skips a length-prefixed byte string. +func (c *cursor) skipName() { + c.skip(uint64(c.u32())) +} + +// limits skips a table or memory limits structure. The flags byte admits the +// shared-memory and memory64 proposals: bit 0 has-max, bit 1 shared, bit 2 +// 64-bit bounds. +func (c *cursor) limits() { + flags := c.u8() + if c.err == nil && flags > 7 { + c.fail(makeFormatErr(uint64(c.pos()-1), "invalid limits flags", flags)) + return + } + bits := 32 + if flags&4 != 0 { + bits = 64 + } + c.uleb(bits) + if flags&1 != 0 { + c.uleb(bits) + } +} + +// valType skips a value or reference type. The typed function references and +// GC proposals prefix a heap type with 0x63 (nullable) or 0x64 (non-null); +// every other type is a single byte. +func (c *cursor) valType() { + switch c.u8() { + case 0x63, 0x64: + c.sleb(33) + } +} diff --git a/build/xwasm/xwasm.go b/build/xwasm/xwasm.go new file mode 100644 index 0000000..f7a31e1 --- /dev/null +++ b/build/xwasm/xwasm.go @@ -0,0 +1,239 @@ +// Package xwasm decodes WebAssembly binary modules in the allocation-conscious +// style of [xelf]. +// +// A module is a header followed by a sequence of sections, each an id and a +// size. There is no section header table: [File.Read] walks the sections once +// and keeps a small fixed-size record of each, never their contents. Everything +// of unbounded size -- section bodies, function bodies, names -- is either read +// through an [io.SectionReader], appended onto a caller-owned buffer, or +// streamed through a caller-owned window by [CodeReader] and [NameReader]. +// +// DWARF in a WebAssembly module lives in custom sections named as in ELF +// (".debug_line", ".debug_info", ...), uncompressed and, in a linked module, +// already relocated. Its addresses are offsets from the start of the Code +// section's payload, the same space [Func.Offset] is expressed in. +// +// [xelf]: github.com/soypat/tinyboot/build/xelf +package xwasm + +import ( + "encoding/binary" + "errors" + "fmt" + "strconv" +) + +const ( + fileBufSize = 512 + + magic uint32 = 0x00 | 'a'<<8 | 's'<<16 | 'm'<<24 + headerSize = 8 // Magic and version. + + // maxSectionFrame is the largest a section's framing can be before its + // payload: id (1), size (5) and, for a custom section, the name length (5). + maxSectionFrame = 1 + 5 + 5 + + // maxSections bounds how many sections [File.Read] will record. A section + // can be as small as two bytes, so the count a file can claim grows with its + // size; real modules carry a dozen standard sections and a handful of + // custom ones. This mirrors xelf's safechunk guard. + maxSections = 1 << 12 +) + +var ( + errBadMagic = errors.New("bad magic number") + errNoSection = errors.New("section not found") + errLEBTooLong = errors.New("LEB128 encoding exceeds maximum length") + errLEBOverflow = errors.New("LEB128 value overflows its type") + errTooManySecs = errors.New("too many sections") + errSectionPastEOF = errors.New("section extends past end of file") +) + +// SectionID identifies the kind of a section. Every id but [SecCustom] may +// appear at most once in a module, in a fixed order. +type SectionID uint8 + +const ( + SecCustom SectionID = 0 + SecType SectionID = 1 + SecImport SectionID = 2 + SecFunction SectionID = 3 + SecTable SectionID = 4 + SecMemory SectionID = 5 + SecGlobal SectionID = 6 + SecExport SectionID = 7 + SecStart SectionID = 8 + SecElement SectionID = 9 + SecCode SectionID = 10 + SecData SectionID = 11 + SecDataCount SectionID = 12 + SecTag SectionID = 13 // Exception handling proposal. +) + +var sectionNames = [...]string{ + SecCustom: "custom", + SecType: "type", + SecImport: "import", + SecFunction: "function", + SecTable: "table", + SecMemory: "memory", + SecGlobal: "global", + SecExport: "export", + SecStart: "start", + SecElement: "element", + SecCode: "code", + SecData: "data", + SecDataCount: "datacount", + SecTag: "tag", +} + +// sectionRank is the position a non-custom section must take in a module. Ids +// were assigned as sections were added to the spec, so they are not in order: +// the tag section sits between memory and global, datacount before code. +var sectionRank = [...]uint8{ + SecType: 1, + SecImport: 2, + SecFunction: 3, + SecTable: 4, + SecMemory: 5, + SecTag: 6, + SecGlobal: 7, + SecExport: 8, + SecStart: 9, + SecElement: 10, + SecDataCount: 11, + SecCode: 12, + SecData: 13, +} + +func (id SectionID) String() string { + if int(id) < len(sectionNames) { + return sectionNames[id] + } + return "SectionID(" + strconv.Itoa(int(id)) + ")" +} + +// Validate reports whether id is a section id this package knows. +func (id SectionID) Validate() error { + if int(id) >= len(sectionNames) { + return makeFormatErr(0, "unknown section id", uint8(id)) + } + return nil +} + +// Header is the module preamble: the magic number, which is checked rather +// than stored, and the binary format version. +type Header struct { + Version uint32 +} + +// DecodeHeader decodes the module preamble at the start of buf. +func DecodeHeader(buf []byte) (h Header, n int, err error) { + if len(buf) < headerSize { + return Header{}, 0, errors.New("too short buffer to decode WASM header") + } + if binary.LittleEndian.Uint32(buf) != magic { + return Header{}, 0, makeFormatErr(0, errBadMagic.Error(), buf[:4]) + } + h.Version = binary.LittleEndian.Uint32(buf[4:]) + if h.Version != 1 { + return Header{}, 0, makeFormatErr(4, "unsupported WASM version", h.Version) + } + return h, headerSize, nil +} + +// Put encodes the module preamble into b. +func (h Header) Put(b []byte) (n int, err error) { + if len(b) < headerSize { + return 0, errors.New("buffer too short to put Header") + } + binary.LittleEndian.PutUint32(b, magic) + binary.LittleEndian.PutUint32(b[4:], h.Version) + return headerSize, nil +} + +// HeaderSize returns the size of the module preamble in bytes. +func (h Header) HeaderSize() int { return headerSize } + +// SectionHeader describes where a section sits in the file. Offsets are +// absolute file offsets. +type SectionHeader struct { + ID SectionID + // Start is the offset of the section's id byte. + Start uint64 + // Offset is the offset of the section's contents: past the id and size + // and, for a custom section, past its name. + Offset uint64 + // Size is the size of the contents in bytes, which for a custom section + // excludes the name. + Size uint64 + // NameOff and NameLen locate a custom section's name. The name is not + // stored: [FileSection.AppendName] reads it when asked, as xelf does with + // string-table names. + NameOff uint64 + NameLen uint32 +} + +// End returns the offset one past the section's last byte. +func (sh SectionHeader) End() uint64 { return sh.Offset + sh.Size } + +// FrameSize returns the bytes the section spends on framing rather than +// contents: its id, size and, for a custom section, its name. +func (sh SectionHeader) FrameSize() uint64 { return sh.Offset - sh.Start } + +// DecodeSectionHeader decodes the framing of the section whose id byte is at +// file offset start and at b[0]. n is the number of framing bytes decoded, +// which excludes a custom section's name: the name need not be in b. +func DecodeSectionHeader(b []byte, start uint64) (sh SectionHeader, n int, err error) { + if len(b) == 0 { + return sh, 0, errors.New("SectionHeader short decode buffer") + } + sh.ID = SectionID(b[0]) + if err = sh.ID.Validate(); err != nil { + return sh, 0, makeFormatErr(start, "unknown section id", b[0]) + } + size, k, err := DecodeULEB128(b[1:], 32) + if err != nil { + return sh, 0, makeFormatErr(start+1, "section size", err) + } + n = 1 + k + sh.Start = start + sh.Offset = start + uint64(n) + sh.Size = size + if sh.ID != SecCustom { + return sh, n, nil + } + namelen, k, err := DecodeULEB128(b[n:], 32) + if err != nil { + return sh, 0, makeFormatErr(start+uint64(n), "custom section name length", err) + } + // The name is part of the payload the size counts. + if uint64(k)+namelen > size { + return sh, 0, makeFormatErr(start+uint64(n), "custom section name exceeds section", namelen) + } + sh.NameOff = sh.Offset + uint64(k) + sh.NameLen = uint32(namelen) + sh.Offset = sh.NameOff + namelen + sh.Size = size - uint64(k) - namelen + return sh, n + k, nil +} + +// BufferTooSmallError reports a caller-owned buffer that cannot hold a single +// item the reader must present whole, and how large it would have to be. A +// caller can grow its buffer to Need and retry. +type BufferTooSmallError struct { + Need int + Have int +} + +func (e BufferTooSmallError) Error() string { + return "xwasm: buffer of " + strconv.Itoa(e.Have) + + " bytes too small, need " + strconv.Itoa(e.Need) +} + +func makeFormatErr(off uint64, msg string, val any) error { + if str, ok := val.(fmt.Stringer); ok { + val = str.String() + } + return fmt.Errorf("WASM format error: %s @ off=%d: %v", msg, off, val) +} diff --git a/build/xwasm/xwasm_test.go b/build/xwasm/xwasm_test.go new file mode 100644 index 0000000..f8bd0a4 --- /dev/null +++ b/build/xwasm/xwasm_test.go @@ -0,0 +1,539 @@ +package xwasm + +import ( + "bufio" + "bytes" + "errors" + "io" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "testing" +) + +var fixtures = []string{ + "../../testdata/hello.wasm", + "../../testdata/hello-nodebug.wasm", +} + +func readFixture(t testing.TB, name string) (*File, []byte) { + t.Helper() + data, err := os.ReadFile(name) + if err != nil { + t.Fatal(err) + } + f := new(File) + if err := f.Read(bytes.NewReader(data)); err != nil { + t.Fatalf("%s: %v", name, err) + } + return f, data +} + +func TestRead_sectionsTileFile(t *testing.T) { + for _, name := range fixtures { + f, data := readFixture(t, name) + // Sections are contiguous from the header to the end of the file. + off := uint64(headerSize) + for i := 0; i < f.NumSections(); i++ { + s, _ := f.Section(i) + sh := s.SectionHeader() + if sh.Start != off { + t.Fatalf("%s: section %d starts at %d, want %d", name, i, sh.Start, off) + } + off = sh.End() + } + if off != uint64(len(data)) { + t.Fatalf("%s: sections end at %d, file is %d bytes", name, off, len(data)) + } + } +} + +func TestRead_sectionNames(t *testing.T) { + f, _ := readFixture(t, fixtures[0]) + for _, name := range []string{"type", "import", "code", "data", "name", ".debug_line", ".debug_str", ".debug_info", "producers"} { + s, err := f.SectionByName(name) + if err != nil { + t.Fatalf("%s: %v", name, err) + } + got, err := s.Name() + if err != nil || got != name { + t.Fatalf("SectionByName(%q).Name() = %q, %v", name, got, err) + } + } + if _, err := f.SectionByName(".debug_nonexistent"); !errors.Is(err, errNoSection) { + t.Fatalf("want errNoSection, got %v", err) + } + code, _ := f.SectionByID(SecCode) + if s, _ := f.SectionByName("code"); s != code { + t.Fatal("SectionByName and SectionByID disagree on code") + } + + nodebug, _ := readFixture(t, fixtures[1]) + if _, err := nodebug.SectionByName(".debug_line"); err == nil { + t.Fatal("-no-debug build has .debug_line") + } + if _, err := nodebug.SectionByName("name"); err != nil { + t.Fatal("-no-debug build lost its name section:", err) + } +} + +func TestAppendData(t *testing.T) { + f, data := readFixture(t, fixtures[0]) + for i := 0; i < f.NumSections(); i++ { + s, _ := f.Section(i) + sh := s.SectionHeader() + got, err := s.AppendData([]byte("prefix")) + if err != nil { + t.Fatal(err) + } + want := append([]byte("prefix"), data[sh.Offset:sh.End()]...) + if !bytes.Equal(got, want) { + t.Fatalf("section %d: AppendData mismatch", i) + } + } +} + +// funcNames walks f's functions and joins them with their names. +func funcNames(t testing.TB, f *File) (funcs []Func, names map[uint32]string) { + t.Helper() + var cr CodeReader + var nr NameReader + if err := cr.Reset(f, make([]byte, 64)); err != nil { + t.Fatal(err) + } + if err := nr.Reset(f, make([]byte, 256)); err != nil { + t.Fatal(err) + } + names = make(map[uint32]string) + for idx, name := range nr.FuncNames { + names[idx] = string(name) + } + if err := nr.Err(); err != nil { + t.Fatal(err) + } + for fn := range cr.Funcs { + funcs = append(funcs, fn) + } + if err := cr.Err(); err != nil { + t.Fatal(err) + } + return funcs, names +} + +func TestFuncs_tileCodeSection(t *testing.T) { + for _, name := range fixtures { + f, data := readFixture(t, name) + funcs, names := funcNames(t, f) + if len(funcs) == 0 || len(names) == 0 { + t.Fatalf("%s: %d funcs, %d names", name, len(funcs), len(names)) + } + code, _ := f.SectionByID(SecCode) + sh := code.SectionHeader() + _, countLen, _ := DecodeULEB128(data[sh.Offset:], 32) + // Bodies follow the count back to back and end exactly at the section end. + next := uint64(countLen) + for _, fn := range funcs { + if fn.Offset != next { + t.Fatalf("%s: func %d at %d, want %d", name, fn.Index, fn.Offset, next) + } + next = fn.End() + } + if next != sh.Size { + t.Fatalf("%s: bodies end at %d, code section is %d", name, next, sh.Size) + } + } +} + +// TestFuncs_matchesLLVM cross-checks function placement and naming against +// llvm-nm, which reports a function's address as the file offset of its size +// prefix. +func TestFuncs_matchesLLVM(t *testing.T) { + nm := llvmTool(t, "llvm-nm") + for _, name := range fixtures { + f, _ := readFixture(t, name) + funcs, names := funcNames(t, f) + code, _ := f.SectionByID(SecCode) + base := code.SectionHeader().Offset + + out, err := exec.Command(nm, name).Output() + if err != nil { + t.Fatal(err) + } + want := make(map[string]uint64) + sc := bufio.NewScanner(bytes.NewReader(out)) + for sc.Scan() { + fields := strings.SplitN(sc.Text(), " ", 3) + if len(fields) != 3 || (fields[1] != "t" && fields[1] != "T") { + continue + } + addr, err := strconv.ParseUint(fields[0], 16, 64) + if err != nil { + t.Fatal(err) + } + want[fields[2]] = addr + } + var matched int + for _, fn := range funcs { + fnName, ok := names[fn.Index] + if !ok { + continue + } + addr, ok := want[fnName] + if !ok { + t.Errorf("%s: %s not in llvm-nm output", name, fnName) + continue + } + if got := base + fn.Offset; got != addr { + t.Errorf("%s: %s at %#x, llvm-nm says %#x", name, fnName, got, addr) + } + matched++ + } + if matched != len(want) { + t.Errorf("%s: matched %d functions, llvm-nm lists %d", name, matched, len(want)) + } + } +} + +// llvmTool finds an LLVM tool on PATH or in a sibling TinyGo checkout, and +// skips the test if there is none. +func llvmTool(t *testing.T, name string) string { + t.Helper() + p := llvmToolOptional(name) + if p == "" { + t.Skip(name + " not found") + } + return p +} + +func TestNameReader_bufferTooSmall(t *testing.T) { + f, _ := readFixture(t, fixtures[0]) + var nr NameReader + if err := nr.Reset(f, make([]byte, maxSectionFrame)); err != nil { + t.Fatal(err) + } + for range nr.FuncNames { + } + var small BufferTooSmallError + if !errors.As(nr.Err(), &small) || small.Need <= small.Have { + t.Fatalf("want BufferTooSmallError, got %v", nr.Err()) + } + // Growing to what was asked for gets past that name. + if err := nr.Reset(f, make([]byte, small.Need)); err != nil { + t.Fatal(err) + } + var n int + for range nr.FuncNames { + n++ + } + if n == 0 { + t.Fatal("no names after growing buffer") + } +} + +// A caller breaking out of one name walk must not narrow the next: the name +// map bounds the cursor by its subsection while it walks, and that bound has to +// come off however the walk ends. +func TestNameReader_earlyStop(t *testing.T) { + f, _ := readFixture(t, fixtures[0]) + var nr NameReader + if err := nr.Reset(f, make([]byte, 256)); err != nil { + t.Fatal(err) + } + var want int + for range nr.DataNames { + want++ + } + if nr.Err() != nil || want == 0 { + t.Fatal("fixture has no data names:", nr.Err()) + } + for range nr.FuncNames { + break + } + var got int + for range nr.DataNames { + got++ + } + if nr.Err() != nil || got != want { + t.Fatalf("after early stop got %d data names, want %d (err %v)", got, want, nr.Err()) + } +} + +func TestAllocs(t *testing.T) { + data, err := os.ReadFile(fixtures[0]) + if err != nil { + t.Fatal(err) + } + r := bytes.NewReader(data) + var f File + var cr CodeReader + var nr NameReader + var dr DataReader + cbuf, nbuf, dbuf := make([]byte, 64), make([]byte, 256), make([]byte, 64) + walk := func() { + if err := f.Read(r); err != nil { + t.Fatal(err) + } + if err := cr.Reset(&f, cbuf); err != nil { + t.Fatal(err) + } + if err := nr.Reset(&f, nbuf); err != nil { + t.Fatal(err) + } + for range cr.Funcs { + } + for range nr.FuncNames { + } + for range nr.DataNames { + } + if err := dr.Reset(&f, dbuf); err != nil { + t.Fatal(err) + } + for range dr.Segments { + } + if cr.Err() != nil || nr.Err() != nil || dr.Err() != nil { + t.Fatal(cr.Err(), nr.Err(), dr.Err()) + } + if _, err := f.SectionByName(".debug_line"); err != nil { + t.Fatal(err) + } + } + walk() // Grow the section table. + if allocs := testing.AllocsPerRun(10, walk); allocs != 0 { + t.Fatalf("walk allocates %v times, want 0", allocs) + } +} + +func TestReadRejects(t *testing.T) { + good, err := os.ReadFile(fixtures[1]) + if err != nil { + t.Fatal(err) + } + hdr := []byte("\x00asm\x01\x00\x00\x00") + for _, tc := range []struct { + name string + data []byte + }{ + {"empty", nil}, + {"short header", hdr[:5]}, + {"bad magic", []byte("\x7fELF\x01\x00\x00\x00")}, + {"bad version", []byte("\x00asm\x02\x00\x00\x00")}, + {"unknown section id", append(hdr[:8:8], 14, 0)}, + {"section past EOF", append(hdr[:8:8], 1, 5, 0)}, + {"out of order", append(hdr[:8:8], 3, 0, 1, 0)}, + {"duplicate", append(hdr[:8:8], 1, 0, 1, 0)}, + {"custom name exceeds section", append(hdr[:8:8], 0, 1, 5)}, + {"overlong size LEB", append(hdr[:8:8], 1, 0x80, 0x80, 0x80, 0x80, 0x80, 0)}, + {"truncated", good[:len(good)-1]}, + } { + var f File + if err := f.Read(bytes.NewReader(tc.data)); err == nil { + t.Errorf("%s: no error", tc.name) + } else if f.NumSections() != 0 { + t.Errorf("%s: failed Read left %d sections", tc.name, f.NumSections()) + } + } + // Tag sorts between memory and global despite its id, and datacount + // between element and code. + var f File + ordered := append(hdr[:8:8], 5, 0, 13, 0, 6, 0, 9, 0, 12, 0, 10, 0) + if err := f.Read(bytes.NewReader(ordered)); err != nil { + t.Fatal("spec section order rejected:", err) + } + // A header-only module is valid and empty. + if err := f.Read(bytes.NewReader(hdr)); err != nil || f.NumSections() != 0 { + t.Fatal("empty module:", err, f.NumSections()) + } +} + +func TestHeaderRoundTrip(t *testing.T) { + var b [headerSize]byte + n, err := Header{Version: 1}.Put(b[:]) + if err != nil || n != headerSize { + t.Fatal(n, err) + } + h, n, err := DecodeHeader(b[:]) + if err != nil || n != headerSize || h.Version != 1 { + t.Fatal(h, n, err) + } +} + +func TestDecodeULEB128(t *testing.T) { + for _, tc := range []struct { + b []byte + bits int + v uint64 + n int + err error + }{ + {[]byte{0}, 32, 0, 1, nil}, + {[]byte{0x7f}, 32, 127, 1, nil}, + {[]byte{0x80, 0x01}, 32, 128, 2, nil}, + {[]byte{0xff, 0xff, 0xff, 0xff, 0x0f}, 32, 1<<32 - 1, 5, nil}, + {[]byte{0x80, 0x80, 0x80, 0x80, 0x00}, 32, 0, 5, nil}, // Padded zero is legal. + {[]byte{0xff, 0xff, 0xff, 0xff, 0x1f}, 32, 0, 5, errLEBOverflow}, + {[]byte{0x80, 0x80, 0x80, 0x80, 0x80, 0x00}, 32, 0, 5, errLEBOverflow}, + {[]byte{0x80}, 32, 0, 1, io.ErrUnexpectedEOF}, + {[]byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01}, 64, 1<<64 - 1, 10, nil}, + {[]byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x02}, 64, 0, 10, errLEBOverflow}, + {[]byte{0x7f}, 7, 127, 1, nil}, + {[]byte{0x80, 0x00}, 7, 0, 1, errLEBTooLong}, + } { + v, n, err := DecodeULEB128(tc.b, tc.bits) + if v != tc.v || n != tc.n || err != tc.err { + t.Errorf("DecodeULEB128(%x, %d) = %d, %d, %v; want %d, %d, %v", tc.b, tc.bits, v, n, err, tc.v, tc.n, tc.err) + } + // The streaming decoder agrees on value and validity. + var c cursor + c.reset(bytes.NewReader(tc.b), make([]byte, 16), 0, int64(len(tc.b))) + sv := c.uleb(tc.bits) + if (c.err == nil) != (tc.err == nil) || sv != tc.v { + t.Errorf("cursor.uleb(%x, %d) = %d, %v; want %d, %v", tc.b, tc.bits, sv, c.err, tc.v, tc.err) + } + } +} + +func TestDecodeSLEB128(t *testing.T) { + for _, tc := range []struct { + b []byte + bits int + v int64 + n int + err error + }{ + {[]byte{0}, 32, 0, 1, nil}, + {[]byte{0x7f}, 32, -1, 1, nil}, + {[]byte{0x3f}, 32, 63, 1, nil}, + {[]byte{0x40}, 32, -64, 1, nil}, + {[]byte{0x80, 0x7f}, 32, -128, 2, nil}, + {[]byte{0xff, 0xff, 0xff, 0xff, 0x07}, 32, 1<<31 - 1, 5, nil}, + {[]byte{0x80, 0x80, 0x80, 0x80, 0x78}, 32, -1 << 31, 5, nil}, + {[]byte{0xff, 0xff, 0xff, 0xff, 0x0f}, 32, 0, 5, errLEBOverflow}, // 2^32-1 as s32. + {[]byte{0x80, 0x80, 0x80, 0x80, 0x70}, 32, 0, 5, errLEBOverflow}, + {[]byte{0x80, 0x80, 0x80, 0x80, 0x80}, 32, 0, 5, errLEBTooLong}, + {[]byte{0x80, 0x80, 0x80, 0x80, 0x70}, 33, -1 << 32, 5, nil}, + {[]byte{0x80, 0x80, 0x80, 0x80, 0x7f}, 33, -1 << 28, 5, nil}, + {[]byte{0x80, 0x80, 0x80, 0x80, 0x60}, 33, 0, 5, errLEBOverflow}, + {[]byte{0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x7f}, 64, -1 << 63, 10, nil}, + {[]byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00}, 64, 1<<63 - 1, 10, nil}, + {[]byte{0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x01}, 64, 0, 10, errLEBOverflow}, + {[]byte{0xc0}, 32, 0, 1, io.ErrUnexpectedEOF}, + } { + v, n, err := DecodeSLEB128(tc.b, tc.bits) + if v != tc.v || n != tc.n || err != tc.err { + t.Errorf("DecodeSLEB128(%x, %d) = %d, %d, %v; want %d, %d, %v", tc.b, tc.bits, v, n, err, tc.v, tc.n, tc.err) + } + var c cursor + c.reset(bytes.NewReader(tc.b), make([]byte, 16), 0, int64(len(tc.b))) + sv := c.sleb(tc.bits) + if (c.err == nil) != (tc.err == nil) || sv != tc.v { + t.Errorf("cursor.sleb(%x, %d) = %d, %v; want %d, %v", tc.b, tc.bits, sv, c.err, tc.v, tc.err) + } + } +} + +func TestDataSegments(t *testing.T) { + for _, name := range fixtures { + f, data := readFixture(t, name) + var dr DataReader + var nr NameReader + if err := dr.Reset(f, make([]byte, 16)); err != nil { + t.Fatal(err) + } + if err := nr.Reset(f, make([]byte, 256)); err != nil { + t.Fatal(err) + } + names := make(map[uint32]string) + for idx, n := range nr.DataNames { + names[idx] = string(n) + } + if nr.Err() != nil { + t.Fatal(nr.Err()) + } + sec, _ := f.SectionByID(SecData) + sh := sec.SectionHeader() + _, countLen, _ := DecodeULEB128(data[sh.Offset:], 32) + next := uint64(countLen) + addrs := make(map[string]uint64) + for seg := range dr.Segments { + if seg.Offset != next { + t.Fatalf("%s: segment %d at %d, want %d", name, seg.Index, seg.Offset, next) + } + next = seg.End() + if !seg.AddrKnown { + t.Errorf("%s: segment %d has no constant address", name, seg.Index) + } + addrs[names[seg.Index]] = seg.Addr + } + if dr.Err() != nil { + t.Fatal(dr.Err()) + } + if next != sh.Size { + t.Fatalf("%s: segments end at %d, data section is %d", name, next, sh.Size) + } + // wasm-ld names segments after their output sections, and llvm-nm + // reports each at its linear-memory address. + for _, seg := range []string{".rodata", ".data"} { + if _, ok := addrs[seg]; !ok { + t.Errorf("%s: no %s segment among %v", name, seg, names) + } + } + nm := llvmToolOptional("llvm-nm") + if nm == "" { + continue + } + out, err := exec.Command(nm, name).Output() + if err != nil { + t.Fatal(err) + } + sc := bufio.NewScanner(bytes.NewReader(out)) + for sc.Scan() { + fields := strings.SplitN(sc.Text(), " ", 3) + if len(fields) != 3 || fields[1] != "d" { + continue + } + want, _ := strconv.ParseUint(fields[0], 16, 64) + if got, ok := addrs[fields[2]]; ok && got != want { + t.Errorf("%s: segment %s at %#x, llvm-nm says %#x", name, fields[2], got, want) + } + } + } +} + +func TestConstExpr(t *testing.T) { + for _, tc := range []struct { + expr []byte + v uint64 + known bool + fail bool + }{ + {[]byte{opI32Const, 0x80, 0x80, 0x04, opEnd}, 0x10000, true, false}, + {[]byte{opI32Const, 0x7f, opEnd}, 0xffffffff, true, false}, // -1 as an i32 address. + {[]byte{opI64Const, 0x10, opEnd}, 16, true, false}, + {[]byte{opGlobalGet, 0x01, opEnd}, 0, false, false}, + // Extended-const arithmetic is skipped, not evaluated. + {[]byte{opGlobalGet, 0x00, opI32Const, 0x10, opI32Add, opEnd}, 0, false, false}, + {[]byte{opPrefixSIMD, simdV128Const, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, opEnd}, 0, false, false}, + {[]byte{0x20, 0x00, opEnd}, 0, false, true}, // local.get is not constant. + {[]byte{opI32Const, 0x10}, 0, false, true}, // Missing end. + } { + var c cursor + c.reset(bytes.NewReader(tc.expr), make([]byte, 32), 0, int64(len(tc.expr))) + v, known := c.constExpr() + if (c.err != nil) != tc.fail || v != tc.v || known != tc.known { + t.Errorf("constExpr(%x) = %d, %v, %v; want %d, %v, fail=%v", tc.expr, v, known, c.err, tc.v, tc.known, tc.fail) + } + } +} + +func llvmToolOptional(name string) string { + if p, err := exec.LookPath(name); err == nil { + return p + } + p := filepath.Join("..", "..", "..", "tinygo", "llvm-build", "bin", name) + if _, err := os.Stat(p); err == nil { + return p + } + return "" +} diff --git a/cmd/bindiff/dwarf.go b/cmd/bindiff/dwarf.go index 990200e..fb9172c 100644 --- a/cmd/bindiff/dwarf.go +++ b/cmd/bindiff/dwarf.go @@ -72,13 +72,16 @@ func buildLineIndex(sec xdwarf.Sections, size int64, aux []byte) (*lineIndex, er for r := range u.Rows { flush(r.Address) if r.EndSequence { - break + // A unit may hold many sequences -- LLVM's WebAssembly backend + // emits one per function -- so this ends a sequence, not the + // unit. + continue } nameBuf, err = u.AppendFileName(nameBuf[:0], r.File) if err != nil { // A row naming a file outside the table is not worth failing - // the whole binary over; it lands in the remainder instead. - break + // the whole binary over; its bytes land in the remainder. + continue } name := xdwarf.CleanPath(string(nameBuf)) canonical, ok := interned[name] @@ -90,6 +93,9 @@ func buildLineIndex(sec xdwarf.Sections, size int64, aux []byte) (*lineIndex, er havePending = true } flush(0) // A sequence that never ended contributes nothing. + if err := u.Err(); err != nil { + return nil, fmt.Errorf("line unit at %d: %w", off, err) + } off = next } sort.Slice(idx.ranges, func(i, j int) bool { @@ -210,7 +216,12 @@ func profileSource(f *xelf.File, mem, byLine bool) ([]Entry, error) { if err != nil { return nil, err } + return attributeSource(t, idx, byLine), nil +} +// attributeSource splits each placement's bytes among the source ranges that +// cover it. It is the format-independent half of [profileSource]. +func attributeSource(t tiling, idx *lineIndex, byLine bool) []Entry { kind := KindFile if byLine { kind = KindLine @@ -230,6 +241,12 @@ func profileSource(f *xelf.File, mem, byLine bool) ([]Entry, error) { if p.size == 0 { continue } + if t.lineSection != "" && p.section != t.lineSection { + // Not in the line table's address space: its address would alias + // unrelated code. + unmapped[p.section] += p.size + continue + } var covered int64 idx.visitOverlaps(p.addr, p.addr+uint64(p.size), func(r srcRange, n int64) { k := key{file: r.file} @@ -269,7 +286,7 @@ func profileSource(f *xelf.File, mem, byLine bool) ([]Entry, error) { }) } } - return entries, nil + return entries } func sourceName(file string, line uint32, byLine bool) string { diff --git a/cmd/bindiff/main.go b/cmd/bindiff/main.go index f380145..9f2b1cf 100644 --- a/cmd/bindiff/main.go +++ b/cmd/bindiff/main.go @@ -1,5 +1,5 @@ -// Command bindiff characterizes where the bytes of an ELF binary go, and what -// changed between two builds of it. +// Command bindiff characterizes where the bytes of an ELF binary or a +// WebAssembly module go, and what changed between two builds of it. // // It reports at several granularities, from whole segments down to source // lines, and every granularity reconciles: the rows of a report sum to the size @@ -10,6 +10,7 @@ // bindiff -kind=package profile firmware.elf // bindiff -json diff old.elf new.elf // bindiff -threshold=1024 diff old.elf new.elf # exits non-zero on growth +// bindiff -kind=line profile app.wasm package main import ( @@ -105,6 +106,9 @@ func profileFile(path string, flags Flags) ([]Entry, error) { } func profileReader(r io.ReaderAt, size int64, flags Flags) ([]Entry, error) { + if isWasm(r) { + return profileWasm(r, size, flags) + } var f xelf.File if err := f.Read(r); err != nil { return nil, err diff --git a/cmd/bindiff/symbols.go b/cmd/bindiff/symbols.go index 7169661..259edb9 100644 --- a/cmd/bindiff/symbols.go +++ b/cmd/bindiff/symbols.go @@ -24,6 +24,74 @@ type tiling struct { remainder map[string]int64 // order preserves section order for deterministic output. order []string + // lineSection, when set, names the only section whose placement addresses + // are in the address space the DWARF line table describes. An ELF binary + // has one address space for everything, and leaves it empty. A WebAssembly + // module does not: line addresses are Code section offsets, and a data + // segment's offset in its own section would alias them. + lineSection string +} + +// claim is one symbol's stake in a section, before overlapping claims are +// resolved. +type claim struct { + name string + start, size uint64 +} + +func newTiling(nsect, nsyms int) tiling { + return tiling{ + placements: make([]placement, 0, nsyms), + remainder: make(map[string]int64, nsect), + order: make([]string, 0, nsect), + } +} + +// tileSection divides a section of size bytes among claims and records what +// they leave over. A section with no claims and no bytes is left out. +func (t *tiling) tileSection(section string, size int64, claims []claim) { + if len(claims) == 0 { + if size > 0 { + t.order = append(t.order, section) + t.remainder[section] = size + } + return + } + t.order = append(t.order, section) + // Tile the section in address order, clamping each symbol so that + // overlapping symbols -- weak aliases and ifunc pairs share an address -- + // are counted once. Without this, a section's symbols can sum past its own + // size and drive the remainder negative. + sort.Slice(claims, func(i, j int) bool { + if claims[i].start != claims[j].start { + return claims[i].start < claims[j].start + } + return claims[i].size > claims[j].size + }) + var cursor uint64 + var attributed int64 + for i, p := range claims { + if i == 0 { + cursor = p.start + } + start, end := p.start, p.start+p.size + if start < cursor { + start = cursor + } + var size int64 + if end > start { + size = int64(end - start) + cursor = end + } + attributed += size + t.placements = append(t.placements, placement{ + name: p.name, + section: section, + addr: start, + size: size, + }) + } + t.remainder[section] = size - attributed } // profileSymbols attributes bytes to individual functions and data objects. @@ -37,6 +105,11 @@ func profileSymbols(f *xelf.File, mem bool) ([]Entry, error) { if err != nil { return nil, err } + return symbolEntries(t), nil +} + +// symbolEntries lists a tiling's placements, then each section's remainder. +func symbolEntries(t tiling) []Entry { entries := make([]Entry, 0, len(t.placements)+len(t.order)) for _, p := range t.placements { entries = append(entries, Entry{ @@ -54,7 +127,7 @@ func profileSymbols(f *xelf.File, mem bool) ([]Entry, error) { }) } } - return entries, nil + return entries } // tileSymbols divides each section's bytes among the symbols that land in it. @@ -92,11 +165,7 @@ func tileSymbols(f *xelf.File, mem bool) (tiling, error) { } // Bucket symbols by section so each section can be tiled independently. - type placed struct { - name string - start, size uint64 - } - bySection := make(map[int][]placed) + bySection := make(map[int][]claim) var strBuf []byte for _, sym := range syms { typ := xelf.SymType(sym.Info & 0xf) @@ -118,60 +187,16 @@ func tileSymbols(f *xelf.File, mem bool) (tiling, error) { if len(strBuf) == 0 { continue } - bySection[idx] = append(bySection[idx], placed{ + bySection[idx] = append(bySection[idx], claim{ name: string(strBuf), start: sym.Value, size: sym.Size, }) } - t.placements = make([]placement, 0, len(syms)) - t.remainder = make(map[string]int64, nsect) - t.order = make([]string, 0, nsect) + t = newTiling(nsect, len(syms)) for idx := 0; idx < nsect; idx++ { - placedSyms := bySection[idx] - if len(placedSyms) == 0 { - if secSizes[idx] > 0 { - t.order = append(t.order, secNames[idx]) - t.remainder[secNames[idx]] = secSizes[idx] - } - continue - } - t.order = append(t.order, secNames[idx]) - // Tile the section in address order, clamping each symbol so that - // overlapping symbols -- weak aliases and ifunc pairs share an address - // -- are counted once. Without this, a section's symbols can sum past - // its own size and drive the remainder negative. - sort.Slice(placedSyms, func(i, j int) bool { - if placedSyms[i].start != placedSyms[j].start { - return placedSyms[i].start < placedSyms[j].start - } - return placedSyms[i].size > placedSyms[j].size - }) - var cursor uint64 - var attributed int64 - for i, p := range placedSyms { - if i == 0 { - cursor = p.start - } - start, end := p.start, p.start+p.size - if start < cursor { - start = cursor - } - var size int64 - if end > start { - size = int64(end - start) - cursor = end - } - attributed += size - t.placements = append(t.placements, placement{ - name: p.name, - section: secNames[idx], - addr: start, - size: size, - }) - } - t.remainder[secNames[idx]] = secSizes[idx] - attributed + t.tileSection(secNames[idx], secSizes[idx], bySection[idx]) } return t, nil } @@ -184,6 +209,11 @@ func profilePackages(f *xelf.File, mem bool) ([]Entry, error) { if err != nil { return nil, err } + return packageEntries(syms), nil +} + +// packageEntries rolls symbol entries up by package. +func packageEntries(syms []Entry) []Entry { order := make([]string, 0, 32) byPkg := make(map[string]*Entry, 32) for _, e := range syms { @@ -200,7 +230,7 @@ func profilePackages(f *xelf.File, mem bool) ([]Entry, error) { for _, name := range order { entries = append(entries, *byPkg[name]) } - return entries, nil + return entries } // Buckets for symbols that name no package of their own. diff --git a/cmd/bindiff/testdata/src/gen.go b/cmd/bindiff/testdata/src/gen.go index cae0b4e..00f1090 100644 --- a/cmd/bindiff/testdata/src/gen.go +++ b/cmd/bindiff/testdata/src/gen.go @@ -2,3 +2,8 @@ package src //go:generate tinygo build -o "../blinky-a.elf" -target=pca10040 "./a //go:generate tinygo build -o "../blinky-b.elf" -target=pca10040 "./b + +// WebAssembly fixtures, shared with build/xwasm. One carries DWARF and one is +// built without it, leaving only the name section. +//go:generate tinygo build -o "../../../../testdata/hello.wasm" -target=wasip1 "./hellowasm" +//go:generate tinygo build -o "../../../../testdata/hello-nodebug.wasm" -target=wasip1 -no-debug "./hellowasm" diff --git a/cmd/bindiff/testdata/src/hellowasm/main.go b/cmd/bindiff/testdata/src/hellowasm/main.go new file mode 100644 index 0000000..73a0326 --- /dev/null +++ b/cmd/bindiff/testdata/src/hellowasm/main.go @@ -0,0 +1,7 @@ +package main + +func add(a, b int) int { return a + b } + +func main() { + println("hello", add(1, 2)) +} diff --git a/cmd/bindiff/wasm.go b/cmd/bindiff/wasm.go new file mode 100644 index 0000000..a9f97fe --- /dev/null +++ b/cmd/bindiff/wasm.go @@ -0,0 +1,223 @@ +package main + +import ( + "encoding/binary" + "errors" + "fmt" + "io" + + "github.com/soypat/tinyboot/build/xdwarf" + "github.com/soypat/tinyboot/build/xwasm" +) + +var errWasmNoDebugLine = errors.New("WebAssembly module has no .debug_line section") + +// isWasm reports whether r holds a WebAssembly module rather than an ELF. +func isWasm(r io.ReaderAt) bool { + var magic [4]byte + n, _ := r.ReadAt(magic[:], 0) + return n == len(magic) && string(magic[:]) == "\x00asm" +} + +// profileWasm is profileReader for a WebAssembly module. +// +// A module has no segments and no load addresses: its code runs from the Code +// section and its linear memory is sized at run time. The segment kind and +// memory mode have no counterpart, and report so rather than guess. +func profileWasm(r io.ReaderAt, size int64, flags Flags) ([]Entry, error) { + var f xwasm.File + if err := f.Read(r); err != nil { + return nil, err + } + if flags.mem { + return nil, errors.New("-mem is not supported for WebAssembly: linear memory is sized at run time") + } + switch flags.kind { + case KindSection: + return profileWasmSections(&f, size) + case KindSymbol, KindPackage, KindFile, KindLine: + default: + return nil, fmt.Errorf("granularity %q does not apply to WebAssembly", flags.kind) + } + t, err := tileWasm(&f) + if err != nil { + return nil, err + } + switch flags.kind { + case KindSymbol: + return symbolEntries(t), nil + case KindPackage: + return packageEntries(symbolEntries(t)), nil + } + idx, err := loadWasmLineIndex(&f) + if err != nil { + return nil, err + } + return attributeSource(t, idx, flags.kind == KindLine), nil +} + +// profileWasmSections attributes a module's bytes to its sections, custom ones +// by their own name. The entries sum exactly to the size of the file: each +// section's framing -- id, size and a custom section's name -- is collected, +// with the module header, under [wasm-headers]. +func profileWasmSections(f *xwasm.File, fileSize int64) ([]Entry, error) { + nsect := f.NumSections() + entries := make([]Entry, 0, nsect+2) + overhead := int64(f.Header().HeaderSize()) + covered := overhead + var name []byte + for i := 0; i < nsect; i++ { + s, err := f.Section(i) + if err != nil { + return nil, err + } + sh := s.SectionHeader() + name, err = s.AppendName(name[:0]) + if err != nil { + return nil, fmt.Errorf("section %d name: %w", i, err) + } + entries = append(entries, Entry{Kind: KindSection, Name: string(name), New: int64(sh.Size)}) + overhead += int64(sh.FrameSize()) + covered += int64(sh.FrameSize() + sh.Size) + } + entries = append(entries, Entry{Kind: KindSection, Name: "[wasm-headers]", New: overhead}) + // Sections tile a module exactly, so this only fires for trailing bytes + // xwasm would have rejected; it is kept so the total cannot silently lie. + if rem := fileSize - covered; rem != 0 { + entries = append(entries, Entry{Kind: KindSection, Name: Unattributed, New: rem}) + } + return entries, nil +} + +// tileWasm divides a module's sections among its functions and data segments. +// Every other section has no symbols and goes wholly to its remainder, as a +// symbol-less ELF section does. +// +// Code placements are Code section offsets and cover a body with its size +// prefix, so only the section's leading function count is left unattributed. +// That is the address space DWARF uses, so the line table can be laid over +// them directly. +func tileWasm(f *xwasm.File) (tiling, error) { + funcNames, err := readNameMap(f, (*xwasm.NameReader).FuncNames) + if err != nil { + return tiling{}, fmt.Errorf("function names: %w", err) + } + dataNames, err := readNameMap(f, (*xwasm.NameReader).DataNames) + if err != nil { + return tiling{}, fmt.Errorf("data segment names: %w", err) + } + var cr xwasm.CodeReader + var dr xwasm.DataReader + // Each reader owns its window; sharing one would let either walk clobber + // the bytes the other believes resident. + var cbuf, dbuf [512]byte + if err := cr.Reset(f, cbuf[:]); err != nil { + return tiling{}, err + } + if err := dr.Reset(f, dbuf[:]); err != nil { + return tiling{}, err + } + + nsect := f.NumSections() + t := newTiling(nsect, int(cr.NumFuncs())+int(dr.NumSegments())) + t.lineSection = xwasm.SecCode.String() + var nameBuf []byte + var claims []claim + for i := 0; i < nsect; i++ { + s, err := f.Section(i) + if err != nil { + return t, err + } + sh := s.SectionHeader() + nameBuf, err = s.AppendName(nameBuf[:0]) + if err != nil { + return t, err + } + claims = claims[:0] + switch sh.ID { + case xwasm.SecCode: + for fn := range cr.Funcs { + claims = append(claims, claim{ + name: nameOr(funcNames, fn.Index, "func"), + start: fn.Offset, + size: fn.End() - fn.Offset, + }) + } + err = cr.Err() + case xwasm.SecData: + for seg := range dr.Segments { + claims = append(claims, claim{ + name: nameOr(dataNames, seg.Index, "data"), + start: seg.Offset, + size: seg.End() - seg.Offset, + }) + } + err = dr.Err() + } + if err != nil { + return t, fmt.Errorf("%s section: %w", sh.ID, err) + } + t.tileSection(string(nameBuf), int64(sh.Size), claims) + } + return t, nil +} + +// nameOr returns the name the name section gives index, or a placeholder +// naming the index space when it gives none. +func nameOr(names map[uint32]string, index uint32, space string) string { + if name, ok := names[index]; ok { + return name + } + return fmt.Sprintf("%s[%d]", space, index) +} + +// readNameMap collects one of the name section's maps, growing the read +// buffer until it holds the longest name. A module without a name section +// yields an empty map. +func readNameMap(f *xwasm.File, walk func(*xwasm.NameReader, func(uint32, []byte) bool)) (map[uint32]string, error) { + buf := make([]byte, 256) + for { + var nr xwasm.NameReader + if err := nr.Reset(f, buf); err != nil { + return nil, err + } + names := make(map[uint32]string) + walk(&nr, func(idx uint32, name []byte) bool { + names[idx] = string(name) + return true + }) + var small xwasm.BufferTooSmallError + if errors.As(nr.Err(), &small) { + buf = make([]byte, small.Need) + continue + } + return names, nr.Err() + } +} + +// loadWasmLineIndex builds the line index from a module's DWARF custom +// sections. They are stored uncompressed and, in a linked module, already +// relocated, so unlike an ELF they are read in place with no preparation. +func loadWasmLineIndex(f *xwasm.File) (*lineIndex, error) { + line, err := f.SectionByName(".debug_line") + if err != nil { + return nil, errWasmNoDebugLine + } + sec := xdwarf.Sections{Line: line.Open(), ByteOrder: binary.LittleEndian} + if s, err := f.SectionByName(".debug_str"); err == nil { + sec.Str = s.Open() + } + if s, err := f.SectionByName(".debug_line_str"); err == nil { + sec.LineStr = s.Open() + } + aux := make([]byte, defaultAux) + for { + idx, err := buildLineIndex(sec, line.Size(), aux) + var small xdwarf.AuxTooSmallError + if errors.As(err, &small) { + aux = make([]byte, small.Need) + continue + } + return idx, err + } +} diff --git a/cmd/bindiff/wasm_test.go b/cmd/bindiff/wasm_test.go new file mode 100644 index 0000000..0d6564a --- /dev/null +++ b/cmd/bindiff/wasm_test.go @@ -0,0 +1,210 @@ +package main + +import ( + "bufio" + "bytes" + "errors" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "testing" +) + +var wasmFixtures = []string{ + "../../testdata/hello.wasm", + "../../testdata/hello-nodebug.wasm", +} + +// wasmDebugFixture is the module carrying DWARF, which the source kinds need. +const wasmDebugFixture = "../../testdata/hello.wasm" + +var wasmKinds = []Kind{KindSection, KindSymbol, KindPackage, KindFile, KindLine} + +func wasmKindsFor(name string) []Kind { + if name == wasmDebugFixture { + return wasmKinds + } + return wasmKinds[:3] +} + +// TestWasmSectionProfileReconciles is TestSectionProfileReconciles for a +// module: sections plus their framing account for every byte of the file. +func TestWasmSectionProfileReconciles(t *testing.T) { + for _, name := range wasmFixtures { + entries, size := profileFixture(t, name, Flags{kind: KindSection}) + _, total := Total(entries) + if total != size { + t.Errorf("%s: section profile totals %d, file is %d bytes", name, total, size) + } + for _, e := range entries { + if e.Name == Unattributed { + t.Errorf("%s: %d bytes no section claims", name, e.New) + } + } + } +} + +// TestWasmKindsAgreeOnTotal: every kind below section describes the same bytes, +// the section contents, which is the section total less the framing. +func TestWasmKindsAgreeOnTotal(t *testing.T) { + for _, name := range wasmFixtures { + sections, _ := profileFixture(t, name, Flags{kind: KindSection}) + var want int64 + for _, e := range sections { + if e.Name != "[wasm-headers]" { + want += e.New + } + } + for _, kind := range wasmKindsFor(name)[1:] { + entries, _ := profileFixture(t, name, Flags{kind: kind}) + if _, total := Total(entries); total != want { + t.Errorf("%s: kind %v totals %d, section contents total %d", name, kind, total, want) + } + } + } +} + +// TestWasmSymbolProfileReconciles checks each section's symbols plus remainder +// against the section, and that functions leave only the Code section's +// leading count unattributed. +func TestWasmSymbolProfileReconciles(t *testing.T) { + for _, name := range wasmFixtures { + sections, _ := profileFixture(t, name, Flags{kind: KindSection}) + entries, _ := profileFixture(t, name, Flags{kind: KindSymbol}) + bySection := make(map[string]int64) + for _, e := range entries { + if e.New < 0 { + t.Errorf("%s: negative size %d for %q", name, e.New, e.Name) + } + bySection[e.Section] += e.New + if e.Name == Unattributed && (e.Section == "code" || e.Section == "data") && e.New > 5 { + t.Errorf("%s: %d bytes of %s unattributed, want only the vector count", name, e.New, e.Section) + } + } + for _, s := range sections { + if s.Name == "[wasm-headers]" { + continue + } + if got := bySection[s.Name]; got != s.New { + t.Errorf("%s: section %q: symbols total %d, section is %d bytes", name, s.Name, got, s.New) + } + } + var hasStart bool + for _, e := range entries { + hasStart = hasStart || e.Name == "_start" + } + if !hasStart { + t.Errorf("%s: no _start among function symbols", name) + } + } +} + +// TestWasmSelfDiffIsEmpty mirrors TestSelfDiffIsEmpty. +func TestWasmSelfDiffIsEmpty(t *testing.T) { + for _, name := range wasmFixtures { + for _, kind := range wasmKindsFor(name) { + entries, _ := profileFixture(t, name, Flags{kind: kind}) + if n := len(DropUnchanged(Diff(entries, entries))); n != 0 { + t.Errorf("%s/%v: self-diff left %d changed rows", name, kind, n) + } + } + } +} + +// TestWasmCrossBuildDiff diffs the debug build against the -no-debug one. +// TinyGo's -no-debug also changes code generation, so the builds genuinely +// differ; what must hold is that the diff's sides are exactly the two profiles. +func TestWasmCrossBuildDiff(t *testing.T) { + for _, kind := range wasmKinds[:3] { + flags := Flags{kind: kind} + a, _ := profileFixture(t, wasmFixtures[0], flags) + b, _ := profileFixture(t, wasmFixtures[1], flags) + _, wantOld := Total(a) + _, wantNew := Total(b) + old, new := Total(Diff(a, b)) + if old != wantOld || new != wantNew { + t.Errorf("%v: diff totals old=%d new=%d, profiles total %d and %d", kind, old, new, wantOld, wantNew) + } + } +} + +func TestWasmUnsupported(t *testing.T) { + for _, flags := range []Flags{ + {kind: KindSegment}, + {kind: KindSection, mem: true}, + } { + fp, err := os.Open(wasmDebugFixture) + if err != nil { + t.Fatal(err) + } + _, err = profileReader(fp, 0, flags) + fp.Close() + if err == nil { + t.Errorf("%+v: no error", flags) + } + } + fp, err := os.Open(wasmFixtures[1]) + if err != nil { + t.Fatal(err) + } + defer fp.Close() + if _, err := profileReader(fp, 0, Flags{kind: KindFile}); !errors.Is(err, errWasmNoDebugLine) { + t.Errorf("module without DWARF: got %v, want %v", err, errWasmNoDebugLine) + } +} + +// TestWasmLineCoverageMatchesLLVM checks the source kinds attribute exactly the +// code bytes llvm-dwarfdump's line table covers: each row runs to the next row +// of its sequence. +func TestWasmLineCoverageMatchesLLVM(t *testing.T) { + dump := llvmTool(t, "llvm-dwarfdump") + out, err := exec.Command(dump, "--debug-line", wasmDebugFixture).Output() + if err != nil { + t.Fatal(err) + } + var want, prev uint64 + var open bool + sc := bufio.NewScanner(bytes.NewReader(out)) + for sc.Scan() { + line := sc.Text() + if !strings.HasPrefix(line, "0x") || len(line) < 18 || line[18] != ' ' { + continue // Not a row of the line matrix. + } + addr, err := strconv.ParseUint(line[2:18], 16, 64) + if err != nil { + t.Fatal(err) + } + if open && addr > prev { + want += addr - prev + } + prev, open = addr, !strings.Contains(line, "end_sequence") + } + if want == 0 { + t.Fatal("parsed no line table coverage from llvm-dwarfdump") + } + entries, _ := profileFixture(t, wasmDebugFixture, Flags{kind: KindFile}) + var got int64 + for _, e := range entries { + if e.Name != Unattributed { + got += e.New + } + } + if uint64(got) != want { + t.Errorf("file kind attributes %d code bytes, llvm-dwarfdump's line table covers %d", got, want) + } +} + +func llvmTool(t *testing.T, name string) string { + t.Helper() + if p, err := exec.LookPath(name); err == nil { + return p + } + p := filepath.Join("..", "..", "..", "tinygo", "llvm-build", "bin", name) + if _, err := os.Stat(p); err == nil { + return p + } + t.Skip(name + " not found") + return "" +} diff --git a/testdata/hello-nodebug.wasm b/testdata/hello-nodebug.wasm new file mode 100644 index 0000000000000000000000000000000000000000..4c4c0d00e8ccda2c53820043a1ece24903846c25 GIT binary patch literal 20581 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 literal 0 HcmV?d00001 From f2f5c31b5d46285d8ea54e5a53abab364207e316 Mon Sep 17 00:00:00 2001 From: Patricio Whittingslow Date: Thu, 24 Sep 2026 10:22:49 -0300 Subject: [PATCH 2/2] remove extraneous wasm large binary tests --- build/xwasm/xwasm_test.go | 18 +- cmd/bindiff/bindiff_test.go | 4 + cmd/bindiff/symbols.go | 26 +- cmd/bindiff/testdata/src/gen.go | 14 +- cmd/bindiff/testdata/src/hellowasm/main.go | 7 - cmd/bindiff/testdata/src/tinywasm/main.go | 26 ++ cmd/bindiff/wasm.go | 89 ++++++- cmd/bindiff/wasm_test.go | 266 +++++++++++---------- testdata/hello-nodebug.wasm | Bin 20581 -> 0 bytes testdata/hello.wasm | Bin 118060 -> 0 bytes testdata/tiny.wasm | Bin 0 -> 31089 bytes 11 files changed, 292 insertions(+), 158 deletions(-) delete mode 100644 cmd/bindiff/testdata/src/hellowasm/main.go create mode 100644 cmd/bindiff/testdata/src/tinywasm/main.go delete mode 100644 testdata/hello-nodebug.wasm delete mode 100644 testdata/hello.wasm create mode 100755 testdata/tiny.wasm diff --git a/build/xwasm/xwasm_test.go b/build/xwasm/xwasm_test.go index f8bd0a4..ca69e5e 100644 --- a/build/xwasm/xwasm_test.go +++ b/build/xwasm/xwasm_test.go @@ -13,10 +13,10 @@ import ( "testing" ) -var fixtures = []string{ - "../../testdata/hello.wasm", - "../../testdata/hello-nodebug.wasm", -} +// fixtures is the single TinyGo module the tests read: small, but with a host +// import, data segments, a name section and DWARF. See +// cmd/bindiff/testdata/src/gen.go. +var fixtures = []string{"../../testdata/tiny.wasm"} func readFixture(t testing.TB, name string) (*File, []byte) { t.Helper() @@ -69,14 +69,6 @@ func TestRead_sectionNames(t *testing.T) { if s, _ := f.SectionByName("code"); s != code { t.Fatal("SectionByName and SectionByID disagree on code") } - - nodebug, _ := readFixture(t, fixtures[1]) - if _, err := nodebug.SectionByName(".debug_line"); err == nil { - t.Fatal("-no-debug build has .debug_line") - } - if _, err := nodebug.SectionByName("name"); err != nil { - t.Fatal("-no-debug build lost its name section:", err) - } } func TestAppendData(t *testing.T) { @@ -306,7 +298,7 @@ func TestAllocs(t *testing.T) { } func TestReadRejects(t *testing.T) { - good, err := os.ReadFile(fixtures[1]) + good, err := os.ReadFile(fixtures[0]) if err != nil { t.Fatal(err) } diff --git a/cmd/bindiff/bindiff_test.go b/cmd/bindiff/bindiff_test.go index 976a5d6..40be9ad 100644 --- a/cmd/bindiff/bindiff_test.go +++ b/cmd/bindiff/bindiff_test.go @@ -91,6 +91,10 @@ func TestPackageOf(t *testing.T) { {"SystemInit", cPackage}, // The remainder bucket passes through so rollups still reconcile. {Unattributed, Unattributed}, + // Placeholders for unnamed WebAssembly items roll up by kind; their + // bracket must not read as the start of a spelled-out type. + {"[data 12]", "[data]"}, + {"[func 7]", "[func]"}, } { if got := packageOf(tc.sym); got != tc.want { t.Errorf("packageOf(%q)=%q want %q", tc.sym, got, tc.want) diff --git a/cmd/bindiff/symbols.go b/cmd/bindiff/symbols.go index 259edb9..5497635 100644 --- a/cmd/bindiff/symbols.go +++ b/cmd/bindiff/symbols.go @@ -209,15 +209,15 @@ func profilePackages(f *xelf.File, mem bool) ([]Entry, error) { if err != nil { return nil, err } - return packageEntries(syms), nil + return packageEntries(syms, packageOf), nil } // packageEntries rolls symbol entries up by package. -func packageEntries(syms []Entry) []Entry { +func packageEntries(syms []Entry, pkgOf func(sym string) string) []Entry { order := make([]string, 0, 32) byPkg := make(map[string]*Entry, 32) for _, e := range syms { - name := packageOf(e.Name) + name := pkgOf(e.Name) agg, ok := byPkg[name] if !ok { order = append(order, name) @@ -251,8 +251,8 @@ const typePunct = ":{}[](),; " // A package path may itself contain dots ("github.com/soypat/x.Func"), so the // boundary is the first dot after the final slash rather than the last dot. func packageOf(sym string) string { - if sym == Unattributed { - return Unattributed + if pkg, ok := bucketOf(sym); ok { + return pkg } s := sym // The gc linker names its own generated symbols with a colon and no @@ -316,6 +316,22 @@ func packageOf(sym string) string { return pkg } +// bucketOf reports the package of one of bindiff's own synthetic names, which +// are bracketed so no real symbol can collide with them: [unattributed], and +// the placeholders a WebAssembly module's unnamed items get, "[func 12]" and +// "[data 7]". A placeholder's bucket is its kind, so that thousands of unnamed +// items roll up into one row -- "[func]", "[data]" -- rather than each passing +// for a package, or for a spelled-out type because of the bracket. +func bucketOf(sym string) (string, bool) { + if !strings.HasPrefix(sym, "[") { + return "", false + } + if kind, _, ok := strings.Cut(sym, " "); ok { + return kind + "]", true + } + return sym, true +} + func isAllDigits(s string) bool { if s == "" { return false diff --git a/cmd/bindiff/testdata/src/gen.go b/cmd/bindiff/testdata/src/gen.go index 00f1090..5d7edee 100644 --- a/cmd/bindiff/testdata/src/gen.go +++ b/cmd/bindiff/testdata/src/gen.go @@ -1,9 +1,11 @@ package src -//go:generate tinygo build -o "../blinky-a.elf" -target=pca10040 "./a -//go:generate tinygo build -o "../blinky-b.elf" -target=pca10040 "./b +//go:generate tinygo build -o "../blinky-a.elf" -target=pca10040 "./a" +//go:generate tinygo build -o "../blinky-b.elf" -target=pca10040 "./b" -// WebAssembly fixtures, shared with build/xwasm. One carries DWARF and one is -// built without it, leaving only the name section. -//go:generate tinygo build -o "../../../../testdata/hello.wasm" -target=wasip1 "./hellowasm" -//go:generate tinygo build -o "../../../../testdata/hello-nodebug.wasm" -target=wasip1 -no-debug "./hellowasm" +// The one WebAssembly fixture, shared with build/xwasm. Every flag after the +// target only shrinks it: the bare wasm-unknown target drops WASI, and the +// rest drop the scheduler, the collector and panic printing. It keeps what the +// tests exercise -- a host import, .rodata and .data segments, the name section +// and DWARF line information -- in about 30 KB, most of it DWARF. +//go:generate tinygo build -o "../../../../testdata/tiny.wasm" -target=wasm-unknown -opt=z -panic=trap -scheduler=none -gc=leaking "./tinywasm" diff --git a/cmd/bindiff/testdata/src/hellowasm/main.go b/cmd/bindiff/testdata/src/hellowasm/main.go deleted file mode 100644 index 73a0326..0000000 --- a/cmd/bindiff/testdata/src/hellowasm/main.go +++ /dev/null @@ -1,7 +0,0 @@ -package main - -func add(a, b int) int { return a + b } - -func main() { - println("hello", add(1, 2)) -} diff --git a/cmd/bindiff/testdata/src/tinywasm/main.go b/cmd/bindiff/testdata/src/tinywasm/main.go new file mode 100644 index 0000000..301828a --- /dev/null +++ b/cmd/bindiff/testdata/src/tinywasm/main.go @@ -0,0 +1,26 @@ +// Package main is the WebAssembly test fixture: small, but with a host +// import, read-only and writable data, and DWARF line information. +package main + +import "unsafe" + +//go:wasmimport env emit +func emit(ptr unsafe.Pointer, n uint32) + +var greetings = [...]string{"hello", "hola", "ciao"} // .rodata + +var counter uint32 = 7 // .data + +func add(a, b uint32) uint32 { return a + b } + +func main() {} + +// run is exported rather than left to main: the bare wasm-unknown target builds +// a library, whose main is never called and would be dropped. +// +//go:wasmexport run +func run() { + counter = add(counter, 1) + s := greetings[counter%uint32(len(greetings))] + emit(unsafe.Pointer(unsafe.StringData(s)), uint32(len(s))) +} diff --git a/cmd/bindiff/wasm.go b/cmd/bindiff/wasm.go index a9f97fe..995d263 100644 --- a/cmd/bindiff/wasm.go +++ b/cmd/bindiff/wasm.go @@ -1,10 +1,12 @@ package main import ( + "bytes" "encoding/binary" "errors" "fmt" "io" + "strings" "github.com/soypat/tinyboot/build/xdwarf" "github.com/soypat/tinyboot/build/xwasm" @@ -47,7 +49,11 @@ func profileWasm(r io.ReaderAt, size int64, flags Flags) ([]Entry, error) { case KindSymbol: return symbolEntries(t), nil case KindPackage: - return packageEntries(symbolEntries(t)), nil + pkgOf := packageOf + if isGcWasm(&f) { + pkgOf = gcWasmPackageOf + } + return packageEntries(symbolEntries(t), pkgOf), nil } idx, err := loadWasmLineIndex(&f) if err != nil { @@ -163,12 +169,89 @@ func tileWasm(f *xwasm.File) (tiling, error) { } // nameOr returns the name the name section gives index, or a placeholder -// naming the index space when it gives none. +// naming the index space when it gives none. Placeholders are bracketed, like +// bindiff's other synthetic names, so [packageOf] can tell them from symbols. +// The gc toolchain names no data segments at all, so on its modules every +// segment is one of these. func nameOr(names map[uint32]string, index uint32, space string) string { if name, ok := names[index]; ok { return name } - return fmt.Sprintf("%s[%d]", space, index) + return fmt.Sprintf("[%s %d]", space, index) +} + +// isGcWasm reports whether f was built by the gc toolchain rather than TinyGo. +// The gc linker writes a go:buildid section when it has a build ID, and names +// itself in the producers section. +func isGcWasm(f *xwasm.File) bool { + if _, err := f.SectionByName("go:buildid"); err == nil { + return true + } + s, err := f.SectionByName("producers") + if err != nil || s.Size() > 4096 { + return false + } + data, err := s.AppendData(nil) + return err == nil && bytes.Contains(data, []byte("Go cmd/compile")) +} + +// gcWasmPackageOf is [packageOf] for a module built by the gc toolchain, whose +// linker rewrites every character of a function name outside [A-Za-z0-9_.] to +// '_' (cmd/link/internal/wasm/asm.go). "github.com/google/gopacket/layers.init" +// arrives as "github.com_google_gopacket_layers.init" and +// "time.(Duration).Truncate" as "time.__Duration_.Truncate", so there is no +// slash left to find the package by. +// +// The package still ends at a recoverable dot. A path keeps dots only in +// elements before its last -- gc escapes the last element's as %2e, arriving +// as "_2e" -- and in practice only in the first, a module's domain. So when a +// name's first '_' comes before its first dot, that dot ends the package: +// "crypto_internal_fips140_nistec_fiat.p521Mul", "net_http.Get". When the dot +// comes first it is ambiguous: "github.com_google_gopacket_layers.init" opens +// with a domain, "time.__Duration_.Truncate" with a whole package. Paths +// without a domain are the standard library's and main, whose roots are a +// short fixed list; anything else is taken to start with a domain and to end +// at the first dot after its first separator. +// +// Paths are reported as mangled rather than restored: '_' is ambiguous between +// a separator and an underscore the path really had. +func gcWasmPackageOf(sym string) string { + if pkg, ok := bucketOf(sym); ok { + return pkg + } + dot := strings.IndexByte(sym, '.') + if dot <= 0 { + return cPackage + } + under := strings.IndexByte(sym, '_') + if under < 0 || under > dot && stdRoots[sym[:dot]] { + return sym[:dot] + } + if under < dot { + return sym[:dot] // Separators precede the first dot: no domain. + } + end := strings.IndexByte(sym[under:], '.') + if end < 0 { + return cPackage + } + return sym[:under+end] +} + +// stdRoots are the first path elements of the standard library, plus main and +// the prefixes of the gc toolchain's own symbols ("type:", "go:"), which arrive +// as "type_" and "go_" and so never reach the lookup with a dot first. +var stdRoots = map[string]bool{ + "archive": true, "arena": true, "bufio": true, "builtin": true, "bytes": true, + "cmp": true, "compress": true, "container": true, "context": true, "crypto": true, + "database": true, "debug": true, "embed": true, "encoding": true, "errors": true, + "expvar": true, "flag": true, "fmt": true, "go": true, "hash": true, "html": true, + "image": true, "index": true, "internal": true, "io": true, "iter": true, "log": true, + "main": true, "maps": true, "math": true, "mime": true, "net": true, "os": true, + "path": true, "plugin": true, "reflect": true, "regexp": true, "runtime": true, + "simd": true, "slices": true, "sort": true, "strconv": true, "strings": true, + "structs": true, "sync": true, "syscall": true, "testing": true, "text": true, + "time": true, "unicode": true, "unique": true, "unsafe": true, "vendor": true, + "weak": true, } // readNameMap collects one of the name section's maps, growing the read diff --git a/cmd/bindiff/wasm_test.go b/cmd/bindiff/wasm_test.go index 0d6564a..edb69ac 100644 --- a/cmd/bindiff/wasm_test.go +++ b/cmd/bindiff/wasm_test.go @@ -10,157 +10,80 @@ import ( "strconv" "strings" "testing" -) -var wasmFixtures = []string{ - "../../testdata/hello.wasm", - "../../testdata/hello-nodebug.wasm", -} + "github.com/soypat/tinyboot/build/xwasm" +) -// wasmDebugFixture is the module carrying DWARF, which the source kinds need. -const wasmDebugFixture = "../../testdata/hello.wasm" +// wasmFixture is the one WebAssembly module the tests read, a TinyGo build +// with DWARF. See testdata/src/gen.go. What the fixture cannot exercise -- +// a module without DWARF, a gc-built one -- is built in memory instead. +const wasmFixture = "../../testdata/tiny.wasm" var wasmKinds = []Kind{KindSection, KindSymbol, KindPackage, KindFile, KindLine} -func wasmKindsFor(name string) []Kind { - if name == wasmDebugFixture { - return wasmKinds +// TestWasmProfiles holds the invariants that make a report trustworthy, over +// every kind that applies to a module: +// - sections plus their framing account for every byte of the file; +// - every kind below section describes the section contents, no more or less; +// - symbols tile the code and data sections, leaving only each vector count; +// - a profile diffed against itself changes nothing. +func TestWasmProfiles(t *testing.T) { + sections, size := profileFixture(t, wasmFixture, Flags{kind: KindSection}) + var contents int64 + sectionSize := make(map[string]int64) + for _, e := range sections { + switch e.Name { + case Unattributed: + t.Errorf("%d bytes no section claims", e.New) + case "[wasm-headers]": + default: + contents += e.New + sectionSize[e.Name] = e.New + } + } + if _, total := Total(sections); total != size { + t.Errorf("section profile totals %d, file is %d bytes", total, size) } - return wasmKinds[:3] -} -// TestWasmSectionProfileReconciles is TestSectionProfileReconciles for a -// module: sections plus their framing account for every byte of the file. -func TestWasmSectionProfileReconciles(t *testing.T) { - for _, name := range wasmFixtures { - entries, size := profileFixture(t, name, Flags{kind: KindSection}) - _, total := Total(entries) - if total != size { - t.Errorf("%s: section profile totals %d, file is %d bytes", name, total, size) - } - for _, e := range entries { - if e.Name == Unattributed { - t.Errorf("%s: %d bytes no section claims", name, e.New) + for _, kind := range wasmKinds { + entries, _ := profileFixture(t, wasmFixture, Flags{kind: kind}) + if kind != KindSection { + if _, total := Total(entries); total != contents { + t.Errorf("kind %v totals %d, section contents total %d", kind, total, contents) } } - } -} - -// TestWasmKindsAgreeOnTotal: every kind below section describes the same bytes, -// the section contents, which is the section total less the framing. -func TestWasmKindsAgreeOnTotal(t *testing.T) { - for _, name := range wasmFixtures { - sections, _ := profileFixture(t, name, Flags{kind: KindSection}) - var want int64 - for _, e := range sections { - if e.Name != "[wasm-headers]" { - want += e.New - } + if n := len(DropUnchanged(Diff(entries, entries))); n != 0 { + t.Errorf("kind %v: self-diff left %d changed rows", kind, n) } - for _, kind := range wasmKindsFor(name)[1:] { - entries, _ := profileFixture(t, name, Flags{kind: kind}) - if _, total := Total(entries); total != want { - t.Errorf("%s: kind %v totals %d, section contents total %d", name, kind, total, want) - } + if kind != KindSymbol { + continue } - } -} - -// TestWasmSymbolProfileReconciles checks each section's symbols plus remainder -// against the section, and that functions leave only the Code section's -// leading count unattributed. -func TestWasmSymbolProfileReconciles(t *testing.T) { - for _, name := range wasmFixtures { - sections, _ := profileFixture(t, name, Flags{kind: KindSection}) - entries, _ := profileFixture(t, name, Flags{kind: KindSymbol}) bySection := make(map[string]int64) + var hasMain bool for _, e := range entries { - if e.New < 0 { - t.Errorf("%s: negative size %d for %q", name, e.New, e.Name) - } bySection[e.Section] += e.New + hasMain = hasMain || strings.HasPrefix(e.Name, "main.") if e.Name == Unattributed && (e.Section == "code" || e.Section == "data") && e.New > 5 { - t.Errorf("%s: %d bytes of %s unattributed, want only the vector count", name, e.New, e.Section) + t.Errorf("%d bytes of %s unattributed, want only the vector count", e.New, e.Section) } } - for _, s := range sections { - if s.Name == "[wasm-headers]" { - continue - } - if got := bySection[s.Name]; got != s.New { - t.Errorf("%s: section %q: symbols total %d, section is %d bytes", name, s.Name, got, s.New) + for name, want := range sectionSize { + if got := bySection[name]; got != want { + t.Errorf("section %q: symbols total %d, section is %d bytes", name, got, want) } } - var hasStart bool - for _, e := range entries { - hasStart = hasStart || e.Name == "_start" - } - if !hasStart { - t.Errorf("%s: no _start among function symbols", name) + if !hasMain { + t.Error("no main function among symbols") } } } -// TestWasmSelfDiffIsEmpty mirrors TestSelfDiffIsEmpty. -func TestWasmSelfDiffIsEmpty(t *testing.T) { - for _, name := range wasmFixtures { - for _, kind := range wasmKindsFor(name) { - entries, _ := profileFixture(t, name, Flags{kind: kind}) - if n := len(DropUnchanged(Diff(entries, entries))); n != 0 { - t.Errorf("%s/%v: self-diff left %d changed rows", name, kind, n) - } - } - } -} - -// TestWasmCrossBuildDiff diffs the debug build against the -no-debug one. -// TinyGo's -no-debug also changes code generation, so the builds genuinely -// differ; what must hold is that the diff's sides are exactly the two profiles. -func TestWasmCrossBuildDiff(t *testing.T) { - for _, kind := range wasmKinds[:3] { - flags := Flags{kind: kind} - a, _ := profileFixture(t, wasmFixtures[0], flags) - b, _ := profileFixture(t, wasmFixtures[1], flags) - _, wantOld := Total(a) - _, wantNew := Total(b) - old, new := Total(Diff(a, b)) - if old != wantOld || new != wantNew { - t.Errorf("%v: diff totals old=%d new=%d, profiles total %d and %d", kind, old, new, wantOld, wantNew) - } - } -} - -func TestWasmUnsupported(t *testing.T) { - for _, flags := range []Flags{ - {kind: KindSegment}, - {kind: KindSection, mem: true}, - } { - fp, err := os.Open(wasmDebugFixture) - if err != nil { - t.Fatal(err) - } - _, err = profileReader(fp, 0, flags) - fp.Close() - if err == nil { - t.Errorf("%+v: no error", flags) - } - } - fp, err := os.Open(wasmFixtures[1]) - if err != nil { - t.Fatal(err) - } - defer fp.Close() - if _, err := profileReader(fp, 0, Flags{kind: KindFile}); !errors.Is(err, errWasmNoDebugLine) { - t.Errorf("module without DWARF: got %v, want %v", err, errWasmNoDebugLine) - } -} - // TestWasmLineCoverageMatchesLLVM checks the source kinds attribute exactly the // code bytes llvm-dwarfdump's line table covers: each row runs to the next row // of its sequence. func TestWasmLineCoverageMatchesLLVM(t *testing.T) { dump := llvmTool(t, "llvm-dwarfdump") - out, err := exec.Command(dump, "--debug-line", wasmDebugFixture).Output() + out, err := exec.Command(dump, "--debug-line", wasmFixture).Output() if err != nil { t.Fatal(err) } @@ -184,7 +107,7 @@ func TestWasmLineCoverageMatchesLLVM(t *testing.T) { if want == 0 { t.Fatal("parsed no line table coverage from llvm-dwarfdump") } - entries, _ := profileFixture(t, wasmDebugFixture, Flags{kind: KindFile}) + entries, _ := profileFixture(t, wasmFixture, Flags{kind: KindFile}) var got int64 for _, e := range entries { if e.Name != Unattributed { @@ -208,3 +131,98 @@ func llvmTool(t *testing.T, name string) string { t.Skip(name + " not found") return "" } + +// wasmModule builds a module from the preamble and the given custom sections, +// each a name and payload. +func wasmModule(custom ...string) []byte { + b := []byte("\x00asm\x01\x00\x00\x00") + for i := 0; i+1 < len(custom); i += 2 { + name, payload := custom[i], custom[i+1] + // Lengths here stay under 128, so each LEB128 is one byte. + b = append(b, 0, byte(1+len(name)+len(payload)), byte(len(name))) + b = append(b, name...) + b = append(b, payload...) + } + return b +} + +func TestWasmUnsupported(t *testing.T) { + fixture, err := os.ReadFile(wasmFixture) + if err != nil { + t.Fatal(err) + } + for _, flags := range []Flags{ + {kind: KindSegment}, + {kind: KindSection, mem: true}, + } { + if _, err := profileReader(bytes.NewReader(fixture), 0, flags); err == nil { + t.Errorf("%+v: no error", flags) + } + } + _, err = profileReader(bytes.NewReader(wasmModule()), 0, Flags{kind: KindFile}) + if !errors.Is(err, errWasmNoDebugLine) { + t.Errorf("module without DWARF: got %v, want %v", err, errWasmNoDebugLine) + } +} + +func TestIsGcWasm(t *testing.T) { + fixture, err := os.ReadFile(wasmFixture) + if err != nil { + t.Fatal(err) + } + for _, tc := range []struct { + name string + module []byte + want bool + }{ + {"tinygo fixture", fixture, false}, + {"go:buildid", wasmModule("go:buildid", "abc/def"), true}, + {"producers", wasmModule("producers", "\x01\x0cprocessed-by\x01\x0eGo cmd/compile\x08go1.26.3"), true}, + {"empty", wasmModule(), false}, + } { + var f xwasm.File + if err := f.Read(bytes.NewReader(tc.module)); err != nil { + t.Fatal(tc.name, err) + } + if got := isGcWasm(&f); got != tc.want { + t.Errorf("%s: isGcWasm=%v want %v", tc.name, got, tc.want) + } + } +} + +func TestGcWasmPackageOf(t *testing.T) { + for _, tc := range []struct{ sym, want string }{ + // Single-element paths: the first dot ends the package. + {"runtime.alloc", "runtime"}, + {"time.__Duration_.Truncate", "time"}, + {"runtime.alloc_m.func1", "runtime"}, // '_' after the dot is the identifier's. + // '/' arrives as '_'; the first dot after it ends the path. + {"github.com_google_gopacket_layers.init", "github.com_google_gopacket_layers"}, + {"golang.zx2c4.com_wireguard_device.__Device_.Up", "golang.zx2c4.com_wireguard_device"}, + {"crypto_internal_fips140_nistec_fiat.p521Mul", "crypto_internal_fips140_nistec_fiat"}, + // The last element's dots are escaped as %2e, arriving as _2e. + {"gopkg.in_yaml_2ev3.yaml_emitter_write_double_quoted_scalar", "gopkg.in_yaml_2ev3"}, + {"_rt0_wasm_wasip1", cPackage}, + {"[data 99997]", "[data]"}, + } { + if got := gcWasmPackageOf(tc.sym); got != tc.want { + t.Errorf("gcWasmPackageOf(%q)=%q want %q", tc.sym, got, tc.want) + } + } +} + +// TestGcWasmLocalModule checks a large gc-built module when one is present in +// the untracked local directory. +func TestGcWasmLocalModule(t *testing.T) { + const name = "../../local/netbird.wasm" + if _, err := os.Stat(name); err != nil { + t.Skip("no local module") + } + entries, _ := profileFixture(t, name, Flags{kind: KindPackage}) + for _, e := range entries { + switch e.Name { + case "github", "golang", "google", "gopkg", "[type]": + t.Errorf("package %q (%d bytes): mangled names split at the wrong dot", e.Name, e.New) + } + } +} diff --git a/testdata/hello-nodebug.wasm b/testdata/hello-nodebug.wasm deleted file mode 100644 index 4c4c0d00e8ccda2c53820043a1ece24903846c25..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20581 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 diff --git a/testdata/tiny.wasm b/testdata/tiny.wasm new file mode 100755 index 0000000000000000000000000000000000000000..9f28fe4f896970aad836495ecaf22d7f5e7625d1 GIT binary patch literal 31089 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 TjAje;!ke`Y{Vu8l2Y&xAo`