利用指針進行編程時碰到程序崩掉的解決方法(內存溢出)


程序崩了,咋辦?
2012-04-22
標籤:程序 休閒 職場
版權聲明:原創作品,允許轉載,轉載時請務必以超鏈接形式標明文章 原始出處 、作者信息和本聲明。否則將追究法律責任。http://xzpeter.blog.51cto.com/783279/329052
這兩天因爲調程序,自己簡單的總結了一下C編程中碰到的內存有關的問題和注意事項。
1. 內存溢出是啥?
舉個棧溢出的例子。所有的在函數內部申請的局部變量都是保存在棧中的。比如:

    #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鎖定被改成亂碼的變量。這樣,如果這個變量被修改,程序便會停下來,我們就可以看到底是哪條語句修改了這個程序。
2. 內存泄漏
內存泄漏只會是在堆中申請的內存沒有釋放而導致的。也就是,我們在malloc()後沒有及時的進行free()。這裏,可以利用現有的一些軟件幫助我們調試,如Valgrind(http://valgrind.org)。使用方法請參見其主頁的幫助文檔。
3. 緩衝區:能大就大點
很多內存溢出的問題都是因爲緩衝區不夠大。因此,我們在開闢緩衝區的時候,一定要給使用打出餘量,不能每次想申請多少就申請多少,要想到這部分內存的用途,並進行上限估計。估不出來的時候儘量放大點。
當然,不能隨便的放大,可能會出現問題,比如:棧內申請空間過大,程序一使用變量直接段錯誤。
4. snprintf比sprintf好,那麼strncpy就比strcpy好?!
有經驗的前輩總是這樣說:”小同志,不要隨便用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()比就不一定了。
總之,程序出問題是怎麼也避免不了的。特別是出現詭異的問題的時候,要學會冷靜分析產生問題的結果。往往這些問題都是我們編程過程中的錯誤導致的,而不是我們見鬼了。要對自己解決問題的能力有信心嘛!
程序這東西就是這樣,用好了,越用越順手;用不好,死都不知道怎麼死的。

二        sovle  the the error:"stack  已放棄":*** glibc detected *** free(): invalid pointer:解決方法
最近寫了一個snmp方面的程序,就是實現snmp的get操作,在編譯運行後報了標題上的錯誤,先在網上搜索了下,發現很多都有同樣的錯誤,大概原因是在用mallc申請了存儲空間後所返回的指針在之後的操作中使所返回的指針的指向發生了變化,例如:

 *sp = (char *)malloc (10*sizeof(int));

 

while (*sp != '/0'){
          c = *sp ++;
          fputc(c,fp);
      }
      free(sp);

 

這樣對sp進行操作以後,使sp的指向發生了變化,在運行時就會出現free invalid pointer的錯誤

 

解決辦法很簡單(可能有點幼稚,如果高手有更好的方法,請指教)

 

在對sp malloc以後申明一個char *p;的指針,用於保存sp的指向,如下:

char *p;

p = sp;

 

而後在while操作完成後,free之前將p重新賦給sp就可以了

sp = p;

最後free就不會有問題了


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章