出于教育目的,我改编了这个来自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();
享受吧!