rust-binutils#
A from-scratch Rust reimplementation of GNU binutils (ar, nm, objdump,
readelf, objcopy, strings, size, addr2line, c++filt, strip, elfedit) aimed
at passing the upstream GNU binutils test suite.
Status#
- 138/138 custom comparison tests passing (100%)
- 329/329 upstream DejaGnu tests passing (100%)
A multicall binary dispatches on argv[0] (or argv[1] for cargo run --) so that the same Rust program serves all the symlinked tool names
under bin/.
Architecture#
1. Custom comparison tests (testsuite.nix)#
A Nix derivation that runs a single named test, comparing rust-binutils
output against reference GNU binutils output. Takes { pkgs, tool, name } as arguments.
Key design decisions:
- Two-level naming: tests are keyed by
tool+name(e.g.,nm/basic,strings/object) - Tool name mapping:
cxxfilt→c++filtbinary name handled in Nix - Shared test object:
bintest.sfrom upstream is assembled once per test viagcc -c - Output normalization: nix store paths, binary names, and trailing whitespace are normalized before diffing
- Standalone test scripts: each test is a shell script in
tests/${tool}/${name}.shthat uses$REF,$RUST,$TESTOBJ,$TMPDIR, and thecomparehelper
2. Upstream DejaGnu tests (dejagnu-testsuite.nix)#
A Nix derivation that runs the real upstream .exp test files from the
GNU binutils source tree using DejaGnu's runtest harness, with all
tool paths pointed at rust-binutils symlinks.
Key design decisions:
- Real upstream tests: runs the actual
.expfiles frombinutils/testsuite/binutils-all/, not hand-written approximations - Generated
site.exp: configures tool paths (NM,AR,OBJDUMP, etc.) to rust-binutils, andAS_FOR_TARGET/CC_FOR_TARGETto GNU as/gcc for assembling test fixtures - Threshold-based pass/fail: each check has
minPass(minimum expected passes) andmaxFail(maximum allowed failures) so regressions are caught but known failures don't block CI - Per-file and aggregate checks: individual
.expfiles get separate Nix checks (e.g.,rust-binutils-dejagnu-size), plus arust-binutils-dejagnu-allinformational check - Subdir tests:
binutils-all/x86-64/x86-64.expandbinutils-all/i386/i386.expare run via a separateruntestcall (basename-only dispatch can't resolve subdir paths) - Skipped tests:
dlltool.exp(Windows PE/COFF) anddebuginfod.exp(server dependency) are skipped
3. default.nix#
rust-binutilspackage — release multicall binary with symlinks for each tool name inpostInstallrust-binutils-devpackage — debug build for fast iterationchecksattribute set wiring per-tool custom tests, per-.expDejaGnu checks, and the aggregate DejaGnu check
Running the tests#
# Run a single custom comparison test
nix build .#checks.x86_64-linux.rust-binutils-test-strings-basic
# Run a single upstream DejaGnu test file
nix build .#checks.x86_64-linux.rust-binutils-dejagnu-cxxfilt
# Run all upstream tests (informational, always passes)
nix build .#checks.x86_64-linux.rust-binutils-dejagnu-all -o result
cat result/test-output.log # full DejaGnu output
cat result/summary.log # PASS/FAIL lines only
cat result/results.txt # machine-readable counts
# Run all checks
nix flake check
Test results#
Custom comparison tests (138/138 passing)#
| Tool | Tests |
|---|---|
ar |
create-list, extract, offsets, print |
addr2line |
addresses, addresses-functions-pretty, basenames, basic, demangle, functions, inlines, multiple, out-of-section, pretty, pretty-functions |
c++filt |
basic, multiple, nested, no-params, no-recurse-limit, no-strip-leading, strip-underscore, style, types |
nm |
all-with-prefix, archive, basic, debug-syms, defined-only, extern-only, extern-only-posix, format-bsd, just-symbols, just-symbols-with-prefix, last-wins-sort, line-numbers, multiple-files, no-demangle, no-recurse-limit, no-sort, no-weak, numeric-extern, numeric-sort, posix, posix-decimal, posix-octal, print-armap, print-file-name, print-file-name-posix, print-size, quiet, radix-decimal, radix-hex, radix-hex-print-file-name, radix-octal, reverse-numeric, reverse-sort, size-sort, special-syms, target-noop, undefined-only, undefined-only-posix |
objdump |
archive, disassemble, disassemble-section, disassemble-with-syms, disassemble-zeroes, file-headers, headers, headers-section-filter, headers-syms, intel-mnemonic, intel-syntax, intel-syntax-bundled, multiple-files, no-show-raw-insn, relocs, section-filter, section-filter-bundled, section-headers-wide, start-address, syms, syms-section-filter |
readelf |
arch-specific, archive-index-not-archive, dynamic, dynamic-arch, dynamic-program-headers, file-header, groups, groups-wide, header-notes, header-program, header-sections, headers-all, headers-combined, hex-dump, hex-dump-data, hex-dump-text, histogram, multiple-files, notes, notes-symbols, notes-wide, notes-with-sections, program-headers, program-wide, relocs, sections, sections-arch-wide, string-dump, string-dump-data, string-dump-missing, string-dump-text, symbols, unwind, use-dynamic, version-info, wide |
size |
basic, common, decimal, hex, multiple-files, octal, sysv, sysv-hex, sysv-octal, totals |
strings |
basic, min-length, multiple-files, object, print-file-name, radix-decimal, radix-hex, radix-octal |
Upstream DejaGnu tests (329/329 passing)#
| Test file | Pass | Fail | Total | Threshold |
|---|---|---|---|---|
cxxfilt.exp |
3 | 0 | 3 | minPass=3, maxFail=0 |
size.exp |
3 | 0 | 3 | minPass=3, maxFail=0 |
nm.exp |
15 | 0 | 15 | minPass=15, maxFail=0 |
ar.exp |
14 | 0 | 14 | minPass=14, maxFail=0 |
readelf.exp |
38 | 0 | 38 | minPass=38, maxFail=0 |
objdump.exp |
32 | 0 | 33 | minPass=32, maxFail=0 |
objcopy.exp |
120 | 0 | 120 | minPass=120, maxFail=0 |
compress.exp |
45 | 0 | 45 | minPass=45, maxFail=0 |
strings.exp |
1 | 0 | 1 | minPass=1, maxFail=0 |
addr2line.exp |
3 | 0 | 3 | minPass=3, maxFail=0 |
update-section.exp |
6 | 0 | 6 | minPass=6, maxFail=0 |
elfedit.exp |
6 | 0 | 6 | minPass=6, maxFail=0 |
x86-64/x86-64.exp |
35 | 0 | 35 | (informational; only the binutils-all-x86-64 subset) |
i386/i386.exp |
8 | 0 | 8 | (informational; binutils-all-i386 subdir) |
| Total | 329 | 0 | 329 |
Coverage gaps#
Untested tools (not covered by upstream .exp files we run):
strip— tested indirectly viaobjcopy.exp(strip is an objcopy mode)ranlib— tested indirectly viaar.expas— stub implementation (delegates to systemas); upstream tests ingas/testsuite/(separate suite, would test the real assembler not ours)ld— stub implementation (delegates to systemld); upstream tests inld/testsuite/
Skipped upstream test files:
dlltool.exp— Windows-specific (PE/COFF)debuginfod.exp— requires debuginfod server
Future work#
gas/testsuite/andld/testsuite/integration whenas/ldare implemented natively rather than delegating- objcopy
--merge-notesto actually merge consecutive GNU build attribute notes (currently a no-op) - Full SFrame v2/v3 Function Index dump (only the header is rendered; FDE/FRE entries with CFA/FP/RA decoding are not yet implemented)
- Improve
readelf -wiaoRlL dw5(DWARF 5 features: loclists/rnglists, decoded line) for more thorough coverage
Workflow#
- Pick a failing test (
grep "^FAIL:" .../rust-binutils-dejagnu-all/test-output.log). - Reproduce locally:
nix build .#checks.x86_64-linux.rust-binutils-dejagnu-<file>. - Read the failure under
nix log <drv>and compare against the expected.r/.dumpfile in the binutils source tree. - Fix the code, rebuild, re-run the threshold-gated check.
- Commit and push the
binutils-testbookmark.