當前位置: 妍妍網 > 碼農

呼叫.NET9 JIT未匯出函式

2024-03-22碼農

點選上方 藍字 江湖評談 設為關註




前言

非托管動態庫的匯出函式,一般是可以直接載入呼叫的函式。但是如果未匯出的呢?比如,想呼叫.NET9 JIT非托管DLL裏面的一個未匯出(No extern)的函式。如何做呢?本篇看下

例子

一個簡單的範例說明:

//檔DaoChu.cpp#include<stdio.h>#include<Windows.h>intmax1(int a, int b){return a > b ? a : b;}extern"C" __declspec(dllexport) voidABCD(){printf("call ABCD");}intmax2(int a, int b){return a > b ? a : b;}intmain(){printf("%d\n", max1(1, 2));printf("%d\n", max2(1, 2)); getchar();return0;}

以上生成DaoChu.dll非托管。 這裏有三個函式:max1,max2,ABCD。只有ABCD函式標記了( extern "C" __declspec(dllexport)) 匯出。所以透過非托管DLL呼叫的話也只能ABCD才能呼叫得到。

HMODULE h = LoadLibraryExA((LPCSTR)"DaoChu.dll", NULL, 0);pFunc pc = (pFunc)GetProcAddress(h, "ABCD");

有沒有可能呼叫max1,max2函式呢?當然有,可以借助匯出函式的偏移來計算未匯出的函式地址。MSCV編譯器有一個規律,當一個函式呼叫的時候,比如max1函式,幀棧如下:

00007FF7FD1119E5 E8 FB F7 FF FF call max1 (07FF7FD1111E5h)

它這裏的地址07FF7FD1111E5h會跳到一個都是jmp的中轉地址,下面程式碼最後一行是ABCD函式的幀棧中轉,第一行是max1函式幀棧中轉。

00007FF7FD1111E5E9 E6 05 00 00 jmp max1 (07FF7FD1117D0h) 00007FF7FD1111EAE9 F1 49 00 00 jmp __scrt_stub_for_is_c_termination_complete (07FF7FD115BE0h) 00007FF7FD1111EFE9 3C 2C 00 00 jmp _get_startup_argv_mode (07FF7FD113E30h) 00007FF7FD1111F4E9 67 30 00 00 jmp __scrt_initialize_winrt (07FF7FD114260h) 00007FF7FD1111F9E9 22 1D 00 00 jmp __raise_securityfailure (07FF7FD112F20h) 00007FF7FD1111FEE9 2D 32 00 00 jmp _RTC_Initialize (07FF7FD114430h) 00007FF7FD111203E9 14 48 00 00 jmp __stdio_common_vfprintf (07FF7FD115A1Ch) 00007FF7FD111208E9 57 48 00 00 jmp _exit (07FF7FD115A64h) 00007FF7FD11120DE9 1E 1C 00 00 jmp sprintf_s (07FF7FD112E30h) 00007FF7FD111212E9 25 49 00 00 jmp QueryPerformanceCounter (07FF7FD115B3Ch) 00007FF7FD111217E9 08 49 00 00 jmp SetUnhandledExceptionFilter (07FF7FD115B24h) 00007FF7FD11121CE9 5F 0B 00 00 jmp _CRT_RTC_INIT (07FF7FD111D80h) 00007FF7FD111221E9 6A 11 00 00 jmp __scrt_narrow_argv_policy::configure_argv (07FF7FD112390h) 00007FF7FD111226E9 0F 48 00 00 jmp __setusermatherr (07FF7FD115A3Ah) 00007FF7FD11122BE9 60 32 00 00 jmp _RTC_Terminate (07FF7FD114490h) 00007FF7FD111230E9 ED 47 00 00 jmp _CrtDbgReport (07FF7FD115A22h) 00007FF7FD111235E9 26 2D 00 00 jmp __scrt_is_user_matherr_present (07FF7FD113F60h) 00007FF7FD11123AE9 01 31 00 00 jmp __scrt_set_unhandled_exception_filter (07FF7FD114340h) 00007FF7FD11123FE9 B4 47 00 00 jmp __current_exception_context (07FF7FD1159F8h) 00007FF7FD111244E9 75 48 00 00 jmp _register_onexit_function (07FF7FD115ABEh) 00007FF7FD111249E9 62 49 00 00 jmp __scrt_stub_for_acrt_thread_detach (07FF7FD115BB0h) 00007FF7FD11124EE9 71 48 00 00 jmp _execute_onexit_table (07FF7FD115AC4h) 00007FF7FD111253E9 68 49 00 00 jmp __scrt_stub_for_acrt_uninitialize (07FF7FD115BC0h) 00007FF7FD111258E9 15 49 00 00 jmp GetProcessHeap (07FF7FD115B72h) 00007FF7FD11125DE9 4E 06 00 00 jmp ABCD (07FF7FD1118B0h)

