2020-03-27 14:33:21
川渝小神丢
码龄11年
关注
前言:
在Linux应用软件中,段错误(segmentation fault)是开发过程中比较棘手的问题,在博文(Linux环境下段错误的产生原因及调试方法小结)中简要介绍了段错误的产生原因和简单的调试方法。
本文主要描述面对大型程序,比如多线程,调用多个动态库的情况下发生段错误分析方法。这个时候如果使用gdb和gcc,由于程序过于复杂,gdb将很难处理,并且对于那些偶尔出现段错误的情况gdb基本上无法定位。
另外,还可以使用dmesg,nm,ldd,objdump等工具结合分析汇编代码调试段错误,但是对于大型程序一般都有编译优化,分析汇编地址将不是那么容易,另外需要一定汇编基础,对于庞大程序分析汇编代码也是噩梦。
最后,使用printf。printf是个最方便方式,如果对于必现的段错误,使用printf再结合二分法,一般都能定位到。但是就算段错误必现,对于那些多线程大型程序,使用printf就需要对代码有较深入的了解,凭经验找到可能触发异常的地方,并且每次加了调试语句后需要重新编译烧写,效率非常低下。对于那些很难复现的段错误,printf更难入手。
综上所述,单单使用上述某一种方法都不能很好地分析满足如下条件的段错误:
1) 大型项目,代码量几十万行以上。
2) 多线程,多动态库。
3) 出现段错误不一定必现(偶尔出现)。
本文将使用backtrace和memory map信息定位出发时异常的大致位置(往往是某个函数名,如果此函数层数非常多,那么还需要printf定位最终位置),然后通过printf二分法精确到具体位置,针对偶尔出现的段错误,使用applog日志记录的方式。下面将详细描述具体方法。
一、段错误信号
在某些大型程序中,程序初始化时首先会安装SIGSEGV信号及指定处理函数,在处理函数中完成特殊指定的处理后,最后在处理函数中调用longjmp函数回到主进程报错并退出主进程。backtrace和memory map信息就是在上述信号处理函数中写入系统日志中的,前面的主进程退出后,系统日志仍然保留在flash上供后续分析。在系统日志中,backtrace提供了触发异常的堆栈调用点地址序列,memory map信息提供各目标文件在内存中的地址分配。先通过backtarce中记录的目标文件和地址数据,结合memory map中目标文件代码段起始地址,计算偏移量。
使用此偏移量和目标文件用addr2line命令可查得对应此地址的代码信息。下面先描述linux下段错误信号描述(注意Unix系统和linux系统中的signal函数有差异)。
1. 如果没有安装SIGSEGV信号和并指定处理函数,系统将对这个信号进行默认处理。通常是杀掉当前进程,并在终端输出Segmentation fault信息。
2. 如果已经安装SIGSEGV信号和并指定处理函数,产生这个信号后,系统先运行信号处理函数,信号处理函数运行完后当前进程继续运行。通过实验,在发生了这个信号后,并且当前进程没有退出,那么系统会多次进入信号处理函数。
3. 当安装了SIGSEGV信号和并指定处理函数后,发生段错误的线程将调用处理函数,因此在处理函数中可以获取哪个线程发生了段错误(一般通过线程ID)。即发生段错误线程的线程ID和段错误处理函数中获取的线程ID是一样的。 (也就是说,当线程A发生段错误,CPU的PC指针指向Linux内核,运行内核程序,Linux内核捕捉到段错误信号,这个时候要运行用户空间中的信号处理函数,因此记录下内核空间相关地址后切换到用户空间,把PC指针指向段错误处理函数并运行。PC指针在进入内核空间之前运行的线程A,因此当时PC处于线程A的地址空间内,因此当段错误发生后,PC指针指向信号处理函数也应该处于线程A的地址空间内,因此在信号处理函数内获取的线程ID就是出现问题的线程的线程ID,在信号处理函数中backtrace信息就是出现问题线程问题点相关地址信息)通过上述分析,已经知道段错误发生线程,为了进一步缩小范围,确定发生段错误线程中到底哪个函数有问题,就是下面即将描述backtrace和memory map信息。
4. 同中断类似,内核也为每个进程准备了一个信号向量表,信号向量表中记录着每个信号所对应的处理机制,默认情况下是调用默认处理机制。当进程为某个信号注册了信号处理程序后,发生该信号时,内核就会调用注册的函数。
5. 详细参考http://www.spongeliu.com/165.html
二、backtrace()
1. int backtrace(void **buffer,int size)
该函数用于获取当前线程的调用堆栈(即获取程序中当前函数的栈回溯信息,即一系列的函数调用关系),获取的信息将会被存放在buffer中,它是 一个指针列表。参数 size 用来指定buffer中可以保存多少个void* 元素。函数返回值是实际获取的指针个数,最大不超过size大小。在buffer中的指针实际是从堆栈中获取的返回地址,每一个堆栈框架有一个返回地址。
注意:某些编译器的优化选项对获取正确的调用堆栈有干扰,另外内联函数没有堆栈框架删除框架指针也会导致无法正确解析堆栈内容.
2. char ** backtrace_symbols (void *const *buffer, int size)
backtrace_symbols将从backtrace函数获取的信息转化为一个字符串数组. 参数buffer应该是从backtrace函数获取的指针数组,size是该数组中的元素个数(backtrace的返回值)。
函数返回值是一个指向字符串数组的指针,它的大小同buffer相同.每个字符串包含了一个相对于buffer中对应元素的可打印信息.它包括函数名,函数的偏移地址,和函数的实际返回地址。比如backtrace信息中,./main-test(SEGV_Test+0x10)[0x8bc8],main-test为程序名称,SEGV_Test函数名,0x10为函数的偏移地址,0x8bc8为此函数实际返回地址。经过翻译后的函数回溯信息放到backtrace_symbols()的返回值中,如果失败则返回NULL。
现在,只有使用ELF二进制格式的程序才能获取函数名称和偏移地址.在其他系统,只有16进制的返回地址能被获取.另外,你可能需要传递相应的符号给链接器,以能支持函数名功能(比如,在使用GNU ld链接器的系统中,需要传递(-rdynamic), -rdynamic可用来通知链接器将所有符号添加到动态符号表中,如果链接器支持-rdynamic,建议将其加上)。
该函数的返回值是通过malloc函数申请的空间,因此调用者必须使用free函数来释放指针。注意:如果不能为字符串获取足够的空间函数的返回值将会为NULL。
3. void backtrace_symbols_fd (void *const *buffer, int size, int fd)
backtrace_symbols_fd()的buffer和size参数和backtrace_symbols()函数相同,只是它翻译后的函数回溯信息不是放到返回值中,而是一行一行的放到文件描述符fd对应的文件中。
4. 在段错误信号处理函数中调用backtrace()和backtrace_symbols()函数用于确定出现问题线程的调用堆栈。
由于发生段错误后,内核才会把pc寄存器值设为信号处理函数地址,继而执行应用空间的信号处理函数,因此在信号处理函数中调用backtrace栈回溯函数即可知道段错误发生的地方附近相关函数调用关系,间接找到段错误发生地点。虽然在信号处理函数中调用backtrace等函数并不能保证是一个安全的信号处理函数,但是用于调试完全可以忽略这一点。
5. 使用上述几个函数需要注意:
1)backtrace的实现依赖于栈指针(fp寄存器),在gcc编译过程中任何非零的优化等级(-On参数)或加入了栈指针优化参数- fomit-frame-pointer后多将不能正确得到程序栈信息。
2)backtrace_symbols的实现需要符号名称的支持,在gcc编译过程中需要加入-rdynamic参数。
3)内联函数没有栈帧,它在编译过程中被展开在调用的位置。
4)未调用优化(Tail-call Optimization)将复用当前函数栈,而不再生成新的函数栈,这将导致栈信息不能正确被获取。
三、memory map信息
1. /proc/PID/maps
Proc/pid/maps显示进程映射了的内存区域和访问权限。
[root@ES_Controller:/var]#cat /proc/1/maps
00008000-0006a000 r-xp 00000000 1f:03 27 /bin/busybox
00071000-00072000 rw-p 00061000 1f:03 27 /bin/busybox
00072000-00095000 rwxp 00072000 00:00 0 [heap]
40000000-40002000 rw-p 40000000 00:00 0
412d8000-412f4000 r-xp 00000000 1f:03 396/lib/ld-2.5.so
412fb000-412fd000 rw-p 0001b000 1f:03 396/lib/ld-2.5.so
41300000-41416000 r-xp 00000000 1f:03 405/lib/libc-2.5.so
41416000-4141d000 ---p 00116000 1f:03 405/lib/libc-2.5.so
4141d000-41420000 rw-p 00115000 1f:03 405/lib/libc-2.5.so
41420000-41423000 rw-p 41420000 00:00 0
beb71000-beb86000 rw-p befeb000 00:00 0 [stack]
内核每进程的vm_area_struct项/proc/pid/maps中的项
在其他进程(如控制台)执行/proc/pid/maps,会显示进程号为pid对应的memory map信息;如果在本进程中想获取本身的memory map信息,需要执行/proc/self/maps。
2. /proc/self/maps
我对/proc/self/maps的理解如下:
如果一个进程正在运行,那么可以在终端执行/proc/进程PID/maps查看到memory map,但在当前进程代码运行时需要memory map信息时,可以在代码中执行/proc/self/maps查看memory map信息,也就是说在哪个程序中执行/proc/self/maps,那么memory map信息就是对应进程的内存布局信息。
3. memory map
(1) 首先是映像文件(静态编译)
00008000-0000a000 r-xp 00000000 00:0d 16281360 /mnt/nfs/main-test/main-test
00011000-00012000 rw-p 00001000 00:0d 16281360 /mnt/nfs/main-test/main-test
映像文件名为main-test,第一行只读可执行,说明为代码段,00008000为代码段的起始地址,如果用backtrace信息中出现问题点栈回溯中的某个函数的实际返回地址减去这个程序代码段起始地址,那么就刚好得到了出现问题点相对于整个程序代码中的位置,再借用addr2line工具就可 以得到出现问题点所在源代码对应行。
上述信息中第二行为可读写,那么一般为程序的数据段。数据段对应信息的下一行如下,即堆(heap),当且仅当malloc调用时存在,是由kernel把匿名内存map到虚存空间,堆则在程序中没有调用malloc的情况下不存在;
00012000-00033000 rwxp 00012000 00:00 0 [heap]
另外就是静态编译下的动态链接库,最后是堆栈[stack]。
(2) 然后是共享库(.SO,动态编译)
40b58000-40bb7000 r-xp 00000000 1f:04 22773 /application/example.so
40bb7000-40bbe000 ---p 0005f000 1f:04 22773 /application/example.so
40bbe000-40bbf000 rw-p 0005e000 1f:04 22773 /application/example.so
同样,第一行对应共享库的代码段;第三行(rw-p)为共享库的数据段,第二行即不读;也不可写,且不可执行,不知道是什么。共享库在一个大型程序中,一般都有个入口函数,如果创建一个线程时,把这个入口函数作为线程函数,那么这个共享库实际上被加载到内存后,在一个线程中运行。
四、addr2line工具定位出错点
1. addr2line定义
Addr2line (它是标准的 GNU Binutils 中的一部分)是一个可以将指令的地址和可执行映像转换成文件名、函数名和源代码行数的工具。
命令参数如下:
Usage: addr2line [option(s)] [addr(s)]
Convert addresses into line number/file name pairs.
If no addresses are specified on the command line, they will be read from stdin
The options are:
@<file> Read options from <file>
-a --addresses Show addresses
-b --target=<bfdname> Set the binary file format
-e --exe=<executable> Set the input file name (default is a.out)
-i --inlines Unwind inlined functions
-j --section=<name> Read section-relative offsets instead of addresses
-p --pretty-print Make the output easier to read for humans
-s --basenames Strip directory names
-f --functions Show function names
-C --demangle[=style] Demangle function names
-h --help Display this information
-v --version Display the program's version
2. 用addr2line定位
(1) 静态链接情况
执行编译命令:arm-linux-gcc -rdynamic -lpthread -g main.c -o main-test
即把所有源程序和库全部连接成一个可执行文件main-test。
backtrace信息如下:
./main-test(pthread_create+0xac4)[0x95ec]
/lib/libc.so.6(__default_rt_sa_restorer_v2+0x0)[0x4132b240]
./main-test(test1+0x44)[0x8cdc]
./main-test(SEGV_Test+0x10)[0x8d5c]
./main-test(thread3+0x40)[0x92ec]
/lib/libpthread.so.0[0x41444858]
memory map信息如下:
00008000-0000a000 r-xp 00000000 00:0d 16281360 /mnt/nfs/main-test/main-test
00011000-00012000 rw-p 00001000 00:0d 16281360 /mnt/nfs/main-test/main-test
00012000-00033000 rwxp 00012000 00:00 0 [heap]
40000000-40001000 rw-p 40000000 00:00 0
40001000-400df000 r-xp 00000000 1f:03 446/lib/preloadable_libiconv.so
400df000-400e7000 ---p 000de000 1f:03 446/lib/preloadable_libiconv.so
400e7000-400e8000 rw-p 000de000 1f:03 446/lib/preloadable_libiconv.so
400e8000-400ea000 rw-p 400e8000 00:00 0
从Memory map信息第一行可以知道静态编译的程序main-test代码段地址空间为0x8000-0xa000,而backtrace信息中0x8cdc等地址也在这个地址空间范围内(这种实际地址不用backtrace地址减去memory map地址计算偏移,然后再用addr2line命令定位)。因此执行如下命令:
addr2line 0x8cdc -e main-test -f
在使用时,用 -e 选项来指定可执行映像是main-test。通过使用 -f 选项,可以告诉工具输出函数名。上述命令将会输出在程序main-test中,指令地址为0x8cdc对应源代码文件、源代码文件中的函数名、地址对应行号。执行输出信息如下:
[root@test main-test]$ addr2line 0x8cdc -e main-test -f
test1
/home/mytest/main-test/main.c:355
上述信息中,说明问题出现在在main.c中的355行附近(test1函数中)。
(2) 动态链接情况
然而,调试的程序往往没有这么简单,通常会加载用到各种各样的动态链接库。如果错误是发生在动态链接库中那么处理将变得困 难一些。下面另外一个例子:
backtrace信息如下:
./application(InitialConfigTest+0x6ec) [0xac00].
/lib/libc.so.6(__default_rt_sa_restorer_v2+0) [0x4132b240].
/application/example.so [0x41c68b48].
/application/example.so(testquery+0x28) [0x41c68f6c].
./application [0x19f58].
./application [0x1a660].
/application/lib/libpub.so [0x401d6a68].
/lib/libpthread.so.0 [0x41444858].
部分memory map信息如下:
41c5f000-41cbe000 r-xp 00000000 1f:04 22773 /application/example.so
41cbe000-41cc5000 ---p 0005f000 1f:04 22773 /application/example.so
41cc5000-41cc6000 rw-p 0005e000 1f:04 22773 /application/example.so
用backtrace中的地址0x41c68b48做实验,即执行命令 addr2line 0x41c68b48 -e example.so -f 后,addr2line输出信息如下:
[root@test main-test]$ addr2line 0x41c68b48 -e example.so -f
??
??:0
出现这种情况是由于动态链接库是程序运行时动态加载的,因此其加载地址每次可能不一样(关于与位置无关映像,用file命令查看elf格式文件,如果是shared object,那么表示此程序支持PIE与位置无关;如果是executable表示编译和连接分别没有加-fPIE和-pie选项。与位置无关编译和链接增加参数:-fPIE for compiler,-pie for linker),可见0x41c68b48是一个非常大的地址, 和能得到正常信息的地址如0xac00相差甚远,0xac00其也不是一个实际的物理地址(用户空间的程序无法直接访问物理地址),而是经过MMU(内存管理单元)映射过的。
因此,只需要得到此次example.so的加载地址后(动态库代码段起始地址,在memory map信息中第一行的最左面那个地址),再用backtrace中的实际地址0x41c68b48减去example.so的加载地址得到的结果再利用addr2line命令就可以正确的得到出错的地方。
另外在backtrace信息中,(testquery+0x28)其实也是在描述出错的地方,这里表示的是发生在符号testquery(函数名)偏移0x28处的地方,也就是说如果我们能得到符号testquery也就是函数testquery在程序中的入口地址再加上偏移量0x28也能得到正常的出错地址。
0x41c68b48-0x41c5f000 = 0x9b48
addr2line 0x9b48 -e example.so -f
执行上述命令后,addr2line输出信息如下,从而能定位到问题所在行:
[root@test main-test]$ addr2line 0x9b48 -e example.so -f
example_delay
/home/example/example.c:1883
注意:就算是动态链接,如果段错误出现在非动态库中,即应用程序中,这个时候执行addr2line命令时,可执行程序backtrace地址不需要 减去memory map信息中的起始地址。
3. 使用addr2line注意
为了在使用addr2line工具后能得到准确的结果,在编译程序时应该加上-g选项通知编译器生成调试符号。
五、总结
1. 为了正确地获取backtrace信息,编译时需要加上-rdynamic选项,为了正确地利用addr2line输出信息,编译时需要加上-g选项。完整命令如下:
arm-linux-gcc -rdynamic -lpthread -g main.c -o main-test
2. 如果是静态编译程序,只需要backtrace信息和addr2line工具即获取出错点。
3. 如果是动态编译程序,且问题出现在动态链接库时(因为动态编译时,动态链接库是运行时才加载的,加载地址就是在memory map信息中第一行的最左面那个地址),那么就需要使用backtrace信息中问题点实际地址减去memory map信息中代码段其实地址,得出地址偏移,然后使用addr2line工具定位到行。
4. 在大型程序中,backtrace信息往往会出现多个目标地址的情况,如下:
./application(InitialConfigTest+0x6ec) [0xac00].
/lib/libc.so.6(__default_rt_sa_restorer_v2+0) [0x4132b240].
/application/example.so [0x41c68b48].
/application/example.so(testquery+0x28) [0x41c68f6c].
./application [0x19f58].
./application [0x1a660].
/application/lib/libpub.so [0x401d6a68].
/lib/libpthread.so.0 [0x41444858].
上述例子属于动态编译,application为应用程序,example.so为动态链接库。不管是应用程序还是动态链接库,都出现了多个目标地址的情况。
如:
/application/example.so [0x40b61b48].
/application/example.so(testquery+0x28) [0x41c68f6c].
一般选择第一个比较准确(没有地址偏移的那个)
六、偶尔出现段错误分析经验
未完待续...
七、其他相关资料:
1. http://blog.csdn.net/jxgz_leo/article/details/53458366
2. http://www.cnblogs.com/panfeng412/archive/2011/11/06/segmentation-fault-in-linux.html
3. http://blog.csdn.net/guoping16/article/details/6583957
4. Linux内核出现段错误,会打印出栈信息(dmesg命令可以看到这些信息)。
5. linux中Oops信息的调试及栈回溯(sù):http://blog.csdn.net/u012839187/article/details/78963443。
6. Linux core 文件介绍:https://www.cnblogs.com/dongzhiquan/archive/2012/01/20/2328355.html
一、栈回溯的概念:栈回溯就是回溯法,是一个既带有系统性又带有跳跃性的的搜索算法。它在包含问题的所有解的解空间树中,按照深度优先的策略,从根结点出发搜索解空间树。算法搜索至解空间树的任一结点时,总是先判断该结点是否肯定不包含问题的解。如果肯定不包含,则跳过对以该结点为根的子树的系统搜索,逐层向其祖先结点回溯。否则,进入该子树,继续按深度优先的策略进行搜索。回溯法在用来求问题的所有解时,要回溯到根,且根结点的所有子树都已被搜索遍才结束。而回溯法在用来求问题的任一解时,只要搜索到问题的一个解就可以结束。这种以深度优先的方式系统地搜索问题的解的算法称为回溯法,它适用于解一些组合数较大的问题。 二、算法框架: 1、问题的解空间:应用回溯法解问题时,首先应明确定义问题的解空间。问题的解空间应到少包含问题的一个(最优)解。 2、回溯法的基本思想:确定了解空间的组织结构后,回溯法就从开始结点(根结点)出发,以深度优先的方式搜索整个解空间。这个开始结点就成为一个活结点,同时也成为当前的扩展结点。在当前的扩展结点处,搜索向纵深方向移至一个新结点。这个新结点就成为一个新的活结点,并成为当前扩展结点。如果在当前的扩展结点处不能再向纵深方向移动,则当前扩展结点就成为死结点。换句话说,这个结点不再是一个活结点。此时,应往回移动(回溯)至最近的一个活结点处,并使这个活结点成为当前的扩展结点。回溯法即以这种工作方式递归地在解空间中搜索,直至找到所要求的解或解空间中已没有活结点时为止。 运用回溯法解题通常包含以下三个步骤: (1)针对所给问题,定义问题的解空间; (2)确定易于搜索的解空间结构; (3)以深度优先的方式搜索解空间,并且在搜索过程中用剪枝函数避免无效搜索。F:在free的时候会执行检查。
Z:表示Red Zone的意思。
P:是Poison的意思。
U:会记录slab的使用者信息,如果打开,会会显示分配释放对象的栈回溯。
Redzone overwritten
Object padding overwritten
Object already free
Poison overwritten
slab-out-of-bounds
user-after-free
测试结果如下:
stack-out-of-bounds
global-out-of-bounds
测试结果如下:
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)