转载如何减小EXE和DLL的文件长度_第1页
转载如何减小EXE和DLL的文件长度_第2页
转载如何减小EXE和DLL的文件长度_第3页
转载如何减小EXE和DLL的文件长度_第4页
转载如何减小EXE和DLL的文件长度_第5页
已阅读5页,还剩3页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

1、转载 如何减小EXE和DLL的文件长度原文地址:如何减小EXE和DLL的文件长度-作者:Knight原始链接也许很老了,但是确实很使用,所以扔过来了作者:Matt Pietrek翻译:lostall原文:Under The Hood:Reduce EXE and DLL Size with LIBCTINY.LIB,January 2001声明:严格地讲,这不是一篇翻译,我更愿意把它叫作意译。即,我尽力保证文章内容与原文完全一致,但我不保证与原文表达完全一致。特别地,对常用术语、我拿不准的句子,或者我懒得写汉字的话,我会直接引用英文。如果可能,读者还是尽量阅读原文为最佳。摘要这篇文章讲述了如何

2、减小EXE和DLL的文件长度。第一个小技巧是使用/OPT:NOWIN98链接选项,可以让Section按512字节对齐,而不是默认的4K字节。这只是小case,这篇文章最动人之处在于它模拟实现了一个简单的Run time Library:libctiny.lib。麻雀虽小五脏俱全。(1)VC的运行时库会创建一个自己的heap,而libctiny.lib里的malloc等函数只是简单地调用GetProcessHeap()。(2)libctiny.lib实现了简单的Startup函数,关键之处在于开始运行时要调用静态C+成员变量的构造函数,结束时调用静态C+成员变量的析构函数。(3)本文最精彩的地

3、方是介绍了VC里是怎么处理静态C+成员的构造和析构的,不过文中对于如何执行析构函数并没有讲全,没有说明atexit是何时调用的。我补充以下测试数据:$E4:00401117 push ebp 00401118 mov ebp,esp 0040111 Acall$E1(0040112 d)/function for executing constructor 0040111 Fcall$E3(00401143)/function for executingatexit00401124 cmp ebp,esp 00401126 call _chkesp(00401727)0040112 Bpop

4、ebp 0040112 Cret$E1:0040112 Dpush ebp 0040112 Emov ebp,esp 00401130 mov ecx,offset g_TestClassInstance(00405758)/this指针00401135 callILT+10(TestClass:TestClass)(0040100 f)0040113 Acmp ebp,esp 0040113 Ccall _chkesp(00401727)00401141 pop ebp 00401142 ret$E3:00401143 push ebp 00401144 mov ebp,esp 004011

5、46 push offset$E2(0040115 c)/function for executing destructor 0040114 Bcall atexit(00401379)/function for adding the function pointer of$E2into the list of static destructors 00401150 add esp,4 00401153 cmp ebp,esp 00401155 call _chkesp(00401727)0040115 Apop ebp 0040115 Bret$E2:0040115 Cpush ebp 00

6、40115 Dmov ebp,esp 0040115 Fmov ecx,offset g_TestClassInstance(00405758)00401164 callILT+0(TestClass:TestClass)(00401005)004011 69 cmp ebp,esp 0040116 Bcall _chkesp(00401727)00401170 pop ebp 00401171 ret在执行_initterm时会调用$E4,而$E4会首先调用$E1执行构造函数,然后紧接着调用$E3把析构函数的地址$E2加入到list of static destructors中。我早在MSJ

7、 1996年10月份的专栏里,就提出了一个关于可执行文件大小的问题。那个时侯一个简单的Hello World的程序编译后有32KB那么大。对于此后的两个编译器版本,问题也只是稍微好了一点而已。现在用VC6.0编译同样的程序也达到了28K。在那个专栏里,我提供了一个运行时库的替代品,可以让你创建非常小的可执行程序。虽然在适用性上有一些限制,但在我自己的很多程序里都用得非常好。经历了这么长时间,我决定是时侯对这些限制做一些修正了。同时这也是一个非常好的机会可以给大家描述一个很少有人知道的链接器选项,它可以更进一步的减小程序的大小。EXE and DLL Size在开始进入运行时库替代品的代码之前,

8、有必要花点时间复习一下为什么简单的EXEs和DLLs会比你想象中的大。考虑下面这个典型的Hello World程序:#include void main()printf(Hello World!n);让我们以大小优先的方式编译程序(译注:即编译器选项/O1,最小化大小),并生成一个map文件。在VC的命令行下,使用如下的语法:Cl/O1 Hello.CPP/link/MAP首先看一看.MAP文件,Figure 1中是一个减化的版本。注意观察main函数的地址(0001:00000000)和printf函数的地址(0001:0000000 C),你可以推断main函数的大小只有0xC个字节。再看

