写点什么

Linux 系列:如何用 C#调用 C 方法造成内存泄露

作者:秃头小帅oi
  • 2025-03-04
    福建
  • 本文字数:2705 字

    阅读完需:约 9 分钟

Linux系列:如何用 C#调用 C方法造成内存泄露

一:背景

1. 讲故事

好久没写文章了,还是来写一点吧,今年准备多写一点 Linux 平台上的东西,这篇从 C# 调用 C 这个例子开始。在 windows 平台上,我们常常在 C++ 代码中用 extern "C" 导出 C 风格 的函数,然后在 C# 中用 DllImport 的方式引入,那在 Linux 上怎么玩的?毕竟这对研究 Linux 上的 C# 程序非托管内存泄露有非常大的价值,接下来我们就来看下。

二:一个简单的非托管内存泄露

1. 构建 so 文件

在 Windows 平台上我们会通过 MSVC 编译器将 C 代码编译出一个成品 .dll,在 Linux 上通常会借助 gcc 将 c 编译成 .so 文件,这个.so 全称 Shared Object,为了方便讲解,先上一段简单的代码:

#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");}

复制代码

接下来使用 gcc 编译,参考如下:

gcc -shared -o libmyleak.so -fPIC myleak.c

复制代码
  • -shared: 编译成共享库

  • -fPIC: 指定共享库可以在内存任意位置被加载(地址无关性)

命令执行完之后,就可以看到一个 .so 文件了,截图如下:



最后可以用 nm 命令验证下 libmyleak.so 中是否有 Text 段下的 heapmalloc 导出函数。

root@ubuntu2404:/data2/c# nm libmyleak.so0000000000004028 b completed.0                 w __cxa_finalize@GLIBC_2.2.500000000000010c0 t deregister_tm_clones0000000000001130 t __do_global_dtors_aux0000000000003e00 d __do_global_dtors_aux_fini_array_entry0000000000004020 d __dso_handle0000000000003e08 d _DYNAMIC000000000000125c t _fini0000000000001170 t frame_dummy0000000000003df8 d __frame_dummy_init_array_entry00000000000020f8 r __FRAME_END__0000000000003fe8 d _GLOBAL_OFFSET_TABLE_                 w __gmon_start__000000000000203c r __GNU_EH_FRAME_HDR0000000000001179 T heapmalloc0000000000001000 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.500000000000010f0 t register_tm_clones                 U __stack_chk_fail@GLIBC_2.40000000000004028 d __TMC_END__

复制代码

2. C# 代码调用

so 构建好了之后,后面就比较好说了,使用 dotnet new console -n CSharpApplication --use-program-main true 新建一个 CS 项目。

root@ubuntu2404:/data2/csharp# dotnet new console -n CSharpApplication --use-program-main trueThe 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# 项目,然后将 libmyleak.so 放到 C#项目的 bin 目录,修改 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(); }}

复制代码

最后用 dotnet CSharpApplication.dll 运行:

root@ubuntu2404:/data2/csharp/CSharpApplication/bin/Debug/net8.0# dotnet CSharpApplication.dll已经分配 1GB 内存在堆上!

复制代码

程序是跑起来了,那真的是吃了 1G 呢? 可以先用 htop 观察程序,从截图看没毛病。



那这 1G 真的在 heap 上吗? 可以用 maps 观察。

root@ubuntu2404:~# ps -ef | grep CSharproot       10764   10730  0 13:35 pts/21   00:00:00 dotnet CSharpApplication.dllroot       11049   11027  0 13:41 pts/22   00:00:00 grep --color=auto CSharp
root@ubuntu2404:~# cat /proc/10764/maps614e1f592000-614e1f598000 r--p 00000000 08:02 1479867 /usr/lib/dotnet/dotnet614e1f598000-614e1f5a4000 r-xp 00005000 08:02 1479867 /usr/lib/dotnet/dotnet614e1f5a4000-614e1f5a5000 r--p 00010000 08:02 1479867 /usr/lib/dotnet/dotnet614e1f5a5000-614e1f5a6000 rw-p 00010000 08:02 1479867 /usr/lib/dotnet/dotnet614e5b5d9000-614e9b8a8000 rw-p 00000000 00:00 0 [heap]...

root@ubuntu2404:~# pmap 1076410764: dotnet CSharpApplication.dll0000614e1f592000 24K r---- dotnet0000614e1f598000 48K r-x-- dotnet0000614e1f5a4000 4K r---- dotnet0000614e1f5a5000 4K rw--- dotnet0000614e5b5d9000 1051452K rw--- [ anon ]...

复制代码

根据 linux 进程的内存布局,可执行 image 之后是 heap 堆,可以看到 [heap] 约等于1G (614e9b8a8000 - 614e5b5d9000),即 pmap 中的 1051452K。

三:总结

部署在 Linux 上的.NET 程序同样存在 非托管内存泄露 的问题,这篇文章的例子虽然很简单,希望能给大家带来一些思考和观测途径吧。

行业拓展

分享一个面向研发人群使用的前后端分离的低代码软件——JNPF,适配国产化,支持主流数据库和操作系统。

提供五十几种高频预制组件,包括表格、图表、列表、容器、表单等,内置常用的后台管理系统使用场景和基本需求,配置了流程引擎、表单引擎、报表引擎、图表引擎、接口引擎、门户引擎、组织用户引擎等可视化功能引擎,超过数百种功能控件以及大量实用模板,使得在拖拉拽的简单操作下,也能完成开发。

对于工程师来说,灵活的使用高质量预制组件可以极大的节省时间,将更多精力花费在更有创造性和建设性的代码上。

用户头像

摸个鱼,顺便发点有用的东西 2023-06-19 加入

互联网某厂人(重生版)

评论

发布
暂无评论
Linux系列:如何用 C#调用 C方法造成内存泄露_秃头小帅oi_InfoQ写作社区