使用LIBCTINY.LIB为EXE和DLL文件减肥
2010-07-15 20:45:18 来源:WEB开发网深度挖掘
我们已经赶跑了程序中不必要的代码,现在我们看看可执行体本身。使用 DUMPBIN /HEADERS Hood.exe, 可以看到下面的输出:1000 section alignment
第二行很有意思。它说明代码的边界是4KB(0x1000)对齐。由于段的存储是连续的,所以很难发现段与段之间那些可能存在的4KB浪费。
1000 file alignment
如果使用Visual C++ 6.0之前版本的连接器,你有可能看到不同的结果:1000 section alignment
200 file alignment
不同的关键在于段边界以512(0x200)字节对齐。这样空间浪费就要少得多。Visual C++ 6.0 将边界对齐适应内存的对齐,这样可以使得 windows9x 的程序装载速度提高,不过文件更大。
幸运的是,Visual C++ 连接器提供返回到过去参数的方法。使用开关 /OPT:NOWIN98,重新编译,如果你使用静态连接(译者注:cl /O1 Hood.c /link /OPT:NOWIN98),那么可执行文件的大小为21KB,减少了7KB;如果使用MSVCRT.DLL动态连接(译者注:cl /O1 /MD Hood.c /link /OPT:NOWIN98),可执行文件只有2560字节。
LIBCTINY: 最小的运行库
现在你明白为什么一个简单的 EXEs 和 DLLs 有如此大了,也是时候介绍我的运行库了。在 October 1996 column,我建立了一个静态的 .LIB 文件代替微软的 LIBC.LIB 和 LIBCMT.LIB 。我称之为 LIBCTINY.LIB,它是从微软运行库分离出来的一个微缩版。
LIBCTINY.LIB臆在支持不需要大运行库的小应用程序,但是,它不适于用在 MFC 以及其它复杂的 Visual C++ 扩展运行库。理想的 LIBCTINY.LIB 使用者是一个只调用 Win32 API 的 DLLs 或 EXEs 来输出信息。
LIBCTINY.LIB 有两个指导性准则。第一,它将标准的 Visual C++ 启动例程替换成非常简单的代码。这段代码不涉及任何复杂的运行库函数,如 __crtLCMapStringA。如你呆会儿要看到的,LIBCTINY.LIB 在启动 WinMain, main 或 DllMain之前只执行一些很小的任务。第二, LIBCTINY.LIB 将复杂的函数实现如 malloc 或 printf 尽量替换为已有的Win32系统调用。所以不仅启动代码短小,大部分其他 LIBCTINY.LIB 的函数实现如 malloc, free, new, delete, printf, strupr, strlwr 等等都是非常简单的,查看一下 printf 在 printf.cpp (Figure2)实现就会明白我所说的了。
老版本的 LIBCTINY.LIB 中的约束令我很是苦恼。首先,原始版本不支持 DLLs。你只能创建控制台或者 GUI 程序,而不能创建一个小的DLL。其次,原始的 LIBCTINY 不支持 C++ 的构造和析构。当然,我说的是在全局范围内申明的构造器和析构器。在新版本中,我添加了对这些的支持。同时也了解到编译器和运行库为了让构造器和析构器运转是多么的复杂的一件事。
构造器内幕
编译器处理一个含有构造器的代码文件的时候,它会做两件事,首先是一小段类似 $E2 用来调用构造器的代码。第二件事就是产生一个指向这段代码的指针。指针被写到 .OBJ 文件的 .CRT$XCU 节中。
为什么使用如此搞笑的命名?哈,有点复杂。先看一段代码来增加理解。如果你查看 Visual C++ 运行库原代码(比如,CINITEXE.C), 你可以看到下列:#pragma data_seg(".CRT$XCA")
上面的代码创建两个节, .CRT$XCA 和 .CRT$XCZ。 每个节都有一个变量(分别是 __xc_a 和 __xc_z)。注意,节的命名和 .CRT$XCU 非常相似。
_PVFV __xc_a[] = { NULL };
#pragma data_seg(".CRT$XCZ")
_PVFV __xc_z[] = { NULL };
这里,我们需要一点链接器方面的知识。当创建一个最终的PE文件的时候,链接器将所有名字相同的节合并。所以,如果 A.OBJ 有一个叫做 .data 的节,而 B.OBJ 也有个 .data 的节的话,那么 A.OBJ 和 B.OBJ 中所有 .data 里面的数据将被连续的写到PE文件唯一的 .data 节中去。
$的作用是一个名字的分隔符。当链接器遇到一个有$的名字的时候,会将前半部分看作是节名。所以 .CRT$XCA 和 .CRT$XCU 以及 CRT$XCZ 在最后的PE文件中都被合并成 .CRT 节。 那么$的后半部分是什么意思呢?链接器在合并这种类型的节的时候,根据后半部分的字母顺序排序。所以 .CRT$XCA 里面的数据放在最前面,接下来是 .CRT$XCU,最后是 .CRT$XCZ 里面的数据。这些就是需要理解的关键点。
接下来,运行库并不知道 EXE 或 DLL 有多少个静态构造器,也就是不知道在 .CRT$XCU 节中有多少个构造器代码的指针。但是当链接器合并 .CRT$XCU 节的时候,通过定义 .CRT$XCA 和 .CRT$XCZ 节的 __xc_a 和 __xc_z 符号来产生一个函数指针数组,运行库就通过函数指针数组的开始和结束来定位函数。
如你所期望的,访问静态构造器是一件简单的事情,只要通过媒举函数指针数组就可以实现。其操作函数是 _initterm (Figure3),这段函数和 Visual C++ 运行库的代码是一致 的。
从上看来,让静态构造器工作是相对容易的,只要正确的定义数据段(.CRT$XCA and .CRT$XCZ)然后调用在启动代码处调用 _initterm 就行了。而静态析构器的工作更加富有技巧性。
和编译器同连接器协同为静态构造器创建函数指针数组不同的是,静态析构器是在运行时被创建的。为了创建此列表,编译器先产生一个对Visual C++运行库 atexit 的调用。atexit函数将析构器函数的指针加到一个先入后出的队列。当 EXE 或 DLL 卸载的时候,运行库将循环调用队列中的函数。
LIBCTINY 中的 atexit 函数相对于 Visual C++ 运行库中的要简单得多。我们在 initterm.cpp 中用了三个函数和若干静态变量来实现 atexit,_atexit_init 简单的分配32位函数指针空间并保存在静态变量到 pf_atexitlist 中。
atexit 函数检查数组是否有足够的空间,如果有,将指针添加到列表中(一个更加健壮的版本将在必要的时候重新分配数组空间)。最后 _DoExit 函数媒举所有数组中的函数指针且分别调用。更加完美的做法是,_DoExit 反向媒举数组,这样更加符合 Visual C++ 运行库的行为。只不过,LIBCTINY 的目的是变得更加简单和小巧,而不是为了去兼容。
更多精彩
赞助商链接