Linux系列:如何用C#調(diào)用C方法造成內(nèi)存泄露
一、背景
1. 講故事
今年準(zhǔn)備多寫(xiě)一點(diǎn) Linux平臺(tái)上的東西,這篇從 C# 調(diào)用 C 這個(gè)例子開(kāi)始。在 windows 平臺(tái)上,我們常常在 C++ 代碼中用 extern "C" 導(dǎo)出 C風(fēng)格 的函數(shù),然后在 C# 中用 DllImport 的方式引入,那在 Linux 上怎么玩的?畢竟這對(duì)研究 Linux 上的 C# 程序非托管內(nèi)存泄露有非常大的價(jià)值,接下來(lái)我們就來(lái)看下。
二、一個(gè)簡(jiǎn)單的非托管內(nèi)存泄露
1. 構(gòu)建 so 文件
在 Windows 平臺(tái)上我們會(huì)通過(guò) MSVC 編譯器將 C代碼編譯出一個(gè)成品 .dll,在 Linux 上通常會(huì)借助 gcc 將 c 編譯成 .so 文件,這個(gè).so 全稱 Shared Object,為了方便講解,先上一段簡(jiǎn)單的代碼:
#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");
}
接下來(lái)使用 gcc 編譯,參考如下:
gcc -shared -o libmyleak.so -fPIC myleak.c
- -shared: 編譯成共享庫(kù)
- -fPIC: 指定共享庫(kù)可以在內(nèi)存任意位置被加載(地址無(wú)關(guān)性)
命令執(zhí)行完之后,就可以看到一個(gè) .so 文件了,截圖如下:
圖片
最后可以用 nm 命令驗(yàn)證下 libmyleak.so 中是否有 Text 段下的 heapmalloc 導(dǎo)出函數(shù)。
root@ubuntu2404:/data2/c# nm libmyleak.so
0000000000004028 b completed.0
w __cxa_finalize@GLIBC_2.2.5
00000000000010c0 t deregister_tm_clones
0000000000001130 t __do_global_dtors_aux
0000000000003e00 d __do_global_dtors_aux_fini_array_entry
0000000000004020 d __dso_handle
0000000000003e08 d _DYNAMIC
000000000000125c t _fini
0000000000001170 t frame_dummy
0000000000003df8 d __frame_dummy_init_array_entry
00000000000020f8 r __FRAME_END__
0000000000003fe8 d _GLOBAL_OFFSET_TABLE_
w __gmon_start__
000000000000203c r __GNU_EH_FRAME_HDR
0000000000001179 T heapmalloc
0000000000001000 t _init
w _ITM_deregisterTMCloneTable
w _ITM_registerTMCloneTable
U malloc@GLIBC_2.2.5
U memset@GLIBC_2.2.5
U puts@GLIBC_2.2.5
00000000000010f0 t register_tm_clones
U __stack_chk_fail@GLIBC_2.4
0000000000004028 d __TMC_END__
2. C# 代碼調(diào)用
so構(gòu)建好了之后,后面就比較好說(shuō)了,使用 dotnet new console -n CSharpApplication --use-program-main true 新建一個(gè)CS項(xiàng)目。
root@ubuntu2404:/data2/csharp# dotnet new console -n CSharpApplication --use-program-main true
The template "Console App" was created successfully.
Processing post-creation actions...
Restoring /data2/csharp/CSharpApplication/CSharpApplication.csproj:
Determining projects to restore...
Restored /data2/csharp/CSharpApplication/CSharpApplication.csproj (in 1.7 sec).
Restore succeeded.
編譯下 C# 項(xiàng)目,然后將 libmyleak.so 放到 C#項(xiàng)目的 bin目錄,修改 C# 代碼如下:
using System.Runtime.InteropServices;
namespaceCSharpApplication;
classProgram
{
[DllImport("libmylib.so", CallingConvention = CallingConvention.Cdecl)]
public static extern void hello();
static void Main(string[] args)
{
hello();
Console.ReadLine();
}
}
最后用 dotnet CSharpApplication.dll 運(yùn)行:
root@ubuntu2404:/data2/csharp/CSharpApplication/bin/Debug/net8.0# dotnet CSharpApplication.dll
已經(jīng)分配 1GB 內(nèi)存在堆上!
程序是跑起來(lái)了,那真的是吃了1G呢? 可以先用 htop 觀察程序,從截圖看沒(méi)毛病。
圖片
那這 1G 真的在 heap 上嗎? 可以用 maps 觀察。
root@ubuntu2404:~# ps -ef | grep CSharp
root 10764 10730013:35 pts/21 00:00:00 dotnet CSharpApplication.dll
root 11049 11027013:41 pts/22 00:00:00 grep --color=auto CSharp
root@ubuntu2404:~# cat /proc/10764/maps
614e1f592000-614e1f598000 r--p 0000000008:021479867 /usr/lib/dotnet/dotnet
614e1f598000-614e1f5a4000 r-xp 0000500008:021479867 /usr/lib/dotnet/dotnet
614e1f5a4000-614e1f5a5000 r--p 0001000008:021479867 /usr/lib/dotnet/dotnet
614e1f5a5000-614e1f5a6000 rw-p 0001000008:021479867 /usr/lib/dotnet/dotnet
614e5b5d9000-614e9b8a8000 rw-p 0000000000:000 [heap]
...
root@ubuntu2404:~# pmap 10764
10764: dotnet CSharpApplication.dll
0000614e1f592000 24K r---- dotnet
0000614e1f598000 48K r-x-- dotnet
0000614e1f5a4000 4K r---- dotnet
0000614e1f5a5000 4K rw--- dotnet
0000614e5b5d9000 1051452K rw--- [ anon ]
...
根據(jù) linux 進(jìn)程的內(nèi)存布局,可執(zhí)行image之后是 heap 堆,可以看到 [heap] 約等于1G (614e9b8a8000 - 614e5b5d9000),即 pmap 中的 1051452K。
三、總結(jié)
部署在 Linux上的.NET程序同樣存在 非托管內(nèi)存泄露的問(wèn)題,這篇文章的例子雖然很簡(jiǎn)單,希望能給大家?guī)?lái)一些思考和觀測(cè)途徑吧。