Callgraph Analysis
April 24, 2026 ยท View on GitHub
This document describes how blint computes callgraph metadata and exports, including
current heuristics, confidence semantics, known limitations, and practical FP/FN triage.
What the Callgraph Represents
blint builds a static callgraph from the disassembled_functions attribute.
- Internal edge: source and destination both map to known internal function nodes.
- External edge: source has call evidence but destination cannot be mapped uniquely.
- Edge kinds:
direct: direct call instruction (call,bl,jal, etc.)tailcall: terminal unconditional jump treated as call transferindirect_hint: inferred indirect target (register/memory-chain heuristic)
- Confidence levels:
high: direct match to concrete internal destinationmedium: deterministic but approximate recovery (for example tailcall recovery)low: heuristic inference or unresolved external entry
Implementation principles:
- Determinism: Node ordering and tie-breaking are explicitly stable.
- Alias collapse: Multiple symbols sharing one entry address are collapsed, reducing false ambiguity.
- Architecture handling: x86, AArch64, and MIPS operand normalization paths are implemented.
- Heuristic transparency: Unresolved targets are retained with reason buckets instead of dropped.
- Operational export: Confidence filtering is implemented for Mermaid, GraphML, and GEXF exports.
Unique heuristics:
- Register-target tracking (
_update_register_target) supports immediate and memory-chain recovery. - Tailcall recovery emits terminal unconditional branch edges as
tailcalltargets. - Windows x86_64 chain propagation is depth-limited (
chain_hop_limit=2) to contain FP growth. - Windows AArch64 excludes indexed memory chain propagation and non-canonical high candidates.
- Mach-O disassembly skips system-library symbol names that start with
/usr/lib/or/System/Library/.
End-to-End Algorithm
Pass 1: Node Construction and Alias Collapse
Source: disassembled_functions entries.
Per function node fields:
id,key,name,address,aliases
Determinism and normalization:
- Nodes are sorted by
(address, name, key)before IDs are assigned. - Multiple symbol names sharing one entrypoint are collapsed into one canonical node.
Why this matters:
- Reduces ambiguous-name false positives in symbol-rich binaries.
- Keeps output stable across runs and host environments.
Pass 2: Call Target Extraction from Disassembly
Each disassembled call-like site emits a target record with:
target_nametarget_addresstarget_address_candidatesraw_operandkind
Address candidate generation includes architecture-aware normalization:
- RIP-relative slot decoding on x86 (
[rip + off]) - decimal relative immediate handling (platform-aware)
- AArch64 signed/absolute immediate normalization
- ARM64 pointer-auth branch forms (
blraa/blrab,braa/brab) with register-target parsing
Pass 3: Tailcall Recovery
If a function ends with an unconditional branch and a concrete operand, blint emits a
tailcall target record.
Goal:
- Capture compiler/optimizer tail dispatch that otherwise fragments the graph.
Pass 4: Indirect Hint Recovery (Register and Memory)
blint tracks lightweight register state to recover likely call targets before indirect
calls.
Supported patterns include:
mov reg, IMM|SYMthencall regadd/sub reg, immpropagation over tracked candidatescall [reg + off]using tracked base candidatescall [rip + off]slot lookup through symbol/address maps
Windows-focused memory-chain passes
Current Windows x86_64 passes include:
mov reg, [base + off]chain propagation to inferred candidate slots.call [reg + off]lookup using inferred candidates.- strict chain hop limit (default
2), so deeper chains stop propagating.
Windows ARM64-specific indirect-target filters
Current PE AArch64 handling adds two precision guardrails:
- Do not propagate register-target chains through indexed memory operands
(for example
ldr x8, [x9, x10, lsl #3]) because target address depends on runtime index. - Drop non-canonical high virtual-address candidates in register chains, reducing
sentinel-constant pollution (for example
0x7fff...ffffarithmetic artifacts).
Example supported depth:
mov rcx, [rax+off] -> mov rdx, [rcx+off] -> call [rdx+off](resolved when map exists)
Example intentionally not propagated:
mov r8, [rdx+off] -> call [r8+off]when this would exceed hop limit.
Pass 5: Internal Destination Matching and Disambiguation
Call targets are matched to internal nodes using priority evidence, including:
- exact address
- normalized address (
& ~1) - RVA/image-base transforms
- range containment fallback
- symbol-name fallback (with alias normalization such as PLT forms)
If multiple candidates tie, deterministic disambiguation is applied (primary address, range width, distance, stable ID order).
Pass 6: Unresolved Bucketing
When no unique internal destination is selected, evidence is emitted to external with
reason buckets such as:
ambiguous_addressambiguous_nameaddress_space_misssymbol_only_missraw_immunresolved
Kind suffixes are appended where applicable, for example
unresolved:indirect_hint.
Export Tuning by Confidence
Callgraph metadata remains complete in *-metadata.json. Export renderers can filter by
confidence threshold:
blint --disassemble --export-callgraph-mermaid --callgraph-min-confidence low
blint --disassemble --export-callgraph-graphml --callgraph-min-confidence medium
blint --disassemble --export-callgraph-gexf --callgraph-min-confidence high
Notes:
lowkeeps all edges/external entries.mediumdrops low-confidence entries (usually manyindirect_hint/external links).highkeeps only strongest internal evidence.
Limitations and Trade-offs
- Indirect calls are heuristic; absence of edge is not proof of no control flow.
- Name recovery from pointer slots depends on available symbols/relocations.
- Aggressive propagation can inflate false positives; strict hop limiting reduces this.
- Indexed ARM64 address forms can look call-like but are frequently data-table lookups; Windows ARM64 now prefers precision over speculative propagation for these sites.
- External reason shifts can occur when one heuristic classifies evidence differently without semantic regression.
Understanding False Positives and False Negatives
Recommended review order:
- Validate
directinternal edges first (highest precision). - Add
tailcalledges to inspect dispatch behavior. - Triage
indirect_hintas hints, not proof. - Inspect top external reason buckets to spot systemic misses.
Common FP signatures:
- Over-propagated register chains in heavily optimized dispatch code.
- Ambiguous-name fallback when aliases are dense.
- ARM64 constant-sentinel math (
max-intstyle values) misread as call targets before candidate filtering.
Common FN signatures:
- Deep virtual dispatch beyond configured hop limit.
- Missing import/relocation symbol context.
- Non-canonical operand formatting not yet normalized.
- ARM64 indexed-memory dispatch (
[base, index, lsl #N]) intentionally not propagated in Windows ARM64 mode to avoid high-FP explosions.
For regression control, use platform baselines and label assertions to track both KPI and local FP/FN behavior across changes.
Weaknesses in current implementation
- No deep value-flow engine: Indirect resolution is heuristic, not full points-to/dataflow analysis.
- Symbol quality dependence: Precision and recall depend on symbol/relocation availability, demangling performance, and formatting.
- Static-only confidence model: Confidence ranks are rule-based.
Comparison with Other Approaches
Rust/cargo callgraph workflows
Relevant facts from upstream docs:
rustc --emitcan producellvm-irandmiroutputs.- LLVM has callgraph printer passes such as
print-callgraphanddot-callgraph(viaopt). cargo-call-stackdescribes itself as a static whole-program stack-usage analyzer, warns about reliance on nightly/experimental behavior, and documents limited utility with heavy indirect calls/dynamic dispatch.
Relative to those workflows:
blintstrengths:- Works directly on released binaries (ELF/PE/Mach-O/WASM) without requiring source build pipelines.
- Produces unresolved buckets and confidence-tagged edges for triage.
- Integrates directly into security-review and metadata flows.
blintweaknesses:- Lacks compiler-internal semantic context available in MIR/LLVM-IR pipelines.
- Cannot provide source-level guarantees that compiler-IR workflows can sometimes provide in controlled builds.
Capstone-centered approaches
Capstone positions itself as a lightweight multi-platform, multi-architecture disassembly framework.
Relative comparison:
blintstrengths:- Delivers end-to-end callgraph outputs, unresolved reason taxonomy, and export/report integration.
- Includes platform-specific heuristics already packaged for security triage use.
blintweaknesses:- A custom Capstone pipeline can be more customizable for niche instruction-level semantics.
blintcurrently favors deterministic heuristics over highly specialized per-target tuning.
Ghidra decompiler-style approaches
Ghidra documents a full SRE framework with disassembly, decompilation, graphing, scripting, and both interactive and automated modes.
Relative comparison:
blintstrengths:- Lightweight CLI-first automation and JSON-focused outputs fit CI/security scanning workflows.
- Deterministic callgraph serialization and KPI regression flow are straightforward to automate.
blintweaknesses:- Does not provide decompiler-level high-level recovery and analyst-driven interactive refinement.
- Not designed to replace full interactive reverse-engineering sessions for complex indirect dispatch recovery.