离线下载
PDF版 ePub版

luoweifu · 更新于 2017-11-18 05:00:55

带你玩转 Visual Studio——incremental linking(增量链接)的作用

今天编译一个 C++ 程序时,报了一个奇怪的错误(之前是好好的): 1>LINK : fatal error LNK1123: failure during conversion to COFF: file invalid or corrupt

Google 上搜了一下解决方案: 把 Project Properties -> Configuration Properties -> Linker (General) -> Enable Incremental Linking 中的 Yes (/INCREMENTAL)改成 No (/INCREMENTAL:NO) 果然管用!

但又有个新的疑问出现了:Incremental Linking 是什么?它有什么作用?

大概总结一下:

Incremental Linking 翻译成中文就是“增量链接”,是一个链接的参数选项,作用就是为了提高链接速度的。什么意思呢?不选用增量链接时,每次修改或新增代码后进行链接时会重新洗牌,把原来的.exe 删了,重新链接成一个新的.exe,这样对于大型项目来说链接会比较慢。而选用增量链接时,在对代码做小的改动时会把新成的函数或数据穿插到已有的.exe 中,而不重新生成 .exe ,只有做了大量修改时才可能会重新编排,这样就可以提高链接的速度。

一般 VS 的默认设置会把 Debug 版的 Incremental Linking 设置成 Yes (/INCREMENTAL),而把 Release 版的设置成 No (/INCREMENTAL:NO)。

关于 Incremental Linking 更详细的介绍和分析请参考另一位博主的原文: http://www.cnblogs.com/Dahaka/archive/2011/08/01/2124256.html

为方便阅读,直接将这文章拷贝了一份:

好的,文接上回,本文我就来讲讲微软 link.exe 连接器的 Incremental Liking 这个特性。当然这个其实不是微软 linker 独有的特性,很多链接器都有这个特性,这个特性实际上是为了提高链接速度的。

想象一下这个场景,我写了两个函数 foo()和 bar(),其中 foo()在 0x400100 处而 bar()紧接着保存在 0x400200 处。现在我将 foo()改写了一下,添加了一些 perfect 的功能,然后编译了新的代码。不过现在的麻烦是 foo()不可避免的变大了,他现在需要 200h 字节来保存了。那么链接器该怎么办?

一般的思考是——重新洗牌,将现有的编译好的 exe 删除了,然后重新布局所有的函数,也即是说 bar()函数向后挪动 0x100h 字节的位置,给 foo()腾出空间来。然后之后所有的函数都需要重新定位……对于大型软件来说这个处理时间开销是痛苦的,但作为程序员我们却不能避免需要不断的调试改代码,不断地重复这个耗时的工作。

不过我们现在并不需要给客户最终的发行代码,我们只是想要尽快地将程序的 bug 改掉然后去休息而已!于是,Incremental Linking 出现了!它的原理如下:

现在连接器不会将所有函数紧挨着放在一块儿了,他们会在函数之间加上 padding ,这个时候函数要想添几句指令就有余地了。只要我们的改动不大,没有超过 padding 的范围连接器就不需要重新洗牌,这大大提高了链接的速度。

先别高兴,加入我们的改动很大,以至于超过 padding 能够搞定的范围怎么办?如上图,我们还会在整个 section 末尾设置一个较大的 padding (当然具体在哪里要看实现,比如我这图是从 GCC 那里搞得,说的就是 ld.exe 的行为方式),这时候就可以将这个函数搬到这里来了。但有个毁灭性的问题——所有调用我这个函数的函数都必须重定位他们的 call 指令啊!

为了解决这个问题,我们引入了一个 ILT 表(Incremental Linking Table),这个表是放在.text 区域中的(我在 IDA 中观察得知)。它的原理是什么呢?我们来看:

;之前我们都是直接调用函数
   call foo

;现在我们来点小把戏
   call foo_stub

foo_stub:
   jmp foo

我们现在不直接调用函数,而是 call 到一个包含 jmp 指令的地方,然后由这个指令将我们的程度带往 foo()函数的实现去。现在如果我们将 foo()的实现改动过大后,linker 直接将 foo()移动了,然后只需要修改这个 jmp 指令就行了。可以看到,这种实现方式开销是 O(1)。然后当很多个函数都用这种方式时,就形成了一个有 jmp 指令构成的表——这就是 ILT 表啦。

