這兩天因為調程序,自己簡單的總結了一下C編程中碰到的內存有關的問題和注意事項。
舉個棧溢出的例子。所有的在函數內部申請的局部變量都是保存在棧中的。比如:
- #include <string.h>
- void fn(void)
- {
- char a[100];
- char *p = a;
- bzero(p, 1000);
- }
- int main(int argc, char *argv[])
- {
- fn();
- return 0;
- }
這裡,數組a就會保存在棧中。當棧溢出時,最容易出現的問題是返回指針被修改,進而函數返回時會發現返回的代碼段指針錯誤,提示:“stack smashing detected...":
- peter@ubuntu-910:~/codes/testspace$ ./testspace
- *** stack smashing detected ***: <unknown> terminated
- ======= Backtrace: =========
- /lib/tls/i686/cmov/libc.so.6(__fortify_fail+0x48)[0x2f7008]
- /lib/tls/i686/cmov/libc.so.6(__fortify_fail+0x0)[0x2f6fc0]
- [0x80484b2]
- [0x0]
- ======= Memory map: ========
- 00215000-00216000 r-xp 00000000 00:00 0 [vdso]
- 00216000-00354000 r-xp 00000000 08:07 5206 /lib/tls/i686/cmov/libc-2.10.1.so
- 00354000-00355000 ---p 0013e000 08:07 5206 /lib/tls/i686/cmov/libc-2.10.1.so
- 00355000-00357000 r--p 0013e000 08:07 5206 /lib/tls/i686/cmov/libc-2.10.1.so
- 00357000-00358000 rw-p 00140000 08:07 5206 /lib/tls/i686/cmov/libc-2.10.1.so
- 00358000-0035b000 rw-p 00000000 00:00 0
- 00c38000-00c4d000 r-xp 00000000 08:07 5220 /lib/tls/i686/cmov/libpthread-2.10.1.so
- 00c4d000-00c4e000 r--p 00014000 08:07 5220 /lib/tls/i686/cmov/libpthread-2.10.1.so
- 00c4e000-00c4f000 rw-p 00015000 08:07 5220 /lib/tls/i686/cmov/libpthread-2.10.1.so
- 00c4f000-00c51000 rw-p 00000000 00:00 0
- 00cfc000-00d18000 r-xp 00000000 08:07 4652 /lib/libgcc_s.so.1
- 00d18000-00d19000 r--p 0001b000 08:07 4652 /lib/libgcc_s.so.1
- 00d19000-00d1a000 rw-p 0001c000 08:07 4652 /lib/libgcc_s.so.1
- 00f63000-00f7e000 r-xp 00000000 08:07 5168 /lib/ld-2.10.1.so
- 00f7e000-00f7f000 r--p 0001a000 08:07 5168 /lib/ld-2.10.1.so
- 00f7f000-00f80000 rw-p 0001b000 08:07 5168 /lib/ld-2.10.1.so
- 08048000-08049000 r-xp 00000000 08:08 264941 /home/peter/codes/testspace/testspace
- 08049000-0804a000 r--p 00000000 08:08 264941 /home/peter/codes/testspace/testspace
- 0804a000-0804b000 rw-p 00001000 08:08 264941 /home/peter/codes/testspace/testspace
- 08a74000-08a95000 rw-p 00000000 00:00 0 [heap]
- b785e000-b7860000 rw-p 00000000 00:00 0
- b7874000-b7876000 rw-p 00000000 00:00 0
- bffad000-bffc2000 rw-p 00000000 00:00 0 [stack]
- 已放棄
這類問題其實比較簡單,起碼在linux系統中,在程序崩潰的同時,系統往往會打印出一些backtrace和memory map之類的東西,其中backtrace可以非常有效的讓我們發現棧溢出發生的函數位置。如果函數比較深比如我們這種情況),或者系統沒有打印bt的信息,而是直接段錯誤了,可以用gdb跟蹤,然後用backtrace命令看:
- peter@ubuntu-910:~/codes/testspace$ gdb
- GNU gdb (GDB) 7.0-ubuntu
- Copyright (C) 2009 Free Software Foundation, Inc.
- License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
- This is free software: you are free to change and redistribute it.
- There is NO WARRANTY, to the extent permitted by law. Type "show copying"
- and "show warranty" for details.
- This GDB was configured as "i486-linux-gnu".
- For bug reporting instructions, please see:
- <http://www.gnu.org/software/gdb/bugs/>.
- (gdb) file testspace
- Reading symbols from /home/peter/codes/testspace/testspace...done.
- (gdb) r
- Starting program: /home/peter/codes/testspace/testspace
- [Thread debugging using libthread_db enabled]
- *** stack smashing detected ***: <unknown> terminated
- ======= Backtrace: =========
- /lib/tls/i686/cmov/libc.so.6(__fortify_fail+0x48)[0x228008]
- /lib/tls/i686/cmov/libc.so.6(__fortify_fail+0x0)[0x227fc0]
- [0x80484b2]
- [0x0]
- ======= Memory map: ========
- 00110000-0012b000 r-xp 00000000 08:07 5168 /lib/ld-2.10.1.so
- 0012b000-0012c000 r--p 0001a000 08:07 5168 /lib/ld-2.10.1.so
- 0012c000-0012d000 rw-p 0001b000 08:07 5168 /lib/ld-2.10.1.so
- 0012d000-0012e000 r-xp 00000000 00:00 0 [vdso]
- 0012e000-00143000 r-xp 00000000 08:07 5220 /lib/tls/i686/cmov/libpthread-2.10.1.so
- 00143000-00144000 r--p 00014000 08:07 5220 /lib/tls/i686/cmov/libpthread-2.10.1.so
- 00144000-00145000 rw-p 00015000 08:07 5220 /lib/tls/i686/cmov/libpthread-2.10.1.so
- 00145000-00147000 rw-p 00000000 00:00 0
- 00147000-00285000 r-xp 00000000 08:07 5206 /lib/tls/i686/cmov/libc-2.10.1.so
- 00285000-00286000 ---p 0013e000 08:07 5206 /lib/tls/i686/cmov/libc-2.10.1.so
- 00286000-00288000 r--p 0013e000 08:07 5206 /lib/tls/i686/cmov/libc-2.10.1.so
- 00288000-00289000 rw-p 00140000 08:07 5206 /lib/tls/i686/cmov/libc-2.10.1.so
- 00289000-0028c000 rw-p 00000000 00:00 0
- 0028c000-002a8000 r-xp 00000000 08:07 4652 /lib/libgcc_s.so.1
- 002a8000-002a9000 r--p 0001b000 08:07 4652 /lib/libgcc_s.so.1
- 002a9000-002aa000 rw-p 0001c000 08:07 4652 /lib/libgcc_s.so.1
- 08048000-08049000 r-xp 00000000 08:08 264941 /home/peter/codes/testspace/testspace
- 08049000-0804a000 r--p 00000000 08:08 264941 /home/peter/codes/testspace/testspace
- 0804a000-0804b000 rw-p 00001000 08:08 264941 /home/peter/codes/testspace/testspace
- 0804b000-0806c000 rw-p 00000000 00:00 0 [heap]
- b7fe8000-b7fea000 rw-p 00000000 00:00 0
- b7ffe000-b8000000 rw-p 00000000 00:00 0
- bffeb000-c0000000 rw-p 00000000 00:00 0 [stack]
- Program received signal SIGABRT, Aborted.
- 0x0012d422 in __kernel_vsyscall ()
- (gdb) bt
- #0 0x0012d422 in __kernel_vsyscall ()
- #1 0x001714d1 in raise () from /lib/tls/i686/cmov/libc.so.6
- #2 0x00174932 in abort () from /lib/tls/i686/cmov/libc.so.6
- #3 0x001a7fc5 in ?? () from /lib/tls/i686/cmov/libc.so.6
- #4 0x00228008 in __fortify_fail () from /lib/tls/i686/cmov/libc.so.6
- #5 0x00227fc0 in __stack_chk_fail () from /lib/tls/i686/cmov/libc.so.6
- #6 0x080484b2 in fn () at test.c:8
- #7 0x00000000 in ?? ()
這裡便看到了:
- # #6 0x080484b2 in fn () at test.c:8
以便我們鎖定問題。
很多時候,當內存溢出問題不嚴重時,並不會直接終止我們程序的運行。但是,我們會在調試程序中碰到非常奇怪的問題,比如某一個變量無緣無故變成亂碼,不管是在堆中,還是棧中。這便很有可能是指針的錯誤使用導致的。這種情況出現時,一種調試方法是:使用gdb加載程序,並用watch鎖定被改成亂碼的變量。這樣,如果這個變量被修改,程序便會停下來,我們就可以看到底是哪條語句修改了這個程序。
內存洩漏只會是在堆中申請的內存沒有釋放而導致的。也就是,我們在malloc()後沒有及時的進行free()。這裡,可以利用現有的一些軟件幫助我們調試,如Valgrind(http://valgrind.org)。使用方法請參見其主頁的幫助文檔。
很多內存溢出的問題都是因為緩沖區不夠大。因此,我們在開辟緩沖區的時候,一定要給使用打出余量,不能每次想申請多少就申請多少,要想到這部分內存的用途,並進行上限估計。估不出來的時候盡量放大點。
當然,不能隨便的放大,可能會出現問題,比如:棧內申請空間過大,程序一使用變量直接段錯誤。
有經驗的前輩總是這樣說:”小同志,不要隨便用sprintf(),要用snprintf(),這樣如果打印的數據溢出了可以保護呀!“我們發現,這樣做雖然要多寫一個參數,但是的確比原來的程序安全了!何樂不為。
之後,我們又看到了strncpy(),一看就高興!又帶一個n!馬上用了一下:
- #include <stdio.h>
- #include <string.h>
- void fn(void)
- {
- char a[10];
- strncpy(a, "hello", 100);
- }
- int main(int argc, char *argv[])
- {
- fn();
- return 0;
- }
很好,程序崩了。
有心的人早就發現了,長度100明顯不對阿。可是有人也就想了,為啥10個字節還不夠放"hello"這些玩意呢?man一下才知道:
- STRCPY(3) Linux Programmer's Manual STRCPY(3)
- NAME
- strcpy, strncpy - copy a string
- SYNOPSIS
- #include <string.h>
- char *strcpy(char *dest, const char *src);
- char *strncpy(char *dest, const char *src, size_t n);
- DESCRIPTION
- The strcpy() function copies the string pointed to by src, including the terminating null byte ('\0'), to the buffer pointed to
- by dest. The strings may not overlap, and the destination string dest must be large enough to receive the copy.
- The strncpy() function is similar, except that at most n bytes of src are copied. Warning: If there is no null byte among the
- first n bytes of src, the string placed in dest will not be null terminated.
- If the length of src is less than n, strncpy() pads the remainder of dest with null bytes.
- A simple implementation of strncpy() might be:
- char*
- strncpy(char *dest, const char *src, size_t n){
- size_t i;
- for (i = 0 ; i < n && src[i] != '\0' ; i++)
- dest[i] = src[i];
- for ( ; i < n ; i++)
- dest[i] = '\0';
- return dest;
- }
關鍵是最後的一句:
- "If the length of src is less than n, strncpy() pads the remainder of dest
- with null bytes. "
也就是說,strncpy並不僅僅是做一個n長度的保護,而會把剩下的字符清為0x00。要知道,snprintf()是沒這檔子事情的。所以,我們要記住:
snprintf()總是比sprintf()安全,但是strncpy()和strcpy()比就不一定了。
總之,程序出問題是怎麼也避免不了的。特別是出現詭異的問題的時候,要學會冷靜分析產生問題的結果。往往這些問題都是我們編程過程中的錯誤導致的,而不是我們見鬼了。要對自己解決問題的能力有信心嘛!
程序這東西就是這樣,用好了,越用越順手;用不好,死都不知道怎麼死的。
本文出自 “LoudMouth Peter” 博客,請務必保留此出處http://xzpeter.blog.51cto.com/783279/329052