歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux基礎 >> 關於Linux

linux中內存洩漏的檢測(四)記錄洩漏的大小

《linux中內存洩漏的檢測(三)定制化的new/delete》講到,利用C++的函數重載的特性,使C++的代碼,也能方便地為new/delete加上用於檢測內存洩漏的統計代碼。然而,也因此引入的新的問題。

目前的統計方式僅僅統計申請/釋放內存的次數,並沒有統計每次申請/釋放內存的大小。
這種方法對於C來說是夠用了,因為在C中申請和釋放的大小是相同的,而在C++中就不一定了。
考慮以下兩種情況:

(1)申請了子類的空間卻只釋放了父類的空間

father *pF = new son;
delete pF;

構造子類的時候申請的是子類所需大小的空間,然後先初始化父類的成員,再初始化子類的成員。

析構的時候,由於是父類的指針,只調用父類的析構函數並釋放父類所占的空間。
不是說多態嗎?既然pF指針子類,為什麼不調用子類的析構函數?
因為多態的前提是虛函數。

正常情況下類的析構函數都應該寫成虛函數,如果忘了,就有可能造成內存洩漏。

(2)申請了一個數組的空間卻只釋放第一項元素的空間

class A *pA = new class[5];
delete pA;

也不是所有這樣的情況都會導致內存洩漏,如果class是一個內置類型,像int, char這種,就沒有問題。對於內置類型,只能說沒有內存洩漏方面,但有可能會有其它未知的潛在問題,所以仍不建議這麼寫。
在C++中,class就不限於內置類型了,如果是自己定義的類,delete pA只是釋放pA所指向的數組的第一項,這樣就產生了內存洩漏。

由於以上原因,僅僅統計申請/釋放的次數,還不能准確地檢測內存洩漏的情況,因此,在申請/釋放的同時,還要記錄大小。

大家在寫代碼的時候,有沒有產生過這樣的疑問,為什麼申請內存時要傳入所需要申請的內存大小,而釋放時不需要說明釋放多大的內存?

那是因為在申請時,把所申請的大小記在了某個地方,釋放時從對應的對方查出大小。那麼記在什麼地方呢?

一般有兩種方式:

1 非入侵式,內存分配器自行先申請內存(和棧配合使用),用作記錄用戶層的申請記錄(地址,大小)。 用戶釋放空間時會查找該表,除了知道釋放空間大小外還能判斷該指針是合法。

2 入侵式,例如用戶要申請1byte的內存,而內存分配器會分配5byte的空間(32位),前面4byte用於申請的大小。釋放內存時會先向前偏移4個byte找到申請大小,再進行釋放。

兩種方法各有優缺點,第一種安全,但慢。第二種快但對程序員的指針控制能力要求更高,稍有不慎越界了會對空間信息做成破壞。

我們linux上的gcc/g++編譯器默認使用入侵式,為了驗證我們找到的地址是否存儲了我們想要的數據,我寫了這樣的測試代碼:

#include 
using namespace std;

#if(defined(_X86_) && !defined(__x86_64))
#define _ALLOCA_S_MARKER_SIZE 4
#elif defined(__ia64__) || defined(__x86_64)
#define _ALLOCA_S_MARKER_SIZE 8
#endif

