Skip to content

Debugging

xwings edited this page Jul 6, 2025 · 2 revisions

Debugging

Qiling Framework provides powerful debugging capabilities including built-in debuggers, GDB server support, and reverse debugging features.

Built-in Debugger (QDB)

QDB is Qiling's built-in debugger that provides an interactive debugging experience similar to GDB.

Enabling QDB

from qiling import Qiling

ql = Qiling(['binary'], 'rootfs')
ql.debugger = "qdb"
ql.run()

QDB Commands

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

QDB Examples

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) continue

Automated 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()

GDB Server

Qiling can act as a GDB server, allowing you to use full-featured GDB for debugging.

Setting Up GDB Server

from qiling import Qiling

ql = Qiling(['binary'], 'rootfs')
ql.debugger = "gdb:localhost:9999"
ql.run()

Connecting with GDB

# Terminal 1: Start Qiling with GDB server
python3 debug_script.py

# Terminal 2: Connect with GDB
gdb
(gdb) target remote localhost:9999
(gdb) continue

GDB Remote Commands

Basic Operations:

(gdb) target remote localhost:9999
(gdb) info registers
(gdb) x/10x $rsp
(gdb) break *0x401000
(gdb) continue
(gdb) step
(gdb) backtrace

Advanced 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")

GDB with IDEs

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
                }
            ]
        }
    ]
}

Reverse Debugging

Qiling supports reverse debugging through snapshots and execution recording.

Snapshot-based Reverse Debugging

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":
            break

Execution Recording

class 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)
        }

Advanced Debugging Techniques

Multi-threaded Debugging

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()

Conditional Debugging

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()

Performance Debugging

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())
        }

Debugging Best Practices

1. Systematic Approach

  • 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

2. State Management

  • Take snapshots before making changes
  • Save and restore execution states
  • Track state changes systematically
  • Use automated snapshot triggers

3. Efficient Debugging

  • Use appropriate verbosity levels
  • Filter irrelevant information
  • Focus on specific analysis goals
  • Combine multiple debugging approaches

4. Common Debugging Scenarios

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}")

Troubleshooting

Common Issues

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.

Clone this wiki locally