动态链接中的函数调用桩(stub)作用
动态链接中的函数调用桩(stub)作用
现象
我发现,编译得到的可执行程序,反汇编或看到的汇编代码,在函数调用中有一个"stub”,比如:
源文件:
void foo() {}
void bar() {
foo();
}
生成的汇编代码的逻辑会类似:
bar:
xxx
jmp foo_stub ; stub for foo
ret
foo_stub: ; 生成的桩
xxx
jmp foo
foo: ; 真正的foo函数体
xxx
这个桩是干啥的?
解释
你观察到的这种“stub”(桩函数)行为,通常出现在动态链接(dynamic linking)的上下文中,尤其是在使用 位置无关代码(Position Independent Code, PIC)或 延迟绑定(Lazy Binding)机制时。但在你的例子中,如果只是静态编译一个简单的 C 文件(没有外部依赖、没有动态库),一般不会生成 stub。
所以,出现 foo_stub 这种结构,很可能是因为以下几种情况之一:
1. 使用了动态链接 + PLT/GOT 机制(最常见)
在 Linux 等系统中,当你调用一个可能来自共享库(如 libc)的函数时,编译器会通过 PLT(Procedure Linkage Table) 来间接调用。即使 foo 是你本地定义的函数,如果你启用了某些链接选项(比如 -fPIC 或默认动态链接),链接器仍可能为它生成 PLT 条目(尤其在可执行文件支持 PIE 时)。
例如,在支持 PIE(Position Independent Executable) 的现代 Linux 发行版中,默认编译可能会启用 -fPIE -pie,此时即使是本地函数,也可能通过 PLT 调用以保持地址无关性。
典型的汇编看起来像:
bar:
call foo@plt
ret
foo@plt: ; PLT stub
jmp *foo@gotpcrel(%rip)
push $index
jmp .plt
foo: ; 实际函数
...
这里的 foo@plt 就是你看到的 “stub”。
✅ 作用:
- 支持动态链接和地址无关代码;
- 允许在运行时解析符号地址(特别是对共享库函数);
- 在首次调用时通过 GOT(Global Offset Table)跳转到真实地址,后续直接跳转。
2. 编译器/链接器优化或调试设置
某些编译器(如 MSVC)在调试模式下会为每个函数生成“thunk”或“trampoline”,用于支持热重载、增量链接或调试器 hook。但 GCC/Clang 在普通情况下不会对内部函数这样做。
3. 你实际链接了动态库,或 foo 被声明为弱符号/外部符号
如果你的 foo 被声明为 extern,或者在另一个编译单元中定义,而你又使用了动态链接,那么链接器可能保守地通过 PLT 调用它,以防它被共享库覆盖(symbol interposition)。
例如:
// file1.c
void bar() {
foo(); // foo 可能在 file2.c 中定义
}
即使 foo 最终是本地符号,若未开启 LTO(Link Time Optimization)或未内联,链接器仍可能保留 PLT 调用。
如何验证?
你可以用以下命令检查是否涉及 PLT:
objdump -d your_program | grep -A5 "call.*foo"
readelf -r your_program | grep foo # 查看重定位项
或者尝试静态链接并禁用 PIE:
gcc -fno-pie -no-pie -O2 test.c -o test
objdump -d test
这时你应该看到 bar 直接 call foo,没有 stub。
总结
你看到的 foo_stub 极大概率是 PLT 桩(Procedure Linkage Table stub),它的作用是:
在支持动态链接和地址无关代码的环境中,提供一个间接跳转层,使得函数地址可以在加载时或运行时解析,并支持共享库的符号替换和延迟绑定。
即使 foo 是本地函数,在 PIE 或潜在符号 interposition 的场景下,编译器/链接器也会保守地使用 PLT。
如果你不希望这种行为,可以:
- 使用
-fno-plt(GCC 选项,对已知定义的函数直接调用) - 静态链接(
-static) - 禁用 PIE(
-fno-pie -no-pie)
例如:
gcc -fno-plt -O2 test.c -o test
这样,对本地函数 foo 的调用就会变成直接 call foo,不再经过 stub。

浙公网安备 33010602011771号