通过反汇编定位段错误

时间:2022-06-07 11:55:25

段错误是程序员最讨厌的问题之一,其发生往往很突然,且破坏巨大。典型的段错误是由于操作内存不当引起的(如使用野指针或访问受保护的地址等),发生段错误时,内核以一个信号SIGSEGV强行终止进程,留下的出错信息极少,从而导致难以定位。但利用gdb和反汇编工具,可以较准确地定位段错误产生的原因。但想用这种方法调试,一些准备工作和工具是必需的。


准备工作

(1)coredump:进程异常中止时,内核生成的记录文件,其中保存了进程异常时所占用的内存和CPU资源,如pc计数器、各个寄存器的值等。这个文件是调试段错误最重要的依据。要使内核生成coredump,需要在内核配置中打开CONFIG_ELF_CORE选项,如果没有打开,将其选上后重新编译内核即可。

此外,利用命令ulimit -c unlimited,可以设置coredump大小为不受限制,可以保存更完整的信息。文件/proc/sys/kernel/core_pattern可以配置生成coredump的命名格式,如果不设置格式,则coredump默认生成的位置在出错进程的目录下,且生成的core同名,也就意味着旧的coredump可能被新的coredump所覆盖。如果我想在/tmp目录下生成以core.pid格式命名的coredump文件,只需执行命令:

echo "/tmp/core.%p" > /proc/sys/kernel/core_pattern

(2)编译:为了利用gdb进行调试,在编译程序时,需要在编译选项中加入-g 选项,以将调试信息编译到目标文件中。

(3)反汇编:顾名思义,反汇编就是将编译好的二进制可执行文件翻译成汇编文件。一般来说,编译器会自带一套反汇编工具,只有选择正确的工具才能正确地进行反汇编,这不难理解。比如我是用gcc4.6.3编译的用于mips的应用程序,那么,在编译器的目录下可以找到gcc463/usr/bin/mipsel-buildroot-linux-uclibc-objdump,这就是我要使用的反汇编工具。将二进制文件反汇编成汇编文件只需执行命令:XXXX-objdump -S XXXX(程序名),即可生成可以阅读的、关联到C代码的汇编代码,如下所示:

	status = httpRpmPost(reqId);	
42f208: 0320f809 jalr t9
42f20c: 00808021 move s0,a0
42f210: 8fbc0018 lw gp,24(sp)
if (RPM_OK != status && RPM_DONE != status)
42f214: 10400009 beqz v0,42f23c <postDataApStatusJson.part.4+0x68>
42f218: 3c120061 lui s2,0x61
42f21c: 24030002 li v1,2
42f220: 10430006 beq v0,v1,42f23c <postDataApStatusJson.part.4+0x68>
42f224: 3c040061 lui a0,0x61
{
RPM_AP_ERROR("httpRpmPost error!");
42f228: 2484c928 addiu a0,a0,-14040
42f22c: 2645cc98 addiu a1,s2,-13160
42f230: 8f999e50 lw t9,-25008(gp)
42f234: 0810bca3 j 42f28c <postDataApStatusJson.part.4+0xb8>
42f238: 24060327 li a2,807
return RPM_ERROR;
}

httpStatusSet(reqId, HTTP_OK);
42f23c: 8f998088 lw t9,-32632(gp)
42f240: 02002021 move a0,s0
42f244: 0320f809 jalr t9
42f248: 00002821 move a1,zero
42f24c: 8fbc0018 lw gp,24(sp)
可以看到,C代码下面跟着一串汇编代码,而汇编代码前面有一段地址,这个地址是什么呢?如果熟悉Linux进程空间的概念,很容易就可以联想到,这个地址其实就是相应的汇编指令在.text段(即代码段)中的地址。也就是说,这个地址就是我们用于定位具体出错地点的依据。(4)gdb:可以说是Linux下调试程序最常用的工具,功能强大,操作也很简单。对于mips程序调试,只需安装相应的gdb:mips-linux-gdb即可。


开始调试

上面的准备工作都完成后,就可以开始调试了。当进程再次异常终止时,就可以在/tmp目录下找到coredump文件:比如core.126(进程id为126的进程生成的coredump)。
用gdb的-c选项打开coredump:mips-linux-gdb -c /tmp/core.126,可以看到如下信息:

GNU gdb (GDB) 7.4.1
Copyright (C) 2012 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 "--host=i686-pc-linux-gnu --target=mips-linux".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Core was generated by `/usr/bin/httpd'.
Program terminated with signal 11, Segmentation fault.
#0 0x2b17ff50 in ?? ()
(gdb)
前面是gdb的版本信息,不必理会。我们主要关注下面的内容:
Core was generated by `/usr/bin/httpd'.Program terminated with signal 11, Segmentation fault.#0  0x2b17ff50 in ?? ()
表示这个coredump是为进程httpd生成的,而进程退出的原因是signal 11,即SIGSEGV,这正是我们想要的。最后一行,0x2b17ff50是一个地址,这里??的地方本来应该显示一个函数名,之所以这里没有显示,我猜想这应该是一个库函数,而编译这个库时,并没有带入-g信息。
不要紧,接下来只需要输入where,即可显示信号产生时程序中止的位置:
Core was generated by `/usr/bin/httpd'.
Program terminated with signal 11, Segmentation fault.
#0 0x2b17ff50 in ?? ()
(gdb) where
#0 0x2b17ff50 in ?? ()
#1 0x0045c034 in ?? ()
Backtrace stopped: previous frame identical to this frame (corrupt stack?)
(gdb)
至此,我们已经拿到最重要的信息:0x0045c034,就是进程中止时停留的位置。对照上面生成的反汇编文件,搜索45c034,即可找到:
    out = cJSON_Print(root);  45c00c:	8f9981ec 	lw	t9,-32276(gp)  45c010:	00000000 	nop  45c014:	0320f809 	jalr	t9  45c018:	02402021 	move	a0,s2  45c01c:	8fbc0010 	lw	gp,16(sp)    httpnPrintf(reqId, strlen(out) + 1, "%s\n", out);  45c020:	00402021 	move	a0,v0  45c024:	8f999fe4 	lw	t9,-24604(gp)  45c028:	00000000 	nop  45c02c:	0320f809 	jalr	t9  45c030:	00408021 	move	s0,v0  45c034:	8fbc0010 	lw	gp,16(sp)  45c038:	8fa47e48 	lw	a0,32328(sp)  45c03c:	3c060062 	lui	a2,0x62  45c040:	8f9981f0 	lw	t9,-32272(gp)  45c044:	24450001 	addiu	a1,v0,1  45c048:	24c6cdb8 	addiu	a2,a2,-12872  45c04c:	0320f809 	jalr	t9  45c050:	02003821 	move	a3,s0  45c054:	8fbc0010 	lw	gp,16(sp)  45c058:	00000000 	nop    RPM_MONITORAP_TRACE("\r\n%s\r\n\r\n", out);

可以看到,出错时,对应的C函数是httpnPrintf,对应的汇编代码为:    lw    gp, 16(sp)。

在反汇编文件中再稍微对照上下文,即可知道具体是哪个模块、哪个文件中的调用。如果看得懂汇编代码,基本可以定位到函数中的具体语句,即使看不懂汇编,利用打印调试或者静态代码分析等常规调试手段也基本可以定位到具体的出错原因了。在本例中,最终确定这个函数出错的原因是操作了调用malloc(0)而获取的一个空指针(malloc(0)返回什么),着实令人始料未及。