內存異常經常導致程序出現莫名其妙的錯誤,往往很難查證,本文介紹在linux下的各種常見內存異常的查證工具和方法。

1 訪問空指針/未初始化指針/重復釋放內存

對于像訪問空指針、未初始化指針(非法地址),重復釋放內存等內存異常,linux默認會拋異常。

比如下面代碼有空指針訪問,編譯運行后會coredump

int?main()
{int?*p=0;*p=6;return?0;
}

對于此類問題,我們只要在gcc編譯程序時加入-g選項,同時在運行時能夠生成coredump文件,利用gdb就可以定位到具體的問題代碼行。

1.1 開啟coredump

**ulimit -c ?unlimited** //unlimited表示不限制coredump文件大小,也可指定一個具體值來限制文件最大長度。

1.2 定制core文件名

默認的coredump文件名為core,如果想自己定制core文件名,可以運行如下命令:

**echo "./core-%e-%p-%t" > /proc/sys/kernel/core_pattern**?

可以在core_pattern模板中使用變量還很多,見下面的列表:

%% 單個%字符

%p 所dump進程的進程ID

%u 所dump進程的實際用戶ID

%g 所dump進程的實際組ID

%s 導致本次core dump的信號

%t core dump的時間 (由1970年1月1日計起的秒數)

%h 主機名

%e 程序文件名

1.3 使用gdb定位代碼行

通過gdb即可定位出錯代碼行

root@ubuntu:/home/zte/test#?gcc?null.cc?-g
root@ubuntu:/home/zte/test#?./a.out?
Segmentation?fault?(core?dumped)
root@ubuntu:/home/zte/test#?gdb?a.out?core
.......
Core?was?generated?by?`./null'.
Program?terminated?with?signal?SIGSEGV,?Segmentation?fault.
#0??0x00000000004004fd?in?main?()?at?null.cc:4
4????*p=6;

2、函數棧溢出

局部變量的寫越界可能會破壞函數棧導致程序出現各種異常行為,但是OS默認不會在越界的第一現場coredump,因此導致問題查證非常困難。

幸運的是我們可以通過gcc的編譯選項-fstack-protector 和 -fstack-protector-all在函數棧被破壞的函數返回時拋異常,從而可以很方便地定位問題所在函數。

代碼示例

int?main()
{int?a=5;int?*p=&a;p[3]=6;return?0;
}


上面代碼會破壞函數棧,如果我們用gcc直接編譯運行,不會拋異常。但是加了編譯參數-fstack-protector 和 -fstack-protector-all后,再運行就會拋異常。下面是具體命令執行結果。

root@ubuntu:/home/zte/test#?gcc?t.c
root@ubuntu:/home/zte/test#?./a.out?
root@ubuntu:/home/zte/test#?gcc?t.c?-fstack-protector?-fstack-protector-all?-g
root@ubuntu:/home/zte/test#?./a.out?
***?stack?smashing?detected?***:?./a.out?terminated
Aborted?(core?dumped)
```
可以進一步用gdb的bt命令定位出問題的函數。
```
root@ubuntu:/home/zte/test#?gdb?a.out?core
。。。。。。。。
Core?was?generated?by?`./a.out'.
Program?terminated?with?signal?SIGABRT,?Aborted.
#0??0x00007f6bcfab5c37?in?__GI_raise?(sig=sig@entry=6)?at?../nptl/sysdeps/unix/sysv/linux/raise.c:56
56../nptl/sysdeps/unix/sysv/linux/raise.c:?No?such?file?or?directory.
(gdb)?bt
#0??0x00007f6bcfab5c37?in?__GI_raise?(sig=sig@entry=6)?at?../nptl/sysdeps/unix/sysv/linux/raise.c:56
#1??0x00007f6bcfab9028?in?__GI_abort?()?at?abort.c:89
#2??0x00007f6bcfaf22a4?in?__libc_message?(do_abort=do_abort@entry=1,?fmt=fmt@entry=0x7f6bcfc01d70?"***?%s?***:?%s?terminated\n")at?../sysdeps/posix/libc_fatal.c:175
#3??0x00007f6bcfb8d83c?in?__GI___fortify_fail?(msg=<optimized?out>,?msg@entry=0x7f6bcfc01d58?"stack?smashing?detected")at?fortify_fail.c:38
#4??0x00007f6bcfb8d7e0?in?__stack_chk_fail?()?at?stack_chk_fail.c:28
#5??0x00000000004005aa?in?main?()?at?t.c:7
(gdb)?q


3 越界讀寫動態分配內存/讀寫已釋放動態分配內存

動態分配內存讀寫越界、讀寫已釋放動態分配內存系統往往不會拋異常,我們可以使用electric-fence來使得讀寫越界內存/已釋放內存后立刻拋異常,加速問題定位。