9、看文件的最后一行,_chkstk函数的地址是0001:00003B10,你可以推断出执行程序中的代码至少有0x3B10个字节。即往屏幕上写个Hello World需要超出14KB的代码。现在浏览一遍.MAP文件中的其它部分。有些函数还是有意义的,比如_initstdio函数。毕竟printf是把输出写到一个文件中,所以一些底层的为stdio提供支持的运行时库函数还是有必要的。同样的,也可以估计到printf的代码可能会调用strlen,所以包含strlen也不会让人感到奇怪。然而,看一看其他的一些函数,比如_sbh_heap_init。这是用于初始化运行时库的small block heap的

10、函数。Win32系统通过HeapAlloc家族的函数提供它们自己的堆。因为使用自己的堆具有潜在的性能上的好处,所以尽管VC库可以选择使用Win32的堆函数,但是它没这么做。结果就是,你的执行程序里包含了比需要的更多的代码。尽管有些人可能不关心运行时库实现了自己的heap,但另外有些情况你不能漠然视之。考虑map文件中靠近最下面的_crtMessageBoxA函数。这个函数允许运行时库调用MessageBox函数,而不用强迫执行程序链接到USER32.DLL。对于一个简单的Hello World程序,你很难预料到它还要调用MessageBox。再看看另一个例子:_crtLCMapStringA函

11、数,它实现字符串的locale-dependent transformations。尽管Microsof是有一些责任提供本地化支持,但很多程序并不真的需要它。为什么要让不使用locales的程序付出使用locales的代价呢?尽管我还可以继续列举一些包含了不需要的代码的例子,但现在这些已经足够证明我的论点。一些典型的小程序里包含了大量没被用到的代码。对于他们各自而言,产生的影响并不大,但是把所有情况累加起来,你就会发现,代码变得太大了!What About the C+Runtime Library DLL?思维敏捷的读者可能会问,Hey Matt!为什么你不用运行时库的DLL版本?要是在过去

12、,我会辩解说没有在Windows 95,98,NT 3.51,NT 4.0等操作系统上命名一致的C+运行时库的DLL版本。幸运的是,现在已经今非昔比,在大多数情况下你可以依赖MSVCRT.DLL,它在你机器上总是存在的。打开这个开关(译注:即选择运行时库的DLL版本,比如/MD)然后重新编译Hello.cpp,生成的可执行程序只有16KB了。结果并不坏,但你还可以做得更好。更重要的是,你只不过是把所有不需要的代码移到了别的地方而已(即:MSVCRT.DLL)。另外,当你的程序启动时,另一个DLL必须被装载并且初始化,这个初始化包括你可能并不关心的本地化支持等等。如果MSVCRT.DLL满足你的

13、需要,当然可以继续用它。但是,我相信一个精简的静态链接地运行时库仍然有它的存在价值。我可能是在和风车作战(译注:取自唐吉诃的风车大战,应该解释为理想主义),但我和读者之间的e-mail交流显示我并不孤单,总是有人想要最小的代码。在现在这个遍布可写CD,DVD,高速Internet连接的年代里,人们很少担心代码的大小。但是在我家里最快的Internet连接也只有24Kbps,我憎恨在浏览网页时要浪费时间下载臃肿的控件。作为原则,我希望我的代码尽可能的小巧。我不想load任何我没有真正用到的DLLs。就算我可能需要一个DLL,我也要尽可能地delayload它,这样我才不会为装载它而付出代价,直到

14、我真正用到了它。Delayloading是我在以前的专栏里讨论过的一个主题,我强烈推荐你去熟悉它。初学者可以从Under the Hood in the December 1998 issue of MSJ开始。(译注:这篇文章的译文在此)Digging Deeper既然已经解决了程序中未使用代码的问题,我们再转向可执行文件本身。如果你对我的Hello.exe运行DUMPBIN/HEADERS的话,会发现输出以下两行:(译注:section alignment是内存中节对齐的大小,是文件中节对齐的大小,详细内容参见IMAGE_OPTIONAL_HEADER)1000 section align

15、ment 1000 第二行很有趣,它是说在执行文件中每一个code和data段都是按4KB(0x1000)字节对齐的。因为sections在文件中是连续存储的,不难发觉在上一个section的结尾和下一个section的起始之间浪费的空间最多可达到4KB。如果我用一个比VC6.0更早的linker链接这个程序的话,会发现有一些不同,如下所示:1000 section alignment 200 关键的差异是section是按512(0x200)字节对齐的,这减少了浪费的空间。VC6.0里,链接器缺省的是让section在文件中的对齐方式等于在内存中的对齐方式。这种做法在Windows 9x上对

