一:背景

1. 讲故事

七月底的时候有位朋友在wx上找到我,说他的程序内存占用8G,托管才占用1.5G,询问剩下的内存哪里去了?截图如下:

从求助内容看,这位朋友真的太客气了,动不动就谈钱,真伤感情,如果有朋友一直关注我的分享,应该知道我一直都是免费分析dump,当然我的知识和经验也是有边界的,有些dump我也搞不定,不过我还是尽自己最大努力去寻找答案。

在这里我有必要说一下职场,在我的潜意识或者在我的团队中,这些很难搞的问题当然由技术领导去搞定,但我发现有好几起却不是这样的,技术经理搞不定转包下来,下面搞不定就让他另请高明。。。 有大佬可以分析下吗。

好了,闲话不多说,当务之急上windbg说话。

二: windbg 分析

1. 真的是非托管泄漏吗?

我在很多分析内存泄漏方面的文章都提到过,先要用二分法确定下是哪一部分的内存泄漏(托管还是非托管)。


0:000> !address -summary --- Usage Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
Free 387 7df2`11ac1000 ( 125.946 TB) 98.39%
<unknown> 2229 20c`a21bb000 ( 2.049 TB) 99.75% 1.60%
Heap 1081 1`33914000 ( 4.806 GB) 0.23% 0.00%
Image 1674 0`0e4be000 ( 228.742 MB) 0.01% 0.00%
Stack 973 0`0a140000 ( 161.250 MB) 0.01% 0.00%
TEB 324 0`00288000 ( 2.531 MB) 0.00% 0.00%
Other 11 0`001d9000 ( 1.848 MB) 0.00% 0.00%
PEB 1 0`00001000 ( 4.000 kB) 0.00% 0.00% --- Type Summary (for busy) ------ RgnCount ----------- Total Size -------- %ofBusy %ofTotal
MEM_MAPPED 300 200`00f9e000 ( 2.000 TB) 97.35% 1.56%
MEM_PRIVATE 3869 d`dd7ed000 ( 55.461 GB) 2.64% 0.04%
MEM_IMAGE 2124 0`0fda4000 ( 253.641 MB) 0.01% 0.00% --- State Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
MEM_FREE 387 7df2`11ac1000 ( 125.946 TB) 98.39%
MEM_RESERVE 1763 20b`d9903000 ( 2.046 TB) 99.60% 1.60%
MEM_COMMIT 4530 2`14c2c000 ( 8.324 GB) 0.40% 0.01% 0:000> !eeheap -gc
Number of GC Heaps: 40
------------------------------
Heap Size: Size: 0x3322e60 (53620320) bytes.
------------------------------
GC Heap Size: Size: 0x603046b0 (1613776560) bytes.

!address -summary!eeheap -gc 两条命令看,确实如朋友所说:MEM_COMMIT=8.3G, GC Heap=1.5G, 我去,果然是难搞的非托管内存泄漏,既然是地狱模式,那就硬着头皮继续看吧,要想继续排查的话,首先得看 windows nt 堆。

2. 查看 windows nt堆

其实不管是托管的C#还是非托管的C,C++,它们分配内存最终都需要调用 Windows 的 VirtualAlloc,HeapAlloc API 到 windows nt 上,接下来的研究方向是如何查找这些 .net 看不到的 nt堆, 可以使用 windbg 的 !heap -s 命令。


0:000> !heap -s ************************************************************************************************************************
NT HEAP STATS BELOW
************************************************************************************************************************
LFH Key : 0x0e4dcfd61ab09dd9
Termination on corruption : ENABLED
Heap Flags Reserv Commit Virt Free List UCR Virt Lock Fast
(k) (k) (k) (k) length blocks cont. heap
-------------------------------------------------------------------------------------
000001bacd190000 00000002 4841944 4810424 4840388 13556 2042 303 2 dd8 LFH
000001baccfb0000 00008000 64 4 64 2 1 1 0 0
000001bacd4d0000 00001002 8772 6748 7216 1045 191 4 0 38 LFH
External fragmentation 15 % (191 free blocks)
000001bacdf90000 00001002 2636 404 1080 33 3 2 0 0 LFH
000001bace620000 00001002 8772 4052 7216 3874 13 7 0 1f LFH
External fragmentation 95 % (13 free blocks)
000001bace610000 00001003 60 8 60 6 1 1 0 N/A
000001bace540000 00001002 1616 24 60 4 2 1 0 1 LFH
000001baceb50000 00001002 4680 1228 3124 504 99 3 0 0 LFH
External fragmentation 41 % (99 free blocks)
000001baceb20000 00041002 60 8 60 5 1 1 0 0
000001baceb10000 00041002 1616 68 60 4 3 1 0 0 LFH
000001c7738a0000 00001002 49336 19316 47780 8249 43 22 0 13b LFH
External fragmentation 42 % (43 free blocks)
000001c7753c0000 00001002 13712 8460 12156 968 29 6 0 1c LFH
External fragmentation 11 % (29 free blocks)
000001c7763f0000 00001002 8772 3944 7216 423 25 4 0 3f LFH
000001ba977c0000 00001002 1080 376 1080 365 3 2 0 0
-------------------------------------------------------------------------------------

