如果在同一翻译单元中调用函数,为什么需要重定位
Why is there a relocation needed if calling function in same translation unit
所以我有两个文件,一个是我的库,一个是主程序可执行文件。
图书馆:
static int internal1(int a, int b){
return a + b;
}
namespace {
int internal2(int a, int b){
return a + b;
}
}
void external2(int qq, int zz){
}
void external(int a, int b){
external2(a, b);
internal1(a, b);
internal2(a, b);
}
编译为
g++ -c -O0 -fPIC -o libtest.o libtest.cpp
和
g++ -shared -o libtest.so libtest.o
主程序:
extern void external(int a, int b);
int main(){
external(1, 2);
return 0;
}
编译为 g++ -O0 -L. -ltest -o tester tester.cpp
现在,如果我转储 tester
的重定位信息,我会得到我期望的结果:
Relocation section '.rela.dyn' at offset 0x4d0 contains 1 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000600a48 000100000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0
Relocation section '.rela.plt' at offset 0x4e8 contains 3 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000600a68 000300000007 R_X86_64_JUMP_SLO 0000000000000000 __libc_start_main + 0
000000600a70 000400000007 R_X86_64_JUMP_SLO 0000000000000000 _Z8externalii + 0
000000600a78 000a00000007 R_X86_64_JUMP_SLO 0000000000400578 __gxx_personality_v0 + 0
外部在重定位列表中,因为它必须找到地址并将其放入。
然而我不明白的是,当我转储共享对象的重定位列表时,为什么我在共享对象的重定位列表上看到 external2。为什么它不像对具有内部链接的函数那样自动输入地址。
Relocation section '.rela.dyn' at offset 0x460 contains 5 entries:
Offset Info Type Sym. Value Sym. Name + Addend
0000002007d8 000000000008 R_X86_64_RELATIVE 00000000002007d8
000000200990 000200000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0
000000200998 000300000006 R_X86_64_GLOB_DAT 0000000000000000 _Jv_RegisterClasses + 0
0000002009a0 000400000006 R_X86_64_GLOB_DAT 0000000000000000 __cxa_finalize + 0
0000002009d0 000500000001 R_X86_64_64 0000000000000000 __gxx_personality_v0 + 0
Relocation section '.rela.plt' at offset 0x4d8 contains 2 entries:
Offset Info Type Sym. Value Sym. Name + Addend
0000002009c0 000400000007 R_X86_64_JUMP_SLO 0000000000000000 __cxa_finalize + 0
0000002009c8 000600000007 R_X86_64_JUMP_SLO 0000000000000646 _Z9external2ii + 0
internal1
和 internal2
的调用不需要重定位,为什么 external2
作为外部符号意味着它现在必须通过 GOT 和 plt 进行查找甚至虽然符号在同一个翻译单元内?为什么它不能像 internal
s
那样进行正常的偏移调用
出于好奇,让我们看看当您尝试编译(甚至 link)启用了大多数优化的相同代码时编译器会做什么。这通常会暴露编译器束手无策的情况。此外,编译器可以在 O0
中非常 sloppy/lazy,所以我避免过多地阅读它。
用 -O3 -fPIC -c
编译 libtest.cpp
得到:
external2(int, int):
ret
external(int, int):
jmp external2(int, int)@PLT
请参阅 godbolt:https://gcc.godbolt.org/z/CnRVEX
这很有趣:GCC 可以明显地分辨出 external2()
是一个空操作,但 它仍然在 O3
下调用它 。
我们可以从中得出什么结论?调用 external2()
不一定会执行 TU 版本的 external2()
中的代码。但这怎么可能呢? ODR 应该允许我们假设同一二进制文件中的任何 external2()
与此 TU 中的二进制文件更差。
这在 C++ 级别是正确的,但 Linux 不加载 C++ 代码。它加载小精灵,小精灵按照不同的规则进行游戏。这些规则之一是您可以使用 LD_PRELOAD
在可执行文件之前加载符号以拦截它们。通过在 PIC 代码中将符号设为外部,linker 将其解释为可重载符号,从而防止内联(在我的示例中)以及本地跳转(在您的示例中)。
所以我有两个文件,一个是我的库,一个是主程序可执行文件。 图书馆:
static int internal1(int a, int b){
return a + b;
}
namespace {
int internal2(int a, int b){
return a + b;
}
}
void external2(int qq, int zz){
}
void external(int a, int b){
external2(a, b);
internal1(a, b);
internal2(a, b);
}
编译为
g++ -c -O0 -fPIC -o libtest.o libtest.cpp
和
g++ -shared -o libtest.so libtest.o
主程序:
extern void external(int a, int b);
int main(){
external(1, 2);
return 0;
}
编译为 g++ -O0 -L. -ltest -o tester tester.cpp
现在,如果我转储 tester
的重定位信息,我会得到我期望的结果:
Relocation section '.rela.dyn' at offset 0x4d0 contains 1 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000600a48 000100000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0
Relocation section '.rela.plt' at offset 0x4e8 contains 3 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000600a68 000300000007 R_X86_64_JUMP_SLO 0000000000000000 __libc_start_main + 0
000000600a70 000400000007 R_X86_64_JUMP_SLO 0000000000000000 _Z8externalii + 0
000000600a78 000a00000007 R_X86_64_JUMP_SLO 0000000000400578 __gxx_personality_v0 + 0
外部在重定位列表中,因为它必须找到地址并将其放入。
然而我不明白的是,当我转储共享对象的重定位列表时,为什么我在共享对象的重定位列表上看到 external2。为什么它不像对具有内部链接的函数那样自动输入地址。
Relocation section '.rela.dyn' at offset 0x460 contains 5 entries:
Offset Info Type Sym. Value Sym. Name + Addend
0000002007d8 000000000008 R_X86_64_RELATIVE 00000000002007d8
000000200990 000200000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0
000000200998 000300000006 R_X86_64_GLOB_DAT 0000000000000000 _Jv_RegisterClasses + 0
0000002009a0 000400000006 R_X86_64_GLOB_DAT 0000000000000000 __cxa_finalize + 0
0000002009d0 000500000001 R_X86_64_64 0000000000000000 __gxx_personality_v0 + 0
Relocation section '.rela.plt' at offset 0x4d8 contains 2 entries:
Offset Info Type Sym. Value Sym. Name + Addend
0000002009c0 000400000007 R_X86_64_JUMP_SLO 0000000000000000 __cxa_finalize + 0
0000002009c8 000600000007 R_X86_64_JUMP_SLO 0000000000000646 _Z9external2ii + 0
internal1
和 internal2
的调用不需要重定位,为什么 external2
作为外部符号意味着它现在必须通过 GOT 和 plt 进行查找甚至虽然符号在同一个翻译单元内?为什么它不能像 internal
s
出于好奇,让我们看看当您尝试编译(甚至 link)启用了大多数优化的相同代码时编译器会做什么。这通常会暴露编译器束手无策的情况。此外,编译器可以在 O0
中非常 sloppy/lazy,所以我避免过多地阅读它。
用 -O3 -fPIC -c
编译 libtest.cpp
得到:
external2(int, int):
ret
external(int, int):
jmp external2(int, int)@PLT
请参阅 godbolt:https://gcc.godbolt.org/z/CnRVEX
这很有趣:GCC 可以明显地分辨出 external2()
是一个空操作,但 它仍然在 O3
下调用它 。
我们可以从中得出什么结论?调用 external2()
不一定会执行 TU 版本的 external2()
中的代码。但这怎么可能呢? ODR 应该允许我们假设同一二进制文件中的任何 external2()
与此 TU 中的二进制文件更差。
这在 C++ 级别是正确的,但 Linux 不加载 C++ 代码。它加载小精灵,小精灵按照不同的规则进行游戏。这些规则之一是您可以使用 LD_PRELOAD
在可执行文件之前加载符号以拦截它们。通过在 PIC 代码中将符号设为外部,linker 将其解释为可重载符号,从而防止内联(在我的示例中)以及本地跳转(在您的示例中)。