C++内存泄漏检测

news2025/9/15 19:04:24

C++进阶专栏:http://t.csdnimg.cn/aTncz

相关系列文章

C++技术要点总结, 面试必备, 收藏起来慢慢看

C++惯用法之RAII思想: 资源管理

C++智能指针的自定义销毁器(销毁策略)

目录

1.内存泄漏概述

1.1.内存泄漏产生原因

1.2 内存泄漏导致的后果

1.3 内存泄漏解决思路

2.宏定义方法

2.1.宏定义

2.2.检测位置

2.3.结果分析

3.hook方法

3.1.hook

3.2.检测位置

3.3 递归调用

3.4.结果分析

3.5.addr2line

4.__libc_malloc 和 __libc_free


1.内存泄漏概述

1.1.内存泄漏产生原因

内存泄漏是在没有自动 gc 的编程语言里面,经常发生的一个问题。

自动垃圾回收(Automatic Garbage Collection,简称 GC)是一种内存管理技术,在程序运行时自动检测和回收不再使用的内存对象,以避免内存泄漏和释放已分配内存的负担。

因为没有 gc,所以分配的内存需要程序员自己调用释放。其核心原因是调用分配与释放没有符合开闭原则,没有配对,形成了有分配,没有释放的指针,从而产生了内存泄漏。

void myTest(size_t s1)
{
	void a1=malloc(s1);
	void a2=malloc(s1);
	free(a1);
}

以上代码段,分配了两个s1大小的内存块,由 a1 与 a2 指向。而代码块执行完以后,释放了 a1,而 a2 没有释放。形成了有分配没有释放的指针,产生了内存泄漏。

1.2 内存泄漏导致的后果

随着工程代码量越来越多,有分配没有释放,自然会使得进程堆的内存会越来越少,直到耗尽。从而导致后面的运行时代码不能成功分配内存,使程序崩溃。

1.3 内存泄漏解决思路

最好的办法肯定是引入自动垃圾回收gc。但是这不适合C/C++语言。

解决内存泄漏,我们需要解决两点:

1)能够检测出来是否发送内存泄漏

2)如果发生内存泄漏,能够检测出来具体是哪一行代码所引起的。

内存泄漏是由于内存分配与内存释放,不匹配所引起的。因此对内存分配函数malloc/calloc/realloc,以及内存释放函数free进行“劫持”hook,就能能够统计出内存分配的位置,内存释放的位置,从而判断是否匹配。

2.宏定义方法

2.1.宏定义

使用宏定义,替换系统的内存分配接口。并利用__FILE__、__LINE__分别获取当前编译文件的文件名、行号,进行追踪位置信息。

#define malloc(size)    _malloc(size, __FILE__, __LINE__)
#define free(ptr)       _free(ptr, __FILE__, __LINE__)

需要注意的是,宏定义一定要放在内存分配之前,这样预编译阶段才会替换为我们自己实现的_malloc和_free。

2.2.检测位置

为了方便观察,我们可以在内存分配_malloc的时候,创建一个文件。文件名为指向新分配内存的指针值,文件内容为指针值、调用_malloc时的文件名、行号。

在该内存释放_free的时候,删除该指针对应的文件。

最后,程序运行结束,如果没有文件说明没有内存泄漏,否则说明存在内存泄漏。

2.3.结果分析

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void *_malloc(size_t size, const char *filename, int line){
    void *ptr = malloc(size);
    
    char buffer[128] = {0};
    sprintf(buffer, "./memory/%p.memory", ptr);

    FILE *fp = fopen(buffer, "w");
    fprintf(fp, "[+]addr: %p, filename: %s, line: %d\n", ptr, filename, line);

    fflush(fp);
    fclose(fp);

    return ptr;
}

void _free(void *ptr, const char *filename, int line){
    char buffer[128] = {0};
    sprintf(buffer, "./memory/%p.memory", ptr);

    if (unlink(buffer) < 0){
        printf("double free: %p\n", ptr);
        return;
    }

    return free(ptr);
}

#define malloc(size)    _malloc(size, __FILE__, __LINE__)
#define free(ptr)       _free(ptr, __FILE__, __LINE__)

int main() {
    void *p1 = malloc(5);
    void *p2 = malloc(18);
    void *p3 = malloc(15);

    free(p1);
    free(p3);
}

