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
获得地址,我会得到一些看起来不正确的东西,原因有两点:
如果我执行 char* p_buf = buf
并使用 p p_buf
检查地址,它会给我一个完全不同的地址,该地址在执行过程中是稳定的(另一个不是)。然后我用 x /255b 0x____
检查那个内存部分,我可以看到 a
然后是零 (97 0 0 0 ... 0).
下一个命令 (printf("%s", buf);
) 实际上会打印 a
.
这让我相信如果我只检查静态变量可能是 GDB 不知道正确的位置。
我应该从哪里开始调试?
编译条件详情:
- 编译标志:
-g -Wall -Wextra -pedantic -nostdlib -nostdinc -fno-builtin -fno-stack-protector -nostartfiles -nodefaultlibs -m32
- qemu-system-i386
- Gcc: i386 elf 目标
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>
对我来说,断点位于 movb
将 0x61
(字母 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 可执行文件生成的二进制文件匹配。
基于 os-dev tutorial.
使用 GDB 在 Qemu 中远程调试代码 运行ning
我的版本是 here。只有在 qemu 中远程调试代码时才会出现此问题,而不会在正常 OS.
代码看起来像这样:
#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
获得地址,我会得到一些看起来不正确的东西,原因有两点:
如果我执行
char* p_buf = buf
并使用p p_buf
检查地址,它会给我一个完全不同的地址,该地址在执行过程中是稳定的(另一个不是)。然后我用x /255b 0x____
检查那个内存部分,我可以看到a
然后是零 (97 0 0 0 ... 0).下一个命令 (
printf("%s", buf);
) 实际上会打印a
.
这让我相信如果我只检查静态变量可能是 GDB 不知道正确的位置。
我应该从哪里开始调试?
编译条件详情:
- 编译标志:
-g -Wall -Wextra -pedantic -nostdlib -nostdinc -fno-builtin -fno-stack-protector -nostartfiles -nodefaultlibs -m32
- qemu-system-i386
- Gcc: i386 elf 目标
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>
对我来说,断点位于 movb
将 0x61
(字母 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 可执行文件生成的二进制文件匹配。