什么是桩代码(Stub)?

stub code大概就是占坑的代码,桩代码给出的实现是临时性的/待编辑的。它使得程序在结构上能够符合标准,又能够使程序员可以暂时不编辑这段代码。

举个《C专家编程》中的例子:

我的一位同事需要编写一个程序,要求在某一地点存储每个文件的文件名和相关信息。数据存储于一个结构表中,他决定使用散列表。这里就需要用到可调试性编码。他并不想一步登天,一次完成所有的任务。他首先让最简单的情况能够运行,就是散列函数总是返回一个0,,这个散列函数如下:int hash_filename(char *s){ return 0;}这个函数的效果就是一个散列表还未被使用。所有的元素都存储在第零个位置后面的链表中,这使得程序很容易调试,因为无需计算散列函数的具体值。

——《C专家编程》186-187页。这个hash_filename函数就是一段桩代码吧?而作者的同事可以放心地完成程序的剩余部分,而无需担心散列表。在最后,他可以再激活这个散列表。


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

你代码引用并调用了外部库的函数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种相同。


你可能感兴趣的:(C++)