《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
- 准確性:
功能
是否支持
說明
是否會有檢測不到的情況
否
是否可以定位到行
否
是否可以確定洩漏空間的大小
是