在用gdb和QEMU调试引导加载程序/bios时如何跳过中断调用



出于教育目的,我改编了这个来自mikeos.berlios.de/write-your-own-os.html的引导加载程序,将其重写为专门在地址0x7c00加载。

最后的代码是:

[BITS 16]           ; Tells nasm to build 16 bits code
[ORG 0x7C00]        ; The address the code will start
start: 
    mov ax, 0       ; Reserves 4Kbytes after the bootloader
    add ax, 288 ; (4096 + 512)/ 16 bytes per paragraph 
    mov ss, ax 
    mov sp, 4096 
mov ax, 0   ; Sets the data segment 
    mov ds, ax 
    mov si, texto   ; Sets the text position 
    call imprime    ; Calls the printing routine
jmp $       ; Infinite loop 
    texto db 'It works! :-D', 0 
imprime:            ; Prints the text on screen
    mov ah, 0Eh     ; int 10h - printing function 
.repeat: 
    lodsb           ; Grabs one char 
    cmp al, 0 
    je .done        ; If char is zero, ends 
    int 10h         ; Else prints char 
jmp .repeat 
.done: 
ret 
times 510-($-$$) db 0 ; Fills the remaining boot sector with 0s 
dw 0xAA55             ; Standard boot signature

我可以逐步执行程序并看到寄存器的变化,以及正在执行的指令,使用gdb (si)逐步执行并使用QEMU监视器检查(信息寄存器,x/I $eip等)。

在我进入int 10h (BIOS打印例程)之后,事情变得有点奇怪。如果我一次执行500条指令,我可以看到字符"I"(我的文本字符串的第一个字符)打印在屏幕上。所以我重新开始,走了400步(si 400),然后我一次走一步,看看到底是哪一步"I"被打印出来。这从来没有发生过。实际上,我一步一步地走了200步,什么也没发生。当我一次走完100步(si 100),屏幕上又出现了"I"。

所以,我想知道是否有一个定时问题(一些系统中断在我做一步一步调试的方式)。这还能是什么?

无论如何,是否有一种方法可以跳过整个BIOS中断和其他功能,只是返回并继续执行引导加载程序代码?正如Peter Quiring在评论中建议的那样,我尝试使用next。

(gdb) next 
Cannot find bounds of current function

所以我试了下一个,它只是表现为si。

谢谢!

我已经用Python脚本自动化了您的过程:

    计算当前指令的长度。
  • 在下一条指令
  • 上设置临时断点继续

这也适用于任何其他指令,但我没有看到它的许多其他用例,因为nexti已经跳过了call

class NextInstructionAddress(gdb.Command):
    """
Run until Next Instruction address.
Usage: nia
Put a temporary breakpoint at the address of the next instruction, and continue.
Useful to step over int interrupts.
See also: http://stackoverflow.com/questions/24491516/how-to-step-over-interrupt-calls-when-debugging-a-bootloader-bios-with-gdb-and-q
"""
    def __init__(self):
        super().__init__(
            'nia',
            gdb.COMMAND_BREAKPOINTS,
            gdb.COMPLETE_NONE,
            False
        )
    def invoke(self, arg, from_tty):
        frame = gdb.selected_frame()
        arch = frame.architecture()
        pc = gdb.selected_frame().pc()
        length = arch.disassemble(pc)[0]['length']
        gdb.Breakpoint('*' + str(pc + length), temporary = True)
        gdb.execute('continue')
NextInstructionAddress()

只需将其放入~/.gdbinit.py并将source ~/.gdbinit.py添加到~/.gdbinit文件中。

在GDB 7.7.1, Ubuntu 14.04上测试

这实际上是一个适合我的目的的工作。我所做的是设置断点,这样我就可以在gdb上使用"continue"one_answers"si",并跟踪屏幕上打印的消息,一次一个字符。以下是步骤。

在第一次运行中,我对引导加载程序进行步进,因此我可以实际检查存储指令的内存位置。

Linux shell:

# qemu-system-i386 -fda loader.img -boot a -s -S -monitor stdio
QEMU 1.5.0 monitor - type 'help' for more information
(qemu) 

