Skip to content
Merged
10 changes: 5 additions & 5 deletions docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -459,15 +459,15 @@ Sources and Libs now mark availability and filter on it (`v` cycles the filter):
`v` cycles all → on-disk → in-cache (`libAvail` via `explorer.ResolveLibPath`
/ `IsDyldSharedCacheLib`).

## 30. Extract syscalls
## 30. Extract syscalls

Extract all the syscalls used in binary

## 31. Extract pathes
## 31. Extract pathes

Extract all the pathes from strings
Comment on lines +466 to 468

## 32. Search
## 32. Search

0x000106b6 should match with $0x106b6

Expand Down Expand Up @@ -519,13 +519,13 @@ Info view: arch + bits + endianness · OS/ABI (ELF `OSABI`; Mach-O — decode
only counted) · static/dynamic/PIE · interpreter · needed-library count · CPU
baseline (from #34).

## 36. Find-anything quick jump
## 36. Find-anything quick jump

Broaden the goto modal (#…/`g`) beyond symbols + addresses to also rank sections
and strings, so one keystroke finds *any* named thing in the binary and jumps to
it — a single fuzzy "jump to anything" entry point.

## 37. Architecture cleanup (internal)
## 37. Architecture cleanup (internal)

Reduce duplication and the cache-invalidation bug surface in `internal/ui`:

Expand Down
11 changes: 6 additions & 5 deletions docs/hyper.sh
Original file line number Diff line number Diff line change
@@ -1,29 +1,30 @@
#!/bin/sh
BIN=${1:-/bin/ls}
EXEX=${2:-exex}
echo "\n============"
hyperfine \
--warmup 5 \
"exex '$BIN' -o strings" \
"$EXEX '$BIN' -o strings" \
"strings -a -t x '$BIN'"

echo "\n============"
hyperfine \
--warmup 5 \
"exex '$BIN' -o syms" \
"$EXEX '$BIN' -o syms" \
"nm -C -n -a '$BIN'"

echo "\n============"
hyperfine --warmup 5 \
"exex '$BIN' -o sections" \
"$EXEX '$BIN' -o sections" \
"objdump -h '$BIN'"

echo "\n============"
hyperfine \
"exex '$BIN' -o disasm" \
"$EXEX '$BIN' -o disasm" \
"objdump -d '$BIN'" \
"otool -tvV '$BIN'"

echo "\n============"
hyperfine \
"exex '$BIN' -o disasm-all" \
"$EXEX '$BIN' -o disasm-all" \
"objdump -D '$BIN'"
106 changes: 63 additions & 43 deletions internal/binfile/elf.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,18 @@ func elfDebugLink(ef *elf.File) string {
return ""
}

func (f *File) elfHasDWARF(ef *elf.File) bool {
if f.debugPath != "" || elfDebugLink(ef) != "" {
return true
}
for _, s := range ef.Sections {
if strings.HasPrefix(s.Name, ".debug") || strings.HasPrefix(s.Name, ".zdebug") {
return true
}
}
return false
}

// loadELF parses f.raw as an ELF object and populates the neutral model.
func (f *File) loadELF() error {
ef, err := elf.NewFile(bytes.NewReader(f.raw))
Expand Down Expand Up @@ -136,6 +148,48 @@ func (f *File) loadELF() error {
f.synthetic = base > 0
}

for _, p := range ef.Progs {
paddr := p.Paddr
if paddr == p.Vaddr {
paddr = 0 // only carry a physical address when it differs
}
f.Segments = append(f.Segments, Segment{
Name: strings.TrimPrefix(p.Type.String(), "PT_"),
Addr: p.Vaddr,
PhysAddr: paddr,
Size: p.Memsz,
Offset: p.Off,
FileSize: p.Filesz,
Align: p.Align,
R: p.Flags&elf.PF_R != 0,
W: p.Flags&elf.PF_W != 0,
X: p.Flags&elf.PF_X != 0,
})
}
// Section load addresses (LMA): map each section through the PT_LOAD segment
// whose file bytes contain it. LMA = p_paddr + (sh_offset - p_offset). Only
// recorded when it differs from the virtual address (higher-half kernels etc.).
for i := range f.Sections {
s := &f.Sections[i]
if s.Addr == 0 || s.FileSize == 0 {
continue
}
for _, p := range ef.Progs {
if p.Type != elf.PT_LOAD || p.Paddr == p.Vaddr || p.Filesz == 0 {
continue
}
if s.Offset >= p.Off && s.Offset < p.Off+p.Filesz {
if lma := p.Paddr + (s.Offset - p.Off); lma != s.Addr {
s.PhysAddr = lma
}
break
}
}
}
if f.layoutOnly {
return nil
}

staticSyms, _ := ef.Symbols()
dynSyms, _ := ef.DynamicSymbols()
type symKey struct {
Expand Down Expand Up @@ -181,53 +235,19 @@ func (f *File) loadELF() error {
}
f.appendELFImportSymbols(ef)

for _, p := range ef.Progs {
paddr := p.Paddr
if paddr == p.Vaddr {
paddr = 0 // only carry a physical address when it differs
}
f.Segments = append(f.Segments, Segment{
Name: strings.TrimPrefix(p.Type.String(), "PT_"),
Addr: p.Vaddr,
PhysAddr: paddr,
Size: p.Memsz,
Offset: p.Off,
FileSize: p.Filesz,
Align: p.Align,
R: p.Flags&elf.PF_R != 0,
W: p.Flags&elf.PF_W != 0,
X: p.Flags&elf.PF_X != 0,
})
}
// Section load addresses (LMA): map each section through the PT_LOAD segment
// whose file bytes contain it. LMA = p_paddr + (sh_offset - p_offset). Only
// recorded when it differs from the virtual address (higher-half kernels etc.).
for i := range f.Sections {
s := &f.Sections[i]
if s.Addr == 0 || s.FileSize == 0 {
continue
}
for _, p := range ef.Progs {
if p.Type != elf.PT_LOAD || p.Paddr == p.Vaddr || p.Filesz == 0 {
continue
}
if s.Offset >= p.Off && s.Offset < p.Off+p.Filesz {
if lma := p.Paddr + (s.Offset - p.Off); lma != s.Addr {
s.PhysAddr = lma
}
break
}
}
}

if d := f.elfDWARF(ef); d != nil {
f.dwarf = d // line table decoded lazily on first source lookup
if f.elfHasDWARF(ef) {
f.dwarfAvail = true
f.dwarfBuild = func() *dwarf.Data { return f.elfDWARF(ef) }
}

f.loadELFInfo(ef)
f.header = f.elfHeaderInfo(ef)
f.rawHeader = f.elfRawHeader(ef)
f.relocBuild = func() []Reloc { return f.elfRelocs(ef) }
f.relocAvail = elfHasRelocs(ef)
f.relocAvailSet = true
if f.relocAvail {
f.relocBuild = func() []Reloc { return f.elfRelocs(ef) }
}
return nil
}

Expand Down Expand Up @@ -573,7 +593,7 @@ func (f *File) elfHeaderInfo(ef *elf.File) []string {
fmt.Sprintf("Entry: 0x%x", h.Entry),
fmt.Sprintf("Sections: %d", len(f.Sections)),
fmt.Sprintf("Symbols: %d", len(f.Symbols)),
fmt.Sprintf("DWARF info: %v", f.dwarf != nil),
fmt.Sprintf("DWARF info: %v", f.HasDWARF()),
}
}

Expand Down
9 changes: 6 additions & 3 deletions internal/binfile/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -340,10 +340,13 @@ func (f *File) AddrDisassemblable(addr uint64) bool {
// HasExecCode reports whether the file has any executable section to disassemble
// in the normal (exec-only) image — false for most relocatable object files.
func (f *File) HasExecCode() bool {
if f.execImage == nil {
f.execImage = f.buildImage(func(s *Section) bool { return s.Exec })
for i := range f.Sections {
s := &f.Sections[i]
if s.Alloc && s.Exec && s.Size != 0 && s.FileSize != 0 {
return true
}
}
return len(f.execImage.Regions) > 0
return false
}

// IncludeInDisasmAll reports whether a section belongs in a disasm-all sweep.
Expand Down
37 changes: 24 additions & 13 deletions internal/binfile/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import "fmt"
type Option func(*openOptions)

type openOptions struct {
debugPath string
arch string
debugPath string
arch string
layoutOnly bool
}

// WithDebugPath points the loader at an explicit external debug-symbols file or
Expand All @@ -24,26 +25,34 @@ func WithArch(name string) Option {
return func(o *openOptions) { o.arch = name }
}

// WithLayoutOnly loads just the container layout: architecture, entry, sections,
// segments and raw bytes. It skips symbols, imports, relocations, DWARF and the
// overview fields, for views that do not need them.
func WithLayoutOnly() Option {
return func(o *openOptions) { o.layoutOnly = true }
}

// Open reads path, detects its container format, and builds the neutral model.
func Open(path string, opts ...Option) (*File, error) {
var o openOptions
for _, opt := range opts {
opt(&o)
}
// mapFile mmaps the file where that's safe (always on Linux; on macOS only
// when the Mach-O carries no code signature, since mmap'ing a signed binary
// gets the process SIGKILL'd), otherwise it reads the file into the heap.
// mapFile mmaps the file where that's supported, otherwise it reads the file
// into the heap. On macOS the mmap path uses MAP_RESILIENT_CODESIGN so signed
// Mach-O pages do not SIGKILL the reader if code-signing validation fails.
raw, closer, err := mapFile(path)
if err != nil {
return nil, err
}
f := &File{
Path: path,
debugPath: o.debugPath,
reqArch: o.arch,
raw: raw,
unmap: closer,
sources: map[string][]string{},
Path: path,
debugPath: o.debugPath,
reqArch: o.arch,
layoutOnly: o.layoutOnly,
raw: raw,
unmap: closer,
sources: map[string][]string{},
}
if err := f.load(); err != nil {
f.Close()
Expand Down Expand Up @@ -87,8 +96,10 @@ func (f *File) load() error {
default:
return fmt.Errorf("unrecognised file format (not ELF, Mach-O, or PE)")
}
f.finalizeSymbols()
f.computeOverview()
if !f.layoutOnly {
f.finalizeSymbols()
f.computeOverview()
}
return nil
}

Expand Down
Loading
Loading