Files
magnus919_agent-skills/binary-analysis/references/function-analysis.md
T

14 KiB

Function-Level Analysis

Deep-dive reference for decompiling, disassembling, and tracing individual functions. Load this when the triage or the user identifies a specific function to investigate, when you need to understand pseudocode output, or when tracing call paths between functions.

When to Go Deep

Load this reference when:

  • The user asks to decompile or disassemble a specific function.
  • A triage heuristic points to a suspicious function (high risk_score, multiple callers, unusual address).
  • An unknown from the triage mentions an unresolved indirect call or obfuscated region that needs manual investigation.
  • You need to trace the call path from one function to another.
  • You need to understand which functions call or are called by a specific function.

Do NOT load this for broad surveys (use triage) or format-level questions (use binary-formats.md).

Prerequisites

Before starting function-level analysis, confirm:

# The project exists and has been analyzed
binary project status <proj> --json
# state should be READY (or STALE — you can still query, but note staleness)

Decompilation

Basic Decompilation

binary decompile --project <proj> function:<name-or-address> --json

The response contains:

  • data.pseudocode: Reconstructed C-like pseudocode.
  • data.address_map: Maps source line numbers to canonical addresses. Use this to correlate pseudocode lines with disassembly addresses.
  • data.diagnostics[]: Any limitations the decompiler encountered (indirect calls, switch tables that couldn't be fully recovered, type ambiguities).

Important: The output is reconstructed pseudocode, not original source. Variables are auto-named (local_10, param_1, etc.). Types are inferred by the decompiler and may not match the original source types. Control flow is reconstructed and may differ from the original layout.

Selector Resolution

Function selectors work by name or address:

# By name (exact or substring match)
binary decompile --project <proj> function:main --json
binary decompile --project <proj> function:CreateProcessW --json

# By address
binary decompile --project <proj> function:0x401000 --json

Ambiguous selectors (exit code 8): If a name matches multiple functions (e.g., main exists in both the binary and an imported library, or a substring matches multiple entries), the CLI returns a list of candidates:

{
  "success": false,
  "data": {
    "candidates": [
      {"address": "0x401000", "name": "main"},
      {"address": "0x402000", "name": "WinMain"}
    ]
  }
}

Resolve by using the full name or address from the candidates list.

Understanding Pseudocode Output

Variable Naming

Prefix Meaning
param_1, param_2, ... Function parameters (calling convention dependent)
local_10, local_18, ... Stack local variables (hex offset from stack frame base)
uVar1, iVar2, ... Temporary variables — the decompiler created these
DAT_<addr>, PTR_<addr> Global data references at that address
FUN_<addr> Indirect function call target (unresolved)

Common Patterns and What They Mean

Memory allocation:

local_10 = FUN_00401200(8);   // malloc(8) or new(8)

Look at the surrounding code to determine if the allocation is checked for NULL.

String operations:

FUN_00401400(local_20, "some_string");   // strcpy or similar
FUN_00401450(local_20, local_10, 0x100); // strncpy(dst, src, 256)

Unchecked string copies → possible buffer overflow. Note the buffer size.

API calls:

uVar1 = FUN_00401800(local_30, 0, 0, 0, 0, 0, 0);

This is likely a call through the IAT (Import Address Table). The address FUN_00401800 is a thunk. Use binary callees to resolve:

binary callees --project <proj> function:<name> --json

Indirect calls:

(*(code *)local_10)(param_1);   // Call through function pointer

The target is resolved at runtime. Use binary xrefs on the address where the function pointer is stored to find possible assignments. Note this as an unknown if the target cannot be determined.

Loop patterns:

local_c = 0;
while (local_c < local_10) { ... local_c = local_c + 1; }

Standard for (i = 0; i < n; i++) loop.

Decompilation Timeouts and Partial Results

If decompilation times out (exit code 12), the response may still contain partial pseudocode. The partial: true flag and diagnostics explain what was incomplete.

Do NOT present partial pseudocode as complete. Include a caveat: "The decompiler produced partial pseudocode before timing out. The following analysis is based on incomplete results."

Large Functions

For functions with hundreds of basic blocks, decompilation may be slow. Use --timeout to set a longer limit:

binary decompile --project <proj> function:<name> --timeout 600 --json

Disassembly

Disassembly provides the raw instruction stream — more granular than pseudocode but lower-level.

Disassembling a Function

binary disassemble --project <proj> function:<name> --json

Each instruction in data.instructions[] contains:

  • mnemonic: The instruction (e.g., "mov", "call", "jmp", "xor")
  • operands: The operands (e.g., "eax, dword ptr [ebp-0x8]")
  • bytes: Raw bytes in hex (e.g., "8b 45 f8")
  • address: Canonical address object

Disassembling an Address Range

binary disassemble --project <proj> 0x401000..0x401200 --json

Use this when:

  • You want to see the code before/after a function boundary.
  • A function boundary is uncertain and you want to check adjacent code.
  • You're investigating a specific address from xrefs or strings output.

Partial Disassembly

If the address range spans both mapped and unmapped regions, the response has partial: true and a diagnostic about the unmapped gap. The data.instructions array contains only the mapped portion. This is expected behavior for ranges that cross segment boundaries.

When to Use Disassembly vs Decompilation

Use Disassembly When Use Decompilation When
You need exact instruction sequence (e.g., for exploit analysis) You want to understand high-level logic
The decompiler produced incomplete/confusing output The function is straightforward and you want readable code
You're looking for specific instructions (syscall, int 0x80, cpuid, rdtsc) You're analyzing control flow and branching
You need to verify the decompiler's interpretation You're presenting findings to someone who doesn't read assembly
The function is small (< 20 instructions) The function is large (> 50 instructions)

Cross-References (XRefs)

Cross-references show which code references a specific entity and which entities the target references.

binary xrefs --project <proj> function:<name> --json
binary xrefs --project <proj> 0x402080 --json

Each reference entry:

  • from, to: Address objects. Direction depends on context.
  • kind: CALL, JUMP, READ, WRITE, DATA, IMPORT, EXPORT, INDIRECT, or UNKNOWN.
  • confidence: Backend confidence in this reference.

Interpreting XRef Kinds

Kind Meaning Example
CALL The source calls the target call sub_401000
JUMP The source jumps to the target jmp loc_401100 (tail call or branch)
READ The source reads data from the target address mov eax, [0x403000]
WRITE The source writes data to the target address mov [0x403000], eax
DATA The target's address appears as a data value Function pointer in a vtable or array
INDIRECT The reference is through a pointer or register call [eax+0x10]
IMPORT Reference to an imported symbol call [__imp_CreateFileW]
EXPORT Reference from an exported symbol A function is exported

Key questions xrefs answers:

  • "Who calls this function?" → Filter for kind: CALL where to matches your function.
  • "What data does this function read?" → Filter for kind: READ where from is within your function.
  • "Is this function's address stored anywhere?" → Filter for kind: DATA where to matches your function (potential callback or function pointer table entry).

Callers and Callees

Focused versions of xrefs that return only CALL relationships.

Finding Callers

binary callers --project <proj> function:<name> --json

Returns data.callers[] — functions that call the target.

Use callers to answer:

  • "What code paths reach this function?"
  • "Is this function reachable from main?" (use binary trace)
  • "Is this function ever called?" (empty callers → dead code or callback only)

Finding Callees

binary callees --project <proj> function:<name> --json

Returns data.callees[] — functions called by the target.

Use callees to answer:

  • "What system APIs does this function invoke?"
  • "What helper functions does it use?"
  • "Is this function a leaf (no callees) or a dispatcher (many callees)?"

Tip: Resolve Import Thunks

If a callee has name_source: "IMPORTED", it's an import thunk — the actual API is the symbol name. For example, kernel32.dll_CreateFileW means the function calls CreateFileW.

Call Graph

Build a bounded graph of call relationships:

binary callgraph --project <proj> function:<name> --depth 3 --json

The response contains:

  • data.graph.nodes[]: Functions in the graph (each with name, address)
  • data.graph.edges[]: Call relationships (from → to)

Depth control:

  • Default depth: 3 (callers + callees up to 3 hops)
  • Maximum depth: 10
  • Depth 0 or negative → exit code 2 (INVALID_ARGS)

When to Use Call Graph

Scenario Action
"What's the call tree under main?" callgraph with main as root
"What functions lead to this suspicious API?" callgraph with the suspicious function as root, then check callers
"How deep is the call chain here?" callgraph with increasing depth until you hit leaves
"Map the attack surface" callgraph from entry points and exports

Breadth limits: If a function calls hundreds of others (e.g., a large switch-based dispatcher), the graph may be truncated. Check diagnostics for truncation warnings. The depth parameter is a hard limit — nodes at depth + 1 are not included.

Trace (Path Finding)

Find call paths between two entities:

binary trace --project <proj> --from function:main --to function:CreateFileW --json

Returns data.paths[] — ordered sequences of functions from source to target.

Path Finding Parameters

Flag Default Purpose
--depth 5 Maximum path length (number of hops)
--from (required) Source entity
--to (required) Target entity

Interpreting Trace Results

  • Multiple paths: There are multiple call chains from source to target. Present the shortest and most interesting paths.
  • No paths (empty array): There is no call chain within the depth limit. The functions may still be related through data flow or indirect calls.
  • Depth limit reached: Paths exist but exceed the depth limit. Increase --depth if needed (be aware of output limits).

Use trace to answer:

  • "Can main() reach this suspicious function?"
  • "What's the shortest call path to the network API?"
  • "Is there a code path from the entry point to the decryption routine?"

Bytes (Raw Memory Read)

Read raw bytes at a specific address:

binary bytes --project <proj> 0x401000 64 --json

Returns:

  • data.hex: Hex string (2 * length chars)
  • data.base64: Base64-encoded bytes
  • data.address: Canonical address
  • data.length: Actual bytes returned (may be less than requested at segment boundaries)

Use bytes to:

  • Inspect data referenced by xrefs (constants, strings, jump tables).
  • Verify instruction encoding by reading bytes at a disassembly address.
  • Extract embedded data referenced by pseudocode (e.g., DAT_00403000).

Analysis Flow for a Suspicious Function

When triage flags a function for investigation, follow this flow:

  1. Decompile to understand the high-level logic:

    binary decompile --project <proj> function:<name> --json
    
  2. Check callees to see what APIs and helpers it uses:

    binary callees --project <proj> function:<name> --json
    
  3. Check callers to understand the context — who invokes this function:

    binary callers --project <proj> function:<name> --json
    
  4. Check cross-references for data access patterns (writes to global state, reads from configuration areas):

    binary xrefs --project <proj> function:<name> --json
    
  5. Trace from entry point to see if the function is reachable:

    binary trace --project <proj> --from function:entry --to function:<name> --json
    
  6. Synthesize your findings. What does this function do? What APIs does it use? Is it reachable from normal program flow? Does it read or write sensitive data?

Output Limits

Function analysis commands enforce limits:

Limit Default Maximum What Happens
Decompile timeout 300s 3600s partial: true with whatever was completed
Disassemble result count 100 1000 Paginated
Callgraph depth 3 10 Depth > 10 is invalid (exit code 2)
Callgraph node count bounded disclosed Truncation diagnostic
Trace path count bounded bounded Truncation diagnostic
Trace depth 5 (implied by depth flag) Paths beyond depth are not returned

Always check diagnostics for truncation or limit warnings.

Presenting Function Analysis Results

When presenting your findings to the user, follow this structure:

## Function: <name> at <address>

### Purpose (Agent Assessment)
<1-2 sentence synthesis of what this function does>

### Pseudocode Summary
<key logic, not the full pseudocode>

### APIs Called
- <api1> (from <module>)
- <api2> (from <module>)

### Called By
- <caller1> at <address>
- <caller2> at <address>

### Agent Notes
- <anything notable: suspicious patterns, anti-analysis, bugs, etc.>
- <confidence gaps: what we can't determine from static analysis alone>