拨开乌云见月明:断点+内存映射终章(CLR 问题)

本文涉及的产品
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
可观测可视化 Grafana 版,10个用户账号 1个月
简介: 【内存映射+断点】,从今年一月份开始遇到这个问题,当时并没有重视。实际上的问题并没有解决,而是掩盖了这个问题。1月份的原文:《Net7的默认构造函数.Ctor下断点出错续》。前几天又遇到了,这种一而再的问题,于是乎必须要解决了。到今天为止,似乎问题的主旨已然清晰。本篇除了阐述问题的来龙去脉,还要更正前两篇的一些错误观点。

前言

微软技术栈中,目前有一种高深莫测的环境变量叫做 DOTNET_EnableWriteXorExecute。如果你去翻看微软文档,发现它的解释非常难懂。但是其实它就做了两件事情:

  1. 第一映射了两块内存区域,
  2. 第二这两块内存区域的权限一个为可执行,可写,可读(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 下断点出错续》。前几天又遇到了,这种一而再的问题,于是乎必须要解决了。到今天为止,似乎问题的主旨已然清晰。本篇除了阐述问题的来龙去脉,还要更正前两篇的一些错误观点。

转载声明:

目录
相关文章
|
4月前
|
Unix 程序员 Linux
【OSTEP】动态内存开辟 | 内存API常见错误 | UNIX: brk/sbrk 系统调用 | mmap创建匿名映射区域 | mmap创建以文件为基础的映射区域
【OSTEP】动态内存开辟 | 内存API常见错误 | UNIX: brk/sbrk 系统调用 | mmap创建匿名映射区域 | mmap创建以文件为基础的映射区域
118 0
|
3月前
|
消息中间件 存储 缓存
Linux内存映射mmap
Linux内存映射mmap
34 0
|
3月前
|
开发框架 .NET API
绝顶技术:断点+内存映射组合的 CLR 超强 BUG?
你见过【断点+内存映射】制造了一个另类隐藏极深,强悍的 BUG 吗?这是一个虚拟机 CLR 的 BUG。不同于之前所遇见的 BUG 这次费时最多,但是问题已然清晰。
183 0
|
4月前
|
存储 缓存 NoSQL
轻松突破文件IO瓶颈:内存映射mmap技术
轻松突破文件IO瓶颈:内存映射mmap技术
|
5月前
|
测试技术 C#
C#不通过byte[],直接对内存映射文件复制内存
C#不通过byte[],直接对内存映射文件复制内存
|
6月前
|
存储 安全 API
2.1 PE结构:文件映射进内存
PE结构是`Windows`系统下最常用的可执行文件格式,理解PE文件格式不仅可以理解操作系统的加载流程,还可以更好的理解操作系统对进程和内存相关的管理知识,在任何一款操作系统中,可执行程序在被装入内存之前都是以文件的形式存放在磁盘中的,在早期DOS操作系统中,是以COM文件的格式存储的,该文件格式限制了只能使用代码段,堆栈寻址也被限制在了64KB的段中,由于PC芯片的快速发展这种文件格式极大的制约了软件的发展。
|
8月前
|
Unix Linux
Linux系统应用编程---进程间通信(二)【内存共享映射mmap】
Linux系统应用编程---进程间通信(二)【内存共享映射mmap】
70 0
|
安全 Go 索引
Go-映射类型详解(遍历、增删改查、判断相等、内存等)
Go-映射类型详解(遍历、增删改查、判断相等、内存等)
165 0
Go-映射类型详解(遍历、增删改查、判断相等、内存等)
|
缓存 Java 大数据
Java 中的内存映射Mmap
Java 中原生读写方式大概可以被分为三种:普通 IO,FileChannel(文件通道),mmap(内存映射)。区分他们也很简单,例如 FileWriter,FileReader 存在于 java.io 中,他们属于普通 IO;FileChannel 存在于 java.nio 中,也是 Java 最常用的文件操作类;而今天的主角 mmap,则是由 FileChannel 调用 map 方法衍生出来的一种特殊读写文件的方式,被称之为内存映射
439 0
Java 中的内存映射Mmap
|
Java Linux
对于 Java MMAP,如何查看文件映射脏页,如何统计MMAP的内存大小?
对于 Java MMAP,如何查看文件映射脏页,如何统计MMAP的内存大小?

相关产品

  • 云消息队列 MQ
  • 微服务引擎
  • 云消息队列 Kafka 版