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

开发 前端
在 windows 平台上,我们常常在 C++ 代码中用 extern "C"​ 导出 C风格 的函数,然后在 C# 中用 DllImport 的方式引入,那在 Linux 上怎么玩的?

一、背景 

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.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# 代码调用

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

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# 项目,然后将 libmyleak.so 放到 C#项目的 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 运行:

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

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

图片图片

那这 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 ]
...

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

三、总结 

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

责任编辑:武晓燕 来源: 一线码农聊技术
相关推荐

2009-08-28 10:14:45

C#内存泄露

2009-08-31 16:33:28

C#调用Dispose

2009-08-31 18:05:14

C#调用WalkTre

2009-08-03 12:57:27

C#调用DLL

2009-09-01 11:04:59

C#调用扩展方法

2009-08-20 10:53:23

C#操作内存

2009-09-11 10:59:06

Effective C调用Dispose()

2009-08-05 09:40:02

C#调用DLL函数

2009-08-05 09:30:39

C#调用DLL函数

2009-09-02 10:49:46

C#调用析构方法

2009-08-26 16:58:12

调用C# Thread

2011-04-08 09:52:44

C++C#DLL

2009-08-24 15:58:00

Visual C#生成

2015-04-02 16:23:50

C++内存泄露检查

2009-08-17 16:18:47

C#调用CreateO

2009-08-05 16:29:18

C#调用C++动态链接

2009-08-11 11:07:49

Java调用C# we

2024-05-16 12:33:37

C#编程指针

2009-08-17 15:34:58

C#创建XML

2009-08-20 16:07:39

C#和ADO.NET访
点赞
收藏

51CTO技术栈公众号