记一次 .NET某数字化协同管理系统 内存暴涨分析
一:背景
1. 讲故事
高级调试训练营里的一位朋友找到我,说他们跑在linux上的.NET程序出现了内存泄露的情况,上windbg观察发现内存都是IMAGE给吃掉了,那些image都标记了 doublemapper__deleted_ 字样,问我为啥会这样?说实话作为我们这些调试者非常喜欢和这样的人打交道,毕竟沟通起来顺畅,也特别能激发对方的探索欲,这也是训练营给予的一种魅力吧。
二:内存暴涨分析
1. 为什么会暴涨
看过我这个系列的朋友都知道观察内存用 !address -summary 命令,但这个命令是为 windows 打造的,所以在 linux 上行不通,为此sos提供了一个专门的命令 !maddress 来替代,接下来使用 !maddress -orderBySize 观察下内存分布情况。
0:000> !maddress -orderBySize
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Memory Kind | StartAddr | EndAddr-1 | Size | Type | State | Protect | Image |
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Image | 7f4000000000 | 7f4007ff6000 | 127.96mb | MEM_IMAGE | MEM_COMMIT | PAGE_READWRITE | doublemapper__deleted_ |
| Image | 7f3fc4000000 | 7f3fcbff5000 | 127.96mb | MEM_IMAGE | MEM_COMMIT | PAGE_READWRITE | doublemapper__deleted_ |
| Image | 7f404c021000 | 7f4051b4c000 | 91.17mb | MEM_IMAGE | MEM_UNKNOWN | PAGE_UNKNOWN | doublemapper__deleted_ |
| Image | 7f3fae82e000 | 7f3fb4000000 | 87.82mb | MEM_IMAGE | MEM_COMMIT | PAGE_EXECUTE_READ | doublemapper__deleted_ |
| Image | 7f406c021000 | 7f40701ff000 | 65.87mb | MEM_IMAGE | MEM_UNKNOWN | PAGE_UNKNOWN | doublemapper__deleted_
...
+----------------------------------------------------------------------+
| Memory Type | Count | Size | Size (bytes) |
+----------------------------------------------------------------------+
| Image | 980 | 3.54gb | 3,801,517,056 |
| PAGE_READWRITE | 1,178 | 1.17gb | 1,255,059,968 |
| Stack | 66 | 499.35mb | 523,604,992 |
...
| NewStubPrecodeHeap | 4 | 64.00kb | 65,536 |
+----------------------------------------------------------------------+
| [TOTAL] | 8,254 | 6.01gb | 6,451,347,968 |
+----------------------------------------------------------------------+
从卦象看,总计 6.4G 的内存使用,Image 就吃了 3.8G,从 details 看确实都标记了 doublemapper__deleted_,说实话我分析了300多例的dump,Image 吃了大头是第二次遇到,这种故障案例一般是可遇不可求的,接下来我们探究下 doublemapper__deleted_ 为何方神圣。
2. doublemapper__deleted_ 是什么
要想找到这个答案,先从 coreclr 源代码中寻找蛛丝马迹,全局检索之后很快发现了关键词 doublemapper相关的代码:
bool VMToOSInterface::CreateDoubleMemoryMapper(void** pHandle, size_t *pMaxExecutableCodeSize)
{
#ifndef TARGET_OSX
#ifdef TARGET_FREEBSD
int fd = shm_open(SHM_ANON, O_RDWR | O_CREAT, S_IRWXU);
#elif defined(TARGET_SUNOS) // has POSIX implementation
char name[24];
sprintf(name, "/shm-dotnet-%d", getpid());
name[sizeof(name) - 1] = '\0';
shm_unlink(name);
int fd = shm_open(name, O_RDWR | O_CREAT | O_EXCL | O_NOFOLLOW, 0600);
#else // TARGET_FREEBSD
int fd = memfd_create("doublemapper", MFD_CLOEXEC);
#endif // TARGET_FREEBSD
*pMaxExecutableCodeSize = MaxDoubleMappedSize;
*pHandle = (void*)(size_t)fd;
#else // !TARGET_OSX
*pMaxExecutableCodeSize = SIZE_MAX;
*pHandle = NULL;
#endif // !TARGET_OSX
return true;
}
从卦象看,真尼玛乱,coreclr 为了兼容各种操作系统核,加了无数的 if,else 判断,无语了,最后在非OSX,非FREEBSD,非SUNOS的情况下走了 memfd_create 函数,到这里事情有了一些进展了。
熟悉 Linux 的朋友应该知道 memfd_create 是一个 Linux 系统调用,用于创建一个匿名文件描述符,如果在 Windows 上找等价函数的话,那就是 win32api 中的 CreateFileMapping 函数,即内存映射文件,这个在源码目录中也能观之一二:

