使用VC++的編譯器創建最小的鏡像文件(DLL/EXE)[譯]

一、序

本文通過描述一些方法來告訴你如何打造一個最小的鏡像文件(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++模塊可能是必需的newdelete_purecall操作。與此同時,也要爲程序提供一個入口函數(EntryPoint)。什麼是入口函數?入口函數就是操作系統在加載進程後,第一個執行您程序代碼的入口點。在我們還沒有去掉C運行時的時候,那個操作是由它來自動定位我們提供的main(console)WinMainwindows)的;

3)  爲了生成不依賴於C運行時環境的目標文件,您的一些編譯器的開關可能需要改變;

4)  爲了防止鏈接器把C運行時的函數庫包含進來,您可能需要改變鏈接器的一些設置;

5)  注意:記住C運行時的啓動代碼主要負責初始化全局對象。不要在已脫離C運行時環境下中使用需要初始化的全局對象,否則應用程序可能會認爲它們是沒有初始化的(例如全局對象的構造函數)。

三、一些可繼續使用的函數

下列以編譯器內在形式存在的函數是可用的。注意儘管這些函數是以內在形式存在的,也可能不是像那些庫函數一樣是最優化的。這意味着您可編寫更高效的替代方案。

l         memcmp

l         memcpy

l         memset

l         strcmp

l         strcpy

l         strlen

l         strcat

l         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函數ExitProcessMASM32FANS們會心一笑)。當然如果您堅決不調用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

由於本人英文較差,如有不正之處請加以指正。多謝大家棒場,我會繼續爲大家獻上更多的好文。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章