Basic Blocks & CFG
A basic block is a maximal straight-line sequence of instructions with one entry point and one exit point. This page covers the Block object, block types, CFG navigation, and common analysis patterns.
What is a Basic Block?
┌─────────────────────┐
│ mov eax, [rdi] │
│ add eax, 1 │ ← basic block
│ cmp eax, 0 │
│ jz exit_label │
└──────┬──────────────┘
│ (conditional)
┌────┴────┐ ┌──────────┐
│ (false) │ │ (true) │
└─────────┘ └──────────┘
The Block Object
Block is a MutableMapping keyed by instruction address:
func = prog.get_function("parse_args", approximative=False)
# Get the entry block
entry = func.get_block(func.start)
# Or access via the function dict
entry = func[func.start]
print(f"Block @ 0x{entry.start:x}")
print(f" Instructions: {len(entry)}")
print(f" Type: {entry.type}")
# Iterate instructions
for addr, inst in entry.items():
print(f" 0x{addr:x}: {inst}")
Block Types
from quokka.types import BlockType
entry.type # BlockType.NORMAL
| Type | Meaning |
|---|---|
NORMAL |
Standard block — falls through or jumps |
RET |
Ends with a return instruction |
NORET |
Ends with a call to a no-return function |
INDJUMP |
Ends with an indirect jump (switch table) |
CNDRET |
Conditional return |
ENORET |
External no-return |
ERROR |
Disassembly error |
EXTERN |
External / unresolved |
CFG Navigation
The CFG is a networkx.DiGraph accessible via func.graph:
cfg = func.graph # networkx DiGraph
# Successors of a block (blocks reachable in one step)
successors = list(cfg.successors(entry_addr))
# Predecessors of a block
predecessors = list(cfg.predecessors(some_block_addr))
# Via Block convenience properties
block = func.get_block(func.start)
for succ_addr in block.successors:
print(f" → 0x{succ_addr:x}")
for pred_addr in block.predecessors:
print(f" ← 0x{pred_addr:x}")
CFG Edge Types
Edges carry a type attribute (a RefType):
for src, dst, data in cfg.edges(data=True):
ref_type = data.get("type")
print(f" 0x{src:x} → 0x{dst:x} [{ref_type}]")
| Edge type | Meaning |
|---|---|
JMP_UNCOND |
Unconditional jump |
JMP_COND |
Conditional jump (true branch) |
JMP_INDIR |
Indirect jump (switch table) |
CALL |
Direct call |
CALL_INDIR |
Indirect call |
Loop Detection
import networkx as nx
cfg = func.graph
# Does the CFG have back-edges? (cycles = loops)
has_loops = not nx.is_directed_acyclic_graph(cfg)
# Find all simple cycles
cycles = list(nx.simple_cycles(cfg))
print(f"Function has {len(cycles)} loops")
# Dominator tree
dom_tree = nx.immediate_dominators(cfg, func.start)
Common Patterns
Finding Conditional Blocks
# Blocks with exactly 2 successors = conditional jump
for block_addr in cfg.nodes():
succs = list(cfg.successors(block_addr))
if len(succs) == 2:
block = func.get_block(block_addr)
print(f"Conditional block @ 0x{block_addr:x}")
print(f" True → 0x{succs[0]:x}")
print(f" False → 0x{succs[1]:x}")
Finding Return Blocks
from quokka.types import BlockType
ret_blocks = [b for b in func.values()
if b.type == BlockType.RET]
Structured Traversal
import networkx as nx
cfg = func.graph
# BFS from entry
for block_addr in nx.bfs_tree(cfg, func.start).nodes():
block = func.get_block(block_addr)
print(f"Block 0x{block_addr:x}: {len(block)} insts, type={block.type}")
# Topological order (for acyclic CFGs)
try:
for block_addr in nx.topological_sort(cfg):
print(f"0x{block_addr:x}")
except nx.NetworkXUnfeasible:
print("CFG has cycles — use BFS instead")
Entry and Exit Blocks
# Entry = the block at func.start
entry = func.get_block(func.start)
# Exit blocks = blocks with no successors in the CFG
exits = [addr for addr in cfg.nodes()
if cfg.out_degree(addr) == 0]
# Or using BlockType
from quokka.types import BlockType
exits = [b for b in func.values()
if b.type in (BlockType.RET, BlockType.NORET)]
Example: Complexity Analyzer
import networkx as nx
from quokka.types import FunctionType
def analyze_cfg(func):
"""Return (n_blocks, n_edges, cyclomatic_complexity, n_loops)"""
cfg = func.graph
n = cfg.number_of_nodes()
e = cfg.number_of_edges()
cc = e - n + 2 # McCabe cyclomatic complexity
loops = len(list(nx.simple_cycles(cfg)))
return n, e, cc, loops
for func in prog.values():
if func.type != FunctionType.NORMAL or len(func) < 5:
continue
n, e, cc, loops = analyze_cfg(func)
if cc > 20:
print(f"HIGH COMPLEXITY: {func.name:30s} CC={cc} loops={loops}")
Summary
Blockis aMutableMappingof instructions, keyed by address- BlockType:
NORMAL,RET,NORET,INDJUMP,CNDRET,ERROR func.graphis anetworkx.DiGraphof block addresses- CFG edges carry
RefType(JMP_COND,JMP_UNCOND,CALL…) block.successors/block.predecessorsfor convenient navigation- Use networkx algorithms: BFS, cycle detection, dominator trees