前言
微軟技術棧中,目前有一種高深莫測的環境變量叫做DOTNET_EnableWriteXorExecute.如果你去翻看微軟文檔,發現它的解釋非常難懂。但是其實它就做了兩件事情,第一映射了兩塊記憶體區域,第二這兩塊記憶體區域的權限一個為可執行,可寫,可讀(pRX)。另外一個記憶體區域的權限則是可讀,可寫(pRW)。可以參考下前面兩篇文章:絕頂技術:斷點+記憶體映射組合的CLR超強BUG?和CLR托管問題,記憶體+斷點映射(lldb+windbg)
這兩篇文章認為斷點加記憶體映射是一個CLR的BUG,實際上是不正确的。
它的本質是通過DOTNET_EnableWriteXorExecute.預設的值為1,開啟了記憶體映射。當JIT編譯完成之後,通過記憶體映射對函數頭的前八位進行指派,放置一些必要的資訊,比如GCInfo等。GCInfo等資訊就是通過兩個記憶體區域的映射完成函數頭前八位指派的。這種記憶體映射可以避開托管代碼的執行,直接跳到非托管代碼,但是它的問題就在于不能在記憶體映射的範圍内下斷點,否則會報異常或者斷點出錯等情況。
如果你想要執行托管代碼,則可設定
DOTNET_EnableWriteXorExecute=0來實作(這是指沒有被hostfxr宿主的程式,比如corerun小型主機以及Debug CLR Source Code等)。當它等于零之後,則是通過普通的指派方式而非記憶體映射的方式來進行函數頭前八位指派。這樣就不存在斷點+記憶體映射的異常,也可以調試托管代碼了。
官方把它稱之為性能回歸。它有性能上的略微退步,卻提高了安全性。
官方建議用記憶體映射的方法。目前在.Net7之後應該是預設開啟了。
概括
簡單的例子
namespace abc { internal class Program { static void Main(string[] args) { Console.ReadLine(); Console.WriteLine("Hello, World!"); Program pm=new Program(); } } }
1.首先通過lldb來驗證下Linux下面的
DOTNET_EnableWriteXorExecute環境變量值的開啟與否
root@ubuntu:/home/tang/opt/dotnet/debug_clr# export DOTNET_EnableWriteXorExecute=1 root@ubuntu:/home/tang/opt/dotnet/debug_clr# lldb-11 clrrun abc.dll Current symbol store settings: -> Cache: /root/.dotnet/symbolcache -> Server: https://msdl.microsoft.com/download/symbols/ Timeout: 4 RetryCount: 0 (lldb) target create "clrrun" Current executable set to '/home/tang/opt/dotnet/debug_clr/clrrun' (x86_64). (lldb) settings set -- target.run-args "abc.dll" (lldb) bpmd abc.dll Program.Main (lldb) r Process 75045 launched: '/home/tang/opt/dotnet/debug_clr/clrrun' (x86_64) 1 location added to breakpoint 1 JITTED abc!abc.Program.Main(System.String[]) Setting breakpoint: breakpoint set --address 0x00007FFF79004B30 [abc.Program.Main(System.String[])] warning: failed to set breakpoint site at 0x7fff79004b30 for breakpoint 3.1: error: 9 sending the breakpoint request Hello, World! new Program Process 75045 exited with status = 0 (0x00000000)
可以看到如果設定
export DOTNET_EnableWriteXorExecute=1,那麼在托管的Main裡面的斷點是無法斷下來的。
root@ubuntu:/home/tang/opt/dotnet/debug_clr# export DOTNET_EnableWriteXorExecute=0 root@ubuntu:/home/tang/opt/dotnet/debug_clr# lldb-11 clrrun abc.dll Current symbol store settings: -> Cache: /root/.dotnet/symbolcache -> Server: https://msdl.microsoft.com/download/symbols/ Timeout: 4 RetryCount: 0 (lldb) target create "clrrun" Current executable set to '/home/tang/opt/dotnet/debug_clr/clrrun' (x86_64). (lldb) settings set -- target.run-args "abc.dll" (lldb) bpmd abc.dll abc.Program.Main (lldb) r Process 75105 launched: '/home/tang/opt/dotnet/debug_clr/clrrun' (x86_64) 1 location added to breakpoint 1 JITTED abc!abc.Program.Main(System.String[]) Setting breakpoint: breakpoint set --address 0x00007FFF78FF4B30 [abc.Program.Main(System.String[])] Process 75105 stopped * thread #1, name = 'clrrun', stop reason = breakpoint 3.1 frame #0: 0x00007fff78ff4b30 -> 0x7fff78ff4b30: push rbp 0x7fff78ff4b31: sub rsp, 0x20 0x7fff78ff4b35: lea rbp, [rsp + 0x20] 0x7fff78ff4b3a: xor eax, eax
而如果設定
export DOTNET_EnableWriteXorExecute=0,則可以立馬斷下來。
2.原理
它的原理其實非常簡單,就是判斷是否設定了
DOTNET_EnableWriteXorExecute環境變量,如果設定了,則判斷它的值是0或者1,然後按照相應的邏輯來處理。比如1則記憶體映射,0則普通指派。
-> 3165 if (ExecutableAllocator::IsWXORXEnabled()) 3166 { 3167 pCodeHdrRW = (CodeHeader *)new BYTE[*pAllocatedSize]; 3168 } (lldb) Process 75149 stopped * thread #1, name = 'clrrun', stop reason = step over frame #0: 0x00007ffff7827a8c libcoreclr.so`EEJitManager::allocCode(this=0x0000555555603750, pMD=0x00007fff78fbb460, blockSize=376, reserveForJumpStubs=0, flag=CORJIT_ALLOCMEM_DEFAULT_CODE_ALIGN, ppCodeHeader=0x00007fffffffbe30, ppCodeHeaderRW=0x00007fffffffbe38, pAllocatedSize=0x00007fffffffbe40, ppCodeHeap=0x00007fffffffbe50, ppRealHeader=0x00007fffffffbe48, nUnwindInfos=1) at codeman.cpp:3171:26 3168 } 3169 else 3170 { -> 3171 pCodeHdrRW = pCodeHdr; 3172 } 3173 3174 #ifdef USE_INDIRECT_CODEHEADER (lldb) source info Lines found in module `libcoreclr.so [0x00007ffff7827a8c-0x00007ffff7827a93): /home/tang/opt/dotnet/runtime/src/coreclr/vm/codeman.cpp:3171:2
ExecutableAllocator::IsWXORXEnabled()判斷它傳回值為0或者1,如果1則進入括号配置設定需要記憶體映射的源位址(pRW),如果為1,則進入else邏輯直接給函數頭前八位的位址裡面指派,不需要記憶體映射。
3.再來看下hostfxr宿主
這種情況就類似于windbg了,它始終是為零。
0:007> .load C:\Users\Administrator\.dotnet\sos\sos.dll 0:007> !bpmd Program.cs:8 MethodDesc = 00007FFEDBD096C8 Setting breakpoint: bp 00007FFEDBC42959 [ConsoleApp3.Program.Main(System.String[])] Adding pending breakpoints... 0:007> g 0:000> p ConsoleApp3!ConsoleApp3.Program.Main+0x35: 00007ffe`dbc42975 e8c68cba5f call coreclr!JIT_TrialAllocSFastMP_InlineGetThread (00007fff`3b7eb640) 0:000> p ConsoleApp3!ConsoleApp3.Program.Main+0x3a: 00007ffe`dbc4297a 488945f8 mov qword ptr [rbp-8],rax ss:00000063`b097eac8=0000000000000000 0:000> p ConsoleApp3!ConsoleApp3.Program.Main+0x3e: 00007ffe`dbc4297e 488b4df8 mov rcx,qword ptr [rbp-8] ss:00000063`b097eac8=00000245800158d8 0:000> p ConsoleApp3!ConsoleApp3.Program.Main+0x42: 00007ffe`dbc42982 e891c7ffff call 00007ffe`dbc3f118 0:000> p ConsoleApp3!ConsoleApp3.Program.Main+0x47: 00007ffe`dbc42987 90 nop
位址00007ffe`dbc42982就是.Ctor運作的地方,可以看到它完全沒有問題,說明windbg并不是通過記憶體映射來進行函數頭前八位指派的。也就是windbg裡面預設了ExecutableAllocator::IsWXORXEnabled==0。
4.疑問點
hostfxr裡面它是何時把
ExecutableAllocator::IsWXORXEnabled函數裡面的傳回值g_isWXorXEnabled指派為0的?
實際跟蹤corerun小型主機的時候,可以看到
ExecutableAllocator::g_isWXorXEnabled的全局變量初始位0,但是在ExecutableAllocator::StaticInitialize函數裡面被重新指派為了1,也就是使用記憶體映射,這是正常的邏輯。代碼如下:
HRESULT ExecutableAllocator::StaticInitialize(FatalErrorHandler fatalErrorHandler) { g_isWXorXEnabled = CLRConfig::GetConfigValue(CLRConfig::EXTERNAL_EnableWriteXorExecute) != 0; } EXTERNAL_EnableWriteXorExecute宏裡面的defaultvalue==1.也就是把g_isWXorXEnabled設定為了1.
但是在hostfxr裡面則是,g_isWXorXEnabled等于0。
跟蹤發現它雖然跟corerun用的同一個函數
ExecutableAllocator::StaticInitialize指派,但裡面的邏輯似乎完全不一樣。這裡因為符号問題,并沒有具體的邏輯,但是依舊可以看到傳回值g_isWXorXEnabled==0.也就是普通指派,不使用記憶體映射,這也就導緻了windbg無法感覺記憶體映射+斷點的異常bug。
結尾
斷點+記憶體映射,從今年一月份開始遇到這個問題,當時并沒有重視。實際上的問題并沒有解決,而是掩蓋了這個問題。1月份的原文:Net7的預設構造函數.Ctor下斷點出錯續。前幾天又遇到了,這種一而再的問題,于是乎必須要解決了。到今天為止,似乎問題的主旨已然清晰。本篇除了闡述問題的來龍去脈,還要更正前兩篇的一些錯誤觀點。