其他Linux shell(有些行被抑制了[…]):

# gdb
GNU gdb (GDB) 7.6.1-ubuntu
[...]
(gdb) target remote localhost:1234
Remote debugging using localhost:1234
0x0000fff0 in ?? ()
(gdb) set architecture i8086
[...]
(gdb) br *0x7c00
Ponto de parada 1 at 0x7c00
(gdb) c
Continuando.
Breakpoint 1, 0x00007c00 in ?? ()
(gdb) si
0x00007c03 in ?? ()

在我运行QEMU监视器的终端中,我在gdb上的每个si之后找到执行该命令的指令的地址:

(qemu) x /i $eip
0x00007c03:  add    $0x120,%ax

对于那些不熟悉QEMU的人,x显示寄存器的内容,/i将其转换为指令,$eip是指令点寄存器。通过重复这些步骤,我找到了lodsb和int 10h指令的地址:

0x00007c29:  lods   %ds:(%si),%al 
0x00007c2e:  int    $0x10 
所以,在gdb上,我只是为这些额外的位置设置断点:
(gdb) br *0x7c29
Ponto de parada 2 at 0x7c29
(gdb) br *0x7c2e
Ponto de parada 3 at 0x7c2e

现在我可以在gdb上使用"continue"(c)和"stepi"(si)的组合,并跳过整个BIOS操作。

可能有更好的方法来做到这一点。然而,就我的教学目的而言,这种方法非常有效。

有一些努力来处理这个问题。当你对irq/异常处理程序不感兴趣时,这真的很烦人。

QEMU

实际上,QEMU已经考虑到了这种情况。QEMU gdbstub内部有两个标志:NOIRQ和NOTIMER。这两个标志将防止在单步模式下向来宾注入irqs和暂停计时器时钟仿真。您可以通过以下命令查询qemu的能力:

(gdb) maintenance packet qqemu.sstepbits
sending: "qqemu.sstepbits"
received: "ENABLE=1,NOIRQ=2,NOTIMER=4"

对于KVM,您可能需要linux内核v5.12+来支持NOIRQ,它实现了ioctl KVM_CAP_SET_GUEST_DEBUG2。

但是请注意,NOIRQ只是防止irqs,异常/陷阱仍然被注入。

GDB

QEMU中的NOIRQ和NOTIMER标志仍然不能阻止异常/trap处理程序的执行。所以你可能会突然进入"意想不到的"代码。例如,存储指令可能导致页面错误异常。因此,最好在gdb客户端中解决这个问题,即使用断点来软步进而不是硬件步进。

对于不同的体系结构,GDB有不同的步进实现:

  • x86:总是使用硬件步进
  • arm:如果目标支持,首选硬件步进。否则采用软步进。
  • aarch64:总是使用硬件步进,但原子顺序。
  • loongarch:总是使用软步进
  • riscv:总是使用软步进

所以在longarch和riscv中不会遇到这样的问题。这里我写了一个gdb扩展名为'gdb-os-helper.py',它基本上可以支持x86/arm/aarch64架构的软步进。

# -*- coding: utf-8 -*-
"""gdb command extensions for better stepping with qemu guest os.
The main purpose is to get rid of the influence of cpu exceptions.
Provided commands:
    - bni/bsi: stepping over/into next instruction.
    - bn/bs: stepping over/into next source line.
Copyright (C) 2022 Author Changbin Du <changbin.du@gmail.com>
"""
try:
    from capstone import *
    from capstone.arm import *
    from capstone.arm64 import *
    from capstone.x86 import *
except ModuleNotFoundError:
    print("python module 'capstone' is not installed")

class BniBreakpoint(gdb.Breakpoint):
    """
    Our special breakpoint.
    """
    def __init__(self, addr):
        if hasattr(gdb, 'BP_HARDWARE_BREAKPOINT'):
            # BP_HARDWARE_BREAKPOINT is not supported on old gdb
            type = gdb.BP_HARDWARE_BREAKPOINT
        else:
            type = gdb.BP_BREAKPOINT
        super().__init__(f'*{addr}', type = type, internal = True, temporary = False)
