前面的文章里面,我们一直都是使用Unicorn 提供的 HOOK_CODE 机制来处理错误。这种类似于打log的方式还是不够方便,那么我们能否实现一个简单的调试器来帮助我们快速定位问题呢?比如,查看出错地方的反汇编代码,寄存器参数,下断点等等。

无名侠的代码中,是有一个实现,今天我们来分析一下源码,看看其实现思路。

调试器介绍

调试器要支持的调试指令如下:

def show_help(self):
    help_info = """
    # commands
    # set reg <regname> <value>
    # set bpt <addr>
    # n[ext]
    # s[etp]
    # r[un]
    # dump <addr> <size>
    # list bpt
    # del bpt <addr>
    # stop
    # f show ins flow
    """
    print (help_info)

调试器使用:

mu = Uc(UC_ARCH_ARM, UC_MODE_ARM)
dbg = udbg.UnicornDebugger(mu, udbg.UDBG_MODE_ALL)
dbg.add_bpt(0x1112233)
mu.emu_start(......)

这段代码在0x1112233处添加了一个断点,当程序执行到0x1112233处的时候就会停下来,陷入调试器的处理,等待调试者的指令。

源码分析

我们先看UnicornDebugger的构造函数,最重要的就是这几行代码:

        if mode == UDBG_MODE_ALL:
            mu.hook_add(UC_HOOK_CODE, _dbg_trace, self)

        mu.hook_add(UC_HOOK_MEM_UNMAPPED, _dbg_memory, self)
        mu.hook_add(UC_HOOK_MEM_FETCH_PROT, _dbg_memory, self)

添加了钩子函数,在每条指令执行前触发回调函数_dbg_trace ,看这个函数:

def _dbg_trace(mu, address, size, self):

    self._tracks.append(address)
    if not self._is_step and self._tmp_bpt == 0:
        if address not in self._list_bpt:
            return

    if self._tmp_bpt != address and self._tmp_bpt != 0:
        return

    return _dbg_trace_internal(mu, address, size, self)

第一行,就是将每条指令都添加到了一个列表当中,等到发生错误的时候,你可将列表元素倒序取出来,就形成了一个类似堆栈的东西。

接下来的两个 if 条件,是判断有没有设置断点或者单步,如果都没有设置就不往下执行。

打印寄存器的值

_dbg_trace_internal方法逻辑:

def _dbg_trace_internal(mu, address, size, self):

    self._is_step = False
    print ("======================= Registers =======================")
    self.dump_reg()
    print ("======================= Disassembly =====================")
    if size == 4:
        mode = 'arm'
    else:
        mode = 'thumb'
    self.dump_asm(address, size * self.dis_count, mode)

首先,打印寄存器的值,这个就比较简单了,直接使用Unicorn自带的方法就行:

eax = mu.reg_read(UC_X86_REG_EAX)
print(f"EAX: {eax}")