-
Notifications
You must be signed in to change notification settings - Fork 758
Debugging
Qiling Framework provides powerful debugging capabilities including built-in debuggers, GDB server support, and reverse debugging features.
QDB is Qiling's built-in debugger that provides an interactive debugging experience similar to GDB.
from qiling import Qiling
ql = Qiling(['binary'], 'rootfs')
ql.debugger = "qdb"
ql.run()Execution Control:
-
run/r- Start or restart execution -
continue/c- Continue execution -
step/s- Execute single instruction -
stepi/si- Execute single instruction (alias) -
next/n- Step over function calls -
finish/f- Run until function return -
quit/q- Exit debugger
Breakpoints:
-
breakpoint <address>/b <address>- Set breakpoint -
breakpoint <symbol>/b <symbol>- Set breakpoint on symbol -
info breakpoints- List all breakpoints -
delete <number>- Delete breakpoint by number -
disable <number>- Disable breakpoint -
enable <number>- Enable breakpoint
Memory Examination:
-
x/<count><format><size> <address>- Examine memory- Count: number of units to display
- Format:
x(hex),d(decimal),u(unsigned),o(octal),t(binary),a(address),c(char),s(string) - Size:
b(byte),h(halfword),w(word),g(giant/8 bytes)
-
x/10x $sp- Display 10 hex words from stack pointer -
x/s 0x401000- Display string at address
Register Operations:
-
info registers/info reg- Show all registers -
info registers <reg>- Show specific register -
set $<register> = <value>- Set register value -
print $<register>/p $<register>- Print register value
Stack Operations:
-
backtrace/bt- Show call stack -
info stack- Show stack information -
up- Move up in call stack -
down- Move down in call stack
Disassembly:
-
disassemble/disas- Disassemble current function -
disassemble <address>- Disassemble at address -
disassemble <start>,<end>- Disassemble range
Basic Debugging Session:
from qiling import Qiling
def debug_binary():
ql = Qiling(['examples/rootfs/x8664_linux/bin/x8664_hello'],
'examples/rootfs/x8664_linux')
ql.debugger = "qdb"
ql.run()
debug_binary()Interactive Session:
Welcome to Qiling Debugger (QDB)
(qdb) b *0x401000
Breakpoint 1 at 0x401000
(qdb) run
Starting program
Breakpoint 1, 0x401000
(qdb) info reg
rax: 0x0
rbx: 0x0
rcx: 0x7ffff7dd5e80
...
(qdb) x/10x $rsp
0x7fffffffe000: 0x00000001 0x00000000 0x7fffffffe2e8 0x00007fff
0x7fffffffe010: 0x00000000 0x00000000 0x00401040 0x00000000
(qdb) step
(qdb) continueAutomated Debugging Script:
from qiling import Qiling
def automated_debug():
ql = Qiling(['binary'], 'rootfs')
# Set breakpoints programmatically
ql.debugger = "qdb"
# Custom debugging hook
def debug_hook(ql):
print(f"Hit address: 0x{ql.arch.regs.rip:x}")
# Automatic analysis at breakpoints
ql.hook_address(debug_hook, 0x401000)
ql.run()Qiling can act as a GDB server, allowing you to use full-featured GDB for debugging.
from qiling import Qiling
ql = Qiling(['binary'], 'rootfs')
ql.debugger = "gdb:localhost:9999"
ql.run()# Terminal 1: Start Qiling with GDB server
python3 debug_script.py
# Terminal 2: Connect with GDB
gdb
(gdb) target remote localhost:9999
(gdb) continueBasic Operations:
(gdb) target remote localhost:9999
(gdb) info registers
(gdb) x/10x $rsp
(gdb) break *0x401000
(gdb) continue
(gdb) step
(gdb) backtraceAdvanced GDB Features:
# Set hardware breakpoints
(gdb) hbreak *0x401000
# Watch memory locations
(gdb) watch *0x601000
# Conditional breakpoints
(gdb) break *0x401000 if $rax == 0x123
# Python scripting in GDB
(gdb) python print("Custom analysis")IDA Pro Integration:
# For IDA Pro debugging
ql = Qiling(['binary'], 'rootfs')
ql.debugger = "gdb:localhost:23946" # IDA's debug port
ql.run()VS Code Integration:
Configure .vscode/launch.json:
{
"version": "0.2.0",
"configurations": [
{
"name": "Qiling GDB",
"type": "cppdbg",
"request": "launch",
"program": "/path/to/binary",
"miDebuggerServerAddress": "localhost:9999",
"miDebuggerPath": "/usr/bin/gdb",
"setupCommands": [
{
"description": "Enable pretty-printing",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
]
}
]
}Qiling supports reverse debugging through snapshots and execution recording.
from qiling import Qiling
class ReverseDebugger:
def __init__(self, ql):
self.ql = ql
self.snapshots = []
self.current_snapshot = -1
def take_snapshot(self, label=""):
"""Take execution snapshot"""
snapshot = {
'label': label,
'registers': self.ql.arch.regs.save(),
'memory': self.ql.mem.save(),
'instruction_count': getattr(self.ql, 'instruction_count', 0)
}
self.snapshots.append(snapshot)
self.current_snapshot = len(self.snapshots) - 1
print(f"Snapshot {self.current_snapshot} taken: {label}")
def restore_snapshot(self, index):
"""Restore to specific snapshot"""
if 0 <= index < len(self.snapshots):
snapshot = self.snapshots[index]
self.ql.arch.regs.restore(snapshot['registers'])
self.ql.mem.restore(snapshot['memory'])
self.current_snapshot = index
print(f"Restored to snapshot {index}: {snapshot['label']}")
else:
print("Invalid snapshot index")
def step_back(self):
"""Step back to previous snapshot"""
if self.current_snapshot > 0:
self.restore_snapshot(self.current_snapshot - 1)
else:
print("Already at earliest snapshot")
def step_forward(self):
"""Step forward to next snapshot"""
if self.current_snapshot < len(self.snapshots) - 1:
self.restore_snapshot(self.current_snapshot + 1)
else:
print("Already at latest snapshot")
def list_snapshots(self):
"""List all snapshots"""
for i, snapshot in enumerate(self.snapshots):
marker = " -> " if i == self.current_snapshot else " "
print(f"{marker}{i}: {snapshot['label']}")
# Usage example
def reverse_debug_example():
ql = Qiling(['binary'], 'rootfs')
reverse_dbg = ReverseDebugger(ql)
# Take snapshots at key points
def snapshot_hook(ql):
reverse_dbg.take_snapshot(f"PC: 0x{ql.arch.regs.arch_pc:x}")
# Automatic snapshots every 1000 instructions
instruction_count = 0
def auto_snapshot(ql, address, size):
nonlocal instruction_count
instruction_count += 1
if instruction_count % 1000 == 0:
reverse_dbg.take_snapshot(f"Instruction {instruction_count}")
ql.hook_code(auto_snapshot)
ql.hook_address(snapshot_hook, 0x401000)
ql.run()
# Interactive reverse debugging
while True:
cmd = input("(reverse-qdb) ").strip().split()
if not cmd:
continue
if cmd[0] == "snapshots":
reverse_dbg.list_snapshots()
elif cmd[0] == "restore" and len(cmd) > 1:
reverse_dbg.restore_snapshot(int(cmd[1]))
elif cmd[0] == "back":
reverse_dbg.step_back()
elif cmd[0] == "forward":
reverse_dbg.step_forward()
elif cmd[0] == "quit":
breakclass ExecutionRecorder:
def __init__(self, ql):
self.ql = ql
self.execution_log = []
self.memory_log = []
def start_recording(self):
"""Start recording execution"""
self.ql.hook_code(self.record_instruction)
self.ql.hook_mem_write(self.record_memory_write)
def record_instruction(self, ql, address, size):
"""Record executed instruction"""
instruction = {
'address': address,
'size': size,
'registers': ql.arch.regs.save(),
'instruction_data': ql.mem.read(address, size)
}
self.execution_log.append(instruction)
def record_memory_write(self, ql, access, address, size, value):
"""Record memory modifications"""
write_record = {
'pc': ql.arch.regs.arch_pc,
'address': address,
'size': size,
'old_value': ql.mem.read(address, size),
'new_value': value
}
self.memory_log.append(write_record)
def replay_to_instruction(self, target_instruction):
"""Replay execution to specific instruction"""
# Reset to initial state
self.ql.arch.regs.restore(self.execution_log[0]['registers'])
# Replay instructions
for i in range(min(target_instruction, len(self.execution_log))):
instruction = self.execution_log[i]
# Execute single instruction
self.ql.emu_start(instruction['address'],
instruction['address'] + instruction['size'],
count=1)
def analyze_execution_path(self):
"""Analyze recorded execution path"""
unique_addresses = set()
function_calls = []
for entry in self.execution_log:
unique_addresses.add(entry['address'])
# Detect function calls
if len(entry['instruction_data']) > 0:
if entry['instruction_data'][0] == 0xe8: # CALL
function_calls.append(entry['address'])
return {
'total_instructions': len(self.execution_log),
'unique_addresses': len(unique_addresses),
'function_calls': len(function_calls),
'coverage': list(unique_addresses)
}def debug_multithreaded():
ql = Qiling(['multithreaded_binary'], 'rootfs', multithread=True)
# Thread-aware debugging
def thread_switch_hook(ql, old_thread, new_thread):
print(f"Thread switch: {old_thread.id} -> {new_thread.id}")
def thread_create_hook(ql, thread):
print(f"Thread created: {thread.id}")
# Set breakpoints for new thread
ql.os.thread_management.hook_thread_switch(thread_switch_hook)
ql.os.thread_management.hook_thread_create(thread_create_hook)
ql.debugger = "qdb"
ql.run()def conditional_debugging():
ql = Qiling(['binary'], 'rootfs')
# Conditional breakpoints
def conditional_break(ql):
if ql.arch.regs.rax == 0x1234:
print("Condition met, entering debugger")
ql.debugger = "qdb"
ql.hook_address(conditional_break, 0x401000)
# State-based debugging
debug_on_malloc_failure = False
def malloc_hook(ql, size):
global debug_on_malloc_failure
addr = ql.os.heap.alloc(size)
if addr == 0 and debug_on_malloc_failure:
print("Malloc failed, entering debugger")
ql.debugger = "qdb"
return addr
ql.set_api("malloc", malloc_hook)
ql.run()class PerformanceDebugger:
def __init__(self, ql):
self.ql = ql
self.hotspots = {}
self.slow_functions = {}
def setup_performance_monitoring(self):
"""Set up performance monitoring hooks"""
self.ql.hook_code(self.track_hotspots)
self.ql.hook_block(self.track_basic_blocks)
def track_hotspots(self, ql, address, size):
"""Track instruction execution frequency"""
self.hotspots[address] = self.hotspots.get(address, 0) + 1
# Break on hot spots
if self.hotspots[address] > 10000: # Threshold
print(f"Hotspot detected at 0x{address:x} ({self.hotspots[address]} hits)")
ql.debugger = "qdb"
def track_basic_blocks(self, ql, address, size):
"""Track basic block execution"""
# Implement basic block performance tracking
pass
def get_performance_report(self):
"""Generate performance report"""
sorted_hotspots = sorted(self.hotspots.items(),
key=lambda x: x[1], reverse=True)
return {
'top_hotspots': sorted_hotspots[:10],
'total_instructions': sum(self.hotspots.values())
}- Start with high-level overview using basic hooks
- Narrow down to specific functions or code sections
- Use conditional breakpoints to avoid noise
- Document findings and hypotheses
- Take snapshots before making changes
- Save and restore execution states
- Track state changes systematically
- Use automated snapshot triggers
- Use appropriate verbosity levels
- Filter irrelevant information
- Focus on specific analysis goals
- Combine multiple debugging approaches
Finding Crashes:
def debug_crash():
ql = Qiling(['binary'], 'rootfs')
# Exception handling
def exception_handler(ql, exception_type, exception_info):
print(f"Exception: {exception_type}")
print(f"PC: 0x{ql.arch.regs.arch_pc:x}")
ql.debugger = "qdb"
ql.hook_exception(exception_handler)
ql.run()Analyzing Algorithm Logic:
def debug_algorithm():
ql = Qiling(['binary'], 'rootfs')
# Track variable changes
variable_addr = 0x601000
def track_variable(ql, access, address, size, value):
if address == variable_addr:
print(f"Variable changed: 0x{value:x} at PC 0x{ql.arch.regs.arch_pc:x}")
ql.hook_mem_write(track_variable)
ql.run()Performance Analysis:
def debug_performance():
ql = Qiling(['binary'], 'rootfs')
perf_dbg = PerformanceDebugger(ql)
perf_dbg.setup_performance_monitoring()
ql.run()
report = perf_dbg.get_performance_report()
print(f"Performance report: {report}")Debugger Not Starting:
- Check port availability for GDB server
- Verify debugger string format
- Ensure proper Qiling initialization
Breakpoints Not Hit:
- Verify address correctness
- Check if code is actually executed
- Use instruction hooks for verification
Memory Access Issues:
- Check memory mapping
- Verify address calculations
- Use memory access hooks for tracking
Performance Problems:
- Limit hook scope
- Use conditional hooks
- Consider snapshot frequency
For additional debugging examples and advanced techniques, see the Examples directory in the Qiling repository.
- Home
- Getting Started
- Core Concepts
- Usage
- Features
- Tutorials
- Development
- Resources