我正在尝试调试 QEMU 模拟器附带的 bios.bin。我按如下方式启动 QEMU:
qemu-system-x86_64 -bios bios.bin -s -S
然后,我开始使用以下命令开始调试:
gdb
target remote localhost:1234
GDB 处于英特尔切换状态0xfffffff0现在固件/BIOS 应该处于的位置。但是,内存中的此位置没有任何内容。即使通过nexti
执行更多指令,它也会解码为全零。
我做错了什么还是误解了什么?我基本上想获得 cpu 调用的第一条指令并从那里继续调试。
QEMU 不是这里的问题,但 GDB 是。如果您打算使用 GDB 调试 BIOS,我将从一个建议开始:
- 不要使用 qemu-system-x86_64。请改用 qemu-system-i386。这将避免数据包太长的问题和显示一堆数字。这可能会也可能不会发生在您身上,具体取决于所使用的 GDB 版本。
话虽如此,GDB的真正问题是它不知道实模式段:偏移寻址。当您引导 QEMU 时,它会以 16 位实模式启动以开始执行旧版 BIOS。GDB 缺乏实模式调试支持才是真正的问题。你可以在我写的另一个Stackoverflow答案中阅读更多关于它的信息。总结一下:
不幸的是,默认情况下 gdb 不做段:偏移量计算 并将 EIP 中的值用于断点。您必须指定 断点作为 32 位地址 (EIP(。
在单步执行实模式代码时,可能会很麻烦 因为 GDB 不处理实模式分段。如果您步入 您会发现中断处理程序 GDB 将显示汇编代码 相对于弹性公网IP而言。有效地 gdb 将向您展示反汇编 错误的内存位置,因为它没有考虑 CS。
多年来,对 GDB 的更改使得调试实模式代码变得更加复杂,并且协商与远程主机的连接变得更加成问题。根据我上面的回答和同一问题下的其他 2 个答案,您可以通过尝试以下内容在旧版本的 GDB 上令人满意地工作:
使用以下内容创建一个名为target.xml
的文件:
<?xml version="1.0"?><!DOCTYPE target SYSTEM "gdb-target.dtd">
<target>
<architecture>i8086</architecture>
<xi:include href="i386-32bit.xml"/>
</target>
使用此 URL 的内容创建一个名为i386-32bit.xml
的文件。或者,您可以使用以下命令从基于 Linux 的操作系统上的命令行检索此文件:
wget https://raw.githubusercontent.com/qemu/qemu/master/gdb-xml/i386-32bit.xml
使用以下内容创建名为gdb_init_real_mode.txt
的脚本文件:
# Special mode for GDB that allows to debug/disassemble REAL MODE x86 code
#
# It has been designed to be used with QEMU or BOCHS gdb-stub
#
# 08/2011 Hugo Mercier - GPL v3 license
#
# Freely inspired from "A user-friendly gdb configuration file" widely available
# on the Internet
set confirm off
set verbose off
set prompt 33[31mreal-mode-gdb$ 33[0m
set output-radix 0d10
set input-radix 0d10
# These make gdb never pause in its output
set height 0
set width 0
# Intel syntax
set disassembly-flavor intel
# Real mode
#set architecture i8086
set $SHOW_CONTEXT = 1
set $REAL_MODE = 1
# By default A20 is present
set $ADDRESS_MASK = 0x1FFFFF
# nb of instructions to display
set $CODE_SIZE = 10
define enable-a20
set $ADDRESS_MASK = 0x1FFFFF
end
define disable-a20
set $ADDRESS_MASK = 0x0FFFFF
end
# convert segment:offset address to physical address
define r2p
if $argc < 2
printf "Arguments: segment offsetn"
else
set $ADDR = (((unsigned long)$arg0 & 0xFFFF) << 4) + (((unsigned long)$arg1 & 0xFFFF) & $ADDRESS_MASK)
printf "0x%05Xn", $ADDR
end
end
document r2p
Convert segment:offset address to physical address
Set the global variable $ADDR to the computed one
end
# get address of Interruption
define int_addr
if $argc < 1
printf "Argument: interruption_numbern"
else
set $offset = (unsigned short)*($arg0 * 4)
set $segment = (unsigned short)*($arg0 * 4 + 2)
r2p $segment $offset
printf "%04X:%04Xn", $segment, $offset
end
end
document int_addr
Get address of interruption
end
define compute_regs
set $rax = ((unsigned long)$eax & 0xFFFF)
set $rbx = ((unsigned long)$ebx & 0xFFFF)
set $rcx = ((unsigned long)$ecx & 0xFFFF)
set $rdx = ((unsigned long)$edx & 0xFFFF)
set $rsi = ((unsigned long)$esi & 0xFFFF)
set $rdi = ((unsigned long)$edi & 0xFFFF)
set $rbp = ((unsigned long)$ebp & 0xFFFF)
set $rsp = ((unsigned long)$esp & 0xFFFF)
set $rcs = ((unsigned long)$cs & 0xFFFF)
set $rds = ((unsigned long)$ds & 0xFFFF)
set $res = ((unsigned long)$es & 0xFFFF)
set $rss = ((unsigned long)$ss & 0xFFFF)
set $rip = ((((unsigned long)$cs & 0xFFFF) << 4) + ((unsigned long)$eip & 0xFFFF)) & $ADDRESS_MASK
set $r_ss_sp = ((((unsigned long)$ss & 0xFFFF) << 4) + ((unsigned long)$esp & 0xFFFF)) & $ADDRESS_MASK
set $r_ss_bp = ((((unsigned long)$ss & 0xFFFF) << 4) + ((unsigned long)$ebp & 0xFFFF)) & $ADDRESS_MASK
end
define print_regs
printf "AX: %04X BX: %04X ", $rax, $rbx
printf "CX: %04X DX: %04Xn", $rcx, $rdx
printf "SI: %04X DI: %04X ", $rsi, $rdi
printf "SP: %04X BP: %04Xn", $rsp, $rbp
printf "CS: %04X DS: %04X ", $rcs, $rds
printf "ES: %04X SS: %04Xn", $res, $rss
printf "n"
printf "IP: %04X EIP:%08Xn", ((unsigned short)$eip & 0xFFFF), $eip
printf "CS:IP: %04X:%04X (0x%05X)n", $rcs, ((unsigned short)$eip & 0xFFFF), $rip
printf "SS:SP: %04X:%04X (0x%05X)n", $rss, $rsp, $r_ss_sp
printf "SS:BP: %04X:%04X (0x%05X)n", $rss, $rbp, $r_ss_bp
end
document print_regs
Print CPU registers
end
define print_eflags
printf "OF <%d> DF <%d> IF <%d> TF <%d>",
(($eflags >> 0xB) & 1), (($eflags >> 0xA) & 1),
(($eflags >> 9) & 1), (($eflags >> 8) & 1)
printf " SF <%d> ZF <%d> AF <%d> PF <%d> CF <%d>n",
(($eflags >> 7) & 1), (($eflags >> 6) & 1),
(($eflags >> 4) & 1), (($eflags >> 2) & 1), ($eflags & 1)
printf "ID <%d> VIP <%d> VIF <%d> AC <%d>",
(($eflags >> 0x15) & 1), (($eflags >> 0x14) & 1),
(($eflags >> 0x13) & 1), (($eflags >> 0x12) & 1)
printf " VM <%d> RF <%d> NT <%d> IOPL <%d>n",
(($eflags >> 0x11) & 1), (($eflags >> 0x10) & 1),
(($eflags >> 0xE) & 1), (($eflags >> 0xC) & 3)
end
document print_eflags
Print eflags register.
end
# dump content of bytes in memory
# arg0 : addr
# arg1 : nb of bytes
define _dump_memb
if $argc < 2
printf "Arguments: address number_of_bytesn"
else
set $_nb = $arg1
set $_i = 0
set $_addr = $arg0
while ($_i < $_nb)
printf "%02X ", *((unsigned char*)$_addr + $_i)
set $_i++
end
end
end
# dump content of memory in words
# arg0 : addr
# arg1 : nb of words
define _dump_memw
if $argc < 2
printf "Arguments: address number_of_wordsn"
else
set $_nb = $arg1
set $_i = 0
set $_addr = $arg0
while ($_i < $_nb)
printf "%04X ", *((unsigned short*)$_addr + $_i)
set $_i++
end
end
end
# display data at given address
define print_data
if ($argc > 0)
set $seg = $arg0
set $off = $arg1
set $raddr = ($arg0 << 16) + $arg1
set $maddr = ($arg0 << 4) + $arg1
set $w = 16
set $i = (int)0
while ($i < 4)
printf "%08X: ", ($raddr + $i * $w)
set $j = (int)0
while ($j < $w)
printf "%02X ", *(unsigned char*)($maddr + $i * $w + $j)
set $j++
end
printf " "
set $j = (int)0
while ($j < $w)
set $c = *(unsigned char*)($maddr + $i * $w + $j)
if ($c > 32) && ($c < 128)
printf "%c", $c
else
printf "."
end
set $j++
end
printf "n"
set $i++
end
end
end
define context
printf "---------------------------[ STACK ]---n"
_dump_memw $r_ss_sp 8
printf "n"
set $_a = $r_ss_sp + 16
_dump_memw $_a 8
printf "n"
printf "---------------------------[ DS:SI ]---n"
print_data $ds $rsi
printf "---------------------------[ ES:DI ]---n"
print_data $es $rdi
printf "----------------------------[ CPU ]----n"
print_regs
print_eflags
printf "---------------------------[ CODE ]----n"
set $_code_size = $CODE_SIZE
# disassemble
# first call x/i with an address
# subsequent calls to x/i will increment address
if ($_code_size > 0)
x /i $rip
set $_code_size--
end
while ($_code_size > 0)
x /i
set $_code_size--
end
end
document context
Print context window, i.e. regs, stack, ds:esi and disassemble cs:eip.
end
define hook-stop
compute_regs
if ($SHOW_CONTEXT > 0)
context
end
end
document hook-stop
!!! FOR INTERNAL USE ONLY - DO NOT CALL !!!
end
# add a breakpoint on an interrupt
define break_int
set $offset = (unsigned short)*($arg0 * 4)
set $segment = (unsigned short)*($arg0 * 4 + 2)
break *$offset
end
define break_int_if_ah
if ($argc < 2)
printf "Arguments: INT_N AHn"
else
set $addr = (unsigned short)*($arg0 * 4)
set $segment = (unsigned short)*($arg0 * 4 + 2)
break *$addr if ((unsigned long)$eax & 0xFF00) == ($arg1 << 8)
end
end
document break_int_if_ah
Install a breakpoint on INT N only if AH is equal to the expected value
end
define break_int_if_ax
if ($argc < 2)
printf "Arguments: INT_N AXn"
else
set $addr = (unsigned short)*($arg0 * 4)
set $segment = (unsigned short)*($arg0 * 4 + 2)
break *$addr if ((unsigned long)$eax & 0xFFFF) == $arg1
end
end
document break_int_if_ax
Install a breakpoint on INT N only if AX is equal to the expected value
end
define stepo
## we know that an opcode starting by 0xE8 has a fixed length
## for the 0xFF opcodes, we can enumerate what is possible to have
set $lip = $rip
set $offset = 0
# first, get rid of segment prefixes, if any
set $_byte1 = *(unsigned char *)$rip
# CALL DS:xx CS:xx, etc.
if ($_byte1 == 0x3E || $_byte1 == 0x26 || $_byte1 == 0x2E || $_byte1 == 0x36 || $_byte1 == 0x3E || $_byte1 == 0x64 || $_byte1 == 0x65)
set $lip = $rip + 1
set $_byte1 = *(unsigned char*)$lip
set $offset = 1
end
set $_byte2 = *(unsigned char *)($lip+1)
set $_byte3 = *(unsigned char *)($lip+2)
set $noffset = 0
if ($_byte1 == 0xE8)
# call near
set $noffset = 3
else
if ($_byte1 == 0xFF)
# A "ModR/M" byte follows
set $_mod = ($_byte2 & 0xC0) >> 6
set $_reg = ($_byte2 & 0x38) >> 3
set $_rm = ($_byte2 & 7)
#printf "mod: %d reg: %d rm: %dn", $_mod, $_reg, $_rm
# only for CALL instructions
if ($_reg == 2 || $_reg == 3)
# default offset
set $noffset = 2
if ($_mod == 0)
if ($_rm == 6)
# a 16bit address follows
set $noffset = 4
end
else
if ($_mod == 1)
# a 8bit displacement follows
set $noffset = 3
else
if ($_mod == 2)
# 16bit displacement
set $noffset = 4
end
end
end
end
# end of _reg == 2 or _reg == 3
else
# else byte1 != 0xff
if ($_byte1 == 0x9A)
# call far
set $noffset = 5
else
if ($_byte1 == 0xCD)
# INTERRUPT CASE
set $noffset = 2
end
end
end
# end of byte1 == 0xff
end
# else byte1 != 0xe8
# if we have found a call to bypass we set a temporary breakpoint on next instruction and continue
if ($noffset != 0)
set $_nextaddress = $eip + $offset + $noffset
printf "Setting BP to %04Xn", $_nextaddress
tbreak *$_nextaddress
continue
# else we just single step
else
nexti
end
end
document stepo
Step over calls
This function will set a temporary breakpoint on next instruction after the call so the call will be bypassed
You can safely use it instead nexti since it will single step code if it's not a call instruction (unless you want to go into the call function)
end
define step_until_iret
set $SHOW_CONTEXT=0
set $_found = 0
while (!$_found)
if (*(unsigned char*)$rip == 0xCF)
set $_found = 1
else
stepo
end
end
set $SHOW_CONTEXT=1
context
end
define step_until_ret
set $SHOW_CONTEXT=0
set $_found = 0
while (!$_found)
set $_p = *(unsigned char*)$rip
if ($_p == 0xC3 || $_p == 0xCB || $_p == 0xC2 || $_p == 0xCA)
set $_found = 1
else
stepo
end
end
set $SHOW_CONTEXT=1
context
end
define step_until_int
set $SHOW_CONTEXT = 0
while (*(unsigned char*)$rip != 0xCD)
stepo
end
set $SHOW_CONTEXT = 1
context
end
# Find a pattern in memory
# The pattern is given by a string as arg0
# If another argument is present it gives the starting address (0 otherwise)
define find_in_mem
if ($argc >= 2)
set $_addr = $arg1
else
set $_addr = 0
end
set $_found = 0
set $_tofind = $arg0
while ($_addr < $ADDRESS_MASK) && (!$_found)
if ($_addr % 0x100 == 0)
printf "%08Xn", $_addr
end
set $_i = 0
set $_found = 1
while ($_tofind[$_i] != 0 && $_found == 1)
set $_b = *((char*)$_addr + $_i)
set $_t = (char)$_tofind[$_i]
if ($_t != $_b)
set $_found = 0
end
set $_i++
end
if ($_found == 1)
printf "Code found at 0x%05Xn", $_addr
end
set $_addr++
end
end
document find_in_mem
Find a pattern in memory
The pattern is given by a string as arg0
If another argument is present it gives the starting address (0 otherwise)
end
define step_until_code
set $_tofind = $arg0
set $SHOW_CONTEXT = 0
set $_found = 0
while (!$_found)
set $_i = 0
set $_found = 1
while ($_tofind[$_i] != 0 && $_found == 1)
set $_b = *((char*)$rip + $_i)
set $_t = (char)$_tofind[$_i]
if ($_t != $_b)
set $_found = 0
end
set $_i++
end
if ($_found == 0)
stepo
end
end
set $SHOW_CONTEXT = 1
context
end
此脚本提供的功能允许用户更好地调试实模式代码。它将显示段和寄存器的值,并尝试通过正确计算物理地址来解码指令来解析段:偏移地址。
获得上述 3 个文件后,您可以尝试以这种方式调试 BIOS:
qemu-system-i386 -bios bios.bin -s -S &
gdb -ix gdb_init_real_mode.txt
-ex 'set tdesc filename target.xml'
-ex 'target remote localhost:1234'
在我之前链接的相关答案中提到了许多其他命令。此脚本负责将体系结构设置为 i8086,然后将自身挂接到 gdb。它提供了许多新的宏,可以更轻松地逐步执行 16 位代码:
break_int:在软件中断向量上添加断点(方式 好的老式MS DOS和BIOS公开了他们的API(
break_int_if_ah:在软件上添加条件断点 中断。AH 必须等于给定参数。这是用来 过滤中断的服务调用。例如,您有时只 当中断 10h 的函数 AH=0h 时想要中断 调用(更改屏幕模式(。
stepo :这是一个用于"跨步"功能的卡巴拉宏和 中断呼叫。它是如何工作的?电流的操作码 提取指令,如果是函数或中断调用, 计算"下一个"指令地址,临时断点为 添加到该地址上,并调用"继续"功能。
step_until_ret:这用于单步,直到我们遇到"RET" 指令。
step_until_iret:这用于单步,直到我们遇到 "IRET"指令。
step_until_int :这用于单步,直到我们遇到 "INT"指令。
如果您使用上面的命令启动 QEMU,您应该看到类似以下内容:
---------------------------[ STACK ]---
0000 0000 0000 0000 0000 0000 0000 0000
0000 0000 0000 0000 0000 0000 0000 0000
---------------------------[ DS:SI ]---
00000000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
---------------------------[ ES:DI ]---
00000000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
----------------------------[ CPU ]----
AX: 0000 BX: 0000 CX: 0000 DX: 0663
SI: 0000 DI: 0000 SP: 0000 BP: 0000
CS: F000 DS: 0000 ES: 0000 SS: 0000
IP: FFF0 EIP:0000FFF0
CS:IP: F000:FFF0 (0xFFFF0)
SS:SP: 0000:0000 (0x00000)
SS:BP: 0000:0000 (0x00000)
OF <0> DF <0> IF <0> TF <0> SF <0> ZF <0> AF <0> PF <0> CF <0>
ID <0> VIP <0> VIF <0> AC <0> VM <0> RF <0> NT <0> IOPL <0>
---------------------------[ CODE ]----
0xffff0: jmp 0xf000:0xe05b
0xffff5: xor BYTE PTR ds:0x322f,dh
0xffff9: xor bp,WORD PTR [bx]
0xffffb: cmp WORD PTR [bx+di],di
0xffffd: add ah,bh
0xfffff: add BYTE PTR [bx+si],al
0x100001: add BYTE PTR [bx+si],al
0x100003: add BYTE PTR [bx+si],al
0x100005: add BYTE PTR [bx+si],al
0x100007: add BYTE PTR [bx+si],al
0x0000fff0 in ?? ()
real-mode-gdb$
如您所见,它打印出堆栈顶部的部分数据,一些实模式程序通用的内存区域,段寄存器和常规寄存器。指令已从内存中的正确位置正确解码。您应该看到程序在0xffff0开始执行。某些 BIOS 可能具有不同的第一条指令,但前几条指令之一将是 FAR JMP 到BIOS中的另一个位置:
0xffff0: jmp 0xf000:0xe05b