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

GDB-remote + qemu reports unexpected memory address for static C variable

基于 os-dev tutorial.
使用 GDB 在 Qemu 中远程调试代码 运行ning 我的版本是 here。只有在 qemu 中远程调试代码时才会出现此问题,而不会在正常 OS.

下直接在 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);
}

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

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

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

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

我应该从哪里开始调试?


编译条件详情:

GDB 的示例输出:

(gdb) p buf
 = "dfghjkl;'`[=11=]0\zxcvbnm,./[=11=]0*[=11=]0 ", '[=11=]0' <repeats 198 times>...
(gdb) p p_buf
 = 0x40c0 <buf+224> "a"
(gdb) p &buf
 = (char (*)[255]) 0x3fe0 <buf>
(gdb) info address buf
Symbol "buf" is static storage at address 0x3fe0.

更新 2:

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

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

; char* p_buf = char_buf; --> `p &char_buf` is 0x4040 (incorrect) but `p p_buf` is 0x4100
0x19f7 <foo+6>          movl   [=12=]x4100,-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   [=12=]x4100,-0xc(%ebp)

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

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

; void foo return
0x1a19 <foo+40>         nop
0x1a1a <foo+41>         leave
0x1a1b <foo+42>         ret

我的 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

对我来说,这似乎没有发生:

Breakpoint 1, main () at test65.c:16
16    printf("%s", buf);
(gdb) p buf
 = "a", '[=10=]0' <repeats 253 times>

Where should I start debugging this?

似乎有两件事可能会出错:

1。 GDB 可能从错误的位置读取

我不确定是什么原因导致的,但很容易验证。检查 p &buf 给你的地址。然后将其与您从 p_buf 获得的内容以及 info address buf 向您显示的内容进行比较。

请注意,由于 address space layout randomization,静态变量的地址将在您启动进程时发生变化。所以在 run 命令之前地址可以是例如0x4040 然后更改为 0x555555558040 一旦代码为 运行:

(gdb) info address buf
Symbol "buf" is static storage at address 0x4040.
(gdb) run
....
Breakpoint 1, main () at test65.c:16
16    printf("%s", buf);
(gdb) p &buf
 = (char (*)[255]) 0x555555558040 <buf>
(gdb) info address buf
Symbol "buf" is static storage at address 0x555555558040.

2。 GDB 正在读取正确的位置,但数据还不存在

这听起来像是编译器优化引起的典型调试问题。例如,编译器可能会将 buf[0] = a 的设置移动到断点所在的位置之后,但它必须在调用 printf() 之前设置它。您可以尝试使用 -O0 进行编译,看看它是否改变了什么。

您还可以使用 disas 命令检查反汇编,以查看到目前为止执行了什么:

(gdb) disas
Dump of assembler code for function main:
   0x000055555555517b <+50>:    movb   [=12=]x61,0x2ebe(%rip)        # 0x555555558040 <buf>
=> 0x0000555555555182 <+57>:    lea    0x2eb7(%rip),%rsi        # 0x555555558040 <buf>
   0x0000555555555189 <+64>:    lea    0xe74(%rip),%rdi        # 0x555555556004
   0x0000555555555190 <+71>:    mov    [=12=]x0,%eax
   0x0000555555555195 <+76>:    callq  0x555555555050 <printf@plt>

对我来说,断点位于 movb0x61(字母 a)设置为 buf 之后的点。

如果您使用 stepi 命令直到到达 callq printf 指令,您可以确定您看到的缓冲区与 printf 看到的完全一样。

这是一个有趣的问题。归结为 LD (linker) 为 ELF 可执行文件 kernel.elf 生成的代码与使用 [= 时 LD 为 kernel.bin 生成的代码不同15=] 选项。虽然人们期望它们是相同的,但它们不是。

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

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

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

看来区别在于 link 使用者在使用和不使用 --oformat binary 时如何对齐部分。 ELF 文件(以及用于调试的符号)被视为位于一个位置,而 QEMU 中实际上 运行 的二进制文件具有以不同偏移量生成的代码和数据。

我从来没有观察到这个问题,因为我使用自己的 linker 脚本,并且我总是使用 OBJCOPY 从 ELF 可执行文件生成二进制文件,而不是使用 LD to link 两次。 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 $^

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