int main(void)
{
    void * p = NULL;
    int a = 5, n = 1;
    while (a--)
    {
        p = new char[n];
        size_t w = *((size_t*)((char*)p -  _ALLOCA_S_MARKER_SIZE));
        cout<<"w = "<< w <<" n = "<

這是運行結果:

w = 33 n = 1

w = 33 n = 10

w = 113 n = 100

w = 1009 n = 1000

w = 10017 n = 10000

當我們讀取申請到的內存的前面幾個字節時,查到的數據與真實申請的數據好像有關系,但是又總是略大一點。這是不是我們要找的數據呢?它和真實申請的大小有什麼關系呢?這要從gcc的內存分配策略說起。

假設現在要申請空間大小為n,實際分配的大小為m,我們讀取到的值為k

(1)當調用malloc申請n個大小的空間,編譯器還會多分配_ALLOCA_S_MARKER_SIZE個字節用於存儲這片空間的管理信息。在我所測試的centos 64上這個管理信息一共8個字節,上文提到的申請空間的大小的信息就在其中。那麼m=n+_ALLOCA_S_MARKER_SIZE

(2)為了減少內存碎片,實現申請的大小為一個數的整數倍,在我所測試的centos 64上測得這個數為16,即實際申請的大小為16的倍數。那麼m=(n+8-1)&0xFFFFFFF0 + 0x10

(3)為了避免申請過小的內存,有這樣一個限定,最小的實際分配空間大小為0x20
m = (n+8-1)&0xFFFFFFF0 + 0x10 if m < 0x20 m = 0x20

(4)因為m一定為16的倍數,所以在二進制中m的最後四位始終為0,並不起作用。因此這4位用於做標准位。於是有k = m + 1

總結m = (n+7)&0xFFFFFFF0 + 0x11 , k = m + 1

為了證明這個結論是正確的,我寫了這樣的代碼:

#include 
using namespace std;

#include
#include
#include 

#if(defined(_X86_) && !defined(__x86_64))
#define _ALLOCA_S_MARKER_SIZE 4
#elif defined(__ia64__) || defined(__x86_64)
#define _ALLOCA_S_MARKER_SIZE 8
#endif

int main(void)
{
    void * p = NULL;
    srand(time(0));
    int a = 100000;
    while (a--)
    {
        int n = rand() % 10000;
        p = new char[n];
        size_t w = *((size_t*)((char*)p -  _ALLOCA_S_MARKER_SIZE));
        if ( n <= 8) n = 9;
        int n2 = ((n+7) & 0xFFFFFFF0) + 0x11;
        assert(n2 == w);
    }
    return 0;
}

實際上我們在統計的時候並不關心調用者申請的大小,而是編譯器真正申請和釋放的大小,即,代碼如下:

#include 
using namespace std;

#include 
#include 

#if(defined(_X86_) && !defined(__x86_64))
#define _ALLOCA_S_MARKER_SIZE 4
#elif defined(__ia64__) || defined(__x86_64)
#define _ALLOCA_S_MARKER_SIZE 8
#endif

size_t count = 0;

extern "C"
{
void* __real_malloc(int c); 
void * __wrap_malloc(int size)
{
    void *p =  __real_malloc(size);
    size_t w = *((size_t*)((char*)p -  _ALLOCA_S_MARKER_SIZE)) - 1;
    count += w;
    cout<<"malloc "<

現在我們分別針對以上提到的兩種情況測試:

(1)申請了子類的空間卻只釋放了父類的空間

class father
{
    int *p1;
public:
    father(){p1 = new int;}
    ~father(){delete p1;}
};
class son : public father
{
    int *p2;
public:
    son(){p2 = new int;}
    ~son(){delete p2;}
};

int main(void)
{
    count = 0;
    father *p = new son;
    delete p;
    if(count != 0)
        cout<<"memory leak!"<

(2)申請了一個數組的空間卻只釋放第一項元素的空間

class A
{
    int *p1;
public:
    A(){p1 = new int;}
    ~A(){delete p1;}
};

int main(void)
{
    count = 0;
    A *p = new A[5];
    delete p;
    if(count != 0)
        cout<<"memory leak!"<

分析:

方便性: 功能 是否支持 說明 運行時檢查 該方法要求運行結束時對運行中產生的打印分析才能知道結果。 修改是否方便 wrap函數實現非常簡單,且只需要實現一次,對所有參與鏈接的文件都有效 使用是否方便 要關掉這一功能,只需要將這個鏈接選項去掉即可

- 全面性:

功能 是否支持 說明 C接口是否可以統一處理 C的每個接口都需要分別寫包裝函數 C++接口是否可以統一處理   動態庫與靜態庫的內存洩漏是否可以檢測到 wrap是個鏈接選項,對所有通過wrap與__wrap_malloc__wrap_free鏈接到一起的文件都起作用,不管是.o、.a或者.so

- 准確性:

功能 是否支持 說明 是否會有檢測不到的情況   是否可以定位到行   是否可以確定洩漏空間的大小  
Copyright © Linux教程網 All Rights Reserved