从上面的信息可以看出,当前有 14个 heap,其中最大的一个heap占了 4.8G,为啥这个heap这么大? 接下来详细看下这个heap,可使用 !ext.heap -stat -h 000001bacd190000


0:000> !ext.heap -stat -h 000001bacd190000
heap @ 000001bacd190000
group-by: TOTSIZE max-display: 20
size #blocks total ( %) (percent of total busy bytes)
20034 8eee - 11df90858 (96.44)
2ee0000 2 - 5dc0000 (1.98)
851 1c2b - ea419b (0.31)
2ac00 28 - 6ae000 (0.14)
27d8 268 - 5fdfc0 (0.13)
24000 28 - 5a0000 (0.12)
d51 564 - 47c8a4 (0.09)
10d1 3e7 - 419f97 (0.09)
fd1 415 - 409025 (0.09)
29d1 12f - 317e5f (0.07)
138 18b0 - 1e1680 (0.04)
12c 188b - 1cc2e4 (0.04)
1000 17e - 17e000 (0.03)
2000 8e - 11c000 (0.02)
200 899 - 113200 (0.02)
ad1 178 - fe2f8 (0.02)
478 367 - f3448 (0.02)
7c8 1b9 - d6788 (0.02)
1c038 7 - c4188 (0.02)
f520 c - b7d80 (0.02)

可能很多人看不懂上面的卦象,首先 busy表示那些最近分配还未释放的,从卦头看,size=20034 的 block 有 36590 个,总占用:11df90858 = 4797827160byte = 4.7G,接下来的疑问很显然了,这些 block 里面到底都是些什么??? 要想找到答案,把这 3w 多的 block 信息都显示出来,可以用命令: !ext.heap -flt s 20034


0:000> !ext.heap -flt s 20034
_HEAP @ 1bacd190000
HEAP_ENTRY Size Prev Flags UserPtr UserSize - state
000001c771f2ad30 2004 0000 [00] 000001c771f2ad40 20034 - (busy)
000001c774a65160 2004 2004 [00] 000001c774a65170 20034 - (busy)
000001c774a851a0 2004 2004 [00] 000001c774a851b0 20034 - (busy)
000001c774aa51e0 2004 2004 [00] 000001c774aa51f0 20034 - (busy)
000001c774ac5220 2004 2004 [00] 000001c774ac5230 20034 - (busy)
000001c774ae5260 2004 2004 [00] 000001c774ae5270 20034 - (busy)
000001c774b052a0 2004 2004 [00] 000001c774b052b0 20034 - (busy)
000001c774b29320 2004 2004 [00] 000001c774b29330 20034 - (busy)
000001c774b49360 2004 2004 [00] 000001c774b49370 20034 - (busy)
000001c774b693a0 2004 2004 [00] 000001c774b693b0 20034 - (busy)
000001c774b893e0 2004 2004 [00] 000001c774b893f0 20034 - (busy)
unknown!noop
000001c774ba9420 2004 2004 [00] 000001c774ba9430 20034 - (busy)
...
...

block块信息太多,这里我就贴一部分上去,上面列的 HEAP_ENTRY 就是 block 的首地址,然后我通过 dc 一顿找,发现不少下面的输出。


