Building Clang with openSUSE Tumbleweed + Virtual Box 6.0

圖片
很久沒有編譯怪物級的專案來折磨 CPU 和硬碟了。 基本上照著 Clang 文件 https://clang.llvm.org/get_started.html 就不會在 compilation 上運到奇怪問題,只有幾個細節需要調校一下: Virtual Box 的 storage controller 要開啟 Host I/O Cache ,不然很容易在大量 I/O 時遇到 Virtual Box 沒有反應,然後出現 Disk I/O 讀取錯誤: 00:39:53.953457 Console: VM runtime error: fatal=false, errorID=BLKCACHE_IOERR message="The I/O cache encountered an error while updating data in medium "ahci-0-0" (rc=VERR_IO_NOT_READY). Make sure there is enough free space on the disk and that the disk is working properly. Operation can be resumed afterwards" 不要編譯 libcxx ,Clang 教學文件雖然提到這是 optional ,但實際跑時會在 linker 階段炸出很多 undefined reference  (乍看下感覺是 linker 只使用了 C runtime 而漏了 C++ runtime) 使用 gold 作為 linker 而不是 gnu ld ,因為 ld 在工作階段會耗用過多 memory ,以我的實驗為例:即使已經設定了 8GB Ram 和 8GB swap 空間,仍然會遇到 ld 耗盡 memory 的問題。 [ 63%] Linking CXX shared library ../../lib/libLTO.so /usr/bin/ld: failed to set dynamic section sizes: Memory exhausted Let's Build 先 checkout llvm svn co http://llvm.org/svn...

Hidden Mismatched Calling Convention

一個使用錯誤 calling convention 卻沒有 crash 的故事⋯

使用錯的 calling convention 進行 function call 往往會在 callee function return 後讓 caller 的 stack pointer 指向錯誤的位址,此時 caller 再透過 stack pointer 去讀寫資料就會產生錯誤;可能讀到錯誤值、寫到錯誤位置,若是使用的資料型別是 pointer 時,更是容易使用到非法位址,引發 access violation 。從高階語言來看,這類的資料讀取往往透過 local variables 使用, return statement 去產生。

但如果在 callee 返回後,caller 都沒有透過 stack pointer 去存取資料,是不是可以神不知、鬼不覺地隱藏這個錯誤呢?正甚者,當 caller function 返回時,stack pointer 還能被還原到正確值呢?

回顧一下在 x86 Windows 系統上常見的 calling convention ,在沒有進行 FPO 最佳化時大都帶有 function prologue 和 epilogue ,像是下面 callModuleFunc() 為例,它的 prologue 和 epilogue 如下:
// main.exe 使用 module.dll 的程式碼
0:000> uf main!callModuleFunc
main!callModuleFunc [...\code\main\mainentry.cpp @ 12]:
; prologue
12 001f1040 55 push ebp
12 001f1041 8bec mov ebp,esp
12 001f1043 83ec08 sub esp,8
; ...
main!callModuleFunc+0x72 [...\code\main\mainentry.cpp @ 30]:
; epillgue
30 001f10b2 8be5 mov esp,ebp
30 001f10b4 5d pop ebp
30 001f10b5 c3 ret
prologue 將 stack pointer 暫存到 ebp (L.6),而 epilogue 會把 ebp 寫回 esp (L.11),透過暫放在 ebp 的方式可以達到 esp 平衡。同時也解答了我們剛剛的問題:在特定條件下,esp 可以透過 ebp 還原,以此讓 caller 有機會從 mismatched calling convention 問題中全身而退的。

那實務我們怎麼從上 C++ 實現這樣的場景呢?回想一下剛剛的描述,我們需要做到:
  1. esp 暫存在某處,並在 mismatched calling convention 發生後有機會從該暫存處讀回
  2. 發生 mismatched calling convention 後不再依賴 esp 去讀取資料
有了這個想法,一個簡單的 sample code 就可以架構出來了:
  1. main.exe 動態載入 module.dll ,並且從中獲取 moduleFunc() 的位置
  2. moduleFunc() 是 __stdcall ,但在 main.exe 中我們故意以 __cdecl 方式呼叫
  3. main.exe 透過 callModuleFunc() 去完成上面的動作,而且故意將 FPO 關閉
  4. mismatched calling convention 發生後不進行 local variables 讀取,避免可能的 esp 使用
  5. 試試看一個額外的 bonus: 在 mismatched calling convention 發生後進行一個正確的 FreeLibrary()
實現上述需求的程式碼如下:
#pragma optimize( "", off )
void callModuleFunc( int v )
{
HMODULE hModule{ ::LoadLibraryW( L"module.dll" ) };
if ( nullptr == hModule ) {
printf( "LoadLibrary() failed: 0x%08x\n", ::GetLastError() );
return;
}
using ModuleFunc = int (*)( int ); // __cdecl: the default calling convention
auto fpModuleFunc = reinterpret_cast(
::GetProcAddress( hModule, "moduleFunc" ) );
if ( nullptr == fpModuleFunc ) {
printf( "GetProcAddress() failed: 0x%08x\n", ::GetLastError() );
}
else {
fpModuleFunc( v ); // a mismatched calling convention
}
::FreeLibrary( hModule ); // our bonus
}
#pragma optimize( "", on )
開始在 Visual C++ 的實踐看看,但很快就發現只有在 release build 中才能逃離 runtime error 魔掌,在 debug build 則會有如下的錯誤視窗彈出。

錯誤內容很好理解,就是 esp 檢查出錯。Visual C++ debug build 中會開啟許多安全檢查,其中 Run-Time Error Checks 的 stack frame 預設開啟 ,反組譯一下,可以看到 L.9 處 VC 插入的代碼 RTC_CheckEsp() 會對 esp 進行檢查,該行在 mov esp,ebp 前,也就因此沒有機會將 ebp 寫回 esp 了。
// Visual C++ 的 stack frame check
0:000> uf main!callModuleFunc
main!callModuleFunc+0xc0 [...\code\main\mainentry.cpp @ 30]:
30 00dd17d0 5f pop edi
30 00dd17d1 5e pop esi
30 00dd17d2 5b pop ebx
30 00dd17d3 81c4d8000000 add esp,0D8h
30 00dd17d9 3bec cmp ebp,esp
30 00dd17db e84cf9ffff call main!ILT+295(__RTC_CheckEsp) (00dd112c)
30 00dd17e0 8be5 mov esp,ebp
30 00dd17e2 5d pop ebp
30 00dd17e3 c3 ret
不要勾選與 stack frame 相關的選項便可使用 Debug build 驗證這次的實驗。

完整的代碼可以在 github 上找到,git clone 後請往 CrashTonightOh/HiddenMismatchedCallingConvention 資料夾走去,不要忘了透過 debugger 觀察一下 esp 。
git clone https://github.com/WCChou/CrashTonightOh.git


留言

這個網誌中的熱門文章

Building Clang with openSUSE Tumbleweed + Virtual Box 6.0

Missing Process Argument

Syntax Hightlight