class BreakpointBasedNextInstruction(gdb.Command):
    """
    Stepping with breakpoints. Useful for debugging OS in QEMU.
    """
    def __init__(self, name, step_into):
        super().__init__(name, gdb.COMMAND_BREAKPOINTS, gdb.COMPLETE_NONE, False)
        self.step_into = step_into
    def invoke(self, arg, from_tty):
        frame = gdb.selected_frame()
        arch = frame.architecture()
        pc = frame.pc()
        # print(arch.disassemble(pc)[0]['asm'])
        if arch.name() == 'aarch64':
            pcs = self.do_aarch64(frame, pc)
        elif arch.name() == 'armv7':
            pcs = self.do_arm(frame, pc)
        elif arch.name() == 'i386:x86-64':
            pcs = self.do_x86(frame, pc, CS_MODE_64)
        elif arch.name() == 'i386':
            pcs = self.do_x86(frame, pc, CS_MODE_32)
        else:
            print(f'not supported arch {arch.name()}')
            return
        # setup breakpoints on all possible pc
        bps = []
        for addr in pcs:
            bps.append(BniBreakpoint(addr))
        # go
        gdb.execute('continue')
        # delete breakpoints after stopped
        for bp in bps:
            bp.delete()
    def do_x86(self, frame, pc, mode):
        insn_len = frame.architecture().disassemble(pc)[0]['length']
        insn = gdb.selected_inferior().read_memory(pc, insn_len)
        md = Cs(CS_ARCH_X86, mode)
        md.detail = True
        insn = next(md.disasm(insn.tobytes(), pc))
        pcs = [pc + insn_len,]
        if insn.group(X86_GRP_JUMP) or (self.step_into and insn.group(X86_GRP_CALL)):
            if insn.operands[0].type == X86_OP_REG:
                addr = frame.read_register(insn.reg_name(insn.operands[0].reg))
                pcs.append(addr)
            elif insn.operands[0].type == X86_OP_IMM:
                pcs.append(insn.operands[0].imm)
            else:
                print(f'unsupported insn {insn}')
        elif insn.group(X86_GRP_RET):
            # get return address from stack
            addr = gdb.selected_inferior().read_memory(frame.read_register('sp'),
                                                                8 if mode == CS_MODE_64 else 4)
            addr = int.from_bytes(addr.tobytes(), "little")
            pcs.append(addr)
        return pcs
    def do_arm(self, frame, pc):
        insn = gdb.selected_inferior().read_memory(pc, 4)
        md = Cs(CS_ARCH_ARM, CS_MODE_ARM)
        md.detail = True
        insn = next(md.disasm(insn.tobytes(), pc))
        # deal with multiple load
        def _ldm(rn, reglist, step, inc):
            addr = frame.read_register(rn) + inc
            for i, opd in enumerate(reglist):
                if opd.type == ARM_OP_REG and opd.reg == ARM_REG_PC:
                    pc = gdb.selected_inferior().read_memory(addr + step * i, 4)
                    pc = int.from_bytes(pc.tobytes(), "little")
                    return pc
            return None
        pcs = [pc + 4,]
        if insn.id == ARM_INS_B or (self.step_into and insn.id == ARM_INS_BL):
            pcs.append(insn.operands[0].imm)
        elif insn.id == ARM_INS_BX or (self.step_into and insn.id == ARM_INS_BLX):
            addr = frame.read_register(insn.reg_name(insn.operands[0].reg))
            pcs.append(addr)
        elif insn.id in (ARM_INS_CBZ, ARM_INS_CBNZ):
            pcs.append(insn.operands[1].imm)
        elif insn.id == ARM_INS_POP:
            addr = _ldm('sp', insn.operands, 4, 0)
            pcs.append(addr)
        elif insn.id in (ARM_INS_LDM, ARM_INS_LDMIB, ARM_INS_LDMDA, ARM_INS_LDMDB):
            step = (4 if insn.id in (ARM_INS_LDM, ARM_INS_LDMIB) else -4)
            inc = (0 if insn.id in (ARM_INS_LDM, ARM_INS_LDMDA) else 1) * step
            addr = _ldm(insn.reg_name(insn.operands[0].reg),
                        insn.operands[1:], step, inc)
            pcs.append(addr)
        elif insn.group(ARM_GRP_JUMP):
            print(f'unsupported insn {insn}')
        return pcs
    def do_aarch64(self, frame, pc):
        insn = gdb.selected_inferior().read_memory(pc, 4)
        md = Cs(CS_ARCH_ARM64, CS_MODE_ARM)
        md.detail = True
        insn = next(md.disasm(insn.tobytes(), pc))
        pcs = [pc + 4,]
        if insn.id == ARM64_INS_B or (self.step_into and insn.id == ARM64_INS_BL):
            pcs.append(insn.operands[0].imm)
        elif insn.id == ARM64_INS_BR or (self.step_into and insn.id == ARM64_INS_BLR):
            addr = frame.read_register(insn.reg_name(insn.operands[0].reg))
            pcs.append(addr)
        elif insn.id in (ARM64_INS_CBZ, ARM64_INS_CBNZ):
            pcs.append(insn.operands[1].imm)
        elif insn.id in (ARM64_INS_TBZ, ARM64_INS_TBNZ):
            pcs.append(insn.operands[2].imm)
        elif insn.id == ARM64_INS_RET:
            reg = insn.reg_name(insn.operands[0].reg) if len(insn.operands) > 0 else 'lr'
            pcs.append(frame.read_register(reg))
        elif insn.group(ARM64_GRP_JUMP):
            print(f'unsupported insn {insn}')
        return pcs
