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

本文涉及的产品
Serverless 应用引擎 SAE,800核*时 1600GiB*时
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
性能测试 PTS,5000VUM额度
简介: 【内存映射+断点】,从今年一月份开始遇到这个问题,当时并没有重视。实际上的问题并没有解决,而是掩盖了这个问题。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 下断点出错续》。前几天又遇到了,这种一而再的问题,于是乎必须要解决了。到今天为止,似乎问题的主旨已然清晰。本篇除了阐述问题的来龙去脉,还要更正前两篇的一些错误观点。

转载声明:

目录
相关文章
|
13天前
|
存储 缓存 Linux
用户态内存映射
【9月更文挑战第20天】内存映射不仅包括物理与虚拟内存间的映射,还涉及将文件内容映射至虚拟内存,使得访问内存即可获取文件数据。mmap 系统调用支持将文件或匿名内存映射到进程的虚拟内存空间,通过多级页表机制实现高效地址转换,并利用 TLB 加速映射过程。TLB 作为页表缓存,存储频繁访问的页表项,显著提升了地址转换速度。
|
3天前
|
存储 安全 Linux
将文件映射到内存,像数组一样访问
将文件映射到内存,像数组一样访问
10 0
|
1月前
|
消息中间件 Linux 容器
共享内存的创建和映射过程
【9月更文挑战第1天】消息队列、共享内存及信号量在使用前需生成key并获取唯一ID,均通过`xxxget`函数实现。
|
4月前
|
监控 Linux
深入了解Linux的pmap命令:进程内存映射的利器
`pmap`是Linux下分析进程内存映射的工具,显示内存区域、权限、大小等信息。通过`/proc/[pid]/maps`获取数据,特点包括详细、实时和灵活。参数如`-x`显示扩展信息,`-d`显示设备。示例:`pmap -x 1234`查看进程1234的映射。注意权限、实时性和准确性。结合其他工具定期监控,排查内存问题。
|
5月前
内存映射mmap拓展
内存映射mmap拓展
|
5月前
内存映射实现无血缘关系进程间通信
内存映射实现无血缘关系进程间通信
|
5月前
内存映射实现父子进程通信
内存映射实现父子进程通信
|
5月前
|
存储 算法 内存技术
深入理解操作系统内存管理:从虚拟内存到物理内存的映射
【4月更文挑战第30天】 在现代操作系统中,内存管理是一个复杂而关键的功能。它不仅确保了系统资源的有效利用,还为每个运行的程序提供了独立的地址空间,保障了程序之间的隔离性和安全性。本文将探讨操作系统如何通过分页机制和虚拟内存技术实现内存的抽象化,以及这些技术是如何影响应用程序性能的。我们将详细解析虚拟地址到物理地址的转换过程,并讨论操作系统在此过程中扮演的角色。文章的目的是为读者提供一个清晰的框架,以便更好地理解内存管理的工作原理及其对系统稳定性和效率的影响。
|
5月前
|
存储 大数据 Python
NumPy中的内存映射文件处理技巧
【4月更文挑战第17天】NumPy的`memmap`模块用于处理大数据,通过内存映射文件技术实现对磁盘文件的高效访问,无需一次性加载到内存。创建内存映射数组使用`numpy.memmap`,并可像操作普通数组一样读写。最佳实践包括选择合适数据类型、规划文件大小和形状、减少磁盘操作、确保文件安全性和一致性及管理内存使用。内存映射是处理超出内存数据集的有效策略。
|
5月前
|
人工智能 缓存 算法
深入理解操作系统内存管理:从虚拟内存到物理内存的映射
【4月更文挑战第8天】 在现代操作系统中,内存管理是核心功能之一,它负责协调和管理计算机的内存资源,确保系统稳定高效地运行。本文深入探讨了操作系统内存管理的关键概念——虚拟内存和物理内存的映射机制。通过剖析分页系统、分段机制和虚拟内存地址转换过程,文章旨在为读者提供一个清晰的理解框架,同时讨论了内存管理的优化技术及其对系统性能的影响。此外,还简要介绍了内存碎片问题以及垃圾回收机制的重要性,并展望了未来内存管理技术的发展趋势。