3.1 安裝Electric fence

sudo apt-get install electric-fence

3.2 使用Electric fence

下面是越界寫代碼

#include?<stdlib.h>
int?main()
{int?*p?=?(int*)malloc(sizeof(int));p[1]?=?6;return?0;
}


如果使用gcc直接編譯運行,不會拋異常。

我們可以加上參數 -lefence -g編譯后運行,就會拋異常。通過gdb的bt打印即可定位到問題代碼行。

root@ubuntu:/home/zte/test#?gcc?malloc_read_free.cc?-lefence?-g
root@ubuntu:/home/zte/test#?./a.out?Electric?Fence?2.2?Copyright?(C)?1987-1999?Bruce?Perens?<bruce@perens.com>
Segmentation?fault?(core?dumped)autogen.sh


4 內存泄漏

C/C++程序經常被內存泄漏問題困擾,本文介紹使用gperftools來快速定位內存泄漏問題。

4.1 安裝gperftools工具

4.1.1 安裝automake

sudo apt-get install automake

4.1.2 編譯安裝libunwind

從https://github.com/libunwind/libunwind/releases下載最新版本的libunwind源碼包

解壓到/usr/local/src目錄

cd 解壓源碼目錄

./autogen.sh

./configure

make -j6

make install

4.1.3 編譯安裝gperftools

從https://github.com/gperftools/gperftools/releases下載最新版本的gperftools源碼包

解壓到/usr/local/src目錄

cd 解壓源碼目錄

./autogen.sh

./configure

make -j6

make install

4.2 內存泄漏檢測

下面是一段簡單的內存泄漏源碼

int?main()
{int?*p?=?(int*)malloc(sizeof(int));return?0;
}

編譯代碼、運行工具檢察內存泄漏,注意設置下

root@ubuntu:/home/zte/#?gcc?leak.cc?-g
root@ubuntu:/home/zte/#?env?HEAPCHECK=normal?LD_PRELOAD=/usr/local/lib/libtcmalloc.so?./a.out?
WARNING:?Perftools?heap?leak?checker?is?active?--?Performance?may?suffer
Have?memory?regions?w/o?callers:?might?report?false?leaks
Leak?check?_main_?detected?leaks?of?4?bytes?in?1?objects
The?1?largest?leaks:
***?WARNING:?Cannot?convert?addresses?to?symbols?in?output?below.
***?Reason:?Cannot?run?'pprof'?(is?PPROF_PATH?set?correctly?)
***?If?you?cannot?fix?this,?try?running?pprof?directly.
Leak?of?4?bytes?in?1?objects?allocated?from:
@?40053f?
@?7f334da06f45?
@?400469?
If?the?preceding?stack?traces?are?not?enough?to?find?the?leaks,?try?running?THIS?shell?command:
pprof?./a.out?"/tmp/a.out.8497._main_-end.heap"?--inuse_objects?--lines?--heapcheck??--edgefraction=1e-10?--nodefraction=1e-10?--gv
If?you?are?still?puzzled?about?why?the?leaks?are?there,?try?rerunning?this?program?with?HEAP_CHECK_TEST_POINTER_ALIGNMENT=1?and/or?with?HEAP_CHECK_MAX_POINTER_OFFSET=-1
If?the?leak?report?occurs?in?a?small?fraction?of?runs,?try?running?with?TCMALLOC_MAX_FREE_QUEUE_SIZE?of?few?hundred?MB?or?with?TCMALLOC_RECLAIM_MEMORY=false,?it?might?help?find?leaks?more?repeatabl
Exiting?with?error?code?(instead?of?crashing)?because?of?whole-program?memory?leaks

上面的關鍵的輸入信息是:

Leak of 4 bytes in 1 objects allocated from:

@ 40053f ? //內存分配的指令地址

@ 7f334da06f45?

@ 400469?

由于工具沒有直接輸出問題代碼行,我們通過反匯編來定位代碼行:

objdump -S a.out ?//反匯編程序

截取匯編代碼如下:

int?main()
{40052d:55???????????????????push???%rbp40052e:48?89?e5?????????????mov????%rsp,%rbp400531:48?83?ec?10??????????sub????$0x10,%rspint?*p?=?(int*)malloc(sizeof(int));400535:bf?04?00?00?00???????mov????$0x4,%edi40053a:e8?f1?fe?ff?ff???????callq??400430?<malloc@plt>40053f:48?89?45?f8??????????mov????%rax,-0x8(%rbp)return?0;400543:b8?00?00?00?00???????mov????$0x0,%eax

我們注意到40053f就是對應代碼行int *p = (int*)malloc(sizeof(int));

至此,內存泄漏的元兇被揪出來了,呵呵。