兴趣的童鞋可以做下实验,在 VS2010 编译一次代码,然后用 IDA 或者 W32Dasm 之类的软件可以看到两个函数之间间隔了不少距离,而这些间隔就是我们所谓 padding。padding 被填充以 0xCCh 的数据。熟悉 win32 汇编的朋友这时候该笑而不语了,是的,这个值就是指令 INT 3。在 WIndows 下,执行这个指令会引发一个异常,然后程序会被终止或是回到调试器去,这当然是出于安全性考虑的。这之后如果你在前一个函数加几句话,编译后可以看到两个函数位置不变,但函数间的 padding 变小了。

和.textbss 的关系

嘛,之前有篇我讨论了 PE 常见的 section,里面提到了这个节,下面我就详细介绍一下它的作用。

首先 Incremental Linking 作用不仅仅是在于减少我们重新连接程序所需要的时间,他还是我们调试时能够动态改动代码的前提。不知你还记得不,在那个炎热的夏天,你正汗流浃背地在没有空调的部屋里调试 C 代码(咳,说远了……)你直接修改了代码,然后 VS 直接在调试的时候将你的改动反映到程序里去了。这就是 VS 在 Debug 模式时动态编译代码的功能。

实际上这个功能是基于 Incremental Linking 机制的,而且是使用的 Incremental Linking 的第二套方案——直接找个大的地儿把修改的函数挪过去。

但是和.textbss 有啥关系?

首先我们看到,.textbss 有关键字 bss,这就说明实际上这个节没有占据实际的硬盘空间。然后 text 关键字告诉我们这里段是包含代码的,另外用工具查知这个段有可执行属性更是印证了这个观点。没有代码,那要这个节有啥用呢?

你想到了么?是的,在 VS 动态编译的时候,他直接将被修改的函数放到了.textbss 节里,然后修改了对应的 ILT 表项,是他指向这个位置。

说是简单,但实际上这个过程还个细节需要注意——你把我的函数挪地儿了,要是我正在执行这个函数怎么办?实际上,在改了 ILT 之后立刻会做的,就是检查当前程序所有线程的 TIB ,如果他们的 EIP 指向老的函数(它们正在执行老版本函数),我们就修改 EIP 使其指向新版本函数的对应位置。当然,这实际上暗示了,这个工作非要在调试程序的帮助下不可了。注意动态编译的功能只在 Debug 版本程序下有效, Release 版本是不行的,因为 Release 版本默认禁用 Incremental Linking。

下面是小亡的实验时间,以验证我的观点:

我就用手头上的程序来测试。有函数 CheckValidPE(),它的 RVA 是 0x12490h(你可理解为内存地址)。我的程序地址空间部分如下:

Section       RVA      Size

.textbss      1000h     10000h

.text        11000h     7000h

可以看到这两个函数实际上是位于.text 节内的。我在 CheckValidPE() 上下个断,可以看到:


bool PEAnalyser::checkValidPE()
{
00D81000  push        ebp  
00D81001  mov         ebp,esp  
00D81003  sub         esp,0FCh  
00D81009  push        ebx  
00D8100A  push        esi  
00D8100B  push        edi  
00D8100C  push        ecx  
00D8100D  lea         edi,[ebp-0FCh]  
00D81013  mov         ecx,3Fh  
00D81018  mov         eax,0CCCCCCCCh  
00D8101D  rep stos    dword ptr es:[edi]  
.....

注意看函数变到这个地址来了!!而且 VS 的调试程序指针也确实说明 EIP 被更改了。

我们来算一下,看看这个地址在哪里?用这里新的函数起始地址减去我们之前计算的的基址得到 RVA = 0xD81000h - 0xD80000h = 0x1000h

回去查一下我的内存地址空间,RVA 为 0x1000h 正是位于.textbss 节的起始位置。看来我的猜测是正确的。

小结一下,关于 Incremental Linking ,由于他的机制所致,势必带来程序体积的臃肿以及执行的低效,但是由于我们只是在 Debug 程序是使用,所以问题不大。另外 VS 默认是在 Debug 是开启 Incremental Linking 而 Release 模式关闭这个特性的。这说明在 Release 时,我们不能够动态的改动代码了。

另外注意 Incremental Linking 是和 /LTCG 选项不兼容的,你不能同时开启 Incremental Linking 和 Link Time Code Generation,从这个角度讲,使用 Incremental Linking 进一步会造成程序执行效率下降。所以,我们应该在发布程序时,注意避免带上这个特性。

上一篇: 结局汇总 下一篇: Property Manager...