什么是桩代码(Stub)?

摘一段使用“桩代码”的场景,来自《程序员的自我修养》第一版第264页: 当延迟载入的API第一次被调用时,由链接器添加的特殊的桩代码就会启动,这个桩代…
关注者
356
被浏览
254,635

11 个回答

题主问题中所说的“stub”的用法,下面几个词是相似的:

  • stub
  • trampoline
  • ricochet
  • thunk

它们说的都是“一小块代码”,通常是有个caller要调用callee的时候,中间需要一些特殊处理的逻辑,就会用这种“小块代码”去做。有意思的是,这些词除了stub以外都跟“跳”有关系:

  • trampoline:跳床
  • ricochet:反弹,弹跳
  • thunk:反弹时的声音(“锵”)

而“跳”正好生动的捕捉了这类stub的作用:它们并不是最终的调用目标,而是做一些简单的处理之后“跳”到真正的目标去。

很多时候这种“跳”都是一次性的:“跳”过去之后就不回来了。

题主提到的例子是动态链接库的延迟加载。还有许多其它类似的场景,举两个例子:

  • 微软.NET的虚拟机CLR是一个纯JIT编译的执行系统(这里请暂时忽略NGEN⋯)。每个方法在第一次被调用的时候触发JIT编译。但是具体是如何“触发”的呢?CLR加载一个类型的时候会为该类型生成一个方法表(MethodTable),表里每一项是该类型里每个方法的入口地址。调用方法就通过该表来查询入口地址并跳转过去。在刚加载好的时候,方法表里的项都指向触发JIT编译的stub(MethodDesc::DoPrestub)。某个方法第一次被调用的时候,实际被调用到的就是这个prestub,它就会负责触发JIT编译,等到JIT编译完成后将MethodTable里对应的项改为指向编译好的代码的入口地址,然后跳转到那个入口地址开始执行第一次调用。后续的调用从方法表里查到的地址是真正的入口,就不会再进到prestub了。这里有个讲解写得不错:.NET Just in Time Compilation and Warming up Your System
  • Haskell语言支持惰性求值(lazy evaluation)——表达式的值在还没被真正用到的时候可以先不求值。在GHC里这通过thunk(unevaluated thunk)来实现。一个Thunk持有求值所需的自由变量和运算,就跟闭包类似;只不过thunk求值完之后会把自己替换为求好的值:scs.stanford.edu/11au-c

链接器的效果就是把所有的函数调用翻译成某个地址的跳转。

你代码引用并调用了外部库的函数X,这些函数在进程中的地址(或者说入口)在进程启动前是不知道的。

所以链接器在链接的时候,把函数名X(C++还有参数类型信息)存起来;调用函数X的地方则先让它调用(跳转到)一个函数Y,Y的地址y是已知的,Y的实现则是跳转到一个随手涂鸦的地址x,然后给你的可执行文件链接了一段特殊代码。

程序一起动,把所有的动态库都加载到进程空间,这段特殊代码就激活去找这些函数X的地址,这些函数只要是编译时被设置为“导出的”,就应该可以找到。然后把找到的正确地址去替换随手涂鸦的x。

这样你对X的函数调用,通过这样一次转接(调用Y),就能够定位到对应的跳转地址(正确的x)。然后调试器再帮着作弊,忽略对Y这种“转发函数”的调用,你调试程序的时候,看到的调用栈就没有Y了,看起来就像直接调用了X一样。

比较牛逼的现代的优化动态库加载技术可以这样优化掉Y这一次中转。下面说一下我大学实现的一个编译器的优化。

假设函数F调用X,如上所说先调用Y,跳转到地址y。如上通常的Y的实现是直接跳转到x,然而某些优化实现,则在跳转之前,先回手一刀,把F中调用代码里Y的的地址改成x(Y是知道返回地址的(函数返回后下一条指令地址),所以Y也可以知道F调用自己的那段指令的地址(返回后的地址向后退一条地址,RISC没问题,CISC只要无条件跳转指令是定长的也没问题,不区分长短jump的32位X86就没问题了))。这样F第二次以后调用X就会直接跳转到x。 加速了以后的重复调用,但是增加了一条指令,会造成链接出来的结果稍大一点。

这样的优化其实对应3种策略,

第1种就是不优化,中转跳转。

第2种是存储所有对X的调用的代码地址,启动时找到x了之后,把它们全部替换。耗费最多空间(有多少“个”调用就要存多少个地址),启动加载也最慢,但以后调用速度最快。

第3种就是上面说的优化,直到真正调用的时候才替换,耗费极少空间(有多少“种"调用就要生成多少个额外代码),启动无代价,调用速度只有第一次调用比第2种慢,其余时候调用速度和第2种相同。

--------------------------------------------------------

我虽然不知道你说的那些术语是什么,但是你理解这个流程,术语什么的就不重要了。