1. 程式人生 > >使用VC++的編譯器建立最小的映象檔案(DLL/EXE)[譯]

使用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執行時環境下中使用需要初始化的全域性物件,否則應用程式可能會認為它們是沒有初始化的(例如全域性物件的建構函式)。

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

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

lmemcmp

lmemcpy

lmemset

lstrcmp

lstrcpy

lstrlen

lstrcat

lstrset

四、必需的函式

毫無疑問,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

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