GDB-remote + qemu 报告静态 C 变量的意外内存地址

ita*_*ato 5 c x86 gdb qemu osdev

基于os-dev 教程,使用 GDB 远程调试 Qemu 中运行的代码。
我的版本在这里。该问题仅在 qemu 中远程调试代码时发生,而不是在正常操作系统下构建普通可执行文件以直接在 GDB 中运行时发生。

代码如下所示:

#define BUFSIZE 255
static char buf[BUFSIZE];

void foo() {
  // Making sure it's all zero.
  for (int i = 0; i < BUFSIZE; i++) buf[i] = 0;

  // Setting first char:
  buf[0] = 'a';

  // >> insert breakpoint right after setting the char <<

  // Prints 'a'.
  printf("%s", buf);
}
Run Code Online (Sandbox Code Playgroud)

如果我在标记的位置放置一个断点并打印缓冲区,p buf我会从随机位置获得随机值,似乎来自我的代码部分。如果我得到地址,p &buf我会得到一些看起来不正确的东西,有两件事:

  1. 如果我执行 achar* p_buf = buf并用p p_buf它检查地址会给我一个完全不同的地址,该地址在执行过程中是稳定的(另一个则不是)。然后我检查那个内存部分,x /255b 0x____我可以看到a然后是零(97 0 0 0 ... 0)。

  2. 下一个命令 ( printf("%s", buf);) 实际上会打印a.

这让我相信如果我只检查静态变量,它可能是 GDB 不知道正确的位置。

我应该从哪里开始调试?


有关编译条件的详细信息:

  • 编译标志: -g -Wall -Wextra -pedantic -nostdlib -nostdinc -fno-builtin -fno-stack-protector -nostartfiles -nodefaultlibs -m32
  • qemu-system-i386
  • Gcc:i386 精灵目标

GDB 的输出示例:

(gdb) p buf
$1 = "dfghjkl;'`\000\\zxcvbnm,./\000*\000 ", '\000' <repeats 198 times>...
(gdb) p p_buf
$2 = 0x40c0 <buf+224> "a"
(gdb) p &buf
$3 = (char (*)[255]) 0x3fe0 <buf>
(gdb) info address buf
Symbol "buf" is static storage at address 0x3fe0.
Run Code Online (Sandbox Code Playgroud)

更新 2:

反汇编了显示差异的代码版本:

; void foo
0x19f1 <foo>            push   %ebp
0x19f2 <foo+1>          mov    %esp,%ebp
0x19f4 <foo+3>          sub    $0x10,%esp

; char* p_buf = char_buf; --> `p &char_buf` is 0x4040 (incorrect) but `p p_buf` is 0x4100
0x19f7 <foo+6>          movl   $0x4100,-0x4(%ebp)

; void* p_p_buf = (void*)p_buf; --> `p p_p_buf` gives 0x4100
0x19fe <foo+13>         mov    -0x4(%ebp),%eax
0x1a01 <foo+16>         mov    %eax,-0x8(%ebp)

; void* p_char_buf = (void*)&char_buf; --> `p p_char_buf` gives 0x4100
0x1a04 <foo+19>         movl   $0x4100,-0xc(%ebp)

; char_buf[0] = 'a'; --> correct address
0x1a0b <foo+26>         movb   $0x61,0x4100

; char_buf[1] = 'b'; --> correct address (asking `p &char_buf` here is still incorrectly 0x4040)
0x1a12 <foo+33>         movb   $0x62,0x4101

; void foo return
0x1a19 <foo+40>         nop
0x1a1a <foo+41>         leave
0x1a1b <foo+42>         ret
Run Code Online (Sandbox Code Playgroud)

Makefile用于构建项目的样子:

C_SOURCES = $(wildcard kernel/*.c drivers/*.c)
C_HEADERS = $(wildcard kernel/*.h drivers/*.h)
OBJ = ${C_SOURCES:.c=.o kernel/interrupt_table.o}
CC = /home/itarato/code/os/i386elfgcc/bin/i386-elf-gcc
# GDB = /home/itarato/code/os/i386elfgcc/bin/i386-elf-gdb
GDB = /usr/bin/gdb
CFLAGS = -g -Wall -Wextra -ffreestanding -fno-exceptions -pedantic -fno-builtin -fno-stack-protector -nostartfiles -nodefaultlibs -m32
QEMU = qemu-system-i386

os-image.bin: boot/boot.bin kernel.bin
    cat $^ > $@

kernel.bin: boot/kernel_entry.o ${OBJ}
    i386-elf-ld -o $@ -Ttext 0x1000 $^ --oformat binary

kernel.elf: boot/kernel_entry.o ${OBJ}
    i386-elf-ld -o $@ -Ttext 0x1000 $^

kernel.dis: kernel.bin
    ndisasm -b 32 $< > $@

run: os-image.bin
    ${QEMU} -drive format=raw,media=disk,file=$<,index=0,if=floppy

debug: os-image.bin kernel.elf
    ${QEMU} -s -S -drive format=raw,media=disk,file=$<,index=0,if=floppy &
    ${GDB} -ex "target remote localhost:1234" -ex "symbol-file kernel.elf" -ex "tui enable" -ex "layout split" -ex "focus cmd"

%.o: %.c ${C_HEADERS}
    ${CC} ${CFLAGS} -c $< -o $@

%.o: %.asm
    nasm $< -f elf -o $@

%.bin: %.asm
    nasm $< -f bin -o $@

build: os-image.bin
    echo Pass

clean:
    rm -rf *.bin *.o *.dis *.elf
    rm -rf kernel/*.o boot/*.bin boot/*.o
Run Code Online (Sandbox Code Playgroud)

Mic*_*tch 3

这是一个有趣的问题。归根结底,LD(链接器)为 ELF 可执行文件生成的代码与使用该选项时kernel.elfLD 生成的代码不同。虽然人们期望它们是相同的,但事实并非如此。kernel.bin--oformat binary

更简单地说,这些Makefile规则不会生成与您预期相同的代码:

kernel.elf: boot/kernel_entry.o ${OBJ}
        i386-elf-ld -o $@ -Ttext 0x1000 $^
Run Code Online (Sandbox Code Playgroud)

kernel.bin: boot/kernel_entry.o ${OBJ}
        i386-elf-ld -o $@ -Ttext 0x1000 $^ --oformat binary
Run Code Online (Sandbox Code Playgroud)

看来差异在于链接器在使用和不使用 时如何对齐各部分--oformat binary。ELF 文件(以及用于调试的符号)被视为位于一处,而实际在 QEMU 中运行的二进制文件在不同的偏移量处生成了代码和数据。

我从未观察到这个问题,因为我使用自己的链接器脚本,并且总是使用 OBJCOPY 从 ELF 可执行文件生成二进制文件,而不是使用 LD 链接两次。OBJCOPY 可以获取 ELF 可执行文件并将其转换为二进制文件。规则Makefile可以修改为:

kernel.bin: kernel.elf
        i386-elf-objcopy -O binary $^ $@

kernel.elf: boot/kernel_entry.o ${OBJ}
        i386-elf-ld -o $@ -Ttext 0x1000 $^
Run Code Online (Sandbox Code Playgroud)

这样做将确保生成的二进制文件与为 ELF 可执行文件生成的文件相匹配。