• C基础 内存越界和内存监测的简单处理


    引言

      突然感觉要出去走走了, 醒了后 刷完牙就在联系coding, 不知不觉到了 黄昏.

    看看天, 打开灯. 又感觉到了 夜夜夜夜 .

    13年到北京务工, 遇到一批批NB的同龄人物. 一块工作, 一块喜欢锻炼, 一块默默的学习.

    从他(她)们身上发现一个事实.

    假如我们一样聪明,

      当你抱怨自己为什么努力了, 确还是 这么水的时候  ;   其实他(她)们在拼命. 而你只是在努力 ,

    假如我们不一样聪明,

      如果还不能开挂,  那会是怎么样精彩 x x x x.

    前言  -  内存越界处理

    我们先看设计图. 内存越界检查原理如下

    上面原理是不是很简单. 而这恰恰是最通用的做法. 美的东西不负责.  美很重要.

    那我们按照上面设计思路. 首先构建 接口文件 checkmem.h

    #ifndef _H_MEMCHECK_CHECKMEM
    #define _H_MEMCHECK_CHECKMEM
    
    #include <stddef.h>
    
    /*
    * 对malloc进行的封装, 添加了边界检测内存块
    * (inline 原本分_DEBUG有宏处理, 后面没加等于没用)
    * sz        : 申请内存长度
    *            : 返回得到的内存首地址
    */
    extern inline void* mc_malloc(size_t sz);
    
    /*
     * 对calloc进行封装, 添加边界检测内存块
     * cut        : 申请的个数
     * sz        : 每个的大小
     */
    extern inline void* mc_calloc(size_t cut, size_t sz);
    
    /*
    * 对relloc进行了封装, 同样添加了边间检测内存块
    */
    extern inline void* mc_realloc(void* ptr, size_t sz);
    
    /*
    * 对内存检测, 看是否出错, 出错直接打印错误信息
    * 只能检测, check_* 得到的内存
    */
    extern inline void mc_check(void* ptr);
    
    #endif // !_H_MEMCHECK_CHECKMEM

     主要是对 malloc, calloc, realloc 进行添加尾部和头部的内存块处理. 就这么简单一步. 假如能看懂上面设计思路图.

    这些代码都可以跳过了.   思路比代码重要.  好那我们继续展现实现部分. checkmem.c

    #include "checkmem.h"
    #include <stdio.h>
    #include <errno.h>
    #include <string.h>
    #include <stdlib.h>
    
    // 控制台打印错误信息, fmt必须是双引号括起来的宏
    #define CERR(fmt, ...) 
        fprintf(stderr,"[%s:%s:%d][error %d:%s]" fmt "
    ",
             __FILE__, __func__, __LINE__, errno, strerror(errno), ##__VA_ARGS__)
    //控制台打印错误信息并退出, t同样fmt必须是 ""括起来的字符串常量
    #define CERR_EXIT(fmt,...) 
        CERR(fmt,##__VA_ARGS__),exit(EXIT_FAILURE)
    
    // 插入字节块的个数
    #define _INT_CHECK        (1<<4)
    
    /*
    * 对malloc进行的封装, 添加了边界检测内存块
    * sz        : 申请内存长度
    *            : 返回得到的内存首地址
    */
    inline void* 
    mc_malloc(size_t sz) {
        // 头和尾都加内存检测块, 默认0x00
        char* ptr = calloc(1, sz + 2 * _INT_CHECK);
        if (NULL == ptr) {
            CERR_EXIT("malloc sz + sizeof struct check is error!");
        }
    
        //前四个字节保存 最后一个内存块地址 大小
        size_t* iptr = (size_t*)ptr;
        *iptr = sz + _INT_CHECK;
    
        return ptr + _INT_CHECK;
    }
    
    /*
    * 对calloc进行封装, 添加边界检测内存块
    * cut        : 申请的个数
    * sz        : 每个的大小
    */
    inline void* 
    mc_calloc(size_t cut, size_t sz) {
        return mc_malloc(cut*sz);
    }
    
    /*
    * 对relloc进行了封装, 同样添加了边间检测内存块
    */
    inline void* 
    mc_realloc(void* ptr, size_t sz) {
        // 先检测一下内存
        mc_check(ptr);
    
        // 重新申请内存
        char* cptr = (char*)ptr - _INT_CHECK;
        char* nptr = calloc(1, sz + 2 * _INT_CHECK);
        if (NULL == nptr) {
            CERR_EXIT("realloc is error:%p.", ptr);
        }
        // 内存移动
        size_t* bsz = (size_t*)cptr;
        memcpy(nptr, cptr, *bsz < sz ? *bsz : sz);
        *bsz = sz;
        
        free(cptr);
        return nptr;
    }
    
    // 检测内存是否错误, 错误返回 true, 在控制台打印信息
    static void _iserror(char* s, char* e) {
        while (s < e) {
            if (*s) {
                CERR_EXIT("Need to debug test!!! ptr is : (%p, %p).check is %d!",s, e, *s);
            }
            ++s;
        }
    }
    
    /*
    * 对内存检测, 看是否出错, 出错直接打印错误信息
    * 只能检测, check_* 得到的内存
    */
    inline void 
    mc_check(void* ptr) {
        char *sptr = (char*)ptr - _INT_CHECK;
    
        //先检测头部
        char* s = sptr + sizeof(size_t);
        char* e = sptr + _INT_CHECK;
        _iserror(s, e);
    
        //后检测尾部
        size_t sz = *(size_t*)sptr;
        s = sptr + sz;
        e = s + _INT_CHECK;
        _iserror(s, e);
    }

    代码实现都很中规中矩, 比较容易. 也就百行. 按照接口文件一个个看实现. 很容易学到开发中技巧. 提高实战技巧.

    扯一点, C, C++ 老开发人员水平都比较高, 不喜欢写注释. 这个强烈推荐不是大牛的选手一定要多写注释.

    不要扯 什么  <代码即注释> . 多写注释容易加深自己二次思考, 加快自己的成长. 不要和老开发人学这个 , 如果你跳槽, 遇到一个大项目

    注释等价无, 你是什么感受. 为我们多留条后路, 多写注释.

    好 看测试代码 main.c

    #include <stdio.h>
    #include <stdlib.h>
    #include "checkmem.h"
    
    /*
     * 演示一种检测内存越界的办法
     * 添加上下限方式
     */
    int main(int argc, char* argv[]) {
    
        // 实验步骤是, 是申请内存, 在操作内存
        char* as = mc_malloc(16);
    
        mc_check(as);
    
        // 内存越界了
        //as[16] = 18;
        //mc_check(as);
    
        // 重新分配内存, 再次越界
        as = mc_realloc(as, 15);
        as[15] = 44;
        mc_check(as);
    
        free(as);
        return 0;
    }

     测试结果

    到这里内存越界的思路和实现都已经完毕了.欢迎思考尝试.

    正文 - 内存全局监测

      内存全局检测思路更简单. 采用引用'计数方式'处理. 扯一点很多自动垃圾回收机制都采用了引用计数方式.

    包括内核层例如 文件描述符, IPC 共享内存, 消息机制等.  先看接口 memglobal.h

    #ifndef _H_MEMGLOBAL_MEMGLOBAL
    #define _H_MEMGLOBAL_MEMGLOBAL
    
    #include <stddef.h>
    #include <stdlib.h>
    
    /*
     * 全局启动内存简单监测
     */
    extern inline void mg_start(void);
    
    /*
     * 增加的全局计数的 malloc
     * sz        : 待分配内存大小
     *            : 返回分配的内存首地址
     */
    extern void* mg_malloc(size_t sz);
    
    /*
     * 增加了全局计数的 calloc
     * sc        : 分配的个数
     * sz        : 每个分配大小
     *            : 返回分配内存的首地址
     */
    extern inline void* mg_calloc(size_t sc, size_t sz);
    
    /*
     * 增加了计数的 realloc
     * ptr        : 上一次分配的内存地址
     * sz        : 待重新分配的内存大小
     *            : 返回重新分配好的内存地址
     */
    extern void* mg_realloc(void* ptr, size_t sz);
    
    /*
     * 增加了计数处理的内存 free
     * ptr        : 上面函数返回地址的指针
     */
    extern inline void mg_free(void* ptr);
    
    // 在测试模式下开启 全局内存使用计数
    #if defined(_DEBUG)
    #    define malloc        mg_malloc
    #    define calloc        mg_calloc
    #    define realloc        mg_realloc
    #    define free            mg_free
    #else
    #    define malloc        malloc
    #    define calloc        calloc
    #    define realloc        realloc
    #    define free            free
    #endif
    
    #endif // !_H_MEMGLOBAL_MEMGLOBAL

     还是比较优美的. 再看 memglobal.c

    #include "memglobal.h"
    #include <stdio.h>
    #include <errno.h>
    #include <string.h>
    #include <stdlib.h>
    
    // 取消内置宏, 防止递归
    #undef malloc
    #undef calloc
    #undef realloc
    #undef free
    
    // 控制台打印错误信息, fmt必须是双引号括起来的宏
    #define IOERR(io, fmt, ...) 
        fprintf(io,"[%s:%s:%d][error %d:%s]" fmt "
    ",
             __FILE__, __func__, __LINE__, errno, strerror(errno), ##__VA_ARGS__)
    
    // 全局内存计数, 系统第一次构造的时候为0
    static int _mct;
    
    #define _STR_MGTXT    "checkmem.log"
    
    // mg内存监测退出时候, 记录一些信息
    static void _mg_exit(void) {
        if (_mct == 0) return;
    
        // 先打印信息到控制台
        IOERR(stderr, "Detect memory leaks _mct = %d!!", _mct);
    
        //输出到文件
        FILE* txt = fopen(_STR_MGTXT, "a");
        if (txt == NULL) {
            IOERR(stderr, "fopen " _STR_MGTXT " a is error!");
            return;
        }
        IOERR(txt, "Detect memory leaks _mct = %d!!", _mct);
        fclose(txt);
    }
    
    /*
    * 全局启动内存简单监测
    */
    inline void 
    mg_start(void) {
        // 注册退出监测事件
        atexit(_mg_exit);
    }
    
    /*
    * 增加的全局计数的 malloc
    * sz        : 待分配内存大小
    *            : 返回分配的内存首地址
    */
    void* 
    mg_malloc(size_t sz) {
        void* ptr = malloc(sz);
        if (!ptr) return NULL;
        ++_mct;
        memset(ptr, 0x00, sz);
        return ptr;
    }
    
    /*
    * 增加了全局计数的 calloc
    * sc        : 分配的个数
    * sz        : 每个分配大小
    *            : 返回分配内存的首地址
    */
    inline void* 
    mg_calloc(size_t sc, size_t sz) {
        return mg_malloc(sc*sz);
    }
    
    /*
    * 增加了计数的 realloc
    * ptr        : 上一次分配的内存地址
    * sz        : 待重新分配的内存大小
    *            : 返回重新分配好的内存地址
    */
    void* 
    mg_realloc(void* ptr, size_t sz) {
        if (!ptr) return mg_malloc(sz);
        return realloc(ptr, sz);
    }
    
    /*
    * 增加了计数处理的内存 free
    * ptr        : 上面函数返回地址的指针
    */
    inline void
    mg_free(void* ptr) {
        if (!ptr) return;
        --_mct;
        free(ptr);
    }

     中间用了

    // 取消内置宏, 防止递归
    #undef malloc
    #undef calloc
    #undef realloc
    #undef free

     这个主要为了解决 引用了 头文件 memglobal.h 会造成递归调用. Linux上还有一种思路, 不包含这个头文件

    链接时候gcc 指定就可以.  但是 vs 是自动推导编译, 如果不引入它推导不出来. 后面就采用了上面通用的做法.

    上面思路是, 先启动 全局内存监测功能, 再通过特殊宏,替代原先的申请和释放函数. 来达到目的.

    测试文件 main.c

    #include <stdio.h>
    #include <stdlib.h>
    #include "memglobal.h"
    
    /*
     * 内存全局计数, 检测内存是否越界
     */
    int main(int argc, char* argv[]) {
        
        // 开启内存全局计数
        mg_start();
    
        int *p = malloc(16);
    
        p = calloc(12, 2);
        *p = 154;
    
        puts("就这样!");
    
        p = realloc(NULL, 6);
        puts("测试这样行!");
        
        return 0;
    }

     测试运行结果如下

    最终打印日志是

    好. 到这里 关于内存全局检测的技巧解释和实现完毕. 很简单很好用.

    重点是理解上面两种方式思路.  哈哈, 是不是发现  好神奇的内存泄露, 内存越界, 内存泄露监测也不过如此.

    开发, 写代码很简单, 但化为生产力就很难了, 也许需要更多有能力的一起转换.

    后记

      错误是难免, 欢迎吐槽交流, 拜~~. 希望早睡早起.

  • 相关阅读:
    Java 7的javax.net.ssl.SSLHandshakeException
    Oracle数据泵导出数据库
    ORA-00054: 资源正忙 --锁表的解决方法
    Linux学习私人笔记-Shell基础
    Linux学习私人笔记-目录和文件的基本命令
    Linux学习私人笔记-文件基本命令
    Linux学习私人笔记-账号管理
    Linux学习私人笔记-Vim
    form提交表单时本地下载
    SQL Servel 中分页解决
  • 原文地址:https://www.cnblogs.com/life2refuel/p/5495567.html
Copyright © 2020-2023  润新知