这两天因为调程序,自己简单的总结了一下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()比就不一定了。
总之,程序出问题是怎么也避免不了的。特别是出现诡异的问题的时候,要学会冷静分析产生问题的结果。往往这些问题都是我们编程过程中的错误导致的,而不是我们见鬼了。要对自己解决问题的能力有信心嘛!
程序这东西就是这样,用好了,越用越顺手;用不好,死都不知道怎么死的。
转载于:https://blog.51cto.com/xzpeter/329052