文章目錄
- 構造超小程序
- 1 編譯器-大小優化
- 2 編譯器-移除 C++ 異常
- 3 鏈接器-移除所有依賴庫
- 4 移除所有函數依賴
- _RTC_InitBase() _RTC_Shutdown()
- __security_cookie __security_check_cookie()
- __chkstk()
- 5 鏈接器-移除清單文件
- 6 鏈接器-移除調試信息
- 7 鏈接器-關閉隨機基址
- 8 移除異常目錄
- 9 小結
- 附錄
- 附錄1 超小 Hello world 程序
構造超小程序
為了更方便查看編譯結果的大小, 可以在 項目屬性頁>配置屬性>生成事件>生成后事件>命令行 添加
powershell -Command "Write-Output 目標大小:%24((Get-Item '$(TargetPath)').Length)"
鏈接器警告 LINK : 已指定 /LTCG,但不需要生成代碼;從鏈接命令行中移除 /LTCG 以提高鏈接器性能
可以到 項目屬性頁>配置屬性>鏈接器>優化>鏈接時間代碼生成 切換 默認配置 來關閉警告
從一個打印 “Hello world!” 的程序開始
#include <print>int main() {std::println("Hello world!");
}
Release 下構建結果大小: 18432B
1 編譯器-大小優化
- 項目屬性頁>配置屬性>C/C++>優化>優化 選 最大優化(優選大小) (/O1)
- 項目屬性頁>配置屬性>C/C++>優化>優選大小或速度 選 代碼大小優先 (/Os)
2 編譯器-移除 C++ 異常
通知編譯器禁用 C++ 異常
- 項目屬性頁>配置屬性>C/C++>代碼生成>啟用C++異常 選 否 (移除 /EH)
通知 C/C++ 庫不使用 C++ 異常
- 項目屬性頁>配置屬性>C/C++>預處理器>預處理器定義 添加 _HAS_EXCEPTIONS=0
3 鏈接器-移除所有依賴庫
- 項目屬性頁>配置屬性>鏈接器>輸入>附加依賴項 清空, 手動輸入下面要依賴的 kernel32.dll
- 項目屬性頁>配置屬性>鏈接器>輸入>忽略所有默認庫 選 是 (/NODEFAULTLIB)
只在代碼里用 pragma 添加
/NODEFAULTLIB
并不夠, 默認情況下新項目會通過附加依賴項直接指名鏈接庫, 這些庫不是通過選項/DEFAULTLIB
附加的, 用/NODEFAULTLIB
不能消除依賴
此時鏈接會報錯, 下面來解決鏈接錯誤
4 移除所有函數依賴
std::println() 函數依賴 ucrtbase.dll 中的函數, CRT 庫相關代碼和依賴比較龐大
- 改用 Windows API WriteConsole 函數 來輸出
所用到的函數可以依賴 kernel32.dll
#include <Windows.h>int __stdcall mainCRTStartup(void* teb) {HANDLE output = GetStdHandle(STD_OUTPUT_HANDLE);WriteConsoleA(output, "Hello world!\n", 13, NULL, NULL);return 0;
}
現在 Release 大小: 3584B
用 Denpendencies 可以看到導入符號列表現在變得非常干凈
_RTC_InitBase() _RTC_Shutdown()
Debug 下默認會開啟基本運行時檢查, 引入 _RTC_InitBase() 和 _RTC_Shutdown() 兩個函數依賴
- 項目屬性頁>配置屬性>C/C++>代碼生成>基本運行時檢查 選 默認值 (移除 /RTC)
通常這兩個函數隨 msvcrt.lib 鏈接進入程序
__security_cookie __security_check_cookie()
部分函數尾部會被插入 cookie 檢查函數, 引入 __security_check_cookie() 函數和 __security_cookie 變量依賴
- 項目屬性頁>配置屬性>C/C++>代碼生成>安全檢查 選 禁用安全檢查 (/GS-)
通常這兩個函數和變量隨 msvcrt.lib 鏈接進入程序, 其中 __security_cookie 定義于 gs_cookie.c 中
__chkstk()
當棧空間占用可能超過 8KB 時(包括局部變量和 _alloca() 調用), 會引入 __chkstk() 函數依賴, 用于提交棧空間
屬性頁中沒有相關配置開關, 需要手動填寫選項來控制這個棧空間大小閾值
- 項目屬性頁>配置屬性>C/C++代碼生成>命令行 填 /GsN 其中N是足夠大的值
通常該函數在 kernelbase.dll 中導出
5 鏈接器-移除清單文件
清單文件用于聲明系統本程序在啟動時請求的資源, 包括請求管理員權限, WIndows版本兼容性, 高 DPI 聲明, 視覺主題等, 但現在不需要
- 項目屬性頁>配置屬性>鏈接器>清單文件>生成清單 選 否 (/MANIFEST:NO)
.rsrc 節 將被移除
6 鏈接器-移除調試信息
調試信息用于幫助編譯器定位每段機器碼在源碼文件中的位置, 移除將導致程序無法在源碼中設置斷點
- 項目屬性頁>配置屬性>鏈接器>調試>生成調試信息 選 否 (移除 /DEBUG)
用 /NOCOFFGRPINFO 移除調試目錄
- 項目屬性頁>配置屬性>鏈接器>命令行 添加 /NOCOFFGRPINFO
7 鏈接器-關閉隨機基址
一些防御技術依賴于隨機基址, 關閉后可能導致程序更容易被攻擊, 不要在生產環境關閉隨機基址
關閉隨機基址使得程序默認加載到 0x140000000 處, 可用 /BASE 改變默認基址
- 項目屬性頁>配置屬性>鏈接器>高級>固定基址 選 是 (/FIXED)
- 項目屬性頁>配置屬性>鏈接器>高級>隨機基址 選 否 (/DYNAMICBASE:NO)
Debug 下的 .reloc 節 將被移除, 而 Release 下 .reloc 節本身就被優化合并了
8 移除異常目錄
異常目錄即 .pdata 節, 可指定當程序跑在某個函數崩潰后,有相對應的異常處理函數可供調用
移除并不會影響程序的正常運行
這篇 Stackoverflow 的回答指出異常目錄是強制生成的, 但可以用 CFF Explorer 手動移除異常目錄
9 小結
至此我們得到了一個徹底剝離所有基礎設施的開發環境
程序大小從 18KB 縮小到 2KB 左右
想問更小的程序? 有的,兄弟😆
本文附錄 1 給出一個超小程序, 在只使用 MSVC 工具并且不使用十六進制編輯器的前提下做到了 480B 的大小
附錄
附錄1 超小 Hello world 程序
C/C++ 生成的代碼太長了, 用匯編吧
code
mainCRTStartup proc ; rcx = PEB; rax = mainCRTStartup; r10, rdx, r8, r9 填函數 1~4 參數; rsp+28h ~ rsp+50h 填函數 5~9 參數mov byte ptr [rsp+38h],14 ; rsp+38h = Length = 14mov ax,0008hmov qword ptr [rsp+30h],rax ; rsp+30h = Buffer = "Hello world\n" 覆蓋返回地址mov dword ptr [rsp+28h],eax ; rsp+28h = IoStatusBlock = Buffermov r10,qword ptr [rcx+20h]mov r10,qword ptr [r10+28h] ; r10 = FileHandle = Peb->ProcessParameter->StandardOutputxor edx,edx ; rdx = Event = NULLsyscall ; ax = NtWriteFile = 8ret
mainCRTStartup endp
end
用 /SECTION 先申請一個具有讀, 寫, 執行的全能節
然后用 /MERGE 將所有節包括代碼節和數據節合并
/SECTION:.all,ERW /MERGE:.text=.all /MERGE:.data=.all /MERGE:.rdata=.all
用 /ALIGN 調整節的對齊大小, 默認值 512B 會導致節的尾部留下大量空白, 最小可設為 16 (只有 1 個節時才能非 512B 對齊加載)
/ALIGN:16
用 /BASE 選項設置基址到 0x80000000 用 32 位地址, 方便使用 32 指令節約代碼大小
/BASE:0x80000000
新建一個 stub.txt 做 DOS 存根程序, 用 /STUB 使 stub.txt 替換默認的 DOS 頭
/STUB:stub.txt
stub.txt 輸入以下內容, 順便在這里存放要輸出的字符串
MZ234567Hello world!
012345678901234567890123456789012345678901
NtWriteFile 的 服務號 為 8, 將字符串設置從偏移 8 開始, 方便復用 rax 寄存器
文件以 MZ 起頭, 用多余字符填充到剛好 64B 大小來滿足鏈接器對存根程序的要求
程序大小 480B
用十六進制編輯器修改 PE 頭還可以讓程序更小, 懶得折騰了🐳
程序的PE頭其實大部分字段都沒有實際功能, 將程序的幾個頭部進行重疊可以得到更小的程序
Tiny PE 詳細討論了最小程序的構造方法, 得到了 133B 的程序, 文章還提到 97B 的程序, 但鏈接失效
只是現在 Windows 10 上加載器允許的最小程序大小是 268B
TinyPE on Win10: 268B 消息彈窗
runcalc.asm: 268B 啟動附件計算器
smallEXE 收集了一些超小的程序
微軟收錄的文章 里也有關于最小程序的討論
snake-qr: 2953B 貪吃蛇