文章探討組合語言中如何實現真正的位置無關程式碼 (Position Independent Code, PIC)。一般資料存取使用標號會被編譯為絕對位址,導致程式碼無法任意搬移。作者提出利用 CALL 指令將 EIP 壓入堆疊,再 POP 取得執行期位址,與編譯期位址相減得修正值,後續存取資料時加上此修正值,達到位置無關效果。
_asm{
DATA_LABEL:
_emit 0x87; _emit 0xa0; _emit 0x49; _emit 0x90
...
mov ebx, dword ptr [DATA_LABEL] ; 編譯期固定為絕對位址
}
若程式碼被載入不同基址,DATA_LABEL 絕對位址失效。高級語言函數雖也有固定位址,但由載入器/動態連結器處理重定位;組合語言若想做到像 Shellcode 一樣「複製即執行」,需自行解決。
_asm{
call FUNC_START
FUNC_START:
pop ebx
sub ebx, offset FUNC_START
mov [ebp-xx], ebx ; 保存修正值 Delta
...
DATA_LABEL:
_emit ...
...
mov eax, [ebp-xx] ; 取得 Delta
mov ebx, dword ptr [DATA_LABEL+eax] ; 相對定址
}
CALL FUNC_START:將**下一條指令位址** (即 FUNC_START: 標號處 POP EBX 的位址) 壓入堆疊。POP EBX:彈出該位址到 EBX,此刻 EBX = 執行期 FUNC_START 位址。SUB EBX, OFFSET FUNC_START:OFFSET FUNC_START 為編譯期位址 (組譯器常數)。相減得 Delta = 執行期基址 - 編譯期基址。[EBP-xx]) 供後續使用。[DATA_LABEL + Delta] = 執行期真實位址。RIP 相對定址更簡單,但此法仍可工作)。MOV RAX, [RIP+DATA_LABEL] 硬體直接支援,無需 CALL-POP。PUSH/POP)。CALL $+5 / POP REG 序列。CALL-POP 技巧是組合語言實現位置無關程式碼的經典核心手法,揭示了 指令指標 (EIP/RIP) 只能透過控制轉移指令間接讀取 這一 x86 架構基本事實。理解此技巧不僅能編寫可任意搬移的組合語言模組,更是分析 Shellcode、惡意程式碼解殼、引導載入器、動態二進制插桩等進階領域的必修基礎。現代編譯器 (GCC/MSVC) 產生 PIC 時亦使用類似原理 (x86 用 CALL/POP 或 GOT,x64 用 RIP-relative),可見其普世價值。