這個幀棧中轉在MSVC裏面是固定的,我們只需要知道匯出函式ABCD的幀棧中轉地址距離max1函式幀棧中轉地址的偏移,即可呼叫max1函式。很明顯本例是0x78. 把它改 造成函式指標:

//函式指標typedefint(*compare)(int a, int b);typedefvoid(*pFunc)();extern"C"intZW(int u1, int u2)//實際程式碼 HMODULE h = LoadLibraryExA((LPCSTR)"DaoChu.dll", NULL, 0); pFunc pc = (pFunc)GetProcAddress(h, "ABCD");char* p = (char*)pc; p = p - 0x78; compare ce = (compare)p; ZW(2,3); ce(12);

原理呢,也非常簡單。先獲取到匯出函式的函式地址,然後透過這個地址計算出未匯出函式的地址。因為幀棧中轉呼叫,ZW函式返回有無問題?實際上根據本例,棧的擴充套件(rsp-0x20)在被呼叫的函式裏面,所以這裏是沒有問題的。

但是其它問題呢?比如幀棧中轉實際上是已經傳參過了,為實際使用參數的中間過程。獲取到的這個地址,參數是已經固定住了。為了讓這個參數用到我們自己的傳參,所這裏還需,嵌入組譯程式碼,即是上面的ZW呼叫:

//檔x64asm.asm.CODE ;ZWPROC ;addrcx,rcx;movrdx,rdx;ret;ZW ENDP ;END;

右擊x64asm.asm-】內容-】從生成計畫中排除選擇否

右擊x64asm.asm-】內容-】項型別-】Microsoft Macro Assembler .

JIT

JIT一般的在如下路徑:

C:\ProgramFiles\dotnet\shared\Microsoft.NETCore.App\8.0.2\clrjit.dll

clrjit.dll裏麵包含了所有JIT操控,C++程式碼高達幾十萬行。但是它只匯出區區五個函式

getJitgetLikely classesgetLikelyMethodsjitBuildStringjitStartup

這裏面有成百上千的函式,如果要呼叫其它的未匯出如何做呢?JIT的C++裏面引入了Cmake程式碼,所以它的生成跟MSVC又有不同之處(這裏需要著重註意MSVC與Cmake的不同,但是原理相通),JIT沒有幀棧中轉。根據上面的例子的原理,只需要知道一個匯出函式的地址,以及在實際執行的時候距離另外一個函式偏移。這裏以匯出函式jitStartup計算未匯出函式dumpILRange的函式地址。

//jitStartup函式指標聲明和原型typedefvoid(*jitStartup)(void* jitHost);extern"C"DLLEXPORT voidjitStartup(ICorJitHost* jitHost){if (g_jitInitialized) {if (jitHost != g_jitHost) { JitConfig.destroy(g_jitHost); JitConfig.initialize(jitHost); g_jitHost = jitHost; }return; }#ifdef HOST_UNIXint err = PAL_InitializeDLL();if (err != 0) {return; }#endif g_jitHost = jitHost; assert(!JitConfig.isInitialized()); JitConfig.initialize(jitHost); Compiler::compStartup(); g_jitInitialized = true;}//dumpILRange函式指標聲明和原型typedefvoid(*dumpILRange)(const BYTE* const codeAddr, unsigned codeSize);voiddumpILRange(const BYTE* const codeAddr, unsigned codeSize)// in bytes{for (IL_OFFSET offs = 0; offs < codeSize;) {char prefix[100]; sprintf_s(prefix, ArrLen(prefix), "IL_x ", offs);unsigned codeBytesDumped = dumpSingleInstr(codeAddr, offs, prefix); offs += codeBytesDumped; }}//實際程式碼HMODULE h1 = LoadLibraryExA((LPCSTR)"C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.2\clrjit.dll", NULL, 0);char* p1 = (char*)(jitStartup)GetProcAddress(h1, "jitStartup");p1 = p1 + 0x2B6380;dumpILRange dumpil = (dumpILRange)p1;

但是實際上我們可能需要做更多,比如JIT環境變量的事先設定,否則即使獲取到函式地址,也依然會有報錯的情況。也就是說盡量滿足未匯出函式的函式裏面所有呼叫情況的可能,才可能順利呼叫未匯出函式。

往期精彩