Как перешагнуть вызовы прерывания при отладке загрузчика / 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();

Наслаждаться!

Другие вопросы по тегам