使用 Visual C++ 6.0 編譯最簡單的 printf("Hello World"),靜態連結下輸出檔約 28 KB,動態連結 MSVCRT.DLL 仍有 16 KB。透過 DUMPBIN /HEADERS 與 .map 檔分析可發現:
MessageBox 包裝等大量「從未呼叫」的函式被連結器拉入。/OPT:WIN98),造成區段間隙浪費。cl /O1 Hello.c /link /OPT:NOWIN98 ; 靜態連結 28KB → 21KB
cl /O1 /MD Hello.c /link /OPT:NOWIN98 ; 動態連結 16KB → 2.5KB
將檔案對齊從 4KB 改回 512 bytes,大幅減少區段填充浪費。
Matt Pietrek 開發的 LIBCTINY.LIB 替代 LIBC(LIBMT).LIB,特點:
errno 等),移除區域對應、堆管理器、例外處理等。malloc/free → HeapAlloc/Free;printf → WriteFile(GetStdHandle);strcpy/strlen 等僅保留常用精簡實作。.CRT$XCU 區段處理缺失等問題。BOOL WINAPI _DllMainCRTStartup(HINSTANCE hInst, DWORD dwReason, LPVOID)
{
if (dwReason == DLL_PROCESS_ATTACH) {
_atexit_init(); // 初始化 atexit 陣列
_initterm(__xc_a, __xc_z); // 呼叫所有全域建構子
}
BOOL ret = DllMain(hInst, dwReason, 0); // 使用者進入點
if (dwReason == DLL_PROCESS_DETACH)
_DoExit(); // 呼叫所有全域解構子
return ret;
}
#pragma comment(linker, "/OPT:NOWIN98") // 強制連結器選項
.CRT$XCU 區段,連結器合併 .CRT$XCA (開頭)、.CRT$XCZ (結尾) 形成指標陣列 __xc_a[] ~ __xc_z[]。_initterm 遍歷陣列呼叫建構子。atexit 登記至靜態陣列,_DoExit 逆序呼叫。int __cdecl printf(const char* fmt, ...)
{
va_list args; va_start(args, fmt);
char buf[1024];
int len = _vsnprintf(buf, sizeof(buf), fmt, args);
va_end(args);
DWORD written;
WriteFile(GetStdHandle(STD_OUTPUT_HANDLE), buf, len, &written, NULL);
return len;
}
LIBCTINY.LIB。/OPT:NOWIN98 (或在原始碼加 #pragma comment(linker, "/OPT:NOWIN98"))。TEST.CPP (含建構子/解構子、strchr 等) 靜態連結從 32KB 降至 3 KB。| 適用 | 不適用 |
|---|---|
| 僅呼叫 Win32 API 的小型 EXE/DLL | 使用 MFC / ATL / STL 大型專案 |
| 需極小體積的安裝程式、引導程式、Shell Extension | 多執行緒需 TLS (如每執行緒 errno) |
| 嵌入式/舊版 Windows 部署 | 依賴 C++ 標準庫完整功能 (iostreams, locale, exception) |
LIBCTINY 展示了「只連結你真正需要的程式碼」的極致。雖然現代網路頻寬寬裕、硬碟極大,但在嵌入式、網路傳輸、啟動速度敏感場合,體積縮減仍具實務價值。原始碼與完整說明可參考 Matt Pietrek 1996/2000 年 MSJ 專欄文章。