无法分配链接描述文件中定义的变量的地址
Cannot assign address of variable defined in linker script
我找到了解决办法,虽然我不明白哪里出了问题。这是原始问题。解决方案在最后。
我正在关注此 Raspberry PI OS tutorial 并进行了一些调整。正如标题所说,一项作业似乎失败了。
这是我的 C 代码:
extern int32_t __end;
static int32_t *arena;
void init() {
arena = &__end;
assert(0 != arena); // fails
...
断言触发!地址当然不应该是 0。 __end
在我的链接描述文件中声明:
ENTRY(_start)
SECTIONS
{
/* Starts at LOADER_ADDR. 0x8000 is a convention. */
. = 0x8000;
__start = .;
.text : {
*(.text)
}
.rodata : { *(.rodata) }
.data : { *(.data) }
/* Define __bss_start and __bss_end for boot.s to set to 0 */
__bss_start=.;
.bss : { *(.bss) }
__bss_end=.;
/* First usable address for the allocator */
. = ALIGN(4);
__end = .;
}
在 GDB 中调查(运行 在 QEMU 中):
Thread 1 hit Breakpoint 1, init () at os.c:75
75 arena = &__end;
(gdb) p &__end
= (int32_t *) 0x9440
(gdb) p arena
= (int32_t *) 0x0
(gdb) n
76 assert(0 != arena);
(gdb) p arena
= (int32_t *) 0x0
GDB 可以找到 __end
但我的程序找不到?
以下是我尝试过的其他一些方法:
- 本教程的代码可以正常工作(暗示 QEMU 和 ARM 编译器可以正常工作)
- 当 运行 没有 GDB 时断言仍然失败(暗示 GDB 不是问题)
- 我可以将
0xccc
分配给 arena
(暗示竞技场不是问题)
- 我无法将
&__end
分配给局部变量(暗示 &__end
是问题所在)。
根据评论中的要求,这就是我尝试分配给局部变量的方式:
void* arena2 = (void*)&__end;
assert(0 != arena2);
断言失败。在 GDB 中:
Thread 1 hit Breakpoint 1, mem_init () at mem.c:77
77 void* arena2 = (void*)&__end;
(gdb) p arena2
= (void *) 0x13
(gdb) p &__end
= (int32_t *) 0x94a4
(gdb) n
78 assert(0 != arena2);
(gdb) p arena2
= (void *) 0x0
(gdb) p &__end
= (int32_t *) 0x94a4
assert(0 != &__end);
成功(暗示 &__end
不是问题?)
N.B。此版本的 assert
与 assert.h
中的版本不同,但我不认为它会导致问题。它只是检查一个条件,打印条件,然后转到断点。我可以在 GDB 中重现该问题,并将断言注释掉。
N.B.2。我之前包含了 C 代码的 ARM 程序集,以防出现编译器错误
我的解决方案是将链接描述文件编辑为:
ENTRY(_start)
SECTIONS
{
/* Starts at LOADER_ADDR. 0x8000 is a convention. */
. = 0x8000;
__start = .;
.text : {
*(.text)
}
. = ALIGN(4096);
.rodata : { *(.rodata) }
. = ALIGN(4096);
.data : { *(.data) }
. = ALIGN(4096);
/* Define __bss_start and __bss_end for boot.s to set to 0 */
__bss_start = .;
.bss : { *(.bss) }
. = ALIGN(4096);
__bss_end = .;
/* First usable address for the allocator */
. = ALIGN(4096);
__end = .;
}
我不明白为什么额外的 ALIGN
很重要。
您在这里遇到的问题是因为 boot.S 中的 "clear the BSS" 循环也在清除 C 代码在运行时使用的 ELF 文件中的一些编译器生成的数据。值得注意的是,它不小心将 .got ELF 部分中的 GOT(全局偏移量 table)归零,[=52= 放置了 __end 标签的实际地址。 ]呃。因此,linker 正确地填写了 ELF 文件中的地址,但随后 boot.S 代码将其归零,当您尝试从 C 读取它时,您得到的是零,而不是您所期望的。
在 linker 脚本中添加所有对齐方式可能会通过巧合地导致 GOT 不在被归零的区域中来解决这个问题。
您可以使用 'objdump -x myos.elf' 查看 link 人把东西放在哪里了。在我基于教程的测试用例中 link 我看到一个 SYMBOL TABLE 其中包括其他条目:
000080d4 l .bss 00000004 arena
00000000 l df *ABS* 00000000
000080c8 l O .got.plt 00000000 _GLOBAL_OFFSET_TABLE_
000080d8 g .bss 00000000 __bss_end
0000800c g F .text 00000060 kernel_main
00008000 g .text 00000000 __start
0000806c g .text.boot 00000000 _start
000080d8 g .bss 00000000 __end
00008000 g F .text 0000000c panic
000080c4 g .text.boot 00000000 __bss_start
所以可以看到linker脚本把__bss_start设置为0x80c4,__bss_end设置为0x80d8,很遗憾,因为GOT在0x80c4/0x80c8。我认为这里发生的事情是因为你没有在你的 linker 脚本中明确指定放置 .got 和 .got.plt 部分的位置,所以 linker 决定把它们在 __bss_start 分配之后和 .bss 部分之前,因此它们被归零代码覆盖。
您可以看到 .got 的 ELF 文件内容是什么'objdump --disassemble-all myos.elf',其中包括:
Disassembly of section .got:
000080c4 <.got>:
80c4: 000080d8 ldrdeq r8, [r0], -r8 ; <UNPREDICTABLE>
所以你可以看到我们有一个 GOT table 条目,其内容是地址 0x80d8,也就是我们想要的 __end 值。当 boot.S 代码将其清零时,您的 C 代码读取的是 0 而不是它期望的常量。
你可能应该确保 bss start/end 至少是 16 对齐的,因为 boot.S 代码通过一次清除 16 个字节的循环工作,但我认为如果你修复你的 linker 脚本以明确地将 .got 和 .got.plt 部分放在某个地方然后你会发现你不需要到处都是 4K 对齐。
FWIW,我使用以下方法进行了诊断:(1) QEMU“-d in_asm,cpu,exec,int,unimp,guest_errors -singlestep”选项以获得寄存器状态和指令执行的转储以及 (2) ELF 文件的 objdump 以找出编译器生成的代码实际在做什么。我怀疑这会变成 "accidentally zeroed data we shouldn't have" 或 "failed to include in the image or otherwise initialize data we should have" 之类的错误,结果是这样。
哦,GDB 为 __end 打印正确值而您的代码不是的原因是 GDB 可以直接在 ELF 文件中的 debug/symbol 信息中查找答案;它不是通过内存中的 GOT 来完成的。
我找到了解决办法,虽然我不明白哪里出了问题。这是原始问题。解决方案在最后。
我正在关注此 Raspberry PI OS tutorial 并进行了一些调整。正如标题所说,一项作业似乎失败了。
这是我的 C 代码:
extern int32_t __end;
static int32_t *arena;
void init() {
arena = &__end;
assert(0 != arena); // fails
...
断言触发!地址当然不应该是 0。 __end
在我的链接描述文件中声明:
ENTRY(_start)
SECTIONS
{
/* Starts at LOADER_ADDR. 0x8000 is a convention. */
. = 0x8000;
__start = .;
.text : {
*(.text)
}
.rodata : { *(.rodata) }
.data : { *(.data) }
/* Define __bss_start and __bss_end for boot.s to set to 0 */
__bss_start=.;
.bss : { *(.bss) }
__bss_end=.;
/* First usable address for the allocator */
. = ALIGN(4);
__end = .;
}
在 GDB 中调查(运行 在 QEMU 中):
Thread 1 hit Breakpoint 1, init () at os.c:75
75 arena = &__end;
(gdb) p &__end
= (int32_t *) 0x9440
(gdb) p arena
= (int32_t *) 0x0
(gdb) n
76 assert(0 != arena);
(gdb) p arena
= (int32_t *) 0x0
GDB 可以找到 __end
但我的程序找不到?
以下是我尝试过的其他一些方法:
- 本教程的代码可以正常工作(暗示 QEMU 和 ARM 编译器可以正常工作)
- 当 运行 没有 GDB 时断言仍然失败(暗示 GDB 不是问题)
- 我可以将
0xccc
分配给arena
(暗示竞技场不是问题) - 我无法将
&__end
分配给局部变量(暗示&__end
是问题所在)。
根据评论中的要求,这就是我尝试分配给局部变量的方式:
void* arena2 = (void*)&__end;
assert(0 != arena2);
断言失败。在 GDB 中:
Thread 1 hit Breakpoint 1, mem_init () at mem.c:77
77 void* arena2 = (void*)&__end;
(gdb) p arena2
= (void *) 0x13
(gdb) p &__end
= (int32_t *) 0x94a4
(gdb) n
78 assert(0 != arena2);
(gdb) p arena2
= (void *) 0x0
(gdb) p &__end
= (int32_t *) 0x94a4
assert(0 != &__end);
成功(暗示&__end
不是问题?)
N.B。此版本的 assert
与 assert.h
中的版本不同,但我不认为它会导致问题。它只是检查一个条件,打印条件,然后转到断点。我可以在 GDB 中重现该问题,并将断言注释掉。
N.B.2。我之前包含了 C 代码的 ARM 程序集,以防出现编译器错误
我的解决方案是将链接描述文件编辑为:
ENTRY(_start)
SECTIONS
{
/* Starts at LOADER_ADDR. 0x8000 is a convention. */
. = 0x8000;
__start = .;
.text : {
*(.text)
}
. = ALIGN(4096);
.rodata : { *(.rodata) }
. = ALIGN(4096);
.data : { *(.data) }
. = ALIGN(4096);
/* Define __bss_start and __bss_end for boot.s to set to 0 */
__bss_start = .;
.bss : { *(.bss) }
. = ALIGN(4096);
__bss_end = .;
/* First usable address for the allocator */
. = ALIGN(4096);
__end = .;
}
我不明白为什么额外的 ALIGN
很重要。
您在这里遇到的问题是因为 boot.S 中的 "clear the BSS" 循环也在清除 C 代码在运行时使用的 ELF 文件中的一些编译器生成的数据。值得注意的是,它不小心将 .got ELF 部分中的 GOT(全局偏移量 table)归零,[=52= 放置了 __end 标签的实际地址。 ]呃。因此,linker 正确地填写了 ELF 文件中的地址,但随后 boot.S 代码将其归零,当您尝试从 C 读取它时,您得到的是零,而不是您所期望的。
在 linker 脚本中添加所有对齐方式可能会通过巧合地导致 GOT 不在被归零的区域中来解决这个问题。
您可以使用 'objdump -x myos.elf' 查看 link 人把东西放在哪里了。在我基于教程的测试用例中 link 我看到一个 SYMBOL TABLE 其中包括其他条目:
000080d4 l .bss 00000004 arena
00000000 l df *ABS* 00000000
000080c8 l O .got.plt 00000000 _GLOBAL_OFFSET_TABLE_
000080d8 g .bss 00000000 __bss_end
0000800c g F .text 00000060 kernel_main
00008000 g .text 00000000 __start
0000806c g .text.boot 00000000 _start
000080d8 g .bss 00000000 __end
00008000 g F .text 0000000c panic
000080c4 g .text.boot 00000000 __bss_start
所以可以看到linker脚本把__bss_start设置为0x80c4,__bss_end设置为0x80d8,很遗憾,因为GOT在0x80c4/0x80c8。我认为这里发生的事情是因为你没有在你的 linker 脚本中明确指定放置 .got 和 .got.plt 部分的位置,所以 linker 决定把它们在 __bss_start 分配之后和 .bss 部分之前,因此它们被归零代码覆盖。
您可以看到 .got 的 ELF 文件内容是什么'objdump --disassemble-all myos.elf',其中包括:
Disassembly of section .got:
000080c4 <.got>:
80c4: 000080d8 ldrdeq r8, [r0], -r8 ; <UNPREDICTABLE>
所以你可以看到我们有一个 GOT table 条目,其内容是地址 0x80d8,也就是我们想要的 __end 值。当 boot.S 代码将其清零时,您的 C 代码读取的是 0 而不是它期望的常量。
你可能应该确保 bss start/end 至少是 16 对齐的,因为 boot.S 代码通过一次清除 16 个字节的循环工作,但我认为如果你修复你的 linker 脚本以明确地将 .got 和 .got.plt 部分放在某个地方然后你会发现你不需要到处都是 4K 对齐。
FWIW,我使用以下方法进行了诊断:(1) QEMU“-d in_asm,cpu,exec,int,unimp,guest_errors -singlestep”选项以获得寄存器状态和指令执行的转储以及 (2) ELF 文件的 objdump 以找出编译器生成的代码实际在做什么。我怀疑这会变成 "accidentally zeroed data we shouldn't have" 或 "failed to include in the image or otherwise initialize data we should have" 之类的错误,结果是这样。
哦,GDB 为 __end 打印正确值而您的代码不是的原因是 GDB 可以直接在 ELF 文件中的 debug/symbol 信息中查找答案;它不是通过内存中的 GOT 来完成的。