Skip to content

Latest commit

 

History

History
225 lines (126 loc) · 10.9 KB

Lecture00.md

File metadata and controls

225 lines (126 loc) · 10.9 KB

Linux 平台 C/C++ 内存调试指南

因为 C/C++ 没有 Garbage Collection 机制,这使得它们可以做到很高效,但因此也容易产生内存方面的 bug 。 主要分为内存非法访问和内存泄漏两方面。 C/C++ 语法上对存在内存泄漏的程序(即申请了动态内存但没释放就退出了)不提供任何保证,即不保证操作系统会回收泄漏的内存。 现代操作系统基本会在程序结束后立刻回收程序使用的所有内存,否则一旦发生内存泄漏的程序很多,系统很容易崩溃。 比如Linux,会为每个程序建立一个虚拟内存空间(程序只知道虚拟内存地址,操作系统完全知道虚拟内存地址到物理内存地址的转换,也即操作系统能够监控到程序使用的所有内存),程序结束后即会销毁这个空间,也即回收了内存。 但是注意,并非所有系统都会做这一步,在一些实时的操作系统中,为了效率考虑(内存回收需要不少的代价)则不会做类似的处理,依赖程序员自身来保证程序正确性。

最麻烦的地方在于,内存错误不一定会立刻报错,而是可能积累到一定程度才报错,而报错的地方不一定是真正出错的地方。 有时候不同机器上表现也不同,小机器(如笔记本)上报错,但大的服务器上可能就不会报错。

虽然内存调试会很麻烦,但是为了性能考虑,几乎所有的操作系统和系统软件都会采用C/C++来实现。 数据库系统也是如此,但凡强调性能的数据库系统,都要在底层使用 C/C++ 。

Valgrind

输出日志 log4cplus

类型溢出问题:比如int爆上界,应用unsigned,或者unsigned也超过上界,应用long(64位机器上是八字节,为了提高可移植性,在使用其字节数时应用sizeof(long)而不应该直接用8)甚至long long

也有可能出现unsigned爆下界,因为其无法取负值,此时反而应该用int


stack

数组太大

递归调用太深,爆栈

类型溢出导致的各种问题,如非法读写


heap memory

delete/free/delete[]/new/new[]/malloc 不匹配

尝试释放p+10,但p才是分配的内容起始地址

释放之后再去读

double free

free an invalid address

read/write an invalid address

C++中检查new的返回值与C语言中对malloc的检查不同,必须处理异常而不是判断指针是否非空:

https://blog.csdn.net/hbyzl/article/details/8096007


Core Dump

操作系统对core dump的收集程序,abrt-hook-ccpp。 系统收集的core dump 非常多时,abrt-hook-ccpp的内存占用会非常大。 猜想:程序发生内存错误时,是否立即报错或到底到何时会报错,与操作系统对core dump的收集程度有关?积累到一定量才会直接报错?

gdb -c core.*

kill -s SIGSEGV PID

http://blog.sina.com.cn/s/blog_81fcea16010130w9.html

默认情况下,core dump生成的文件名为core,而且就在程序当前目录下。新的core会覆盖已存在的core。通过修改/proc/sys/kernel/core_uses_pid文件,可以将进程的pid作为作为扩展名,生成的core文件格式为core.xxx,其中xxx即为pid

https://blog.csdn.net/zzhongcy/article/details/42873015

http://blog.chinaunix.net/uid-20279362-id-4962658.html

若是一个程序反复地跑查询,每个查询处理过程中都可能出现一些问题如越界操作,但是不一定会报错。 操作系统会收集这些信息,直到问题累积到一定量才可能报错,而且维护程序abrt-hook-ccpp也会占用很大。 因为一个程序是以一个进程的形式运行的,有自己的虚拟内存空间,所做的内存操作和引发的一切问题都会被记录。 但若一个程序只跑一个查询,通过启动这个程序多次来跑多个查询,就不会有这个隐患。 也不会出现一个个查询分开来跑可以,全合到一个程序跑就会报错停止的情况。 因为一旦程序跑完一个查询,这个进程终止,对应的虚拟空间会被销毁,一切内存问题会被解决,abrt-hook-ccpp也不会占用越来越大!


Shared Pointer in C++

防止内存泄漏的有效工具,使用很方便可以在不同作用域维持对象的生存周期,,但也不可避免地会带来一些性能的下降。

使用中同样要小心,只有当没有引用指向这个对象时,这个对象才会被析构。

之前在一次C++ web服务的开发中,因为response是shared_ptrHttpServer::Response&类型,里面有一个ostream表示输出流,可以向客户端返回消息。 里面还有一个引用计数,当引用计数降为0,这个对象就会被析构,ostream缓冲区的内容才会真正发给用户。 如果我们自己定义了一个Task类,在里面使用了response,这样就多出了一个对Response对象的引用。 只要Task类没被析构,Response对象就不会被析构,客户端也就永远无法接到内容。


心得体会

就我的个人看法,做研究的人,从问题中升华得到经验和教训,远比解决一个具体的问题要重要。如果是一个比较有代码经验的人来做这个事情,当常规调试策略全部失败的时候,可以考虑以下几种做法:

  1. 请一个在这方面非常专业的人来帮忙寻找问题,这种是最快的,但也更可能找不到。也可以请一个这方面不太了解的人来帮忙调试,毕竟不了解就更可能提出问题。(但这2种让别人帮忙调试的做法,是最不可取的)

  2. 用gdb或cuda-gdb强行单步调试,这需要你去学这个工具,但一旦学会,受益终生

  3. 通读文档。就像之前我遇到的那个动态申请堆内存的bug,以及你这个,其实都是可以通过先读一遍cuda文档解决的。上面说的很精要很明白,只是我们很多时候不愿花时间,总想取巧罢了

  4. 强行倒推。我们可以先精简代码,比如一千行的代码跑不起来,我们可以先尝试注释掉某个功能模块,把它变为五百行。不行的话就再精简,去除一些功能,变成一个简单的代码框架,直到不报bug为止。然后,从这个运行正确的代码开始,我们可以不断地把已有的功能加进去,一直加到出问题为止。这样,我们就找到了出问题的功能模块,接下来只要对这个功能模块展开分析就行

以上这些就是调试时一些非常规的方法,都是我在做gstore系统的时候总结出来的,毕竟我接手这个系统的时候,并没有人指导。 但是所有的问题,总是能找到解决方法的。 对于一些不是很关键的问题,大可以用一些简单粗暴的方法,比如: 不行就分,喜欢就买,重启试试。


类型转换

提高代码可读性与程序的可移植性

10000000000不能被unsigned接收,如果是使用宏定义,必须加上L,即便要赋值给long类型,也必须加上L,输出时应使用ld选项

类型截断,long赋值给unsigned

要发现此类问题可多开警告选项 -Wall -Werror

指针的赋值是不能隐式进行的,比如long* 赋值给unsigned或者unsigned long会报错

size_t与ssize_t

为了增强程序的可移植性,便有了size_t,它是为了方便系统之间的移植而定义的,不同的系统上,定义size_t可能不一样。 在32位系统上 定义为 unsigned int 也就是说在32位系统上是32位无符号整形。在64位系统上定义为 unsigned long 也就是说在64位系统上是64位无符号整形。size_t一般用来表示一种计数,比如有多少东西被拷贝等。例如:sizeof操作符的结果类型是size_t,该类型保证能容纳实现所建立的最大对象的字节大小。 它的意义大致是“适于计量内存中可容纳的数据项目个数的无符号整数类型”。所以,它在数组下标和内存管理函数之类的地方广泛使用。而ssize_t这个数据类型用来表示可以被执行读写操作的数据块的大小.它和size_t类似,但必需是signed.意即:它表示的是signed size_t类型的。

typedef unsigned long size_t

ssize_t是signed size_t


Heap Corruption

当输入超出了预分配的空间大小,就会覆盖该空间之后的一段存储区域,这就叫Heap Corruption。这通常也被用作黑客攻击的一种手段,因为如果在该空间之后的那段存储区域如果是比较重要的数据,就可以利用Heap Corruption来把这些数据修改掉了,后果当然可想而知了。

http://www.cnblogs.com/lzjsky/archive/2010/09/27/1836807.html

https://software.intel.com/en-us/forums/intel-visual-fortran-compiler-for-windows/topic/300734#comment-1558704


File Descriptor

fflush(NULL) is valid, but fclose(NULL) will cause segmenttaion fault

xfree(p) free(p);p=NULL

free(NULL) is valid


Memory

pmap -d PID

一个进程到底有没有内存泄露?如果内存泄露数量太少,其他方法很难看到具体内存数量的变化。有什么办法呢?

  查看进程wuxi,进程号是29131,则:

cat /proc/29131/status

其中的VmRSS,就是该进程占用的内存。微小变化也能看到。据此判断内存有没有泄露。

  如果觉得输出太多,可以用新命令:

cat /proc/5643/status | grep VmRSS

top or ps

using top, shift+M or shift+F then press n


类型强制转换

比如char转换为short,在多处的八字节默认是补1的。 如果是unsigned char,则补0.

大端小端法是对于多字节数据而言的,如果是一个char数组,则各个char数据是按内存地址从低到高排列的。 在实现bitset时会遇到大端小端的问题,以及还有一个字节内部的各bit存储的问题。 GPU中titan XP也是采用小端法存储的,这些都是可以检验的。

如果存取都是以unsigned为单位,还存在大小端的问题么?

C语言中有返回值的函数,如果没有写上return 返回值的语句,编译和运行不会报错,但结果很可能有问题。

double的小数位可高达16位,输出浮点数时不一定会输全,也不一定会把非0部分输出来,有时候必须指定宽度。

int的最小值可用INT_MIN,是负值。 但浮点数的最小值有所不同,DBL_MIN是指最小正数,-DBL_MAX才是最小负数。 浮点数的临界值定义在floats.h中,而其他类型的最值定义在limits.h头文件中。

double的指数部分是11位,是2的指数,整体显示成10进制最大的是e+308

有一位是符号位,另外52位是尾数部分,所以小数点后的精度最多到16位。

https://en.wikipedia.org/wiki/Double-precision_floating-point_format

https://blog.csdn.net/eickandy/article/details/48494943


二进制编辑

ghex

vim -d 后 :%!xxd``


将信号处理函数用于调试

一个核虽然支持超线程技术,但如果两个线程真是满负荷工作,效率可能比串行执行还低。 比较好的方式是一个线程负荷高,一个线程负荷低。 或者两个线程各自有一些耗时的访存操作,穿插执行。