v3 candidate

This commit is contained in:
Andy Dixon
2026-06-16 11:51:07 +01:00
parent fcb19c96c8
commit 493bfed599
91 changed files with 28824 additions and 80 deletions
+288
View File
@@ -0,0 +1,288 @@
package disasm
import (
"encoding/binary"
"fmt"
)
// diAArch64 is a heuristic AArch64 (ARMv8 A64) decoder. It recognises a
// subset of instructions commonly seen in disassembly and falls back to
// ".word 0x…" for everything else. 32-bit fixed-width little-endian.
func diAArch64(buf []byte, base int64, maxInstr int) []map[string]any {
out := []map[string]any{}
i := 0
for i+4 <= len(buf) && len(out) < maxInstr {
w := binary.LittleEndian.Uint32(buf[i : i+4])
mn, ops, conf := decodeA64(w, base+int64(i))
out = append(out, map[string]any{
"offset": base + int64(i),
"bytes": hexStr(buf[i : i+4]),
"mnemonic": mn,
"operands": ops,
"confident": conf,
})
i += 4
}
// Tail bytes (not multiple of 4).
for i < len(buf) && len(out) < maxInstr {
end := i + 1
out = append(out, map[string]any{
"offset": base + int64(i),
"bytes": hexStr(buf[i:end]),
"mnemonic": "db",
"operands": hexCommaList(buf[i:end]),
"confident": false,
})
i++
}
return out
}
func decodeA64(w uint32, pc int64) (string, string, bool) {
// NOP
if w == 0xd503201f {
return "nop", "", true
}
// RET Xn (default x30)
if w&0xfffffc1f == 0xd65f0000 {
rn := (w >> 5) & 0x1f
if rn == 30 {
return "ret", "", true
}
return "ret", regX(rn), true
}
// BR Xn
if w&0xfffffc1f == 0xd61f0000 {
return "br", regX((w >> 5) & 0x1f), true
}
// BLR Xn
if w&0xfffffc1f == 0xd63f0000 {
return "blr", regX((w >> 5) & 0x1f), true
}
// B / BL (unconditional branch, imm26 * 4, signed)
if w&0x7c000000 == 0x14000000 {
imm := signExtend64(uint64(w&0x03ffffff)<<2, 28)
target := pc + int64(imm)
if w&0x80000000 != 0 {
return "bl", fmt.Sprintf("0x%x", target), true
}
return "b", fmt.Sprintf("0x%x", target), true
}
// B.cond (conditional branch, imm19*4)
if w&0xff000010 == 0x54000000 {
cond := int(w & 0xf)
imm := signExtend64(uint64((w>>5)&0x7ffff)<<2, 21)
return "b." + condName(cond), fmt.Sprintf("0x%x", pc+int64(imm)), true
}
// CBZ / CBNZ
if w&0x7e000000 == 0x34000000 {
sf := (w >> 31) & 1
op := (w >> 24) & 1
rt := w & 0x1f
imm := signExtend64(uint64((w>>5)&0x7ffff)<<2, 21)
mn := "cbz"
if op == 1 {
mn = "cbnz"
}
r := regX(rt)
if sf == 0 {
r = regW(rt)
}
return mn, fmt.Sprintf("%s, 0x%x", r, pc+int64(imm)), true
}
// MOVZ / MOVN / MOVK (wide immediate)
if w&0x1f800000 == 0x12800000 {
sf := (w >> 31) & 1
opc := (w >> 29) & 0x3
hw := (w >> 21) & 0x3
imm16 := (w >> 5) & 0xffff
rd := w & 0x1f
mn := "movn"
switch opc {
case 0b10:
mn = "movz"
case 0b11:
mn = "movk"
}
shift := hw * 16
r := regX(rd)
if sf == 0 {
r = regW(rd)
}
if shift == 0 {
return mn, fmt.Sprintf("%s, #0x%x", r, imm16), true
}
return mn, fmt.Sprintf("%s, #0x%x, lsl #%d", r, imm16, shift), true
}
// ADR / ADRP
if w&0x1f000000 == 0x10000000 {
page := (w >> 31) & 1
rd := w & 0x1f
immlo := uint64((w >> 29) & 0x3)
immhi := uint64((w >> 5) & 0x7ffff)
imm := (immhi << 2) | immlo
mn := "adr"
shift := uint(21)
signed := signExtend64(imm, shift)
if page == 1 {
mn = "adrp"
signed = signExtend64(imm<<12, 33)
// adrp target is page-aligned: pc & ~0xFFF
return mn, fmt.Sprintf("%s, 0x%x", regX(rd), (pc & ^int64(0xfff))+int64(signed)), true
}
return mn, fmt.Sprintf("%s, 0x%x", regX(rd), pc+int64(signed)), true
}
// ADD / SUB (immediate)
if w&0x1f000000 == 0x11000000 {
sf := (w >> 31) & 1
op := (w >> 30) & 1
sh := (w >> 22) & 1
imm12 := (w >> 10) & 0xfff
rn := (w >> 5) & 0x1f
rd := w & 0x1f
mn := "add"
if op == 1 {
mn = "sub"
}
lhs := regX(rd)
mid := regX(rn)
if sf == 0 {
lhs = regW(rd)
mid = regW(rn)
}
if sh == 1 {
return mn, fmt.Sprintf("%s, %s, #0x%x, lsl #12", lhs, mid, imm12), true
}
return mn, fmt.Sprintf("%s, %s, #0x%x", lhs, mid, imm12), true
}
// LDR / STR (unsigned offset, 32/64-bit int)
if w&0x3fc00000 == 0x39400000 || w&0x3fc00000 == 0x39000000 {
size := (w >> 30) & 0x3
l := (w >> 22) & 1
imm12 := (w >> 10) & 0xfff
rn := (w >> 5) & 0x1f
rt := w & 0x1f
var mn string
if l == 1 {
mn = "ldr"
} else {
mn = "str"
}
scale := int(1 << size)
imm := int(imm12) * scale
var reg string
if size == 3 {
reg = regX(rt)
} else {
reg = regW(rt)
}
base := regXorSP(rn)
if imm == 0 {
return mn, fmt.Sprintf("%s, [%s]", reg, base), true
}
return mn, fmt.Sprintf("%s, [%s, #0x%x]", reg, base, imm), true
}
// LDR (literal / PC-rel)
if w&0xbf000000 == 0x18000000 {
sf := (w >> 30) & 1
imm := signExtend64(uint64((w>>5)&0x7ffff)<<2, 21)
rt := w & 0x1f
reg := regW(rt)
if sf == 1 {
reg = regX(rt)
}
return "ldr", fmt.Sprintf("%s, 0x%x", reg, pc+int64(imm)), true
}
// ORR / AND / EOR (shifted reg) — simplified detection, no shifts reported.
if w&0x7f200000 == 0x2a000000 { // ORR (32/64) w/ shift=0
sf := (w >> 31) & 1
rm := (w >> 16) & 0x1f
rn := (w >> 5) & 0x1f
rd := w & 0x1f
// MOV (register) alias: ORR Xd, XZR, Xm → MOV Xd, Xm (when Rn==31 and shift==0)
if rn == 31 && ((w>>10)&0x3f) == 0 && ((w>>22)&0x3) == 0 {
if sf == 1 {
return "mov", fmt.Sprintf("%s, %s", regX(rd), regX(rm)), true
}
return "mov", fmt.Sprintf("%s, %s", regW(rd), regW(rm)), true
}
if sf == 1 {
return "orr", fmt.Sprintf("%s, %s, %s", regX(rd), regX(rn), regX(rm)), true
}
return "orr", fmt.Sprintf("%s, %s, %s", regW(rd), regW(rn), regW(rm)), true
}
// SVC #imm16
if w&0xffe0001f == 0xd4000001 {
imm16 := (w >> 5) & 0xffff
return "svc", fmt.Sprintf("#0x%x", imm16), true
}
// BRK #imm16
if w&0xffe0001f == 0xd4200000 {
imm16 := (w >> 5) & 0xffff
return "brk", fmt.Sprintf("#0x%x", imm16), true
}
// HINT family → treat common ones
if w&0xfffff01f == 0xd503201f {
op := (w >> 5) & 0x7f
switch op {
case 0:
return "nop", "", true
case 1:
return "yield", "", true
case 2:
return "wfe", "", true
case 3:
return "wfi", "", true
case 4:
return "sev", "", true
case 5:
return "sevl", "", true
}
return "hint", fmt.Sprintf("#0x%x", op), true
}
// Fallback
return ".word", fmt.Sprintf("0x%08x", w), false
}
func regX(r uint32) string {
r &= 0x1f
if r == 31 {
return "xzr"
}
return fmt.Sprintf("x%d", r)
}
func regW(r uint32) string {
r &= 0x1f
if r == 31 {
return "wzr"
}
return fmt.Sprintf("w%d", r)
}
// regXorSP is used for the "Rn" slot in memory ops where 31 means SP, not XZR.
func regXorSP(r uint32) string {
r &= 0x1f
if r == 31 {
return "sp"
}
return fmt.Sprintf("x%d", r)
}
func condName(c int) string {
names := []string{
"eq", "ne", "cs", "cc", "mi", "pl", "vs", "vc",
"hi", "ls", "ge", "lt", "gt", "le", "al", "nv",
}
if c >= 0 && c < 16 {
return names[c]
}
return fmt.Sprintf("c%d", c)
}
func signExtend64(v uint64, bits uint) int64 {
mask := uint64(1) << (bits - 1)
if v&mask != 0 {
v |= ^uint64(0) << bits
}
return int64(v)
}
+209
View File
@@ -0,0 +1,209 @@
package disasm
import (
"encoding/binary"
"fmt"
"strings"
)
// diARM32 is a heuristic ARM state decoder for 32-bit little-endian ARM
// instructions. Thumb/T32 is intentionally not decoded here.
func diARM32(buf []byte, base int64, maxInstr int) []map[string]any {
out := []map[string]any{}
i := 0
for i+4 <= len(buf) && len(out) < maxInstr {
w := binary.LittleEndian.Uint32(buf[i : i+4])
mn, ops, conf := decodeARM32(w, base+int64(i))
out = append(out, map[string]any{
"offset": base + int64(i),
"bytes": hexStr(buf[i : i+4]),
"mnemonic": mn,
"operands": ops,
"confident": conf,
})
i += 4
}
for i < len(buf) && len(out) < maxInstr {
out = append(out, map[string]any{
"offset": base + int64(i),
"bytes": hexStr(buf[i : i+1]),
"mnemonic": "db",
"operands": hexCommaList(buf[i : i+1]),
"confident": false,
})
i++
}
return out
}
func decodeARM32(w uint32, pc int64) (string, string, bool) {
cond := (w >> 28) & 0xf
suf := armCondSuffix(cond)
if cond == 0xf {
suf = ".nv"
}
// BX Rn
if w&0x0ffffff0 == 0x012fff10 {
return "bx" + suf, armReg(w & 0xf), true
}
// SVC #imm24
if w&0x0f000000 == 0x0f000000 {
return "svc" + suf, fmt.Sprintf("#0x%x", w&0x00ffffff), true
}
// B / BL, offset is sign-extended imm24 << 2 from PC+8.
if w&0x0e000000 == 0x0a000000 {
imm := signExtend64(uint64(w&0x00ffffff)<<2, 26)
target := pc + 8 + int64(imm)
if w&0x01000000 != 0 {
return "bl" + suf, fmt.Sprintf("0x%x", target), true
}
return "b" + suf, fmt.Sprintf("0x%x", target), true
}
// PUSH/POP common aliases.
if w&0xffff0000 == 0xe92d0000 {
return "push", armRegList(w & 0xffff), true
}
if w&0xffff0000 == 0xe8bd0000 {
return "pop", armRegList(w & 0xffff), true
}
// LDR/STR immediate offset.
if w&0x0c000000 == 0x04000000 {
p := (w >> 24) & 1
u := (w >> 23) & 1
b := (w >> 22) & 1
wb := (w >> 21) & 1
l := (w >> 20) & 1
rn := (w >> 16) & 0xf
rd := (w >> 12) & 0xf
imm := w & 0xfff
mn := "str"
if l == 1 {
mn = "ldr"
}
if b == 1 {
mn += "b"
}
mn += suf
sign := ""
if u == 0 {
sign = "-"
}
addr := ""
switch {
case p == 1 && imm == 0 && wb == 0:
addr = fmt.Sprintf("[%s]", armReg(rn))
case p == 1:
addr = fmt.Sprintf("[%s, #%s0x%x]", armReg(rn), sign, imm)
if wb == 1 {
addr += "!"
}
default:
addr = fmt.Sprintf("[%s], #%s0x%x", armReg(rn), sign, imm)
}
return mn, fmt.Sprintf("%s, %s", armReg(rd), addr), true
}
// Data processing immediate/register subset.
if w&0x0c000000 == 0x00000000 || w&0x0e000000 == 0x02000000 {
immediate := w&0x02000000 != 0
opcode := (w >> 21) & 0xf
s := (w >> 20) & 1
rn := (w >> 16) & 0xf
rd := (w >> 12) & 0xf
mn := armDataProcName(opcode)
if mn == "" {
return ".word", fmt.Sprintf("0x%08x", w), false
}
mn += suf
if s == 1 && opcode != 8 && opcode != 9 && opcode != 10 && opcode != 11 {
mn += "s"
}
op2 := ""
if immediate {
imm8 := w & 0xff
rot := ((w >> 8) & 0xf) * 2
imm := (imm8 >> rot) | (imm8 << ((32 - rot) & 31))
op2 = fmt.Sprintf("#0x%x", imm)
} else {
rm := w & 0xf
shift := (w >> 4) & 0xff
if shift == 0 {
op2 = armReg(rm)
} else {
op2 = fmt.Sprintf("%s, %s", armReg(rm), armShift(shift))
}
}
switch opcode {
case 8, 9, 10, 11: // tst/teq/cmp/cmn
return mn, fmt.Sprintf("%s, %s", armReg(rn), op2), true
case 13, 15: // mov/mvn
return mn, fmt.Sprintf("%s, %s", armReg(rd), op2), true
default:
return mn, fmt.Sprintf("%s, %s, %s", armReg(rd), armReg(rn), op2), true
}
}
return ".word", fmt.Sprintf("0x%08x", w), false
}
func armCondSuffix(cond uint32) string {
return [...]string{".eq", ".ne", ".cs", ".cc", ".mi", ".pl", ".vs", ".vc", ".hi", ".ls", ".ge", ".lt", ".gt", ".le", "", ""}[cond&0xf]
}
func armDataProcName(op uint32) string {
return [...]string{"and", "eor", "sub", "rsb", "add", "adc", "sbc", "rsc", "tst", "teq", "cmp", "cmn", "orr", "mov", "bic", "mvn"}[op&0xf]
}
func armReg(r uint32) string {
switch r & 0xf {
case 13:
return "sp"
case 14:
return "lr"
case 15:
return "pc"
default:
return fmt.Sprintf("r%d", r&0xf)
}
}
func armRegList(mask uint32) string {
regs := []string{}
start := -1
flush := func(end int) {
if start < 0 {
return
}
if start == end {
regs = append(regs, armReg(uint32(start)))
} else {
regs = append(regs, fmt.Sprintf("%s-%s", armReg(uint32(start)), armReg(uint32(end))))
}
start = -1
}
for i := 0; i < 16; i++ {
if mask&(1<<uint(i)) != 0 {
if start < 0 {
start = i
}
continue
}
flush(i - 1)
}
flush(15)
return "{" + strings.Join(regs, ", ") + "}"
}
func armShift(v uint32) string {
typ := (v >> 1) & 0x3
imm := (v >> 3) & 0x1f
names := [...]string{"lsl", "lsr", "asr", "ror"}
if imm == 0 {
if typ == 0 {
return "lsl #0"
}
if typ == 3 {
return "rrx"
}
imm = 32
}
return fmt.Sprintf("%s #%d", names[typ], imm)
}
+541
View File
@@ -0,0 +1,541 @@
package disasm
import (
"sort"
"strconv"
"strings"
)
// cfg.go builds control-flow graphs and cross-references on top of the
// per-architecture decoders. It performs a recursive-descent sweep from an
// entry point: it follows direct branches and the fall-through after calls
// (calls leave the function, so their targets are recorded as xrefs rather
// than followed), splits the decoded instructions into basic blocks, and
// records every direct code/data reference it can resolve statically.
// Block is one basic block: a maximal straight-line instruction run.
type Block struct {
Start int64 `json:"start"`
End int64 `json:"end"` // address after the last instruction
Instrs []map[string]any `json:"instrs"`
Succs []int64 `json:"succs"` // successor block start addresses
Kind string `json:"kind"` // ret | jmp | cond | call | fallthrough
}
// Xref is a single static reference from one address to another.
type Xref struct {
From int64 `json:"from"`
To int64 `json:"to"`
Kind string `json:"kind"` // call | jump | data
}
// CFG is the analysis result for one entry point.
type CFG struct {
Entry int64 `json:"entry"`
Arch string `json:"arch"`
Blocks []*Block `json:"blocks"`
Calls []int64 `json:"calls"` // distinct call targets (in or out of range)
Xrefs []Xref `json:"xrefs"` // all resolved references
Unknown int `json:"unknown"` // indirect/unresolved branch+call count
}
// Function is a discovered function and its structural metrics.
type Function struct {
Name string `json:"name"`
Entry int64 `json:"entry"`
End int64 `json:"end"`
Blocks []*Block `json:"blocks"`
Instructions int `json:"instructions"`
Complexity int `json:"complexity"`
Calls []int64 `json:"calls"`
CalledBy []int64 `json:"calledBy"`
Unknown int `json:"unknown"`
}
// CallEdge is one direct call from a discovered function.
type CallEdge struct {
Caller int64 `json:"caller"`
CallSite int64 `json:"callSite"`
Callee int64 `json:"callee"`
External bool `json:"external"`
}
// Seed is a known or suspected function entry supplied by executable metadata.
type Seed struct {
Name string `json:"name"`
Entry int64 `json:"entry"`
}
// Program is a bounded recursive function-discovery result for one byte range.
type Program struct {
Entry int64 `json:"entry"`
Base int64 `json:"base"`
End int64 `json:"end"`
Arch string `json:"arch"`
Functions []*Function `json:"functions"`
Edges []CallEdge `json:"edges"`
ExternalCalls []int64 `json:"externalCalls"`
Instructions int `json:"instructions"`
Unknown int `json:"unknown"`
Truncated bool `json:"truncated"`
}
type rawInstr struct {
addr int64
length int
mnem string
class string
target int64
hasTgt bool
data int64
hasDat bool
m map[string]any
}
// BuildCFG decodes from entry (within buf, which starts at address base) and
// returns the control-flow graph plus discovered references.
func BuildCFG(buf []byte, base int64, arch string, entry int64, maxInstr int) *CFG {
if maxInstr <= 0 {
maxInstr = 8192
}
cfg := &CFG{Entry: entry, Arch: arch}
decoded := map[int64]*rawInstr{}
inRange := func(a int64) bool { return a >= base && a < base+int64(len(buf)) }
var stack []int64
push := func(a int64) {
if inRange(a) {
stack = append(stack, a)
}
}
push(entry)
for len(stack) > 0 && len(decoded) < maxInstr {
addr := stack[len(stack)-1]
stack = stack[:len(stack)-1]
for inRange(addr) && len(decoded) < maxInstr {
if _, seen := decoded[addr]; seen {
break
}
ri := decodeAt(buf, addr, base, arch)
if ri == nil {
break
}
decoded[addr] = ri
next := addr + int64(ri.length)
switch ri.class {
case "ret":
addr = -1
case "jmp":
if ri.hasTgt {
addr = ri.target
} else {
cfg.Unknown++
addr = -1
}
case "cond":
if ri.hasTgt {
push(ri.target)
} else {
cfg.Unknown++
}
addr = next
case "call":
if !ri.hasTgt {
cfg.Unknown++
}
addr = next
default:
addr = next
}
if addr < 0 {
break
}
}
}
// Collect references and call targets.
callSet := map[int64]bool{}
for _, ri := range decoded {
if ri.hasTgt {
switch ri.class {
case "call":
cfg.Xrefs = append(cfg.Xrefs, Xref{From: ri.addr, To: ri.target, Kind: "call"})
if !callSet[ri.target] {
callSet[ri.target] = true
cfg.Calls = append(cfg.Calls, ri.target)
}
case "jmp", "cond":
cfg.Xrefs = append(cfg.Xrefs, Xref{From: ri.addr, To: ri.target, Kind: "jump"})
}
}
if ri.hasDat {
cfg.Xrefs = append(cfg.Xrefs, Xref{From: ri.addr, To: ri.data, Kind: "data"})
}
}
sort.Slice(cfg.Calls, func(i, j int) bool { return cfg.Calls[i] < cfg.Calls[j] })
sort.Slice(cfg.Xrefs, func(i, j int) bool {
if cfg.Xrefs[i].From != cfg.Xrefs[j].From {
return cfg.Xrefs[i].From < cfg.Xrefs[j].From
}
return cfg.Xrefs[i].To < cfg.Xrefs[j].To
})
cfg.Blocks = buildBlocks(decoded, inRange)
return cfg
}
// DiscoverFunctions recursively follows direct calls from entry, building a
// bounded call graph over buf. Indirect calls are counted but cannot be
// discovered statically. maxInstr is a total budget across all functions.
func DiscoverFunctions(buf []byte, base int64, arch string, entry int64, maxFunctions, maxInstr int) *Program {
return DiscoverFunctionsFromSeeds(buf, base, arch, []Seed{{Entry: entry}}, maxFunctions, maxInstr)
}
// DiscoverFunctionsFromSeeds analyzes named and unnamed roots plus every
// direct in-range call target reached from them.
func DiscoverFunctionsFromSeeds(buf []byte, base int64, arch string, seeds []Seed, maxFunctions, maxInstr int) *Program {
if maxFunctions <= 0 || maxFunctions > 4096 {
maxFunctions = 256
}
if maxInstr <= 0 || maxInstr > 1_000_000 {
maxInstr = 65_536
}
end := base + int64(len(buf))
entry := base
if len(seeds) > 0 {
entry = seeds[0].Entry
}
program := &Program{Entry: entry, Base: base, End: end, Arch: arch}
inRange := func(addr int64) bool { return addr >= base && addr < end }
queue := make([]int64, 0, len(seeds))
queued := map[int64]bool{}
names := map[int64]string{}
for _, seed := range seeds {
if !inRange(seed.Entry) {
continue
}
if !queued[seed.Entry] {
queued[seed.Entry] = true
queue = append(queue, seed.Entry)
}
if seed.Name != "" {
names[seed.Entry] = seed.Name
}
}
if len(queue) == 0 {
program.Truncated = true
return program
}
discovered := map[int64]*Function{}
callerSets := map[int64]map[int64]bool{}
externalSet := map[int64]bool{}
for len(queue) > 0 && len(discovered) < maxFunctions && program.Instructions < maxInstr {
fnEntry := queue[0]
queue = queue[1:]
if discovered[fnEntry] != nil {
continue
}
remaining := maxInstr - program.Instructions
perFunction := remaining
if perFunction > 8192 {
perFunction = 8192
}
cfg := BuildCFG(buf, base, arch, fnEntry, perFunction)
fn := summarizeFunction(cfg, names[fnEntry])
discovered[fnEntry] = fn
program.Functions = append(program.Functions, fn)
program.Instructions += fn.Instructions
program.Unknown += fn.Unknown
for _, xref := range cfg.Xrefs {
if xref.Kind != "call" {
continue
}
external := !inRange(xref.To)
program.Edges = append(program.Edges, CallEdge{
Caller: fnEntry, CallSite: xref.From, Callee: xref.To, External: external,
})
if external {
externalSet[xref.To] = true
continue
}
if callerSets[xref.To] == nil {
callerSets[xref.To] = map[int64]bool{}
}
callerSets[xref.To][fnEntry] = true
if !queued[xref.To] {
queued[xref.To] = true
queue = append(queue, xref.To)
}
}
}
if len(queue) > 0 || program.Instructions >= maxInstr {
program.Truncated = true
}
for _, fn := range program.Functions {
for caller := range callerSets[fn.Entry] {
fn.CalledBy = append(fn.CalledBy, caller)
}
sort.Slice(fn.CalledBy, func(i, j int) bool { return fn.CalledBy[i] < fn.CalledBy[j] })
}
for target := range externalSet {
program.ExternalCalls = append(program.ExternalCalls, target)
}
sort.Slice(program.Functions, func(i, j int) bool { return program.Functions[i].Entry < program.Functions[j].Entry })
sort.Slice(program.Edges, func(i, j int) bool {
if program.Edges[i].Caller != program.Edges[j].Caller {
return program.Edges[i].Caller < program.Edges[j].Caller
}
if program.Edges[i].CallSite != program.Edges[j].CallSite {
return program.Edges[i].CallSite < program.Edges[j].CallSite
}
return program.Edges[i].Callee < program.Edges[j].Callee
})
sort.Slice(program.ExternalCalls, func(i, j int) bool { return program.ExternalCalls[i] < program.ExternalCalls[j] })
return program
}
func summarizeFunction(cfg *CFG, name string) *Function {
if name == "" {
name = unknownFunctionName(cfg.Entry)
}
fn := &Function{
Name: name, Entry: cfg.Entry, Blocks: cfg.Blocks,
Calls: append([]int64(nil), cfg.Calls...), Unknown: cfg.Unknown,
}
edges := 0
for _, block := range cfg.Blocks {
if block.End > fn.End {
fn.End = block.End
}
fn.Instructions += len(block.Instrs)
edges += len(block.Succs)
}
fn.Complexity = edges - len(cfg.Blocks) + 2
if fn.Complexity < 1 {
fn.Complexity = 1
}
return fn
}
func unknownFunctionName(entry int64) string {
return "sub_" + strconv.FormatInt(entry, 16)
}
func buildBlocks(decoded map[int64]*rawInstr, inRange func(int64) bool) []*Block {
if len(decoded) == 0 {
return nil
}
addrs := make([]int64, 0, len(decoded))
for a := range decoded {
addrs = append(addrs, a)
}
sort.Slice(addrs, func(i, j int) bool { return addrs[i] < addrs[j] })
// Leaders: first instr, every branch target, and the instruction after any
// branch/call/ret.
leaders := map[int64]bool{addrs[0]: true}
for _, a := range addrs {
ri := decoded[a]
next := a + int64(ri.length)
switch ri.class {
case "ret":
if _, ok := decoded[next]; ok {
leaders[next] = true
}
case "jmp":
if ri.hasTgt {
if _, ok := decoded[ri.target]; ok {
leaders[ri.target] = true
}
}
if _, ok := decoded[next]; ok {
leaders[next] = true
}
case "cond", "call":
if ri.hasTgt {
if _, ok := decoded[ri.target]; ok {
leaders[ri.target] = true
}
}
if _, ok := decoded[next]; ok {
leaders[next] = true
}
}
}
var blocks []*Block
var cur *Block
flush := func() {
if cur != nil {
blocks = append(blocks, cur)
cur = nil
}
}
for idx, a := range addrs {
ri := decoded[a]
if leaders[a] {
flush()
cur = &Block{Start: a}
}
if cur == nil {
cur = &Block{Start: a}
}
cur.Instrs = append(cur.Instrs, ri.m)
next := a + int64(ri.length)
cur.End = next
// Decide whether this instruction terminates the block.
isLast := idx == len(addrs)-1
nextIsLeader := false
if !isLast {
if leaders[addrs[idx+1]] {
nextIsLeader = true
}
}
terminator := ri.class == "ret" || ri.class == "jmp" || ri.class == "cond"
if terminator || nextIsLeader || isLast {
cur.Kind = blockKind(ri.class)
switch ri.class {
case "ret":
// no successors
case "jmp":
if ri.hasTgt && inRange(ri.target) {
cur.Succs = []int64{ri.target}
}
case "cond":
if ri.hasTgt && inRange(ri.target) {
cur.Succs = append(cur.Succs, ri.target)
}
if _, ok := decoded[next]; ok {
cur.Succs = append(cur.Succs, next)
}
default:
if _, ok := decoded[next]; ok {
cur.Succs = []int64{next}
}
}
flush()
}
}
flush()
return blocks
}
func blockKind(class string) string {
switch class {
case "ret", "jmp", "cond", "call":
return class
default:
return "fallthrough"
}
}
// decodeAt decodes a single instruction at address addr (buf starts at base).
func decodeAt(buf []byte, addr, base int64, arch string) *rawInstr {
off := int(addr - base)
if off < 0 || off >= len(buf) {
return nil
}
end := off + 16 // max x86 instruction length; fixed-width arches need <= 4
if end > len(buf) {
end = len(buf)
}
res := Disassemble(buf[off:end], addr, arch, 1)
if len(res) == 0 {
return nil
}
m := res[0]
ri := &rawInstr{addr: addr, m: m}
ri.length = byteCount(m["bytes"].(string))
if ri.length <= 0 {
ri.length = 1
}
ri.mnem, _ = m["mnemonic"].(string)
ops, _ := m["operands"].(string)
ri.class = classify(ri.mnem)
if t, ok := directTarget(ops); ok {
ri.target, ri.hasTgt = t, true
}
if dataReferencing(ri.class) {
if d, ok := dataTarget(ops); ok {
ri.data, ri.hasDat = d, true
}
}
return ri
}
func byteCount(s string) int {
if s == "" {
return 0
}
return len(strings.Fields(s))
}
func classify(mnem string) string {
switch mnem {
case "ret", "retf", "iret":
return "ret"
case "jmp", "jmpf", "b", "br":
return "jmp"
case "call", "callf", "bl", "blr", "jal", "jalr":
return "call"
case "cbz", "cbnz", "tbz", "tbnz", "loop", "loope", "loopne", "jecxz", "jrcxz":
return "cond"
}
if strings.HasPrefix(mnem, "b.") { // AArch64 conditional branch
return "cond"
}
if strings.HasPrefix(mnem, "j") { // x86 Jcc (jmp already handled above)
return "cond"
}
if strings.HasPrefix(mnem, "beq") || strings.HasPrefix(mnem, "bne") { // MIPS
return "cond"
}
return "normal"
}
func dataReferencing(class string) bool {
// RIP-relative resolved targets show up as "; 0x.." comments on any
// instruction; only mine them on non-control-flow ops as data xrefs.
return class == "normal" || class == "call"
}
// directTarget extracts a branch/call target when the final operand is a bare
// absolute address (a direct, statically known target). Register/memory
// indirect operands return false.
func directTarget(operands string) (int64, bool) {
if i := strings.Index(operands, ";"); i >= 0 {
operands = operands[:i]
}
if c := strings.LastIndex(operands, ","); c >= 0 {
operands = operands[c+1:]
}
operands = strings.TrimSpace(operands)
return parseHexAddr(operands)
}
// dataTarget extracts a resolved address from a "; 0x.." trailing comment
// (e.g. an x86 RIP-relative memory operand).
func dataTarget(operands string) (int64, bool) {
i := strings.Index(operands, ";")
if i < 0 {
return 0, false
}
return parseHexAddr(strings.TrimSpace(operands[i+1:]))
}
func parseHexAddr(s string) (int64, bool) {
if !strings.HasPrefix(s, "0x") {
return 0, false
}
v, err := strconv.ParseUint(s[2:], 16, 64)
if err != nil {
return 0, false
}
return int64(v), true
}
+74
View File
@@ -0,0 +1,74 @@
package disasm
import (
"github.com/andydixon/nexus/internal/leviathan/document"
"github.com/andydixon/nexus/internal/leviathan/parsers"
)
// ImageAnalysis is a metadata-seeded analysis across file-backed executable
// regions in an ELF or PE image.
type ImageAnalysis struct {
Info *parsers.ExecutableInfo `json:"info"`
Programs []*Program `json:"programs"`
Truncated bool `json:"truncated"`
}
// DiscoverExecutable analyzes executable regions using header entries,
// symbols and exports as roots. Budgets apply across the whole image.
func DiscoverExecutable(d *document.Document, maxBytes int64, maxFunctions, maxInstr int) (*ImageAnalysis, error) {
info, err := parsers.InspectExecutable(d)
if err != nil {
return nil, err
}
if maxBytes <= 0 || maxBytes > 32<<20 {
maxBytes = 8 << 20
}
if maxFunctions <= 0 || maxFunctions > 4096 {
maxFunctions = 512
}
if maxInstr <= 0 || maxInstr > 1_000_000 {
maxInstr = 131_072
}
analysis := &ImageAnalysis{Info: info}
remainingBytes := maxBytes
remainingFunctions := maxFunctions
remainingInstr := maxInstr
for _, region := range info.Regions {
if remainingBytes <= 0 || remainingFunctions <= 0 || remainingInstr <= 0 {
analysis.Truncated = true
break
}
length := region.Size
if length > 4<<20 {
length = 4 << 20
analysis.Truncated = true
}
if length > remainingBytes {
length = remainingBytes
analysis.Truncated = true
}
if length <= 0 {
continue
}
buf, err := d.Range(region.Offset, length)
if err != nil {
return nil, err
}
seeds := make([]Seed, 0)
for _, seed := range info.Seeds {
if seed.Offset >= region.Offset && seed.Offset < region.Offset+length {
seeds = append(seeds, Seed{Name: seed.Name, Entry: seed.Offset})
}
}
if len(seeds) == 0 {
seeds = append(seeds, Seed{Name: region.Name, Entry: region.Offset})
}
program := DiscoverFunctionsFromSeeds(buf, region.Offset, info.Arch, seeds, remainingFunctions, remainingInstr)
analysis.Programs = append(analysis.Programs, program)
remainingBytes -= length
remainingFunctions -= len(program.Functions)
remainingInstr -= program.Instructions
analysis.Truncated = analysis.Truncated || program.Truncated
}
return analysis, nil
}
+144
View File
@@ -0,0 +1,144 @@
package disasm
import (
"encoding/binary"
"fmt"
)
func diMIPS32(buf []byte, base int64, little bool, maxInstr int) []map[string]any {
out := []map[string]any{}
for i := 0; i+4 <= len(buf) && len(out) < maxInstr; i += 4 {
var w uint32
if little {
w = binary.LittleEndian.Uint32(buf[i : i+4])
} else {
w = binary.BigEndian.Uint32(buf[i : i+4])
}
mn, ops, conf := decodeMIPS32(w, base+int64(i))
out = append(out, map[string]any{
"offset": base + int64(i),
"bytes": hexStr(buf[i : i+4]),
"mnemonic": mn,
"operands": ops,
"confident": conf,
})
}
for i := len(buf) - len(buf)%4; i < len(buf) && len(out) < maxInstr; i++ {
out = append(out, map[string]any{
"offset": base + int64(i),
"bytes": hexStr(buf[i : i+1]),
"mnemonic": "db",
"operands": hexCommaList(buf[i : i+1]),
"confident": false,
})
}
return out
}
func decodeMIPS32(w uint32, pc int64) (string, string, bool) {
if w == 0 {
return "nop", "", true
}
op := (w >> 26) & 0x3f
rs := (w >> 21) & 0x1f
rt := (w >> 16) & 0x1f
rd := (w >> 11) & 0x1f
shamt := (w >> 6) & 0x1f
funct := w & 0x3f
imm := uint16(w)
simm := int16(imm)
target26 := w & 0x03ffffff
switch op {
case 0x00:
switch funct {
case 0x00:
return "sll", fmt.Sprintf("%s, %s, %d", mipsReg(rd), mipsReg(rt), shamt), true
case 0x02:
return "srl", fmt.Sprintf("%s, %s, %d", mipsReg(rd), mipsReg(rt), shamt), true
case 0x03:
return "sra", fmt.Sprintf("%s, %s, %d", mipsReg(rd), mipsReg(rt), shamt), true
case 0x08:
return "jr", mipsReg(rs), true
case 0x09:
return "jalr", fmt.Sprintf("%s, %s", mipsReg(rd), mipsReg(rs)), true
case 0x0c:
return "syscall", fmt.Sprintf("0x%x", (w>>6)&0xfffff), true
case 0x20:
return "add", fmt.Sprintf("%s, %s, %s", mipsReg(rd), mipsReg(rs), mipsReg(rt)), true
case 0x21:
return "addu", fmt.Sprintf("%s, %s, %s", mipsReg(rd), mipsReg(rs), mipsReg(rt)), true
case 0x22:
return "sub", fmt.Sprintf("%s, %s, %s", mipsReg(rd), mipsReg(rs), mipsReg(rt)), true
case 0x23:
return "subu", fmt.Sprintf("%s, %s, %s", mipsReg(rd), mipsReg(rs), mipsReg(rt)), true
case 0x24:
return "and", fmt.Sprintf("%s, %s, %s", mipsReg(rd), mipsReg(rs), mipsReg(rt)), true
case 0x25:
return "or", fmt.Sprintf("%s, %s, %s", mipsReg(rd), mipsReg(rs), mipsReg(rt)), true
case 0x26:
return "xor", fmt.Sprintf("%s, %s, %s", mipsReg(rd), mipsReg(rs), mipsReg(rt)), true
case 0x27:
return "nor", fmt.Sprintf("%s, %s, %s", mipsReg(rd), mipsReg(rs), mipsReg(rt)), true
case 0x2a:
return "slt", fmt.Sprintf("%s, %s, %s", mipsReg(rd), mipsReg(rs), mipsReg(rt)), true
}
case 0x02:
return "j", fmt.Sprintf("0x%x", mipsJumpTarget(pc, target26)), true
case 0x03:
return "jal", fmt.Sprintf("0x%x", mipsJumpTarget(pc, target26)), true
case 0x04:
return "beq", fmt.Sprintf("%s, %s, 0x%x", mipsReg(rs), mipsReg(rt), mipsBranchTarget(pc, simm)), true
case 0x05:
return "bne", fmt.Sprintf("%s, %s, 0x%x", mipsReg(rs), mipsReg(rt), mipsBranchTarget(pc, simm)), true
case 0x08:
return "addi", fmt.Sprintf("%s, %s, %d", mipsReg(rt), mipsReg(rs), simm), true
case 0x09:
return "addiu", fmt.Sprintf("%s, %s, %d", mipsReg(rt), mipsReg(rs), simm), true
case 0x0a:
return "slti", fmt.Sprintf("%s, %s, %d", mipsReg(rt), mipsReg(rs), simm), true
case 0x0c:
return "andi", fmt.Sprintf("%s, %s, 0x%x", mipsReg(rt), mipsReg(rs), imm), true
case 0x0d:
return "ori", fmt.Sprintf("%s, %s, 0x%x", mipsReg(rt), mipsReg(rs), imm), true
case 0x0e:
return "xori", fmt.Sprintf("%s, %s, 0x%x", mipsReg(rt), mipsReg(rs), imm), true
case 0x0f:
return "lui", fmt.Sprintf("%s, 0x%x", mipsReg(rt), imm), true
case 0x20:
return "lb", mipsMem(rt, rs, simm), true
case 0x21:
return "lh", mipsMem(rt, rs, simm), true
case 0x23:
return "lw", mipsMem(rt, rs, simm), true
case 0x28:
return "sb", mipsMem(rt, rs, simm), true
case 0x29:
return "sh", mipsMem(rt, rs, simm), true
case 0x2b:
return "sw", mipsMem(rt, rs, simm), true
}
return ".word", fmt.Sprintf("0x%08x", w), false
}
func mipsJumpTarget(pc int64, target uint32) int64 {
upper := uint64(pc+4) & 0xfffffffff0000000
return int64(upper | uint64(target<<2))
}
func mipsBranchTarget(pc int64, imm int16) int64 {
return pc + 4 + int64(imm)*4
}
func mipsMem(rt, rs uint32, off int16) string {
return fmt.Sprintf("%s, %d(%s)", mipsReg(rt), off, mipsReg(rs))
}
func mipsReg(r uint32) string {
return [...]string{
"$zero", "$at", "$v0", "$v1", "$a0", "$a1", "$a2", "$a3",
"$t0", "$t1", "$t2", "$t3", "$t4", "$t5", "$t6", "$t7",
"$s0", "$s1", "$s2", "$s3", "$s4", "$s5", "$s6", "$s7",
"$t8", "$t9", "$k0", "$k1", "$gp", "$sp", "$fp", "$ra",
}[r&0x1f]
}
+117
View File
@@ -0,0 +1,117 @@
package disasm
import (
"encoding/binary"
"fmt"
)
func diThumb16(buf []byte, base int64, maxInstr int) []map[string]any {
out := []map[string]any{}
for i := 0; i+2 <= len(buf) && len(out) < maxInstr; i += 2 {
h := binary.LittleEndian.Uint16(buf[i : i+2])
mn, ops, conf := decodeThumb16(h, base+int64(i))
out = append(out, map[string]any{
"offset": base + int64(i),
"bytes": hexStr(buf[i : i+2]),
"mnemonic": mn,
"operands": ops,
"confident": conf,
})
}
for i := len(buf) - len(buf)%2; i < len(buf) && len(out) < maxInstr; i++ {
out = append(out, map[string]any{
"offset": base + int64(i),
"bytes": hexStr(buf[i : i+1]),
"mnemonic": "db",
"operands": hexCommaList(buf[i : i+1]),
"confident": false,
})
}
return out
}
func decodeThumb16(h uint16, pc int64) (string, string, bool) {
switch {
case h == 0xbf00:
return "nop", "", true
case h&0xff87 == 0x4700:
return "bx", thumbHighReg((h >> 3) & 0xf), true
case h&0xff87 == 0x4780:
return "blx", thumbHighReg((h >> 3) & 0xf), true
case h&0xf800 == 0xe000:
imm := signExtend64(uint64(h&0x07ff)<<1, 12)
return "b", fmt.Sprintf("0x%x", pc+4+imm), true
case h&0xf000 == 0xd000 && h&0x0f00 != 0x0f00:
cond := uint32((h >> 8) & 0xf)
imm := signExtend64(uint64(h&0x00ff)<<1, 9)
return "b" + armCondSuffix(cond), fmt.Sprintf("0x%x", pc+4+imm), true
case h&0xff00 == 0xdf00:
return "svc", fmt.Sprintf("#0x%x", h&0x00ff), true
case h&0xfe00 == 0xb400:
mask := uint32(h & 0x00ff)
if h&0x0100 != 0 {
mask |= 1 << 14
}
return "push", armRegList(mask), true
case h&0xfe00 == 0xbc00:
mask := uint32(h & 0x00ff)
if h&0x0100 != 0 {
mask |= 1 << 15
}
return "pop", armRegList(mask), true
case h&0xf800 == 0x2000:
rd := uint32((h >> 8) & 0x7)
return "movs", fmt.Sprintf("%s, #0x%x", armReg(rd), h&0x00ff), true
case h&0xf800 == 0x2800:
rn := uint32((h >> 8) & 0x7)
return "cmp", fmt.Sprintf("%s, #0x%x", armReg(rn), h&0x00ff), true
case h&0xf800 == 0x3000:
rd := uint32((h >> 8) & 0x7)
return "adds", fmt.Sprintf("%s, #0x%x", armReg(rd), h&0x00ff), true
case h&0xf800 == 0x3800:
rd := uint32((h >> 8) & 0x7)
return "subs", fmt.Sprintf("%s, #0x%x", armReg(rd), h&0x00ff), true
case h&0xfc00 == 0x4000:
op := (h >> 6) & 0xf
rs := uint32((h >> 3) & 0x7)
rd := uint32(h & 0x7)
names := [...]string{"ands", "eors", "lsls", "lsrs", "asrs", "adcs", "sbcs", "rors", "tst", "rsbs", "cmp", "cmn", "orrs", "muls", "bics", "mvns"}
mn := names[op]
if op == 8 || op == 10 || op == 11 {
return mn, fmt.Sprintf("%s, %s", armReg(rd), armReg(rs)), true
}
return mn, fmt.Sprintf("%s, %s", armReg(rd), armReg(rs)), true
case h&0xfe00 == 0x1800:
rm := uint32((h >> 6) & 0x7)
rn := uint32((h >> 3) & 0x7)
rd := uint32(h & 0x7)
mn := "adds"
if h&0x0200 != 0 {
mn = "subs"
}
return mn, fmt.Sprintf("%s, %s, %s", armReg(rd), armReg(rn), armReg(rm)), true
case h&0xe000 == 0x6000:
imm := uint32((h>>6)&0x1f) * 4
rn := uint32((h >> 3) & 0x7)
rd := uint32(h & 0x7)
mn := "str"
if h&0x0800 != 0 {
mn = "ldr"
}
return mn, fmt.Sprintf("%s, [%s, #0x%x]", armReg(rd), armReg(rn), imm), true
}
return ".hword", fmt.Sprintf("0x%04x", h), false
}
func thumbHighReg(r uint16) string {
if r == 13 {
return "sp"
}
if r == 14 {
return "lr"
}
if r == 15 {
return "pc"
}
return fmt.Sprintf("r%d", r)
}
+91
View File
@@ -0,0 +1,91 @@
// Package disasm provides a honest, limited heuristic disassembler.
// It does NOT aim to be a full x86/ARM decoder. It recognises a
// subset of common opcodes so the UI can show plausible disassembly
// for inspection, and is explicit in the output when instructions
// are unknown.
package disasm
import "fmt"
// Instr is one decoded instruction.
type Instr struct {
Offset int64
Bytes []byte
Mnemonic string
Operands string
Confident bool
}
// Disassemble returns a slice of Instr for buf starting at `base`.
// arch is one of "x86", "x86_64", "arm", "thumb", "aarch64", "mips".
// For unsupported arches we emit a byte stream with "db" entries.
func Disassemble(buf []byte, base int64, arch string, maxInstr int) []map[string]any {
if maxInstr <= 0 {
maxInstr = 4096
}
switch arch {
case "x86", "x86_64":
return diX86(buf, base, arch == "x86_64", maxInstr)
case "aarch64", "arm64":
return diAArch64(buf, base, maxInstr)
case "arm", "arm32":
return diARM32(buf, base, maxInstr)
case "thumb", "thumb16", "t16":
return diThumb16(buf, base, maxInstr)
case "mips", "mipsbe", "mips32":
return diMIPS32(buf, base, false, maxInstr)
case "mipsle", "mipsel":
return diMIPS32(buf, base, true, maxInstr)
}
return diRaw(buf, base, arch, maxInstr)
}
// diRaw emits `db` lines 16 bytes at a time for unsupported arches.
func diRaw(buf []byte, base int64, arch string, maxInstr int) []map[string]any {
out := []map[string]any{}
step := 16
for i := 0; i < len(buf) && len(out) < maxInstr; i += step {
end := i + step
if end > len(buf) {
end = len(buf)
}
out = append(out, map[string]any{
"offset": base + int64(i),
"bytes": hexStr(buf[i:end]),
"mnemonic": "db",
"operands": hexCommaList(buf[i:end]),
"confident": false,
"note": arch + " decode not implemented; raw bytes only",
})
}
return out
}
// diX86 decodes x86 / x86-64 instructions. The full decoder lives in
// x86_decode.go; this dispatcher only forwards to it.
func diX86(buf []byte, base int64, is64 bool, maxInstr int) []map[string]any {
return decodeX86(buf, base, is64, maxInstr)
}
func hexStr(b []byte) string {
const h = "0123456789abcdef"
out := make([]byte, 0, len(b)*3)
for i, c := range b {
if i > 0 {
out = append(out, ' ')
}
out = append(out, h[c>>4], h[c&0x0f])
}
return string(out)
}
func hexCommaList(b []byte) string {
out := ""
for i, c := range b {
if i > 0 {
out += ","
}
out += fmt.Sprintf("0x%02x", c)
}
return out
}
File diff suppressed because it is too large Load Diff