在ELF文件中,ABS符号的数值该如何解释?
我在查看libc.a文件中的符号,发现有一些“ABS”符号。
例如,有一个名为 "_nl_current_LC_COLLATE_used" 的符号。
下面是对libc.a文件的readelf输出。
符号们:
File: libc.a(setlocale.o)
Symbol table '.symtab' contains 77 entries:
Num: Value Size Type Bind Vis Ndx Name
...
39: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _nl_current_LC_COLLATE_used
...
File: libc.a(uselocale.o)
Symbol table '.symtab' contains 34 entries:
Num: Value Size Type Bind Vis Ndx Name
...
6: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _nl_current_LC_COLLATE_used
...
File: libc.a(lc-collate.o)
Symbol table '.symtab' contains 5 entries:
Num: Value Size Type Bind Vis Ndx Name
...
1: 0000000000000002 0 NOTYPE GLOBAL DEFAULT ABS _nl_current_LC_COLLATE_used
...
重定位:
File: libc.a(setlocale.o)
Relocation section '.rela.text' at offset 0x1b98 contains 124 entries:
Offset Info Type Symbol's Value Symbol's Name + Addend
...
00000000000009a3 0000002700000009 R_X86_64_GOTPCREL 0000000000000000 _nl_current_LC_COLLATE_used - 5
...
Relocation section '.rela.data.rel.ro' at offset 0x2738 contains 13 entries:
Offset Info Type Symbol's Value Symbol's Name + Addend
...
0000000000000098 0000002700000001 R_X86_64_64 0000000000000000 _nl_current_LC_COLLATE_used + 0
...
File: libc.a(uselocale.o)
Relocation section '.rela.text' at offset 0x838 contains 29 entries:
Offset Info Type Symbol's Value Symbol's Name + Addend
...
0000000000000029 0000000600000009 R_X86_64_GOTPCREL 0000000000000000 _nl_current_LC_COLLATE_used - 5
...
因此,"_nl_current_LC_COLLATE_used" 符号是以下重定位的目标:
- 两条R_X86_64_GOTPCREL重定位
- 一条R_X86_64_64重定位
如果我理解正确,这意味着需要这个符号的地址,因此这个符号必须在进程内存的某处被定义。
但这个符号在三个文件中存在:
- 两次作为一个 "WEAK UNDEFINED" 符号,这没问题,链接器后续应能找到一个定义
- 一次作为一个 "ABS" 符号,值为2
在其他地方没有任何定义,所以当我编译一个简单的helloworld.c文件并把它链接到libc.a时,应该会使用对 "_nl_current_LC_COLLATE_used" 的一个定义来生成最终的二进制,对吗?
但对这个符号没有一个与之相关联的节,只有数值 "2"。
根据ELF规范:
SHN_ABS 值为对应引用指定绝对值。这意味着如果一个符号引用了这个节,那么它已经具有绝对值,并且不受重定位影响。
那么,难道“2”就是这个符号的绝对地址吗?我不这么认为。
在libc的源代码中,_nl_current_LC_COLLATE_used是通过一个宏并使用汇编指令定义为一个常量整型,值为2。
我不理解的是:
- 在最终二进制中,“2”这个值存放在哪里?是否由链接器来创建一个新的数据节来存放所有绝对符号的值?
- 如果是这样,链接器如何知道每个绝对符号值该使用多大的尺寸?从源代码看,似乎值“2”应以64位整数存储,但.symtab的 size字段设为0
- 链接器如何知道符号应该存放在只读数据段(R)还是可写数据段(RW)?
解决方案
让我们逐条回答你的问题。
如果我理解正确,那就意味着需要 _nl_current_LC_COLLATE_used符号的地址,因此这个符号必须在进程内存中被定义。
确实需要符号的地址;然而这并不意味着该符号一定要放在某个节中。一个符号获得地址的常见方式是被放在某个节中,链接器基于该放置来生成地址;不过也存在其他获得符号地址的方式,ABS类型符号就是一种完全有效的方式(例如在嵌入式代码中,用来放置需要处于地址空间特定位置的MMIO变量等)。
在这种特定情况下,_nl_current_LC_COLLATE_used 被“放置”在地址 2;当然这是一个伪地址,那里什么也没有!但glibc代码并不访问这个地址,所以没问题(就C 来说这大概是各种未定义行为,但这是glibc);唯一的用法是glibc拿到符号的地址,如 &_nl_current_LC_COLLATE_used != NULL。这段代码只关心链接器是否使用了弱定义(也就是符号被放置在0 的位置),还是绝对定义(符号被放置在2)。
那么,“2”就是这个符号的绝对地址吗?
是的。
链接器怎么知道符号应该存放在R 还是RW数据段?
绝对符号不会被放入任何节中。
如果是这样,链接器怎么知道每个绝对符号值该使用多大的尺寸?这里从源代码看,似乎值 “2” 应作为64位整数存储,但.symtab的 size字段设为0
符号大小为0。该符号地址的大小是8。
最终二进制中,“2” 的值存放在哪里?是否由链接器创建一个新的数据节来存放所有绝对符号的值?
是的,在一定程度上如此。链接器会在必要时创建新节并把地址存放在那里(但不仅限于绝对值)。不过未必总需要。
对于 R_X86_64_64,链接器把常量 2 作为重定位;被重定位的代码中直接存储该值,不需要额外的存储。
对于 R_X86_64_GOTPCREL 等,链接器有时会进行放松(relaxation),可能得到与 R_X86_64_64 情况下的 2 常量相同的结果。例如,在我的玩具测试对象文件中是这样的:
0: f3 0f 1e fa endbr64
4: 8b 05 00 00 00 00 mov 0x0(%rip),%eax
6: R_X86_64_GOTPCRELX _nl_current_LC_COLLATE_used-0x4
a: c3 ret
请注意RIP相对寻址和GOT的使用;编译器对符号的地址进行间接访问;因此该地址(2)应该放入GOT,那个地址的地址是一个重定位。然而链接器有时会推断被加载地址的值并对这次加载进行优化。在最终链接的可执行文件中:
401000: f3 0f 1e fa endbr64
401004: c7 c0 02 00 00 00 mov $0x2,%eax
40100a: c3 ret
链接器推断通过那个GOT访问加载的值一定是 2,因此它将间接加载完全替换为简单的常量加载。
现在,如果我们禁用放松优化,或者链接器因为无法证明符号地址无论如何都会是常量而不能执行放松,则会得到:
401000: f3 0f 1e fa endbr64
401004: 8b 05 d6 2f 00 00 mov 0x2fd6(%rip),%eax # 403fe0
40100a: c3 ret
在这里,我们看到间接加载保持原样,2 正在从内存中加载。它到底在何处,以及它是怎么到达那里?
RELRO off 0x0000000000002fe0 vaddr 0x0000000000403fe0 paddr 0x0000000000403fe0 align 2**0
filesz 0x0000000000000020 memsz 0x0000000000000020 flags r--
那是它被放入的只读数据段。
3 .got 00000008 0000000000403fe0 0000000000403fe0 00002fe0 2**3
CONTENTS, ALLOC, LOAD, DATA
以及这一节。该节是链接器自己创建的,用来存储地址 2(以及通过GOT相对重定位引用的其他地址)。它的内容是值 2:
Contents of section .got:
403fe0 02000000 00000000