Linux系列:如何用heaptrack跟蹤.NET程序的heap泄露
一:背景
1. 講故事
前面跟大家分享過一篇 C# 調(diào)用 C代碼引發(fā)非托管內(nèi)存泄露 的文章,這是一個(gè)故意引發(fā)的正向泄露,這一篇我們從逆向的角度去洞察引發(fā)泄露的禍根代碼,這東西如果在 windows 上還是很好處理的,很多人知道開啟一個(gè) ust 即可,讓操作系統(tǒng)幫忙介入,在linux上就相對(duì)復(fù)雜一點(diǎn)了,畢竟Linux系統(tǒng)是一個(gè)萬物生的場地,沒有一個(gè)人統(tǒng)管全局,在調(diào)試領(lǐng)域這塊還是蠻大的一個(gè)弊端。
二:案例分析
1. 一個(gè)小案例
這里我還是用之前的例子,對(duì)應(yīng)的 C 代碼 和 C#代碼 如下:
- C 代碼
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#define BLOCK_SIZE (10 * 1024) // 每個(gè)塊 10K
#define TOTAL_SIZE (1 * 1024 * 1024 * 1024) // 總計(jì) 1GB
#define BLOCKS (TOTAL_SIZE / BLOCK_SIZE) // 計(jì)算需要的塊數(shù)
void heapmalloc()
{
uint8_t *blocks[BLOCKS]; // 存儲(chǔ)每個(gè)塊的指針
// 分配 1GB 內(nèi)存,分成多個(gè)小塊
for (size_t i = 0; i < BLOCKS; i++)
{
blocks[i] = (uint8_t *)malloc(BLOCK_SIZE);
if (blocks[i] == NULL)
{
printf("內(nèi)存分配失敗!\n");
return;
}
// 確保每個(gè)塊都被實(shí)際占用
memset(blocks[i], 20, BLOCK_SIZE);
}
printf("已經(jīng)分配 1GB 內(nèi)存在堆上!\n");
}
- C#代碼
using System.Runtime.InteropServices;
namespace CSharpApplication;
class Program
{
[DllImport("libmyleak.so", CallingConvention = CallingConvention.Cdecl)]
public static extern void heapmalloc();
static void Main(string[] args)
{
heapmalloc();
Console.ReadLine();
}
}
2. heaptrack 跟蹤
heaptrack 是一款跟蹤 C/C++ heap分配的工具,它會(huì)攔截所有的 malloc、calloc、realloc 和 free 函數(shù)調(diào)用,并記錄分配的調(diào)用棧信息,總的來說這工具和 C# 半毛錢關(guān)系都沒有,主要是圖它的如下三點(diǎn):
- 能夠記錄到分配的調(diào)用棧信息,雖然只有非托管部分。
- 對(duì)程序的影響相對(duì)小。
- 有可視化的工具觀察跟蹤文件。
依次安裝 heaptrack 和 heaptrack-gui ,參考如下:
root@ubuntu2404:/data# sudo apt install heaptrack
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
heaptrack is already the newest version (1.5.0+dfsg1-2ubuntu3).
0 upgraded, 0 newly installed, 0 to remove and 217 not upgraded.
root@ubuntu2404:/data/CSharpApplication/bin/Debug/net8.0# sudo apt install heaptrack-gui
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
heaptrack-gui is already the newest version (1.5.0+dfsg1-2ubuntu3).
0 upgraded, 0 newly installed, 0 to remove and 217 not upgraded.
安裝好以后可以用 heaptrack dotnet CSharpApplication.dll 對(duì) dotnet 程序進(jìn)行跟蹤,當(dāng)泄露到一定程序之后,可以用 dotnet-dump 生成一個(gè)轉(zhuǎn)儲(chǔ)文件,然后 Ctrl+C 進(jìn)行中斷,
root@ubuntu2404:/data/CSharpApplication/bin/Debug/net8.0# heaptrack dotnet CSharpApplication.dll
heaptrack output will be written to "/data/CSharpApplication/bin/Debug/net8.0/heaptrack.dotnet.4368.zst"
starting application, this might take some time...
NOTE: heaptrack detected DEBUGINFOD_URLS but will disable it to prevent
unintended network delays during recording
If you really want to use DEBUGINFOD, export HEAPTRACK_ENABLE_DEBUGINFOD=1
已經(jīng)分配 1GB 內(nèi)存在堆上!
[createdump] Gathering state for process 4383 dotnet
[createdump] Writing full dump to file /data/CSharpApplication/bin/Debug/net8.0/core_20250307_102814
[createdump] Written 1252216832 bytes (305717 pages) to core file
[createdump] Target process is alive
[createdump] Dump successfully written in 23681ms
root@ubuntu2404:/data/CSharpApplication/bin/Debug/net8.0# heaptrack stats:
allocations: 122151
leaked allocations: 108551
temporary allocations: 4118
root@ubuntu2404:/data/CSharpApplication/bin/Debug/net8.0# ls -lh
total 1.2G
-rwxr-xr-x 1 root root 74K Mar 5 22:38 CSharpApplication
-rw-r--r-- 1 root root 421 Mar 5 21:52 CSharpApplication.deps.json
-rw-r--r-- 1 root root 4.5K Mar 5 22:38 CSharpApplication.dll
-rw-r--r-- 1 root root 11K Mar 5 22:38 CSharpApplication.pdb
-rw-r--r-- 1 root root 257 Mar 5 21:52 CSharpApplication.runtimeconfig.json
-rw------- 1 root root 1.2G Mar 7 10:28 core_20250307_102814
-rw-r--r-- 1 root root 277K Mar 7 10:32 heaptrack.dotnet.4368.zst
-rwxr-xr-x 1 root root 16K Mar 5 21:52 libmyleak.so
從卦中看已產(chǎn)生了一個(gè) heaptrack.dotnet.4368.zst 文件,這是一種專有的壓縮格式,可以借助 heaptrack_print 轉(zhuǎn)成 txt 文件,方便從生產(chǎn)上拿下來分析。
root@ubuntu2404:/data/CSharpApplication/bin/Debug/net8.0# heaptrack_print heaptrack.dotnet.4368.zst > heaptrack.txt
真實(shí)的場景下肉眼觀察 heaptrack.txt 是不大現(xiàn)實(shí)的,所以還得借助可視化工具,觀察 Bottom-Up 選擇項(xiàng),信息如下:
- 左邊面板
可以觀察到 Leaked 最多的是 libmyleak.so 中的 heapmalloc 函數(shù)。
- 右邊面板
可以觀察到執(zhí)行 heapmalloc 方法的上層函數(shù),給大家截圖二張。


