一:背景
1. 講故事
前面跟大家分享過一篇 C# 調用 C代碼引發非托管內存泄露 的文章,這是一個故意引發的正向泄露,這一篇我們從逆向的角度去洞察引發泄露的禍根代碼,這東西如果在 windows 上還是很好處理的,很多人知道開啟一個 ust 即可,讓操作系統幫忙介入,在linux上就相對復雜一點了,畢竟Linux系統是一個萬物生的場地,沒有一個人統管全局,在調試領域這塊還是蠻大的一個弊端
二:案例分析
1. 一個小案例
這里我還是用之前的例子,對應的 C 代碼 和 C#代碼 如下:
C 代碼
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#define BLOCK_SIZE (10 * 1024) // 每個塊 10K
#define TOTAL_SIZE (1 * 1024 * 1024 * 1024) // 總計 1GB
#define BLOCKS (TOTAL_SIZE / BLOCK_SIZE) // 計算需要的塊數
void heapmalloc()
{
uint8_t *blocks[BLOCKS]; // 存儲每個塊的指針
// 分配 1GB 內存,分成多個小塊
for (size_t i = 0; i < BLOCKS; i++)
{
blocks[i] = (uint8_t *)malloc(BLOCK_SIZE);
if (blocks[i] == NULL)
{
printf("內存分配失敗!\n");
return;
}
// 確保每個塊都被實際占用
memset(blocks[i], 20, BLOCK_SIZE);
}
printf("已經分配 1GB 內存在堆上!\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分配的工具,它會攔截所有的 malloc、calloc、realloc 和 free 函數調用,并記錄分配的調用棧信息,總的來說這工具和 C# 半毛錢關系都沒有,主要是圖它的如下三點:
能夠記錄到分配的調用棧信息,雖然只有非托管部分
對程序的影響相對小
有可視化的工具觀察跟蹤文件
依次安裝 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 對 dotnet 程序進行跟蹤,當泄露到一定程序之后,可以用 dotnet-dump 生成一個轉儲文件,然后 Ctrl+C 進行中斷,
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
已經分配 1GB 內存在堆上!
[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
從卦中看已產生了一個 heaptrack.dotnet.4368.zst 文件,這是一種專有的壓縮格式,可以借助 heaptrack_print 轉成 txt 文件,方便從生產上拿下來分析
root@ubuntu2404:/data/CSharpApplication/bin/Debug/net8.0# heaptrack_print heaptrack.dotnet.4368.zst > heaptrack.txt
真實的場景下肉眼觀察 heaptrack.txt 是不大現實的,所以還得借助可視化工具,觀察 Bottom-Up 選擇項,信息如下:
左邊面板
可以觀察到 Leaked 最多的是?libmyleak.so?中的 heapmalloc 函數
右邊面板
可以觀察到執行 heapmalloc 方法的上層函數,給大家截圖二張
稍微仔細看的話,會發現Backtrace上有很多的 unresolved 符號,這個沒辦法,畢竟人家是 C/C++ 的跟蹤器,和你C#沒關系,那這些未解析的符號到底是什么函數呢?
3. 未解析符號的地址在哪里
既然是 C# 程序,大概率就是 C#方法了,那如何把方法名給找出來呢?熟悉.NET高級調試的朋友此時應該輕車熟路了,思路如下:
尋找 指令地址[-](查詢持倉 · 開發文檔 · jvQuant)
一般來說解析不出來都會生成對應的 指令地址 的,這個可以到 heaptrack.txt 中尋找蛛絲馬跡,截圖如下:
抓 core 文件
要想抓 .NET 的 core 文件,dotnet-dump 即可,這個就不介紹了哈,參考如下:
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 尋找這個指令地址對應的C#方法名了[-](注冊Token · 開發文檔 · jvQuant)
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
到這里恍然大悟,然來調用路徑為:CSharpApplication.Program.Main -> PInvoke -> heapmalloc ,至此真相大白