最后在memory文件夹里,可以看到存在一个文件,说明有一个地方出现内存泄漏

23fc643d5acd44c99c23e5c0c0b6e2bf.png

[+]addr: 0x559e55b6e8b0, filename: fun1.c, line: 39

从结果上看,内存泄漏发生第39行。

3.hook方法

利用 hook 机制改写系统的内存分配函数。

3.1.hook

hook方法的实现分三个步骤

1)定义函数指针。

typedef void *(*malloc_t)(size_t size);
malloc_t malloc_f = NULL;

typedef void (*free_t)(void *ptr);
free_t free_f = NULL;

2)函数实现,函数名与目标函数名一致。

void *malloc(size_t size)
{
	//改写的功能
}

void free(void *ptr)
{
	//改写的功能
}

3)初始化hook,调用dlsym()。

void init_hook(){
    if (!malloc_f){
        malloc_f = dlsym(RTLD_NEXT, "malloc");
    }

    if (!free_f){
        free_f = dlsym(RTLD_NEXT, "free");
    }
}

3.2.检测位置

宏定义的方法在检测调用所在行号的时候使用了系统定义的__LINE__,因为是宏定义的malloc,预编译时候直接嵌入。因此__LINE__返回的就是调用malloc的位置。

但是hook方法不一样,系统定义的__LINE__在函数内部调用,无法确定在主函数中的调用位置。比如

fprintf(fp, "[+]addr: %p, filename: %s, line: %d\n", ptr, filename, line);

返回的就是fprintf所在的行号。

因此使用gcc 提供的__builtin_return_address,该函数返回当前函数或其调用者之一的返回地址。参数level 表示向上扫描调用堆栈的帧数。比如对于 main --> f1() --> f2() --> f3() ,f3()函数里面调用 __builtin_return_address (0),返回f3的地址;调用 __builtin_return_address (1),返回f2的地址;

3.3 递归调用

hook的时候,要考虑其他函数也用到所hook住的函数,比如在printf()函数里面也调用了malloc,那么就需要防止内部递归进入死循环。

dfe968f0844c4a8882197a4282565d98.png

通过gdb调试,在第23行打断点,发现每次运行都回到了23行。

这是因为sprintf隐含调用了malloc,这样就陷入一个循环:

23行的sprintf —> 自定义的malloc —> 23行的sprintf —> 自定义的malloc --> 23行的sprintf —> 自定义的malloc --> ……

解决办法是,限制调用次数。当进入 malloc 函数内部后,根据自己的需要,设置 hook 的开关。在关闭的区域内调用 malloc 后进入到 else 部分执行原来的 hook 函数,避免了无限递归的发生。

int enable_malloc_hook = 1;
void *malloc(size_t size) { 
    // 执行改写的 malloc 函数
    if (enable_malloc_hook) {
        enable_malloc_hook = 0;
        // 关闭 hook, printf 内部的 malloc 执行 else 的部分
       // 其他代码
        enable_malloc_hook = 1;
    }
    // 执行原来的 malloc 函数
    else {
        p = malloc_f(size);
    }
}

3.4.结果分析

// gcc -o fun2 fun2.c -ldl -g

#define _GNU_SOURCE
#include <dlfcn.h>

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <link.h>


typedef void *(*malloc_t)(size_t size);
malloc_t malloc_f = NULL;

typedef void (*free_t)(void *ptr);
free_t free_f = NULL;


int enable_malloc_hook = 1;
int enable_free_hook = 1;

void *malloc(size_t size){


    void *ptr = NULL;
    if (enable_malloc_hook ){
        enable_malloc_hook = 0; 
        enable_free_hook = 0;

        ptr = malloc_f(size);

        void *caller = __builtin_return_address(0);

        char buffer[128] = {0};
        sprintf(buffer, "./memory/%p.memory", ptr);

        FILE *fp = fopen(buffer, "w");
        fprintf(fp, "[+] caller: %p, addr: %p, size: %ld\n", caller, ptr, size);

        fflush(fp);
        fclose(fp);

        enable_malloc_hook = 1;
        enable_free_hook = 1;
    }
    else {
        ptr = malloc_f(size);
    }
    return ptr;
}