可能有些朋友对 memfd_create 的使用还是有些模糊,我让 chatgpt 帮我生成一段简单的 demo 辅助大家理解下,简化后如下:
int main() {
const char *name = "example_memfd";
int fd;
size_t size = 1024; // 1 KB
void *map;
const char *text = "Hello, memfd_create!";
// Create the memory file descriptor
fd = memfd_create(name, MFD_CLOEXEC);
// Resize the memory file to the desired size
ftruncate(fd, size)
// Map the memory file into the address space
map = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
// Write some data to the memory file
strncpy(map, text, strlen(text));
// Print the data from the memory file
printf("Data in memory file: %s\n", (char *)map);
// Unmap the memory
munmap(map, size)
// Close the file descriptor
close(fd);
return 0;
}
卦中的逻辑非常简单,需要注意的是这里有一个重要步骤就是通过 mmap 将 fd 挂上物理内存,即 fd -> mmap <- memory,挂上之后就可以轻松的往里面写数据了。
有了这些基础之后,大家再看 doublemapper__deleted_ 字样是不是有种豁然开朗的感觉?大概就是资源释放中只执行了 close(fd),但没有执行 mummap,参考如下:
// Unmap the memory (某种原因未执行)
//munmap(map, size)
// Close the file descriptor
close(fd);
哈哈,当然我的推测不一样对,熟悉 linux 的朋友可以指点指点。 接下来研究方向在哪里呢?既然我已经推测出貌似存在某种逻辑bug,但 coreclr 代码不是我们写的,所以我能不能绕过去呢?
3. 可以绕过 memfd_create 吗?
要想知道能不能绕过去,还得从源代码中寻找答案,天不负有心人,还真给找到了,简化后的代码如下:
bool ExecutableAllocator::Initialize()
{
if (IsDoubleMappingEnabled())
{
if (!VMToOSInterface::CreateDoubleMemoryMapper(&m_doubleMemoryMapperHandle, &m_maxExecutableCodeSize))
{
g_isWXorXEnabled = false;
return true;
}
m_CriticalSection = ClrCreateCriticalSection(CrstExecutableAllocatorLock,CrstFlags(CRST_UNSAFE_ANYMODE | CRST_DEBUGGER_THREAD));
}
return true;
}
bool ExecutableAllocator::IsDoubleMappingEnabled()
{
#if defined(HOST_OSX) && defined(HOST_ARM64)
return false;
#else
return g_isWXorXEnabled;
#endif
}
bool ExecutableAllocator::g_isWXorXEnabled = CLRConfig::GetConfigValue(CLRConfig::EXTERNAL_EnableWriteXorExecute) != 0;
RETAIL_CONFIG_DWORD_INFO(EXTERNAL_EnableWriteXorExecute, W("EnableWriteXorExecute"), 1, "Enable W^X for executable memory.");
从卦中代码看,最终是由 EnableWriteXorExecute 外部变量控制的,那这个变量是什么意思呢?其实它是操作系统和CPU联合提供的功能,在 https://en.wikipedia.org/wiki/W%5EX 上对 W^X 特性做了介绍,大概意思就是:
它是一种内存保护策略,根据该策略,进程或内核地址空间中的每个页面要么是可写的,要么是可执行的,但不能同时具备这两种属性,如果没有这种保护,程序就可以在原本用于存储数据的内存区域中写入(作为数据 “W”)CPU 指令,然后运行(作为可执行代码 “X”;或读 - 执行 “RX”)这些指令。如果写入内存的一方怀有恶意,这就会带来危险。
而且 EnableWriteXorExecute 这东西导致的问题在 github 上有很多的讨论:
- https://github.com/dotnet/runtime/issues/97765
- https://stackoverflow.com/questions/77164379/how-do-i-debug-a-net-core-console-app-with-windbg-by-launch-executable
- https://github.com/dotnet/runtime/issues/79469
大家给出的建议都是将其关闭,操作方式如下:
export DOTNET_EnableWriteXorExecute=0
让朋友关闭了这个选项之后,朋友反馈程序运行正常。
4. 到底是什么代码导致的
虽然可以通过 export DOTNET_EnableWriteXorExecute=0 搞定这个问题,那到底是什么业务导致产生了很多的 doublemapper 呢?这就需要从这些内存段上寻找答案了,仔细想想,既然是内存文件嘛,大概率承载了 .NET 的 dll 文件,而 dll 文件都是魔术 MZ 开头的。所以使用 s-a 抽查其中一个内存段。
0:000> s-a 7f3fc4000000 7f3fcbff5000-0x1 "MZ"
00007f3f`c4059ce4 4d 5a 00 00 00 00 00 00-00 00 00 00 7c 00 00 00 MZ..........|...
00007f3f`c44f2989 4d 5a 3c 40 7f 00 00 b1-05 00 00 94 99 00 00 80 MZ<@............
00007f3f`c44f2b69 4d 5a 3c 40 7f 00 00 b1-05 00 00 98 99 00 00 40 MZ<@...........@
00007f3f`c44f3d99 4d 5a 3c 40 7f 00 00 b2-05 00 00 ac 99 00 00 80 MZ<@............
00007f3f`c44f4d49 4d 5a 3c 40 7f 00 00 b2-05 00 00 b6 99 00 00 80 MZ<@............
00007f3f`c45a3c61 4d 5a c4 3f 7f 00 00 00-00 00 00 00 00 00 00 cd MZ.?............
00007f3f`c45a3ca1 4d 5a c4 3f 7f 00 00 00-00 00 00 00 00 00 00 cd MZ.?............
00007f3f`c45a3ce1 4d 5a c4 3f 7f 00 00 00-00 00 00 00 00 00 00 cd MZ.?............
00007f3f`c45a3d21 4d 5a c4 3f 7f 00 00 00-00 00 00 00 00 00 00 cd MZ.?............
...
然后用了一段私藏的脚本导出来后,发现是大量的项目dll,这个就不截图了,朋友也有说他们程序有动态生成代码的逻辑。
四:总结
EnableWriteXorExecute 特性是在 .NET7 之后默认将0设为1的,在某些开源linux上会因为各种兼容性问题导致各种奇葩的问题发生,这东西我感觉目前还是能禁掉就禁掉吧。

记一次 .NET某数字化协同管理系统 内存暴涨分析的更多相关文章
- 记一次 .NET 某RFID标签管理系统 CPU 暴涨分析
一:背景 1. 讲故事 前段时间有位朋友说他的程序 CPU 出现了暴涨现象,由于程序是买来的,所以问题就比较棘手了,那既然找到我,就想办法帮朋友找出来吧,分析下来,问题比较经典,有必要和大家做一下分享 ...
- 记一次 .NET WPF布草管理系统 挂死分析
一:背景 1. 讲故事 这几天看的 dump 有点多,有点伤神伤脑,晚上做梦都是dump,今天早上头晕晕的到公司就听到背后同事抱怨他负责的WPF程序挂死了,然后测试的小姑娘也跟着抱怨...嗨,也不知道 ...
- 记一次 .NET 某风控管理系统 内存泄漏分析
一:背景 1. 讲故事 上个月中旬,星球里的一位朋友在微信找我,说他的程序跑着跑着内存会不断的缓慢增长并无法释放,寻求如何解决 ? 得,看样子星球还得好好弄!!! 不管怎么说,先上 windbg 说话 ...
- 记一次 .NET医疗布草API程序 内存暴涨分析
一:背景 1. 讲故事 我在年前写过一篇关于CPU爆高的分析文章 再记一次 应用服务器 CPU 暴高事故分析 ,当时是给同济做项目升级,看过那篇文章的朋友应该知道,最后的结论是运维人员错误的将 IIS ...
- 记一次 .NET 某外贸Web站 内存泄漏分析
一:背景 1. 讲故事 上周四有位朋友加wx咨询他的程序内存存在一定程度的泄漏,并且无法被GC回收,最终机器内存耗尽,很尴尬. 沟通下来,这位朋友能力还是很不错的,也已经做了初步的dump分析,发现了 ...
- 记一次 .NET 某三甲医院HIS系统 内存暴涨分析
一:背景 1. 讲故事 前几天有位朋友加wx说他的程序遭遇了内存暴涨,求助如何分析? 和这位朋友聊下来,这个dump也是取自一个HIS系统,如朋友所说我这真的是和医院杠上了,这样也好,给自己攒点资源, ...
- 记一次 .NET 某WMS仓储打单系统 内存暴涨分析
一:背景 1. 讲故事 七月中旬有一位朋友加wx求助,他的程序在生产上跑着跑着内存就飙起来了,貌似没有回头的趋势,询问如何解决,截图如下: 和这位朋友聊下来,感觉像是自己在小县城当了个小老板,规律的生 ...
- 记一次 .NET 某招聘网后端服务 内存暴涨分析
一:背景 1. 讲故事 前段时间有位朋友wx找到我,说他的程序存在内存阶段性暴涨,寻求如何解决,和朋友沟通下来,他的内存平时大概是5G 左右,在某些时点附近会暴涨到 10G+, 画个图大概就是这样. ...
- 记一次 .NET 某电厂Web系统 内存泄漏分析
一:背景 1. 讲故事 前段时间有位朋友找到我,说他的程序内存占用比较大,寻求如何解决,截图就不发了,分析下来我感觉除了程序本身的问题之外,.NET5 在内存管理方面做的也不够好,所以有必要给大家分享 ...
- 记一次 .NET 某工控软件 内存泄露分析
一:背景 1.讲故事 上个月 .NET调试训练营 里的一位老朋友给我发了一个 8G 的dump文件,说他的程序内存泄露了,一时也没找出来是哪里的问题,让我帮忙看下到底是怎么回事,毕竟有了一些调试功底也 ...
随机推荐
- 从0搭建一个FIFO模块-01(基础知识)
1. FIFO介绍 基本概念 FIFO(First In, First Out)是一种常用的数据结构,用于存储和处理数据.它的工作原理与排队的顺序类似,遵循"先进先出"的原则.即, ...
- 低功耗4G模组:MQTT通信功能
今天我们来学习使用合宙Air708E开发板的MQTT通信基本功能. 一.MQTT介绍 MQTT是一种低开销.低带宽占用的即时通讯协议,可以用极少的代码和带宽为远程设备提供实时可靠的消息服务.它适用 ...
- 洛谷 NOIP 2023 模拟赛 T2 汪了个汪
洛谷 NOIP 2023 模拟赛 T2 汪了个汪 考试建出正解图不知道怎么处理,题解区樱雪喵博客薄纱. 樱雪喵题解链接 Ps:笔者语文爆炸,不建议阅读本文 思路 首先你会发现,一共有 \(\frac{ ...
- BLOG-1
前言 回顾这三次作业的心路历程,可以说每一次都带来了新的挑战与收获,随着题目数量和复杂度的增加,对Java编程的理解和面向对象设计的认知逐步加深.作为Java编程初学者,最初对编程架构.模块分层和错误 ...
- PA1-总结
前言 代码全是自己写的,没看过参考代码,思路也有部分和指导书不一样,算是个原创?然后毕竟pa1是简单的部分,也没有什么值得骄傲的地方,只是作为一次记录. 毕竟自己的水平还是有限,可能部分地方会有些bu ...
- AbstractQueuedSynchronizer源码解析之ReentrantLock(二)
上篇文章分析了ReentrantLock的lock,tryLock,unlock方法,继续分析剩下的方法,首先开始lockInterruptibly,先看其API说明:lockInterruptibl ...
- B+树原理详解
B树 与 B+树 我们今天要介绍的是工作开发中最常接触到的 InnoDB 存储引擎中的 B+ 树索引.要介绍 B+ 树索引,就不得不提二叉查找树,平衡二叉树和 B 树这三种数据结构.B+ 树就是从他们 ...
- Tornado框架之深入(二)
知识点 Application设置 debug模式 路由设置扩展 RequestHandler的使用 输入方法 输出方法 可重写接口 目录: Application settings 路由映射 输入 ...
- 问题解决:windows主机开机不插屏幕不能自动进入桌面
操作系统一般都有这种设定,不论是windows还是Linux系统,那就是主机开机不插屏幕不能自动进入桌面操作系统一般都有这种设定,不论是windows还是Linux系统,那就是主机开机不插屏幕不能自动 ...
- 入门 .NET Aspire: 使用 .NET 简化云原生应用开发
入门 .NET Aspire: 使用 .NET 简化云原生应用开发 https://devblogs.microsoft.com/dotnet/introducing-dotnet-aspire-si ...