0:000> dc 000001c774a65160 L 50
000001c7`74a65160 dddddddd 00000000 cada2944 0c85cc02 ........D)......
000001c7`74a65170 74a21070 000001c7 74a851b0 000001c7 p..t.....Q.t....
000001c7`74a65180 00000000 00000000 00000000 00000001 ................
000001c7`74a65190 00020000 00000000 0000007a fdfdfdfd ........z.......
000001c7`74a651a0 00c801aa 55028000 05040355 44b60706 .......UU......D
000001c7`74a651b0 693d6f55 69502c31 32693d6e 7361502c Uo=i1,Pin=i2,Pas
000001c7`74a651c0 726f7773 33733d64 6f72472c 693d7075 sword=s3,Group=i
000001c7`74a651d0 74532c34 54747261 3d656d69 452c3569 4,StartTime=i5,E
000001c7`74a651e0 6954646e 693d656d 75532c36 41726570 ndTime=i6,SuperA
000001c7`74a651f0 6f687475 657a6972 0a37693d 72657375 uthorize=i7.user
000001c7`74a65200 68747561 7a69726f 2c323d65 3d6e6950 authorize=2,Pin=
000001c7`74a65210 412c3169 6f687475 657a6972 656d6954 i1,AuthorizeTime
000001c7`74a65220 656e6f7a 693d6449 75412c32 726f6874 zoneId=i2,Author
000001c7`74a65230 44657a69 49726f6f 33693d64 6c6f680a izeDoorId=i3.hol
000001c7`74a65240 79616469 482c333d 64696c6f 693d7961 iday=3,Holiday=i
000001c7`74a65250 6f482c31 6164696c 70795479 32693d65 1,HolidayType=i2
000001c7`74a65260 6f6f4c2c 33693d70 6d69740a 6e6f7a65 ,Loop=i3.timezon
000001c7`74a65270 2c343d65 656d6954 656e6f7a 693d6449 e=4,TimezoneId=i
000001c7`74a65280 75532c31 6d69546e 693d3165 75532c32 1,SunTime1=i2,Su
000001c7`74a65290 6d69546e 693d3265 75532c33 6d69546e nTime2=i3,SunTim

说实话用dc一个一个找,真的太累,这里我就写一个简单的脚本,把前1w个block都dc出来看看内容咋样?


"use strict"; var index = 1; function initializeScript() { return [new host.apiVersionSupport(1, 7)]; }
function log(str) { host.diagnostics.debugLog(str + "\n"); }
function exec(str) { log("\n" + str); return host.namespace.Debugger.Utility.Control.ExecuteCommand(str); } function invokeScript() {
show_heap_s();
} function show_heap_s() { //get top 1
var output = exec("!heap -s").Skip(10).First(); var h_address = output.split(' ')[0]; show_max_blocksize(h_address);
} function show_max_blocksize(address) { var output = exec("!ext.heap -stat -h " + address).Skip(3).First(); var block_size = output.trim().split(' ')[0]; show_all_blocksize(block_size);
} function show_all_blocksize(blocksize) { var output = exec("!ext.heap -flt s " + blocksize).Take(10000);
for (var line of output) { var heap_entry_address = line.trim().split(' ')[0]; if (heap_entry_address.indexOf("00") == -1) continue; show_heap_entry(heap_entry_address);
}
} function show_heap_entry(heap_entry_address) { var pageIndex = (index++); var path = ".writemem D:\\dumps\\winform-memory-leak\\file\\" + pageIndex + ".txt " + heap_entry_address + " L?0x500"; var output = exec(path); log("pageIndex=" + pageIndex);
}

脚本执行后,输出结果如下:

问了下朋友这些字符串大概是干嘛的? 为啥非托管中有这么多的string没有得到释放,朋友告诉我这个大概是门禁相关业务,是通过 plc 方式和 C# 进行交互,分析到这里我能提供的信息都已提供了,接下来就要和门禁业务方确认下如何进一步定位和改进了。

三:总结

貌似这是20篇dump案例分享中第一个聊到非托管泄露的问题,曾今我在B站上说只专注于分析.NET托管内存泄漏,看样子很难实现哈,确实 C# 和 lua,C++,COM,内嵌浏览器 的交互造成非托管内存泄漏的例子数不胜数哈

更多高质量干货:参见我的 GitHub: dotnetfly

