前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >断点+内存映射终章(CLR问题)

断点+内存映射终章(CLR问题)

作者头像
江湖评谈
发布2023-08-31 21:11:09
1470
发布2023-08-31 21:11:09
举报
文章被收录于专栏:天下风云天下风云

前言

微软技术栈中,目前有一种高深莫测的环境变量叫做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之后应该是默认开启了。

概括

简单的例子

代码语言:javascript
复制
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环境变量值的开启与否

代码语言:javascript
复制
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里面的断点是无法断下来的。

代码语言:javascript
复制
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则普通赋值。

代码语言:javascript
复制
-> 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了,它始终是为零。

代码语言:javascript
复制
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,也就是使用内存映射,这是正常的逻辑。代码如下:

代码语言:javascript
复制
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下断点出错续。前几天又遇到了,这种一而再的问题,于是乎必须要解决了。到今天为止,似乎问题的主旨已然清晰。本篇除了阐述问题的来龙去脉,还要更正前两篇的一些错误观点。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2023-06-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 江湖评谈 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 结尾
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档