今天调试代码的时候,遇到一个问题就是出现了“段错误”。出现“段错误”的原因就是:访问的内存超出了系统给这个程序所设定的内存空间。知道原因是一个很好的开始,但是并不代表就很容易解决,特别是在代码量较大的情况下,如何才能定位到出错的地方?接下来,我就大概讲一下自己的一点经验,如何在Linux C中用几个命令搞定“段错误”。
1、dmesg
通过dmesg命令可以查看发生段错误的程序名称、引起段错误发生的内存地址、指令指针地址、堆栈指针地址、错误代码、错误原因等。
如,运行dmesg命令后,
[91046.776582] Test[26966]: segfault at 4 ip 0804f57f sp bfa0a224 error 6 in Test[8048000+1e000]
可以看出,发生段错误的地址:4, 和指令指针地址:0804f57f
2、ldd
使用ldd命令查看二进制程序的共享链接库依赖,包括库的名称、起始地址,这样可以确定段错误到底是发生在了自己的程序中还是依赖的共享库中。
如,运行 ldd bin/Test
linux-gate.so.1 => (0xb77d7000)
libevent-2.0.so.5 => /usr/lib/libevent-2.0.so.5 (0xb777d000)
libpthread.so.0 => /lib/tls/i686/cmov/libpthread.so.0 (0xb7764000)
librt.so.1 => /lib/tls/i686/cmov/librt.so.1 (0xb775a000)
libxml2.so.2 => /usr/lib/libxml2.so.2 (0xb762f000)
libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0xb74d5000)
/lib/ld-linux.so.2 (0xb77d8000)
libdl.so.2 => /lib/tls/i686/cmov/libdl.so.2 (0xb74d1000)
libz.so.1 => /lib/libz.so.1 (0xb74bc000)
libm.so.6 => /lib/tls/i686/cmov/libm.so.6 (0xb7495000)
通过以上信息可以排除“段错误”发生在共享链接库的可能。
3、nm
使用nm命令列出二进制文件中的符号表,包括符号地址、符号类型、符号名等,这样可以帮助定位在哪里发生了段错误。
如,执行 nm bin/Test | grep 0804f5
0804f560 T nwGtpv1uMsgAddIeTV1
0804f5a0 T nwGtpv1uMsgAddIeTV2
0804f5e0 T nwGtpv1uMsgAddIeTV4
0804f500 T nwGtpv1uMsgGetTpduHandle
0804f530 T nwGtpv1uMsgGetTpduLength
在步骤1的时候,发生段错误的指令指针地址:0804f57f。结合以上信息,可以定位出错误的发生应该是在执行 nwGtpv1uMsgAddIeTV1 函数的时候。
4、objdump
objdump命令是Linux下的反汇编目标文件或者可执行文件的命令,使用objdump生成二进制的相关信息,并重定向到文件中
如,执行 objdump -d bin/Test > testDump
接下来,需要在 testDump 文件中查找发生段错误的地址
如,执行 grep -n -A 10 -B 10 "0804f57f" ./testDump
3327:0804f560 :
3328- 804f560:83 ec 08 sub $0x8,%esp
3329- 804f563:8b 44 24 0c mov 0xc(%esp),%eax
3330- 804f567:89 34 24 mov %esi,(%esp)
3331- 804f56a:8b 74 24 10 mov 0x10(%esp),%esi
3332- 804f56e:89 7c 24 04 mov %edi,0x4(%esp)
3333- 804f572:8b 7c 24 14 mov 0x14(%esp),%edi
3334- 804f576:0f b7 50 0c movzwl 0xc(%eax),%edx //装入的32位寄存器 edx 前16位
3335- 804f57a:03 50 18 add 0x18(%eax),%edx
3336- 804f57d:89 f1 mov %esi,%ecx
3337- 804f57f:88 0a mov %cl,(%edx)
可以发现,段错误就发生的位置:在 nwGtpv1uMsgAddIeTV1 函数中,执行汇编命令
3337- 804f57f:88 0a mov %cl,(%edx)
找到对应的 nwGtpv1uMsgAddIeTV1 函数源码:
411 NwGtpv1uRcT
412 nwGtpv1uMsgAddIeTV1(NW_IN NwGtpv1uMsgHandleT hMsg,
413 NW_IN uint8_t type,
414 NW_IN uint8_t value)
415 {
416 NwGtpv1uMsgT *pMsg = (NwGtpv1uMsgT *) hMsg;
417 NwGtpv1uIeTv1T *pIe;
418
419
420 pIe = (NwGtpv1uIeTv1T *) (pMsg->msgBuf + pMsg->msgLen);
421 pIe->t = type;
422 pIe->v = value;
423
424 pMsg->msgLen += sizeof(NwGtpv1uIeTv1T);
425
426 return NW_GTPV1U_OK;
427 }
很明显,其实就是指针pIe出现的问题。
5、fprintf
加上打印语句,看看变量值究竟是个什么鬼。
411 NwGtpv1uRcT
412 nwGtpv1uMsgAddIeTV1(NW_IN NwGtpv1uMsgHandleT hMsg,
413 NW_IN uint8_t type,
414 NW_IN uint8_t value)
415 {
416 NwGtpv1uMsgT *pMsg = (NwGtpv1uMsgT *) hMsg;
417 NwGtpv1uIeTv1T *pIe;
418 fprintf(stderr,"pMsg->msgBuf:%d \n",pMsg->msgBuf);//0
419 fprintf(stderr,"pMsg->msgLen:%d \n",pMsg->msgLen);//4
420 pIe = (NwGtpv1uIeTv1T *) (pMsg->msgBuf + pMsg->msgLen);
421 pIe->t = type;
422 pIe->v = value;
423
424 pMsg->msgLen += sizeof(NwGtpv1uIeTv1T);
425
426 return NW_GTPV1U_OK;
427 }
6、gdb
使用gdb工具来对程序进行调试
如,执行 gdb --args ./bin/Test 2 127.0.0.1 127.0.0.1
加断点
(gdb) b NwGtpv1uMsg.c:418
(gdb) r
(gdb) s
调试信息发现,pMsg->msgBuf的值为0,pMsg->msgLen的值为4,很明显:指针pIe访问的地址就出问题了!!
再往下调试就会发现
Program received signal SIGSEGV, Segmentation fault.
nwGtpv1uMsgAddIeTV1 (hMsg=539354136, type=14 '\016', value=0 '\000') at /home/zlj/openair4G/openair-cn/GTPV1-U/nw-gtpv1u/src/NwGtpv1uMsg.c:424
424 pIe->t = type;
(gdb)
Program terminated with signal SIGSEGV, Segmentation fault.
The program no longer exists.
段错误产生了!!
总结
在Linux环境下使用C做项目,“段错误”的出现比较常见,首先就是要知道段错误发生的原因,然后快速准确地找到错误位置,结合代码分析问题,最后干掉bug。