16、启动速度有轻微的提高,但是使执行程序变大了。幸运的是,VC linker有一种方法可以回到以前的做法,这个开关就是/OPT:NOWIN98。Rebuild Hello.cpp,增加的这个开关可以使执行文件的大小减少到21KB,节省了7KB。如果链接到MSVCRT.DLL并且同时使用/OPT:NOWIN98,执行文件的大小一下子降到了2560个字节!LIBCTINY:A Minimal Runtime Library既然你已经理解了为什么简单的EXEs和DLLs会那么大,那么是时侯介绍我的新的改进了的运行时库的替代品了。在October 1996的专栏里(前面提过),我创建了一个小的静态.LIB

17、文件,用来替换或者补充Microsoft的LIBC.LIB和LIBCMT.LIB。我把它叫做LIBCTINY.LIB,因为它是Microsoft自己的运行时库源程序的一个非常stripped-down的版本。LIBCTINY.LIB是给那些简单的不需要很多运行时库支持的应用程序使用的。因此,它不适用于MFC程序,或者其它需要大量使用C+运行时库的复杂情况。LIBCTINY.LIB的理想对象是调用了一些Win32 API函数并且可能会显示一些简单输出的小程序或DLLs。LIBCTINY.LIB背后有两个指导原则。首先,它用非常简单的代码替换了Visual C+的标准启动函数。这个简化的代码不引用

18、任何更深奥的运行时库函数,比如_crtLCMapStringA。正因如此,极大地减少了链接到你的执行文件中的无关代码。正如我稍后将说明的那样,LIBCTINY的函数在调用你的WinMain,main或DllMain之前仅仅执行了最少的任务。LIBCTINY.LIB的第二个指导原则是用已经存在于Win32系统DLLs的代码实现相当规模的函数,比如malloc,free,new,delete,printf,strupr,strlwr等等。看一眼printf.cpp(Figure 2)中printf的实现,体会一下我说的意思。在LIBCTINY.LIB的原始版本里有两个限制一直困扰着我。第一,原始的

19、版本里不支持DLLs。你可以创建小的Console和GUI执行程序,但不幸的是你不能创建一个小的DLL。第二,原始的版本里不支持静态C+构造和析构。我这里指的是在全局范围内声明的构造和析构。在新版本里我已经加上了提供这种支持的基本代码。在这个过程中,我也学到了很多关于编译器和运行时库为实现静态构造和析构玩的一个复杂游戏的知识。The Dark Underbelly of Constructors当编译器处理一个有静态Constructor的源文件时,它生成两个东西。首先是一个名字类似于$E2的一小段代码,负责调用Constructor。其次是一个指向这段代码的指针。这个指针被写到.OBJ文件中

20、一个叫做.CRT$XCU的特殊节中。为什么叫这么有趣的名字?原因有点复杂。让我提供一点别的数据帮助解释。如果你检查VC运行时库源代码(比如,CINITEXE.C),你会发现下面这段代码:#pragma data_seg(.CRT$XCA)_PVFV _xc_a=NULL;#pragma data_seg(.CRT$XCZ)_PVFV _xc_z=NULL;上面的代码创建了两个数据段:.CRT$XCA和.CRT$XCZ。在每个段中放了一个变量(分别是_xc_a和_xc_z)。注意,段的名字非常类似于编译器把Constructor代码指针放到的.CRT$XCU段的名字。这里需要了解一点Linker

21、的理论。当处理所有的段以产生最终的PE文件时,链接器合并所有相同名字的段的数据。所以,如果A.OBJ有一个段叫.data,B.OBJ也有一个段叫.data,那么A.OBJ里.data段的数据和B.OBJ里.data段的数据会被顺序写到PE文件中一个单独的.data段中。段名里使用的$符号有特别的作用。当遇到段名里有$符号的时侯,链接器把$之前的名字做为最终的段名。这样,.CRT$XCA,.CRT$XCU,和.CRT$XCZ段被合并在一起,成为最终的执行文件中的.CRT段。那么段名里$之后的部分是干什么用的呢?当合并这种类型的段时,链接器根据$之后的字符串的字典顺序进行处理。所以,.CRT$XC

