使用VC++的编译器创建最小的镜像文件(DLL/EXE)[译]

版权声明:转载时请以超链接形式标明文章原始出处和作者信息及本声明
http://dreamerate.blogbus.com/logs/4747515.html

一、序

 

本文通过描述一些方法来告诉你如何打造一个最小的镜像文件(DLL/EXE)。这些方法包括:

1)  剔除C运行时Stub;

2)  编译器(cl.exe)和链接器(link.exe)的一些参数设置。

 

如题,这里所指的编译器及链接器我主要集中在MSVC6上(这些方法通常也适用于MSVC5)。当一些出现在这里的观念在应用于其它开发环境中的命令行参数及#pragmas出现明显差异时,请参考您的环境文档。

 

 

 

二、抛开C运行时(C-Runtime)

C运行时是一个专为程序员准备的函数库。这些函数是独立于平台的,并且它担当了一个位于你程序与操作系统之间的抽像层角色。虽然这些函数都是汇编语言所编,但它在某一方面,会为我们的程序带来一些负面影响:

 

1)  BUG。尽管大多数的C运行时函数都测试的很好,但是也有一些在您引入这些函数到您程序中时,可能会带来更多的Bug;

2)  它会占用程序空间。为了使用C运行时函数,您的应用程序必须包含C运行时的代码,或是根据你的指示仅调用一个共享的DLL。一个通常的动作是,编译器在编译代码时会把C运行时函数的代码塞到你的程序中(这就是C运行时Stubs);

3)  这个抽像层并不是比操作系统提供的操作更简单,它仅仅是能跨平台而已。其实,多数的任务都能直接使用操作系统层提供的API以更少的代码量来完美的演绎完成;

4)  使用C运行时也同样会牺牲由操作系统带来的更多的功能,牺牲创建一个应用程序的更简洁、更多的可提升性能的潜力。

 

如你和我一样,无法接受如上的折衷,那么就应该做几个完全地去除C运行时的工作。如下

 

1)  停止不再使用C运行时函数。但是,还有一些以内部形式存在的函数您可继续使用(字符串及内存操作)。当然,您也可以直接地使用操作系统提供的等价的API函数。但是你要做的,更多的工作是替换那些操作系统没有提供等价的服务的函数;

2)  实现几个C/C++编译器假定存在的C运行时函数。如C++模块可能是必需的new、delete及_purecall操作。与此同时,也要为程序提供一个入口函数(EntryPoint)。什么是入口函数?入口函数就是操作系统在加载进程后,第一个执行您程序代码的入口点。在我们还没有去掉C运行时的时候,那个操作是由它来自动定位我们提供的main(console)、WinMain(windows)的;

3)  为了生成不依赖于C运行时环境的目标文件,您的一些编译器的开关可能需要改变;

4)  为了防止链接器把C运行时的函数库包含进来,您可能需要改变链接器的一些设置;

5)  注意:记住C运行时的启动代码主要负责初始化全局对象。不要在已脱离C运行时环境下中使用需要初始化的全局对象,否则应用程序可能会把它当作是没有定义的对象。

 

 

三、一些可继续使用的函数

下列以编译器内在形式存在的函数是可用的。注意尽管这些函数是以内在形式存在的,也可能不是像那些库函数一样是最优化的。这意味着您可编写更高效的替代方案。

 

memcmp

memcpy

memset

strcmp

strcpy

strlen

strcat

strset

 

 

四、需要构造的函数

 

毫无疑问,C++编译器需要您实现__purecallnewdelete。如果您开启了C++异常处理可能需要更多,我不会教你怎么写那些代码,您只能从两个方案中选其一:

1)  不要使用C++异常处理;

2)  找到那些已实现异常处理的*.obj目标文件,然后链接到您的工程中。

 

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
__purecallnewdelete简单实现如下:

void* __cdecl operator new ( unsigned int cb )
{
      return HeapAlloc( GetProcessHeap(), 0, cb );
}

void __cdecl operator delete ( void* pv )
{  
     if ( pv )
        HeapFree( GetProcessHead(), 0, pv );
}

extern "C" int _cdecl _purecall( void )
{
     return 0;
}

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

