Как перешагнуть вызовы прерывания при отладке загрузчика / BIOS с помощью GDB и QEMU?
В образовательных целях я адаптировал этот загрузчик из http://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 шагов (например, 400), а затем сделал один шаг за раз, чтобы увидеть, на каком именно шаге "Я" был напечатан. Это никогда не происходило. Я фактически шагнул 200 шагов один за другим, и ничего не произошло. Как только я сделал 100 шагов одновременно (например, 100), я снова напечатал "я" на экране.
Итак, мне интересно, есть ли проблема синхронизации (некоторые системные прерывания мешают, когда я делаю пошаговую отладку). Что еще это может быть?
Во всяком случае, есть ли способ пропустить все прерывания BIOS и другие функции и просто вернуться и продолжить шаг кода загрузчика? Как предложил Peter Quiring в комментариях, я попытался использовать следующий. Это не сработало.
(gdb) next
Cannot find bounds of current function
Поэтому я попробовал nexti, и он просто ведет себя как си.
Спасибо!
3 ответа
Я автоматизировал вашу процедуру с помощью скрипта 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://stackru.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.
На самом деле это работа, которая подходит моим целям. Я установил точки останова, чтобы я мог использовать "continue" на gdb вместе с "si" и следить за сообщением, выводимым на экран, по одному символу за раз. Вот шаги.
При первом запуске я делаю шаг в своем загрузчике, чтобы реально проверить позиции памяти, в которых хранятся инструкции.
Оболочка Linux:
# qemu-system-i386 -fda loader.img -boot a -s -S -monitor stdio
QEMU 1.5.0 monitor - type 'help' for more information
(qemu)
Другая оболочка Linux (некоторые строки были скрыты [...]):
# 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 monitor, я нахожу адрес инструкций, выполняющих эту команду после каждого si на gdb:
(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
Теперь я могу использовать комбинацию "continue" (c) и stepi (si) на GDB и пропустить весь материал BIOS.
Вероятно, есть лучшие способы сделать это. Однако для моих педагогических целей этот метод работает довольно хорошо.
Есть некоторые попытки справиться с этой проблемой. Это действительно раздражает, когда вы не в курсе обработчиков прерываний/исключений.
QEMU
Собственно, QEMU уже рассматривал эту ситуацию. QEMU gdbstub внутри имеет два флага: NOIRQ и NOTIMER. Эти два флага предотвратят ввод прерываний в гостевую систему и приостановят эмуляцию часов таймера в пошаговом режиме. Вы можете запросить возможности вашего 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 предотвращает только прерывания, исключения/ловушки все еще вводятся.
ГБД
Флаги NOIRQ и NOTIMER в QEMU по-прежнему не могут предотвратить выполнение обработчика исключений/ловушек. Таким образом, вы, вероятно, внезапно войдете в «неожиданный» код. Например, инструкция сохранения может привести к исключению ошибки страницы. Таким образом, лучше решить это в клиенте gdb, то есть использовать точки останова для мягкого степпинга вместо аппаратного степпинга.
GDB имеет различную пошаговую реализацию для разных архитектур:
- x86: всегда использует аппаратный степпинг.
- arm: предпочитать аппаратный степпинг, если он поддерживается целью. В противном случае используется мягкий степпинг.
- aarch64: всегда использует аппаратный степпинг, но атомарную последовательность.
- loongarch: всегда использует плавный шаг.
- riscv: всегда использует плавный шаг.
Таким образом, вы никогда не столкнетесь с такой проблемой для loongarch и 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();
Наслаждаться!