本文所討論的「內存」主要指(靜態)數據區、堆區和棧區空間(詳細的佈局和描述參考《Linux虛擬地址空間佈局》一文)。數據區內存在程序編譯時分配,該內存的生存期爲程序的整個運行期間,如全局變量和static關鍵字所聲明的靜態變量。函數執行時在棧上開闢局部自動變量的儲存空間,執行結束時自動釋放棧區內存。堆區內存亦稱動態內存,由程序在運行時調用malloc/calloc/realloc等庫函數申請,並由使用者顯式地調用free庫函數釋放。堆內存比棧內存分配容量更大,生存期由使用者決定,故很是靈活。然而,堆內存使用時很容易出現內存泄露、內存越界和重複釋放等嚴重問題。html
數據區的內存訪問越界能夠分爲讀越界和寫越界,數據區內存越界主要指讀寫某一數據區內存(如全局或靜態變量、數組或結構體等)時,超出該內存區域的合法範圍。讀越界表示讀取不屬於本身的數據,如讀取的字節數多於分配給目標變量的字節數。若所讀的內存地址無效,則程序當即崩潰;若所讀的內存地址有效,則可讀到隨機的數據,致使不可預料的後果。寫越界亦稱「緩衝區溢出」,所寫入的數據對目標地址而言也是隨機的,所以一樣致使不可預料的後果。linux
內存越界訪問會嚴重影響程序的穩定性,其危險在於後果和症狀的隨機性。這種隨機性使得故障現象和本源看似無關,給排障帶來極大的困難。你永遠也不知道是否是有其餘線程操做時候偷偷改動了你的數據。若是是通常的業務數據,唔,一個bug。可是是若是該內存塊指向一個對象,而後就呵呵了——你持有了一個無效的內存地址,通常來講會crash,無止境的debug在等待你。c++
寫越界的主要緣由有兩種:1) memset/memcpy/memmove等內存覆寫調用;2) 數組下標超出範圍。數組
#include <string.h>
#include <stdio.h>多線程
#define NAME_SIZE 8
#define NAME_LEN 9函數
char name1[NAME_SIZE] = "ABCDEFGH";
char name2[NAME_LEN] = "123456789";工具
int main() {
strncpy(name1, name2, NAME_LEN);
printf("name2: %s\n", name2);
return 0;
}佈局
輸出結果顯然是name2: 923456789。常見的所謂數組越界方法實現起來比較繁瑣。用工具(VALGRIND等)能夠發現,可是對於生產系統(採用了全局數組+多線程之類的高級技巧……),通常來講是難以查找到的,特別是若是其餘線程由其餘團隊成員開發,你對其代碼缺乏相關知識的時候。測試
對於這個問題,gdb提供了一種可能的方法:觀察點(watch命令)。用法以下:watch name2[0]。這樣當該變量被改寫的時候進程將會停下來。固然你也能夠watch某個地址:watch *(data type*)addr。若是你懷疑是特定線程改寫了該變量的時候,可使用watch expr thread threadnum,在某個線程改寫的時候讓進程中止。使用這個方法,在絕大多數狀況下能夠發現未知的變量改寫問題。ui
(gdb) watch name2[0] Hardware watchpoint 1: name2[0] (gdb) r Starting program: /home/afreet/sourcecodes/memdemo/build/bin/memdemo Hardware watchpoint 1: name2[0] Old value = 49 '1' New value = 57 '9' __strncpy_ssse3 () at ../sysdeps/x86_64/multiarch/strcpy-ssse3.S:2443 2443 ../sysdeps/x86_64/multiarch/strcpy-ssse3.S: No such file or directory. (gdb) bt #0 __strncpy_ssse3 () at ../sysdeps/x86_64/multiarch/strcpy-ssse3.S:2443 #1 0x000000000040080e in main () at /home/afreet/sourcecodes/memdemo/memdemo.c:11
若是在調試狀態下運行仍然沒有發現問題或者是嵌入式環境根本沒法調試,那麼是否是就只能去燒香?或者拜基督(取決於你的宗教信仰,可是財神我相信大多數現代中國人是不會拒絕去拜拜的)。Linux還提供了一個殺手鐗級的API:mprotect。
mprotect函數的原型以下:
int mprotect(const void *addr, size_t len, int prot);
其中addr是待保護的內存首地址,必須按頁對齊;len是待保護內存的大小,必須是頁的整數倍,prot表明模式,可能的取值有PROT_READ(表示可讀)、PROT_WRITE(可寫)等。
不一樣體系結構和操做系統,一頁的大小不盡相同。如何得到頁大小呢?經過PAGE_SIZE宏或者getpagesize()系統調用便可。下面是另外一個簡單的例子:
#include <string.h> #include <stdio.h> #include <unistd.h> #include <assert.h> #include <thread> #define BUF_LEN 4096 using namespace std; int buf[BUF_LEN] = {0}; int* p = &buf[2048]; void func1() { char* q = reinterpret_cast<char*>(p); *q = 0xFF; } void func2() { sleep(5); for ( auto x: buf) { assert(x == 0); } } int main() { std::thread t1(func1); std::thread t2(func2); t1.join(); t2.join(); return 0; }
因爲buf[2048]在func1中被改寫,因此斷言會失敗。所以引入mprotect函數,對問題所在進行檢測。改進後的版本以下:
#include <string.h> #include <stdio.h> #include <unistd.h> #include <assert.h> #include <sys/mman.h> #include <thread> #define BUF_LEN 1024 using namespace std; int buf[BUF_LEN] = {0}; int* p = &buf[512]; void func2() { char* q = reinterpret_cast<char*>(p); *q = 0xFF; } void func1() { long pageSize = sysconf(_SC_PAGESIZE); void *pageStart = (void*)((long)p - (long)p % pageSize); int rst = mprotect(pageStart, pageSize, PROT_READ); if ( rst == -1 ) printf("mprotect failed: %s", strerror(errno)); sleep(10); } int main() { std::thread t1(func1); std::thread t2(func2); t1.join(); t2.join(); return 0; }
而後再來測試一下:
(gdb) r Starting program: /home/afreet/sourcecodes/memdemo/build/bin/memdemo [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". [New Thread 0x7ffff6ff1700 (LWP 2794)] [New Thread 0x7ffff67f0700 (LWP 2795)] [Thread 0x7ffff67f0700 (LWP 2795) exited] Program received signal SIGSEGV, Segmentation fault. [Switching to Thread 0x7ffff6ff1700 (LWP 2794)] _dl_fixup (l=<optimized out>, reloc_arg=<optimized out>) at ../elf/dl-runtime.c:148 148 ../elf/dl-runtime.c: No such file or directory. (gdb) bt #0 _dl_fixup (l=<optimized out>, reloc_arg=<optimized out>) at ../elf/dl-runtime.c:148 #1 0x00007ffff7df02e5 in _dl_runtime_resolve () at ../sysdeps/x86_64/dl-trampoline.S:45 #2 0x000000000040740b in func1 () at /home/afreet/sourcecodes/memdemo/memdemo.cpp:33 #3 0x0000000000408aac in void std::_Bind_simple<void (*())()>::_M_invoke<>(std::_Index_tuple<>) (this=0x60eda8) at /usr/include/c++/4.9/functional:1700 #4 0x00000000004089d2 in std::_Bind_simple<void (*())()>::operator()() (this=0x60eda8) at /usr/include/c++/4.9/functional:1688 #5 0x0000000000408939 in std::thread::_Impl<std::_Bind_simple<void (*())()> >::_M_run() (this=0x60ed90) at /usr/include/c++/4.9/thread:115 #6 0x00007ffff796a970 in ?? () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6 #7 0x00007ffff7bc70a4 in start_thread (arg=0x7ffff6ff1700) at pthread_create.c:309 #8 0x00007ffff70da87d in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:111
注意調用棧#2,明確的指出了試圖改寫buf[256]的函數名。這樣就能夠輕鬆的找到犯罪分子早點下班吃飯了。
可是仍是沒有完全解決問題:若是被測代碼沒法使用調試器運行怎麼辦?另外一個問題,mprotect須要保護整個頁面,那麼不少時候被保護的數據會和其餘全局數據共存在一個頁面上。若是其餘線程訪問了這個頁面,同樣會發生segment fault。顯然這不是咱們須要的結果。若是在被保護的數據以前人爲加padding,讓被改寫的數組後退到某個頁面起始處,那麼越界訪問每每就不會發生了——由於訪問到了padding上,這樣也沒法重現錯誤。辦法仍是有的:利用信號處理函數,斷定發生頁面訪問錯誤的地址是不是咱們指望的某個元素所在,若是不是,那麼什麼都不作就能夠了;若是是,那就打印調用棧到指定文件。用一個包裝類來實現這個目的:
class MemoryDetector { public: typedef void (*segv_handler) (int sig, siginfo_t *si, void *unused); static void init(const char *path) { register_handler(handler); fd_ = open(path, O_RDWR|O_CREAT, 777); } static int protect(void *ptr, int len) { address_ = reinterpret_cast<uint64_t>(ptr); len_ = len; uint64_t start_address = (address_ >> PAGE_SHIFT) << PAGE_SHIFT; return mprotect(reinterpret_cast<void *>(start_address), PAGE_SIZE, PROT_READ); } static int umprotect(void *ptr, int len) { uint64_t addr = reinterpret_cast<uint64_t>(ptr); uint64_t start_address = (addr >> PAGE_SHIFT) << PAGE_SHIFT; return mprotect(reinterpret_cast<void *>(start_address), PAGE_SIZE, PROT_READ | PROT_WRITE); } static int umprotect() { uint64_t start_address = (address_ >> PAGE_SHIFT) << PAGE_SHIFT; return mprotect(reinterpret_cast<void *>(start_address), PAGE_SIZE, PROT_READ | PROT_WRITE); } static void finish() { close(fd_); }
private: static void register_handler(segv_handler sh) { struct sigaction act; act.sa_sigaction = sh; sigemptyset(&act.sa_mask); act.sa_flags = SA_SIGINFO; if(sigaction(SIGSEGV, &act, NULL) == -1){ perror("Register hanlder failed"); exit(EXIT_FAILURE); } } static void handler(int sig, siginfo_t *si, void *unused) { uint64_t address = reinterpret_cast<uint64_t>(si->si_addr); if (address >= address_ && address < address_ + len_) { umprotect(si->si_addr, PAGE_SIZE); my_backtrace(); } } static void my_backtrace() { const int N = 100; void* array[100]; int size = backtrace(array, N); backtrace_symbols_fd(array, size, fd_); } static uint64_t address_; static int len_; static int fd_; }; uint64_t MemoryDetector::address_; int MemoryDetector::len_; int MemoryDetector::fd_;
隨後咱們把測試程序改爲這個樣子:
void func() { char* q = reinterpret_cast<char*>(p); *q = static_cast<char>(0xFF); //Line 101 } int main() { MemoryDetector::init("memdemo.rst"); MemoryDetector::protect(p, 4); std::thread t(func); t.join(); sleep(5); MemoryDetector::finish(); return 0; }
再運行一把,獲得了memdemo.rst文件,內容以下:
./memdemo(_ZN14MemoryDetector12my_backtraceEv+0x2b)[0x407b79] ./memdemo(_ZN14MemoryDetector7handlerEiP9siginfo_tPv+0x64)[0x407b4c] /lib/x86_64-linux-gnu/libpthread.so.0(+0xf8d0)[0x7fb039e928d0] ./memdemo(_Z4funcv+0x1c)[0x4076fc] ./memdemo(_ZNSt12_Bind_simpleIFPFvvEvEE9_M_invokeIIEEEvSt12_Index_tupleIIXspT_EEE+0x2a)[0x408fa4] ./memdemo(_ZNSt12_Bind_simpleIFPFvvEvEEclEv+0x22)[0x408eca] ./memdemo(_ZNSt6thread5_ImplISt12_Bind_simpleIFPFvvEvEEE6_M_runEv+0x21)[0x408e31] /usr/lib/x86_64-linux-gnu/libstdc++.so.6(+0xb6970)[0x7fb039c2e970] /lib/x86_64-linux-gnu/libpthread.so.0(+0x80a4)[0x7fb039e8b0a4] /lib/x86_64-linux-gnu/libc.so.6(clone+0x6d)[0x7fb03939e87d]
接着addr2line命令看看:
addr2line -e memdemo 0x4076fc
/home/afreet/sourcecodes/memdemo/memdemo.cpp:101
也很輕鬆的找到了肇事者所在。