另外,在添加上面的函数之后,将要为您的应用程序提供一个新的入口点。一个应用程序典型的启动是由操作系统调用函数main/WinMian/DllMain。实际上,那些函数是由C运行时入口点调用的。下面就是C运行时入口点函数的原型及名称:(它们内部的实现过程和MASM32的入口点代码几乎一样,学过MASM32的一定会知道如何实现下面的函数代码^_^

EXTERN_C int WINAPI mainCRTStartup();
EXTERN_C int WINAPI WinMainCRTStartup();
EXTERN_C BOOL WINAPI _DllMainCRTStartup(
    HINSTANCE hInstDll,           // handle to the DLL module
    DWORD fdwReason,              // reason for calling function
    LPVOID lpvReserved,           // reserved
);


五、应用程序的结束

通常,在我们的代码执行到离开main(或是WinMain)函数时,应用程序将结束。导致这个原因的是上面的函数缺省实现了:在我们的主函数执行完时调用了操作系统的API函数ExitProcess(MASM32的FANS们会心一笑)。当然如果您坚决不调用ExitProcess也行,那么您的应用程序这时将不会根据您的指示而结束,它会一直等到——当它所有的线程完全地关闭时才会善罢甘休结束。


六、一个实例

自己手工实现一个如C运行时入口点代码一样的入口点函数是非常有用的,例如调试器。如下:

EXTERN_C int WINAPI WinMainCRTStartup()
{
    HINSTANCE hInstance = GetModuleHandle(NULL);
    LPSTR lpszCmdLine = GetCommandLine();
    int r = WinMain(hInstance,NULL,lpszCmdLine,SW_SHOWDEFAULT);
    ExitProcess(r);
    return r; // this will never be reached.
}


七、编译器开关

下面的那张表格描述了MSVC++6编译器应该设置的确保成功编译的开关:

开关

动作

说明

/GX

删除

这个开关激活了需要涉及到那些需要展开堆栈操作的函数的C++异常处理

/GZ

删除

这个开关激活了一些高级的C运行时调试特性。当这个特性激活后,链接器将会搜索_chkstk的调用。

/Oi (第一个是大写的字母o而不是罗马数字O)

添加

添加这个开关可确保编译器内在形式的函数激活。

/Zl(大写的Z和小写的L)

添加

通常编译器会嵌入一个“defaultlib”来引用.obj文件内的C运行时,这个开关确保deafultlib不会写入到产生的目标文件(*.obj)内。

 

 

八、链接器开关

如果编译器已正确配置的话,下面的开关是可选的。但是,如果刚好工程中有一个obj文件漏网的话,C运行时的入口点代码可能会被调用。当然,如查你不放心的话,那么,下面的一个或多个开关你可能需要设置:

开关

动作

说明

/nodefaultlib

添加

如果您在编译器开关中已使用了/Zl,这个开关可不需设。正如编译器的开关说明一样,这个开关的意思是忽略缺省库。如果你使用的第三方函数库或是一些旧的obj文件中仍然包含了一个defaultlib,除非你使用下面的开关,否则链接器将会忽略掉你定义的入口点。

/entry:function

添加

如果你希望使用一个非标准的入口点函数名称,那么这个开关你就要使用。如果你需要链接一些第三方的函数库或是目标代码中包含了defaultlib的指示的话,这将是一个不错的想法!否则若给了一半的机会与链接器,只要它能找到它,将会使用C运行时的函数库入口点。

/opt:nowin98

添加

在windows98的平台下,MSVC6链接器将会缺省的为PE文件分配4KB左右的节对齐方式用来优化加载速度的时间。如果激活这个开关,将会对非常小的工程受益,控制PE的大小约在16KB左右。

 

 

九、更多的MSVC6链接器设置

微软的链接器早在6.0版本之前,产生的所有PE镜像的文件标准节对齐都是512字节。这在6.0开始,为了优化98下的加载速度,对齐将改为4KB左右。但是也为了兼容原因,98加载文件也必须支持旧的对齐方式(但是那么做将会牺牲效率),并且,如果你的目标机器是NT的话,你可以使用旧的512字节不会浪费你一丝效率。在代码中嵌入链接器开关的代码行如下:(第二行的意思,在.C的文件中嵌入一个命令到链接器的选项)

// linker options can be embedded directly in .cpp code thus:
#if defined(_MSC_VER) && _MSC_VER >= 1200
#pragma comment(linker, "/OPT:NOWIN98" )
#endif


文笔走到这里,整篇文章终于已宣告翻译完毕!另附上原文链接:http://www.mvps.org/user32/nocrt.html
由于本人英文较差,如有不正之处请加以指正。多谢大家棒场,我会继续为大家献上更多的好文。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值