void free(void *ptr){

    if (enable_free_hook ){
        enable_free_hook = 0;
        enable_malloc_hook = 0;

        char buffer[128] = {0};
        sprintf(buffer, "./memory/%p.memory", ptr);

        if (unlink(buffer) < 0){
            printf("double free: %p\n", ptr);
            return;
        }

        free_f(ptr);

        enable_malloc_hook = 1;
        enable_free_hook = 1;
    }
    else {

        free_f(ptr);
    }
}

void init_hook(){
    if (!malloc_f){
        malloc_f = dlsym(RTLD_NEXT, "malloc");
    }

    if (!free_f){
        free_f = dlsym(RTLD_NEXT, "free");
    }
}
int main(){
    init_hook();

    void *p1 = malloc(5);
    void *p2 = malloc(18);
    void *p3 = malloc(15);

    free(p1);
    free(p3);
}

bb751a65b5174dadb8a8cb1e3cd59486.png

从结果看存在一个内存泄漏,但是 caller:0x16bb 是地址,不是具体行号。使用addr2line可以将地址转换为文件名和行号。

3.5.addr2line

利用addr2line工具,将地址转换为文件名和行号,得到源文件的行数(根据机器码地址定位到源码所在行数)

addr2line -f -e fun2 -a 0x16bb

参数:-f:显示函数名信息。-e filename:指定需要转换地址的可执行文件名。-a address:显示指定地址(十六进制)。

但是,高版本 gcc 下使用 addr2line 命令会出现乱码问题。

??
??:0

addr2line 作用于 ELF 可执行文件,而高版本的 gcc 调用 __builtin_return_address返回的地址 caller 位于内存映像上,所以会产生乱码。

d48e4cbda9b848cc882877e7206bd79a.png

解决办法是利用动态链接库的dladdr函数 ,作用于共享目标,可以获取某个地址的符号信息。使用该函数可以解析符号地址。如下:

// gcc -o fun2 fun2.c -ldl -g

#define _GNU_SOURCE
#include <dlfcn.h>

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <link.h>

// 解析地址
void* converToELF(void *addr) {
    Dl_info info;
    struct link_map *link;
    dladdr1(addr, &info, (void **)&link, RTLD_DL_LINKMAP);
    // printf("%p\n", (void *)(size_t)addr - link->l_addr);
    
    return (void *)((size_t)addr - link->l_addr);
}


typedef void *(*malloc_t)(size_t size);
malloc_t malloc_f = NULL;

typedef void (*free_t)(void *ptr);
free_t free_f = NULL;


int enable_malloc_hook = 1;
int enable_free_hook = 1;

void *malloc(size_t size){


    void *ptr = NULL;
    if (enable_malloc_hook ){
        enable_malloc_hook = 0; 


        ptr = malloc_f(size);

        void *caller = __builtin_return_address(0);

        char buffer[128] = {0};
        sprintf(buffer, "./memory/%p.memory", ptr);

        FILE *fp = fopen(buffer, "w");
        // converToELF(caller)
        fprintf(fp, "[+] caller: %p, addr: %p, size: %ld\n", converToELF(caller), ptr, size);

        fflush(fp);
        fclose(fp);

        enable_malloc_hook = 1;
    }
    else {
        ptr = malloc_f(size);
    }
    return ptr;
}

void free(void *ptr){

    if (enable_free_hook ){
        enable_free_hook = 0;

        char buffer[128] = {0};
        sprintf(buffer, "./memory/%p.memory", ptr);

        if (unlink(buffer) < 0){
            printf("double free: %p\n", ptr);
            return;
        }

        free_f(ptr);

        enable_free_hook = 1;
    }
    else {

        free_f(ptr);
    }
}

void init_hook(){
    if (!malloc_f){
        malloc_f = dlsym(RTLD_NEXT, "malloc");
    }

    if (!free_f){
        free_f = dlsym(RTLD_NEXT, "free");
    }
}
int main(){
    init_hook();

    void *p1 = malloc(5);
    void *p2 = malloc(18);
    void *p3 = malloc(15);

    free(p1);
    free(p3);
}

4.__libc_malloc 和 __libc_free

思路和hook的一样,因为malloc和free底层调用的也是__libc_malloc和__libc_free。

// gcc -o fun3 fun3.c
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <link.h>


void* converToELF(void *addr) {
    Dl_info info;
    struct link_map *link;
    dladdr1(addr, &info, (void **)&link, RTLD_DL_LINKMAP);
    // printf("%p\n", (void *)(size_t)addr - link->l_addr);
    
    return (void *)((size_t)addr - link->l_addr);
}



extern void *__libc_malloc(size_t size);
extern void *__libc_free(void *ptr);


int enable_malloc_hook = 1;
int enable_free_hook = 1;

void *malloc(size_t size){


    void *ptr = NULL;
    if (enable_malloc_hook ){
        enable_malloc_hook = 0; 
        enable_free_hook = 0;

        ptr = __libc_malloc(size);

        void *caller = __builtin_return_address(0);

        char buffer[128] = {0};
        sprintf(buffer, "./memory/%p.memory", ptr);

        FILE *fp = fopen(buffer, "w");
        fprintf(fp, "[+] caller: %p, addr: %p, size: %ld\n", converToELF(caller), ptr, size);

        fflush(fp);
        fclose(fp);

        enable_malloc_hook = 1;
        enable_free_hook = 1;
    }
    else {
        ptr = __libc_malloc(size);
    }
    return ptr;
}

void free(void *ptr){

    if (enable_free_hook ){
        enable_free_hook = 0;
        enable_malloc_hook = 0;

        char buffer[128] = {0};
        sprintf(buffer, "./memory/%p.memory", ptr);

        if (unlink(buffer) < 0){
            printf("double free: %p\n", ptr);
            return;
        }

        __libc_free(ptr);

        enable_malloc_hook = 1;
        enable_free_hook = 1;
    }
    else {

        __libc_free(ptr);
    }
}