class BreakpointBasedNextLine(gdb.Command):
    """
    Run until next line. Soure level stepping with breakpoints.
    """
    def __init__(self, name, step_into):
        super().__init__(name, gdb.COMMAND_BREAKPOINTS, gdb.COMPLETE_NONE, False)
        self.step_into = step_into
    def do_step(self):
        gdb.execute('bsi' if self.step_into else 'bni', to_string = True)
    def invoke(self, arg, from_tty):
        pc = gdb.selected_frame().pc()
        cur_line = gdb.current_progspace().find_pc_line(pc)
        if cur_line.symtab is None:
            # on source info, stepping by instruction
            self.do_step()
        else:
            # okay, stepping until leaving current line
            while True:
                self.do_step()
                pc = gdb.selected_frame().pc()
                line = gdb.current_progspace().find_pc_line(pc)
                if line.symtab is None or line.line != cur_line.line:
                    break
BreakpointBasedNextInstruction('bni', False)
BreakpointBasedNextInstruction('bsi', True)
BreakpointBasedNextLine('bn', False)
BreakpointBasedNextLine('bs', True)
print("""usage:
    - bni/bsi: stepping over/into next instruction.
    - bn/bs:   stepping over/into next source line.""")

你可以这样使用:

(gdb) target remote :1234
Remote debugging using :1234
0xffffffff81eb4234 in default_idle () at arch/x86/kernel/process.c:731
731 }
=> 0xffffffff81eb4234 <default_idle+20>:    c3  ret    
   0xffffffff81eb4235:  66 66 2e 0f 1f 84 00 00 00 00 00    data16 cs nopw 0x0(%rax,%rax,1)
(gdb) source ~/work/gdb-os-helper.py 
usage:
    - bni/bsi: stepping over/into next instruction.
    - bn/bs:   stepping over/into next source line.
(gdb) bni
[Switching to Thread 1.5]
Thread 5 hit Breakpoint -2, default_idle_call () at kernel/sched/idle.c:117
117         raw_local_irq_disable();
(gdb) bn
Thread 1 hit Breakpoint -3, default_idle_call () at kernel/sched/idle.c:119
119         ct_idle_exit();
=> 0xffffffff81eb4562 <default_idle_call+114>:  e8 e9 d0 fe ff  call   0xffffffff81ea1650 <ct_idle_exit>
(gdb) 
Thread 1 hit Breakpoint -4, default_idle_call () at kernel/sched/idle.c:121
121         raw_local_irq_enable();

享受吧!

相关内容

  • 没有找到相关文章

最新更新