前言
微软技术栈中,目前有一种高深莫测的环境变量叫做 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
之后应该是默认开启了。
概括
1、简单的例子
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 下断点出错续》
。前几天又遇到了,这种一而再的问题,于是乎必须要解决了。到今天为止,似乎问题的主旨已然清晰。本篇除了阐述问题的来龙去脉,还要更正前两篇的一些错误观点。
.Net7
的默认构造函数.Ctor
下断点出错续,https://mp.weixin.qq.com/s/V-oGmng2OzrSIhLkOvef2w
转载声明: