【C/C++】什么是内存泄漏?如何检测内存泄漏?

一、内存泄漏概述

1.1 什么是内存泄漏

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

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

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

void func(size_t s1)
{
	void p1=malloc(s1);
	void p2=malloc(s1);
	free(p1);
}

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

1.2 内存泄漏导致的后果

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

1.3 内存泄漏解决思路

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

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

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

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

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

二、宏定义方法

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文件夹里,可以看到存在一个文件,说明有一个地方出现内存泄漏

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

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

相关视频推荐

c/c++后端服务器开发必学基础组件设计与实现,实操+手写代码,2024跳槽涨薪必备教程!内容包含(线程池、内存池、多线程锁、死锁检测、内存泄漏、分布式锁..)icon-default.png?t=N7T8https://www.bilibili.com/video/BV11Z421v7Vx/

免费学习地址:Linux C/C++开发(后端/音视频/游戏/嵌入式/高性能网络/存储/基础架构/安全)

需要C/C++ Linux服务器架构师学习资料加qun579733396获取(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享

三、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,那么就需要防止内部递归进入死循环。

通过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);

}

从结果看存在一个内存泄漏,但是 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 位于内存映像上,所以会产生乱码。

解决办法是利用动态链接库的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);

}

四、__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);

}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

MySQL进阶-----limit、count、update优化

目录 前言 一、limit优化 1. 未优化案例 2.优化后案例 二、count优化 count用法 三、update优化 1.锁行情况&#xff08;有索引&#xff09; 2.锁表情况&#xff08;无索引&#xff09; 前言 上一期我们学习了order by优化和group by优化&#xff0c;本期我们就继续学习…

不需要GPU就可以玩转模型,同时支持本地化部署

简单一款不需要GPU就可以在Win 机器跑的模型&#xff1a;Ollama&#xff1b;用于本地运行和部署大型语言模型&#xff08;LLMs&#xff09;的开源工具 关于Ollama的简要介绍 平台兼容性&#xff1a;Ollama支持多种操作系统&#xff0c;包括macOS、Linux和Windows&#xff0c;…

Spectre漏洞 v2 版本再现,影响英特尔 CPU + Linux 组合设备

近日&#xff0c;网络安全研究人员披露了针对英特尔系统上 Linux 内核的首个原生 Spectre v2 漏洞&#xff0c;该漏洞是2018 年曝出的严重处理器“幽灵”&#xff08;Spectre&#xff09;漏洞 v2 衍生版本&#xff0c;利用该漏洞可以从内存中读取敏感数据&#xff0c;主要影响英…

一维非线性扩展卡尔曼滤波|matlab的EKF程序|一维例程源代码

为了满足不同条件下的用途,编了一个简单的一维状态量下的EKF,后面准备出UKF和CKF的版本。 使用的系统是非线性的,以体现算法对于非线性系统的性能。(状态方程和观测方程均设计成非线性的) 程序运行截图 程序都在一个m文件里面,粘贴到matlab的编辑器就能运行,如果中文注…

vivado 写入 ILA 探针信息、读取 ILA 探针信息

写入 ILA 探针信息 “调试探针 (Debug Probes) ”窗口中的“ ILA 核 (ILA Cores) ”选项卡视图包含有关您在自己的设计中使用 ILA 核探测的 信号线的信息。此 ILA 探针信息提取自您的设计 &#xff0c; 并存储在数据文件内 &#xff0c; 此数据文件通常带有 .ltx 文件扩…

React 集成三方登录按钮样式的插件库

按钮不提供任何社交逻辑。 效果如下&#xff1a; 原地址&#xff1a;https://www.npmjs.com/package/react-social-login-buttons 时小记&#xff0c;终有成。

基于注解以及配置类使用SpringIoc

四 基于注解方式使用SpringIoc 和 XML 配置文件一样&#xff0c;注解本身并不能执行&#xff0c;注解本身仅仅只是做一个标记&#xff0c;具体的功能是框架检测到注解标记的位置&#xff0c;然后针对这个位置按照注解标记的功能来执行具体操作。 本质上&#xff1a;所有一切的…

UML简单小结

文章目录 一、UML概述二、UML建模工具三、类图1、概念2、组成 四、类与类之间的关系1、继承2、实现3、依赖4、关联5、聚合6、组合 五、常见UML图1、用例图1&#xff09; 概念2&#xff09;组成3&#xff09;用例图所包含的的关系关联(Association)泛化(Inheritance)包含(Includ…

web笔记再整理

前四天笔记在此连接: web前端笔记表单练习题五彩导航栏练习题-CSDN博客https://blog.csdn.net/simply_happy/article/details/136917265?spm1001.2014.3001.5502 # 1.边框弧度​ div {​ width: 300px;​ height: 50px;​ background-color: aqua;​ …

sql注入之时间注入

一、时间注入 时间注入又名延时注入&#xff0c;属于盲注入的一种&#xff0c;通常是某个注入点无法通过布尔型注入获取数据&#xff0c;而采用一种突破注入的技巧。 在 mysql 里 函数 sleep() 是延时的意思&#xff0c;sleep(10)就是数据库延时 10 秒返回内容。判断注入可以使…

G2D图像处理硬件调用和测试-基于米尔-全志T113-i开发板

本篇测评由电子工程世界的优秀测评者“jf_99374259”提供。 本文将介绍基于米尔电子MYD-YT113i开发板的G2D图像处理硬件调用和测试。 MYC-YT113i核心板及开发板 真正的国产核心板&#xff0c;100%国产物料认证 国产T113-i处理器配备2*Cortex-A71.2GHz &#xff0c;RISC-V 外置…

Selenium自动化测试网页加载太慢如何解决?

&#x1f345; 视频学习&#xff1a;文末有免费的配套视频可观看 &#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 遇到网页加载慢&#xff0c;selenium运行效率降低&#xff0c;可以通过修改页面加载策略提升自动…

docker-compose yaml指定具体容器网桥ip网段subnet;docker创建即指定subnet;docker取消自启动

1、docker-compose yaml指定具体容器网桥ip网段subnet docker-compose 启动yaml有时可能的容器网段与宿主机的ip冲突导致宿主机上不了网&#xff0c;这时候可以更改yaml指定subnet 宿主机内网一般是192**&#xff0c;这时候容器可以指定172* version: 3.9 services:coredns:…

Django之rest_framework(四)

扩展的视图类介绍 rest_framework提供了几种后端视图(对数据资源进行增删改查)处理流程的实现,如果需要编写的视图属于这几种,则视图可以通过继承相应的扩展类来复用代码,减少自己编写的代码量 官网:3 - Class based views - Django REST framework rest_framework.mixi…

比特币突然暴跌

作者&#xff1a;秦晋 周末愉快。 今天给大家分享两则比特币新闻&#xff0c;也是两个数据。一则是因为中东地缘政治升温&#xff0c;传统资本市场的风险情绪蔓延至加密市场&#xff0c;引发加密市场暴跌。比特币跌至66000美元下方。杠杆清算金额高达8.5亿美元。 二则是&#x…

【Node.js】Express学习笔记(黑马)

目录 初识 ExpressExpress 简介Express 的基本使用托管静态资源nodemon Express 路由路由的概念路由的使用 Express 中间件中间件的概念Express 中间件的初体验中间件的分类 初识 Express Express 简介 什么是 Express&#xff1f; 官方给出的概念&#xff1a;Express 是基于…

书生·浦语大模型全链路开源体系-第3课

书生浦语大模型全链路开源体系-第3课 书生浦语大模型全链路开源体系-第3课相关资源RAG 概述在 InternLM Studio 上部署茴香豆技术助手环境配置配置基础环境下载基础文件下载安装茴香豆 使用茴香豆搭建 RAG 助手修改配置文件 创建知识库运行茴香豆知识助手 在茴香豆 Web 版中创建…

Windows 10明年退役

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、Windows 10 生命周期终止&#xff0c;收费预警二、收费标准&#xff1a;基于云的方式&#xff0c;使用 Windows 10 的 PC 也能接收 ESU高定价背后还是主推 …

如何查看个人大数据信用报告?查询报告哪家好呢?

大数据信用报告是现代社会中非常重要的信用评估工具&#xff0c;对于个人来说也具有非常重要的意义。那么&#xff0c;如何查看个人大数据信用报告?查询报告哪家好呢?本文将为您介绍。 首先&#xff0c;查看个人大数据信用报告需要了解报告的内容和格式 一般来说&#xff0c;…

vite+vue3+antDesignVue 记录-持续记录

记录学习过程 持续补充 每天的学习点滴 开始时间2024-04-12 1&#xff0c;报错记录 &#xff08;1&#xff09;env.d.ts文件 解决方法&#xff1a; 在env.d.ts文件中添加以下代码&#xff08;可以看一下B站尚硅谷的讲解视频&#xff09; declare module *.vue {import { Defi…