记一次 .NET 某智慧水厂API 非托管内存泄漏分析的更多相关文章

  1. 记一次 .NET 某打印服务 非托管内存泄漏分析

    一:背景 1. 讲故事 前段时间有位朋友在微信上找到我,说他的程序出现了内存泄漏,能不能帮他看一下,这个问题还是比较经典的,加上好久没上非托管方面的东西了,这篇就和大家分享一下,话不多说,上 WinD ...

  2. 记一次 .NET 某桌面奇侠游戏 非托管内存泄漏分析

    一:背景 1. 讲故事 说实话,这篇dump我本来是不准备上一篇文章来解读的,但它有两点深深的感动了我. 无数次的听说用 Unity 可做游戏开发,但百闻不如一见. 游戏中有很多金庸武侠小说才有的名字 ...

  3. 记一次 .NET 某智能服装智造系统 内存泄漏分析

    一:背景 1. 讲故事 上个月有位朋友找到我,说他的程序出现了内存泄漏,不知道如何进一步分析,截图如下: 朋友这段话已经说的非常言简意赅了,那就上 windbg 说话吧. 二:Windbg 分析 1. ...

  4. 记一次 .NET 某HIS系统后端服务 内存泄漏分析

    一:背景 1. 讲故事 前天那位 his 老哥又来找我了,上次因为CPU爆高的问题我给解决了,看样子对我挺信任的,这次另一个程序又遇到内存泄漏,希望我帮忙诊断下. 其实这位老哥技术还是很不错的,他既然 ...

  5. 记一次 .NET 某外贸Web站 内存泄漏分析

    一:背景 1. 讲故事 上周四有位朋友加wx咨询他的程序内存存在一定程度的泄漏,并且无法被GC回收,最终机器内存耗尽,很尴尬. 沟通下来,这位朋友能力还是很不错的,也已经做了初步的dump分析,发现了 ...

  6. 记一次 .NET 某风控管理系统 内存泄漏分析

    一:背景 1. 讲故事 上个月中旬,星球里的一位朋友在微信找我,说他的程序跑着跑着内存会不断的缓慢增长并无法释放,寻求如何解决 ? 得,看样子星球还得好好弄!!! 不管怎么说,先上 windbg 说话 ...

  7. 记一次 WinDbg 分析 .NET 某工厂MES系统 内存泄漏分析

    一:背景 1. 讲故事 上个月有位朋友加微信求助,说他的程序跑着跑着就内存爆掉了,寻求如何解决,截图如下: 从聊天内容看,这位朋友压力还是蛮大的,话说这貌似是我分析的第三个 MES 系统了,看样子 . ...

  8. 记一次 .NET 某消防物联网 后台服务 内存泄漏分析

    一:背景 1. 讲故事 去年十月份有位朋友从微信找到我,说他的程序内存要炸掉了...截图如下: 时间有点久,图片都被清理了,不过有点讽刺的是,自己的程序本身就是做监控的,结果自己出了问题,太尴尬了 二 ...

  9. 记一次 .NET 某电厂Web系统 内存泄漏分析

    一:背景 1. 讲故事 前段时间有位朋友找到我,说他的程序内存占用比较大,寻求如何解决,截图就不发了,分析下来我感觉除了程序本身的问题之外,.NET5 在内存管理方面做的也不够好,所以有必要给大家分享 ...

随机推荐

  1. Docker 优雅终止方案

    作为一名系统工程师,你可能经常需要重启容器,毕竟Kubernetes的优势就是快速弹性伸缩和故障恢复,遇到问题先重启容器再说,几秒钟即可恢复,实在不行再重启系统,这就是系统重启工程师的杀手锏.然而现实 ...

  2. Redis之Sentinel

    Redis的主从复制模式下,一旦主节点由于故障不能提供服务,需要人工将从节点晋升为主节点,同时还要通知应用方更新主节点地址,对于很多应用场景这种故障处理的方式是无法接受的.可喜的是Redis从 2.8 ...

  3. ICMP路由重定向攻击

    ICMP介绍 ICMP(Internet Control Message Protocol)Internet控制报文协议.它是TCP/IP协议簇的一个子协议,用于在IP主机.路由器之间传递控制消息.控 ...

  4. 教你写百分九十的shell

    本文章主要内容来自菜鸟教程 , 也添加了一些知识点 shell脚本? 在说什么是shell脚本之前,先说说什么是shell.shell是外壳的意思,就是操作系统的外壳.我们可以通过shell命令来操作 ...

  5. Redis:银河麒麟arm服务器安装redis5.0.3,配置开机自启

    百度网盘下载地址 链接:https://pan.baidu.com/s/1f2ghL2-0brPt0IodjfqOqQ提取码:9al1    解压tar包 #解压tar包 tar -xvf arm-r ...

  6. 资源:Redis下载地址

    Redis的下载路径 http://download.redis.io/releases/  3.x(支持集群)  2.x不支持集群   (推荐使用3.0+版本)     副版本号为偶数时,表示是稳定 ...

  7. XCTF Hello CTF

    一.查壳 二.拖入ida x86静态分析 shift +F12找到字符串. 发现关键字please input your serial 点击进入 这是我们的主函数,F5反编译一下,看一下逻辑. 这里有 ...

  8. buu pyre

    一.下载附件是是pyc的字节码文件,找个在线网站反编译一下 思路还是挺清晰: 先逆着求出code, 这里就是求余,有点麻烦,那个+128%128其实没啥用的,省略就好了 算法里面再处理一下细节,跑一下 ...

  9. Adaptive AUTOSAR 学习笔记 3 - AP 背景、技术及特征(中文翻译)

    本系列学习笔记基于 AUTOSAR Adaptive Platform 官方文档 R20-11 版本.本文从AUTOSAR_EXP_PlatformDesign.pdf开始,一边学习,一边顺带着翻译一 ...

  10. ti

    一.选择题DCBCDCDACAACBBABACBDCBBDA二.简答题(每小题5分,共20分)1. 1)简洁紧凑,灵活方便2)运算符丰富3)数据类型丰富4)C语言是结构化语言5)语法限制较少,程序设计 ...