22、A段内的数据先处理,紧跟着的是.CRT$XCU段内的数据,最后是.CRT$XCZ。这是理解的关键点。运行时库代码不知道一个指定的EXE或DLL里有多少个静态的构造函数需要被调用。但是,它知道在.CRT$XCu段内的指向constructor code块的指针。当链接器合并所有的.CRT$XCU段时,它产生的实际效果是创建了一个函数指针数组。通过定义在.CRT$XCA和.CRT$XCZ段内的_xc_a和_xc_z变量,运行时库能够可靠地定位函数指针数组的起点和终点。正如你可能料到的,调用模块内所有的静态构造函数,只是简单的遍历函数指针数组,依次调用每个指针。做这件事的函数是_initterm(F

23、igure 3),它与Visual C+运行时库里的源代码是一样的。考虑完所有的事以后,LIBCTINY让static constructors运行起来相对就简单了。主要就是定义正确的数据段(特别是.CRT$XCA和.CRT$XCZ),然后从启动代码的正确地方调用_initterm。但是处理静态析构就要有点技巧了。不同于编译器和链接器合谋为静态constructor创建的函数指针数组,静态destructor列表是在运行时创建的。为了建立这个列表,编译器产生对atexit函数的调用,它是Visual C+运行时库里的一个函数。atexit接受一个函数指针,然后把这个指针添加到一个FILO(先进

24、后出)列表(译注:其实就是stack)中。当EXE或DLL卸载时,运行时库遍历这个列表,并且调用每个函数指针。LIBCTINY对atexit的实现比VC运行时库里的实现简单了很多。有三个函数和几个静态变量用于实现它,也在initterm.cpp中。_atexit_init简单地分配一个容纳32个函数指针的数组,并把数组指针存到pf_atexitlist静态变量中。atexit函数检查数组中是否还有空间,如果有,就把函数指针加到数组的末尾。这段代码的一个更强健的做法是当需要时reallocate数组。最后,_DoExit函数使用你的朋友,_initterm,去遍历数组,调用每个函数指针。理想情况

25、下,_DoExit应该模仿VC运行时库的实现,以相反的方向遍历数组。但是LIBCTINY的目的只是为了简单小巧,没有必要追求完美的兼容性。(译注:C+标准规定Destructor的顺序应该与Constructor的顺序相反。LIBCTINY没有遵循这个规定,但也没有什么影响。应用程序也应该尽量避免使用构造或析构顺序有依赖关系的全局变量。)LIBCTINYs Minimal Startup Routines现在让我们看看LIBCTINY对小DLL的新支持。像EXE一样,技巧在于使DLL的入口点代码尽可能的小,并且去除可能引起大量其他代码的无用程序。Figure 4展示了最小的DLL启动代码。当你

26、的DLL被装载时,是这段代码,而不是你的DllMain首先被执行。_DllMainCRTStartup函数是你的DLL里执行的起始点。在LIBCTINY的实现中,它首先检查是否DLL处在DLL_PROCESS_ATTACH调用中。如果是,就调用_atexit_init(前面讲过),然后通过_initterm调用静态Constructor。函数的核心是调用DllMain,它是你在Dll里自己提供的。这个DllMain的调用对四种通知类型都适用(process attach/detach,and thread attach/detach)。DllMainCRTStartup要做的最后一件事是检查D

27、ll是否处在DLL_PROCESS_DETACH调用中。如果是,就调用_DoExit。与前面讲的一样,这会导致所有的静态析构函数被调用。如果你对Console和GUI类型的EXE的启动代码感到好奇的话,一定要看看CRT0TCON.CPP和CRT0TWIN.CPP。另一个值得查看的是在DLLCRTO.CPP(看Figure 4)的靠近最上面的一行代码:#pragma comment(linker,/OPT:NOWIN98)它把一个链接器命令放到DLLCRTO.OBJ文件中,其作用是通知链接器使用/OPT:NOWIN98开关。好处是你不用再手工地把/OPT:NOWIN98添加到你的make文件或工

28、程文件中。我考虑到如果你使用LIBCTINY,你应该也会想使用/OPT:NOWIN98。Using LIBCTINY.LIB使用LIBCTINY非常简单。你需要做的所有事就是把LIBCTINY.LIB加到链接器的.LIB列表中。如果你正在使用Visual Studios IDE,应该在Projects|Settings|Link下面。哪种类型的执行文件都可以(console EXE,GUI EXE,or DLL),因为LIBCTINY.LIB为每种类型都提供了适当的入口点。看一看Figure 5中的TEST.CPP。这个程序简单地使用了几个LIBCTINY.LIB实现的函数,并且包含了一个静态

29、constructor和destructor的调用。当我用Visual C+以正常方式编译它时,CL/O1 TEST.CPP生成的执行文件是32768字节。简单地在命令行上加上LIBCTINY.LIB:CL/O1 TEST.CPP LIBCTINY.LIB生成的执行文件缩减到了3072字节。你可能很想知道哪些运行时库函数LIBCTINY没有实现。例如,在TEST.CPP中,有一个对strrchr的调用。这里没有问题,因为Visual C+提供的LIBC.LIB和LIBCMT.LIB包含了这个函数。LIBCTINY.LIB和LIBC.LIB都实现了各种各样的函数。LIBCTINY提供的明显比LIBC.LIB的少。重要的是要让链接器在解析函数调用时首先找到LIBCTINY的函数,这样LIBCTINY的函数才会被使用。如果有些函数LIBCTINY没有实现,链接

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论