动态链接中的函数调用桩(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。

posted @ 2026-01-29 15:42  3的4次方  阅读(3)  评论(0)    收藏  举报