int main(){

    void *p1 = malloc(5);
    void *p2 = malloc(18);
    void *p3 = malloc(15);

    free(p1);
    free(p3);
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1496125.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux——线程(2)

在上一篇博客中我介绍了Linux中的线程是什么样的&#xff0c;就如同进程可以通过 fork创建&#xff0c;可以被终止&#xff0c;可以退出一样&#xff0c;线程也可以被我们用户控制&#xff0c;这 篇博客我会介绍线程的控制&#xff0c;并且基于线程的控制所产生的一些问题进行 …

安装系统后,如何单个盘空间扩展多个盘空间?

1、计算机-管理-存储-磁盘空间 2、压缩C盘符&#xff0c;分出多余空间 3、将多余空间扩展&#xff0c;然后修改盘符名称

为什么会不断出现低价窜货链接

品牌在做控价的过程中&#xff0c;会进入一个怪圈&#xff0c;就是不管如何治理&#xff0c;低价、乱价、窜货链接都在不断出现&#xff0c;甚至有些低价链接会占据电商首页的位置&#xff0c;其实这些在一定程度上讲是正常的&#xff0c;品牌在不断发展&#xff0c;链接也是动…

Cyber RT 开发工具

在Cyber RT中还提供了一些工具&#xff0c;这些工具可以拓展Cyber RT功能、提高开发调试效率&#xff0c;本章主要介绍这些工具的使用。 本章内容: 1.cyber record工具的应用&#xff1b; 2.常用命令工具的使用&#xff1b; 学习收获: 1.可以通过cyber record将发布的话题消息…

基础50刷题之一(交替合并字符串)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、题目二、力扣官方题解&#xff08;双指针&#xff09;三、文心一言解释总结 前言 刚上研一&#xff0c;有人劝我好好学C&#xff0c;当时用的不多就没学&a…

Javascript进阶课程——大纲

JavaScript进阶教程_哔哩哔哩_bilibili

Windows下 OracleXE_21 数据库的下载与安装

Oracle 数据库的下载与安装 数据库安装包下载数据库安装访问数据库进行测试Navicat连接数据库 1. 数据库安装包的下载 1.1 下载地址 Oracle Database Express Edition | Oracle 中国 1.2 点击“下载 Oracle Database XE”按钮&#xff0c;进去到下载页面&#xff08;选择对…

图论练习5

Going Home Here 解题思路 模板 二分图最优匹配&#xff0c;前提是有完美匹配&#xff08;即存在一一配对&#xff09;左右集合分别有顶标&#xff0c;当时&#xff0c;为有效边&#xff0c;即选中初始对于左集合每个点&#xff0c;选择其连边中最优的&#xff0c;然后对于每…

uniapp 解决请求出现 /sockjs-node/info?t=问题

1. uniapp请求出现 /sockjs-node/info?t问题 1.1. 问题 uniapp项目老是出现 http://192.168.2.106:8080/sockjs-node/info?t1709704280949 1.1. sockjs-node介绍 sockjs-node 是一个JavaScript库&#xff0c;提供跨浏览器JavaScript的API&#xff0c;创建了一个低延迟、全…

spring 注解缓存查询方法使用

spring-boot项目启动类上加注解: EnableCaching 查询方法注解: Cacheable

基于springboot的精准扶贫管理系统论文

精准扶贫管理系统 摘要 随着信息技术在管理上越来越深入而广泛的应用&#xff0c;管理信息系统的实施在技术上已逐步成熟。本文介绍了精准扶贫管理系统的开发全过程。通过分析精准扶贫管理系统管理的不足&#xff0c;创建了一个计算机管理精准扶贫管理系统的方案。文章介绍了精…

【Java JVM】Class 文件的加载

Java 虚拟机把描述类的数据从 Class 文件加载到内存, 并对数据进行校验, 转换解析和初始化, 最终形成可以被虚拟机直接使用的 Java 类型, 这个过程被称作虚拟机的类加载机制。 与那些在编译时需要进行连接的语言不同, 在 Java 语言里面, 类的加载, 连接和初始化过程都是在程序…

SpringBoot整合【RocketMQ】

目录 1.POM文件添加依赖及yml配置 2.RocketmqUtil 3.生产者&#xff08;异步发送示例&#xff09; 4.消费者 5.测试 1.POM文件添加依赖及yml配置 <dependency><groupId>org.apache.rocketmq</groupId><artifactId>rocketmq-spring-boot-starter&l…

html--3D爱心

文章目录 代码效果 代码 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>爱心</title><style type"text/css">*{margin: 0px;border: 0px;}body{overflow: hidden;background-…

简单整理vue-router,路由知识

1.项目中引入 1.1 安装注册 1.2 封装抽离 在main.js中 书写,会造成单个js文件过于臃肿的情况,需要将路由配置部分抽离出来,在src下新建router文件夹,新建index.js文件 import Vue from vue import VueRouter from vue-router import HomeView from ../views/HomeView.vue im…

Wireshark——获取指定协议的数据包

1、问题 使用Wireshark捕获了大量的数据包&#xff0c;但是只想要指定协议的数据包。 2、方法 例如&#xff0c;只想要Modbus/TCP协议的数据包。 在应用显示过滤器中输入协议的名称&#xff08;小写&#xff09;&#xff0c;回车。 选择文件&#xff0c;导出特定分组。 将所…

Matlab 机器人工具箱 RobotArm类

文章目录 1 RobotArm1.1 方法1.2 注意2 RobotArm.RobotArm3 RobotArm.cmove4 其他官网:Robotics Toolbox - Peter Corke 1 RobotArm 串联机械臂类 1.1 方法 方法描述plot显示机器人的图形表示teach驱动物理和图形机器人mirror使用机器人作为从机来驱动图形</

影响哈默纳科Harmonic减速机使用寿命的5大因素

哈默纳科HarmonicDrive减速机以其轻量、小型、传动效率高、减速范围广、精度高等特点&#xff0c;被广泛应用于各种传动系统中。然而&#xff0c;尽管哈默纳科Harmonic减速机具有诸多优势&#xff0c;但其使用寿命仍可能受到多种因素的影响。 首先&#xff0c;环境因素对哈默纳…

【ESP32 IDF快速入门】点亮第一个LED灯与流水灯

文章目录 前言一、有哪些工作模式&#xff1f;1.1 GPIO的详细介绍1.2 GPIO的内部框图输入模式输出部分 二、GPIO操作函数2.1 GPIO 汇总2.2 GPIO操作函数gpio_config配置引脚reset 引脚函数设置引脚电平选中对应引脚设置引脚的方向 2.3 点亮第一个灯 三、流水灯总结 前言 ESP32…

基于深度学习的苹果叶片病害检测系统(含UI界面、yolov8、Python代码、数据集)

项目介绍 项目中所用到的算法模型和数据集等信息如下&#xff1a; 算法模型&#xff1a;     yolov8 yolov8主要包含以下几种创新&#xff1a;         1. 可以任意更换主干结构&#xff0c;支持几百种网络主干。 数据集&#xff1a;     网上下载的数据集&#x…