程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> 關於C語言 >> C基礎 內存越界和內存監測的簡單處理,越界監測

C基礎 內存越界和內存監測的簡單處理,越界監測

編輯:關於C語言

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 "\r\n",\
         __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 "\r\n",\
         __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;
}

 測試運行結果如下

最終打印日志是

好. 到這裡 關於內存全局檢測的技巧解釋和實現完畢. 很簡單很好用.

重點是理解上面兩種方式思路.  哈哈, 是不是發現  好神奇的內存洩露, 內存越界, 內存洩露監測也不過如此.

開發, 寫代碼很簡單, 但化為生產力就很難了, 也許需要更多有能力的一起轉換.

 

後記

  錯誤是難免, 歡迎吐槽交流, 拜~~. 希望早睡早起.

 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved