feat(cli): add gasm diff command for comparing assembly encodings
Assisted-by: DeepSeek V4 Pro
This commit is contained in:
@@ -55,6 +55,8 @@ func main() {
|
|||||||
os.Exit(cmdVerify(os.Args[2:]))
|
os.Exit(cmdVerify(os.Args[2:]))
|
||||||
case "debug":
|
case "debug":
|
||||||
os.Exit(cmdDebug(os.Args[2:]))
|
os.Exit(cmdDebug(os.Args[2:]))
|
||||||
|
case "diff":
|
||||||
|
os.Exit(cmdDiff(os.Args[2:]))
|
||||||
case "lsp":
|
case "lsp":
|
||||||
os.Exit(cmdLSP(os.Args[2:]))
|
os.Exit(cmdLSP(os.Args[2:]))
|
||||||
case "version", "--version", "-V":
|
case "version", "--version", "-V":
|
||||||
@@ -115,6 +117,7 @@ func usage(w io.Writer) {
|
|||||||
{"asm", "assemble .s files to machine code (amd64, riscv64)"},
|
{"asm", "assemble .s files to machine code (amd64, riscv64)"},
|
||||||
{"verify", "JIT-assemble and run dynamic checks (amd64, riscv64)"},
|
{"verify", "JIT-assemble and run dynamic checks (amd64, riscv64)"},
|
||||||
{"debug", "interactive source-level debugger (amd64)"},
|
{"debug", "interactive source-level debugger (amd64)"},
|
||||||
|
{"diff", "compare machine code of two .s files"},
|
||||||
{"lsp", "run the language server over stdio"},
|
{"lsp", "run the language server over stdio"},
|
||||||
{"version", "print the version (same as --version)"},
|
{"version", "print the version (same as --version)"},
|
||||||
}
|
}
|
||||||
@@ -528,6 +531,112 @@ requires -p, the package path, and the installed Go toolchain).
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cmdDiff compares the machine code of two assembly files.
|
||||||
|
func cmdDiff(args []string) int {
|
||||||
|
fs := newCommand("diff", "gasm diff <file1.s> <file2.s>", `
|
||||||
|
Compare the machine code produced by assembling two files.
|
||||||
|
Shows which functions differ and the byte-level differences.
|
||||||
|
Useful for verifying that two implementations produce identical code,
|
||||||
|
or for tracking encoding changes between Go assembler versions.
|
||||||
|
`)
|
||||||
|
fs.Parse(args)
|
||||||
|
if fs.NArg() != 2 {
|
||||||
|
fmt.Fprintln(os.Stderr, "usage: gasm diff <file1.s> <file2.s>")
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
path1, path2 := fs.Arg(0), fs.Arg(1)
|
||||||
|
|
||||||
|
// Assemble both files.
|
||||||
|
img1, err := assembleFile(path1)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "gasm diff: %s: %v\n", path1, err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
img2, err := assembleFile(path2)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "gasm diff: %s: %v\n", path2, err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare functions by name.
|
||||||
|
funcs1 := make(map[string][]byte)
|
||||||
|
for _, fn := range img1.Funcs {
|
||||||
|
funcs1[fn.Name] = img1.Code[fn.Offset : fn.Offset+fn.Size]
|
||||||
|
}
|
||||||
|
funcs2 := make(map[string][]byte)
|
||||||
|
for _, fn := range img2.Funcs {
|
||||||
|
funcs2[fn.Name] = img2.Code[fn.Offset : fn.Offset+fn.Size]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find common functions and compare.
|
||||||
|
diffs := 0
|
||||||
|
for name, code1 := range funcs1 {
|
||||||
|
code2, ok := funcs2[name]
|
||||||
|
if !ok {
|
||||||
|
fmt.Printf("%s: only in %s\n", name, path1)
|
||||||
|
diffs++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !bytes.Equal(code1, code2) {
|
||||||
|
fmt.Printf("%s: DIFFERS (%d vs %d bytes)\n", name, len(code1), len(code2))
|
||||||
|
printByteDiff(code1, code2)
|
||||||
|
diffs++
|
||||||
|
} else {
|
||||||
|
fmt.Printf("%s: identical (%d bytes)\n", name, len(code1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for name := range funcs2 {
|
||||||
|
if _, ok := funcs1[name]; !ok {
|
||||||
|
fmt.Printf("%s: only in %s\n", name, path2)
|
||||||
|
diffs++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if diffs == 0 {
|
||||||
|
fmt.Println("all functions identical")
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// assembleFile assembles a file and returns the image.
|
||||||
|
func assembleFile(path string) (*asm.Image, error) {
|
||||||
|
src, err := readSource(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
f, errs := parser.Parse(path, src)
|
||||||
|
for _, e := range errs {
|
||||||
|
fmt.Fprintf(os.Stderr, "%s: %v\n", path, e)
|
||||||
|
}
|
||||||
|
if len(errs) > 0 {
|
||||||
|
return nil, fmt.Errorf("parse errors")
|
||||||
|
}
|
||||||
|
targetArch := arch.FromFilename(path)
|
||||||
|
if targetArch == arch.RISCV {
|
||||||
|
return asm.AssembleFileRISCV(f)
|
||||||
|
}
|
||||||
|
return asm.AssembleFile(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// printByteDiff shows the first few byte differences between two code blocks.
|
||||||
|
func printByteDiff(a, b []byte) {
|
||||||
|
maxLen := len(a)
|
||||||
|
if len(b) < maxLen {
|
||||||
|
maxLen = len(b)
|
||||||
|
}
|
||||||
|
shown := 0
|
||||||
|
for i := 0; i < maxLen && shown < 8; i++ {
|
||||||
|
if a[i] != b[i] {
|
||||||
|
fmt.Printf(" offset %#04x: %02x vs %02x\n", i, a[i], b[i])
|
||||||
|
shown++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(a) != len(b) {
|
||||||
|
fmt.Printf(" length: %d vs %d\n", len(a), len(b))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// cmdVerifyRISCV handles the verify subcommand for RISC-V files.
|
// cmdVerifyRISCV handles the verify subcommand for RISC-V files.
|
||||||
// JIT requires RISC-V hardware; only ground-truth and profile are available.
|
// JIT requires RISC-V hardware; only ground-truth and profile are available.
|
||||||
func cmdVerifyRISCV(path string, groundTruth, profile bool) int {
|
func cmdVerifyRISCV(path string, groundTruth, profile bool) int {
|
||||||
|
|||||||
Reference in New Issue
Block a user