稍微仔細(xì)看的話,會(huì)發(fā)現(xiàn)Backtrace上有很多的 unresolved 符號(hào),這個(gè)沒辦法,畢竟人家是 C/C++ 的跟蹤器,和你C#沒關(guān)系,那這些未解析的符號(hào)到底是什么函數(shù)呢?
3. 未解析符號(hào)的地址在哪里
既然是 C# 程序,大概率就是 C#方法了,那如何把方法名給找出來呢?熟悉.NET高級(jí)調(diào)試的朋友此時(shí)應(yīng)該輕車熟路了,思路如下:
- 尋找 指令地址。
一般來說解析不出來都會(huì)生成對(duì)應(yīng)的 指令地址 的,這個(gè)可以到 heaptrack.txt 中尋找蛛絲馬跡,截圖如下:

- 抓 core 文件
要想抓 .NET 的 core 文件,dotnet-dump 即可,這個(gè)就不介紹了哈,參考如下:
root@ubuntu2404:/data/CSharpApplication/bin/Debug/net8.0# ps -ef | grep CSharp
root 4368 2914 0 10:25 pts/0 00:00:00 /bin/sh /usr/bin/heaptrack dotnet CSharpApplication.dll
root 4383 4368 2 10:25 pts/0 00:00:03 dotnet CSharpApplication.dll
root 4421 4336 0 10:28 pts/3 00:00:00 grep --color=auto CSharp
root@ubuntu2404:/data/CSharpApplication/bin/Debug/net8.0# dotnet-dump collect -p 4383
Writing full to /data/CSharpApplication/bin/Debug/net8.0/core_20250307_102814
Complete
root@ubuntu2404:/data/CSharpApplication/bin/Debug/net8.0# ls -lh
total 1.2G
-rwxr-xr-x 1 root root 74K Mar 5 22:38 CSharpApplication
-rw-r--r-- 1 root root 421 Mar 5 21:52 CSharpApplication.deps.json
-rw-r--r-- 1 root root 4.5K Mar 5 22:38 CSharpApplication.dll
-rw-r--r-- 1 root root 11K Mar 5 22:38 CSharpApplication.pdb
-rw-r--r-- 1 root root 257 Mar 5 21:52 CSharpApplication.runtimeconfig.json
-rw------- 1 root root 1.2G Mar 7 10:28 core_20250307_102814
-rw-r--r-- 1 root root 0 Mar 7 10:25 heaptrack.dotnet.4368.zst
-rwxr-xr-x 1 root root 16K Mar 5 21:52 libmyleak.so
core_20250307_102814 生成好之后,就可以借助 sos 的 ip2md 尋找這個(gè)指令地址對(duì)應(yīng)的C#方法名了。
root@ubuntu2404:/data/CSharpApplication/bin/Debug/net8.0# dotnet-dump analyze core_20250307_102814
Loading core dump: core_20250307_102814 ...
Ready to process analysis commands. Type 'help' to list available commands or 'help [command]' to get detailed help on a command.
Type 'quit' or 'exit' to exit the session.
> ip2md 0x7ea6627119f6
MethodDesc: 00007ea6627cd3d8
Method Name: ILStubClass.IL_STUB_PInvoke()
Class: 00007ea6627cd300
MethodTable: 00007ea6627cd368
mdToken: 0000000006000000
Module: 00007ea66279cec8
IsJitted: yes
Current CodeAddr: 00007ea662711970
Version History:
ILCodeVersion: 0000000000000000
ReJIT ID: 0
IL Addr: 0000000000000000
CodeAddr: 00007ea662711970 (MinOptJitted)
NativeCodeVersion: 0000000000000000
> ip2md 0x7ea662711947
MethodDesc: 00007ea66279f328
Method Name: CSharpApplication.Program.Main(System.String[])
Class: 00007ea6627bb640
MethodTable: 00007ea66279f358
mdToken: 0000000006000002
Module: 00007ea66279cec8
IsJitted: yes
Current CodeAddr: 00007ea662711920
Version History:
ILCodeVersion: 0000000000000000
ReJIT ID: 0
IL Addr: 00007ea6de8f1250
CodeAddr: 00007ea662711920 (MinOptJitted)
NativeCodeVersion: 0000000000000000
Source file: /data/CSharpApplication/Program.cs @ 12
到這里恍然大悟,然來調(diào)用路徑為:CSharpApplication.Program.Main -> PInvoke -> heapmalloc ,至此真相大白。
三:總結(jié)
Linux 上的調(diào)試總覺得少了一位總管太監(jiān),能分析 非托管內(nèi)存的工具 不鳥dotnet, 同樣的,能分析 dotnet托管內(nèi)存的工具 也不鳥非托管內(nèi)存,大家各自為政。。。 讓習(xí)慣使用通殺一切的windbg使用者太不可思議了。
惠促銷公眾號(hào)關(guān)注二維碼.jpg)

浙公網(wǎng)安備 33010602011771號(hào)