调试segfault 经验总结

最近遇到一个棘手的问题,用在现场的程序崩溃了,但是却无法复现,隔个一个月左右出一次,已经出现了三次,各种测试并没有能复现问题。现在只好把目光聚焦在能够获得的仅有的信息,系统日志上了,查看/var/log/message,找到了关键的一套信息:

 kernel: myapp[1427]: segfault at 494d1f84 ip 00d73e54 sp b17fac20 error 6 in libThdDllAlg.so[d63000+1e000]

这条信息太宝贵了,这是唯一能够获得的信息。但是怎么用呢?一头雾水。

经过查找资料终于有了一点眉目。

1.myapp是崩溃的程序名,后面中括号中的是PID号,对我没什么用。

2.后面的at 494d1f84 ip 00d73e54 sp b17fac20 ,at后面是发生段错误时访问的地址,是错误当时的地址,好像对我的调试用处不大,ip后面是指令地址,这个就非常重要了,配合后面库的基地址就可以找到出错的代码了。sp后面的stack pointer栈顶指针,貌似我也没用上。

3.error后面的6,这个还是比较重要的,但是得到的信息不多,只能得到信息:由于用户态程序写操作访问越界造成的。

error number是由三个字位组成的,从高到底分别为bit2 bit1和bit0,所以它的取值范围是0~7.
bit2: 值为1表示是用户态程序内存访问越界,值为0表示是内核态程序内存访问越界
bit1: 值为1表示是写操作导致内存访问越界,值为0表示是读操作导致内存访问越界
bit0: 值为1表示没有足够的权限访问非法地址的内容,值为0表示访问的非法地址根本没有对应的页面,也就是无效地址

4. libThdDllAlg.so[d63000+1e000]这个是崩在了哪个库上,这个非常重要,方框号里第一个是运行时库的基地址,跟ip后面的一块用,用ip后面的值减去基地址就可以得到出错在这个库里的哪个位置了。在这里是00d73e54-d63000=10e54.至于+号后面是什么意思,我还没搞懂。希望以后能补充

下面知道了出错的库和出错的指令地址,就可以看看是出在了哪条指令上了

还好这个库是我自己 写的库,不是系统库。用objdump -d libThdDllAlg.so>dumpcode 把文件反汇编写进dumpcode文件。这个libThdDllAlg.so是从现场拷贝回来了,而不是自己重新编译的。

打开dumpcode文件,找到10e54地址,往上翻能够找到是在哪个函数里,再结合源文件,找到上下文,大概就能知道出错在哪里了。

------------------------------------------------------------------------------------------------------------------------------------------------------------------------

前面讲到了把崩溃点定位到了函数级别,那么怎么找到是那一行代码出了问题呢?

经过后来的实践,总结了定位到行的方法。

之前的方法使用的前提是获得的release版程序的崩溃信息,由于时release版,没有调试信息,所以没法直接使用objdump来显示出源代码行号等信息。不过,我们仍然可以重新编译一个debug版本的汇编代码,与release版的反汇编来对照,也可以找到出错代码所在的行。

debug版objdump使用(引自objdump反汇编用法示例

-d:将代码段反汇编
-S:将代码段反汇编的同时,将反汇编代码和源代码交替显示,编译时需要给出-g,即需要调试信息。
-C:将C++符号名逆向解析。
-l:反汇编代码中插入源代码的文件名和行号。
-j section:仅反汇编指定的section。可以有多个-j参数来选择多个section。
 

怎样通过现有代码生成带有源代码和行号的汇编代码呢?

1.使用g++ -S -g -fverbose-asm来生成汇编代码,当然可以直接在原有的编译命令基础上增加这几个参数,由于已经定位到了函数,所以可以直接只编译函数所在的cpp文件,将cpp编译为s

例如:

g++ -Wall -L./lib -DLINUX -O2 -fPIC -S -g -fverbose-asm -o xxx.s xxx.cpp

2使用as来查看,当然也可以打印到文件中来与objdump出来的对比

例如:

as -alhnd xxx.s>dump.txt

3.现在我们有了两个汇编版本,一个是release版,也就是实际运行的版本反汇编出来的(以下简称release版),另一个是带调试信息和源代码的(以下简称debug版)。下面就是对比这两个文件了。

首先找到release版中崩溃发生的代码,从上下寻找特征代码,一般可以找特征数字

例如

   274da:    0f b7 4f 06              movzwl 0x6(%edi),%ecx
   274de:    0f b7 c0                 movzwl %ax,%eax
   274e1:    0f b7 57 02              movzwl 0x2(%edi),%edx
   274e5:    89 85 38 fe ff ff        mov    %eax,-0x1c8(%ebp)
   274eb:    66 89 8d 28 fe ff ff     mov    %cx,-0x1d8(%ebp)
   274f2:    89 95 30 fe ff ff        mov    %edx,-0x1d0(%ebp)
   274f8:    0f b7 85 28 fe ff ff     movzwl -0x1d8(%ebp),%eax
   274ff:    8b 8d 30 fe ff ff        mov    -0x1d0(%ebp),%ecx
   27505:    8b 95 10 fe ff ff        mov    -0x1f0(%ebp),%edx
   2750b:    89 44 24 10              mov    %eax,0x10(%esp)
   2750f:    8b 85 38 fe ff ff        mov    -0x1c8(%ebp),%eax
   27515:    89 4c 24 0c              mov    %ecx,0xc(%esp)
   27519:    89 44 24 08              mov    %eax,0x8(%esp)
   2751d:    e9 2c ff ff ff           jmp    2744e <_ZN9SlClass3RunEtt+0xdde>
   27522:    8d b6 00 00 00 00        lea    0x0(%esi),%esi
   27528:    8b 95 34 fe ff ff        mov    -0x1cc(%ebp),%edx
   2752e:    0f b7 c0                 movzwl %ax,%eax
   27531:    89 85 30 fe ff ff        mov    %eax,-0x1d0(%ebp)
   27537:    8b 42 04                 mov    0x4(%edx),%eax
   2753a:    8b 95 30 fe ff ff        mov    -0x1d0(%ebp),%edx
   27540:    8b 04 90                 mov    (%eax,%edx,4),%eax
   27543:    85 c0                    test   %eax,%eax
   27545:    0f 84 99 02 00 00        je     277e4 <_ZN9SlClass3RunEtt+0x1174>
   2754b:    0f b7 57 02              movzwl 0x2(%edi),%edx
   2754f:    3b 50 0c                 cmp    0xc(%eax),%edx
   27552:    89 95 04 fe ff ff        mov    %edx,-0x1fc(%ebp)
   27558:    0f 83 6a 01 00 00        jae    276c8 <_ZN9SlClass3RunEtt+0x1058>
   2755e:    8b 40 08                 mov    0x8(%eax),%eax
   27561:    8b 95 04 fe ff ff        mov    -0x1fc(%ebp),%edx
   27567:    8b 04 90                 mov    (%eax,%edx,4),%eax
   2756a:    85 c0                    test   %eax,%eax
   2756c:    89 85 28 fe ff ff        mov    %eax,-0x1d8(%ebp)
   27572:    0f 84 50 01 00 00        je     276c8 <_ZN9SlClass3RunEtt+0x1058>
   27578:    8b 95 28 fe ff ff        mov    -0x1d8(%ebp),%edx
   2757e:    0f b7 47 06              movzwl 0x6(%edi),%eax
   27582:    66 3b 42 06              cmp    0x6(%edx),%ax

这是我的relase版汇编代码,程序崩溃在27582行,我找的特征代码是274e5行,之所以选这个,是因为后面连续的几个代码都带有不同的数字,这个重复的概率就比较小了。这里有个地方要注意,这里是16进制,debug版文件里是10进制,需要转换一下。

-0x1c8转为-456,直接在debug版中搜索-456,找到movl    %eax, -456,再搜索%eax, -456,还好,只搜到4处,然后结合下一行代码,可以定位到是哪一个。看看上下文,就定位到了源文件所在的行。

-----------------------------------------------------------------------------------------------

引用以下文章

1.一次segfault错误的排查过程,引用地址https://blog.csdn.net/zhaohaijie600/article/details/45246569

                                                                                                 一次segfault错误的排查过程

正常运行了几年的程序忽然崩溃了,由于机器没有设置CORE文件,无法从CORE中取得错误信息,程序运行在centOS 7上, 本来对centOS用的也是不熟,只能边查资料边查问题。

首先、我需要确认程序是否真的崩溃了,还是别人误操作关闭了。如果程序真的崩溃了,会在系统中留下痕迹,我查了一下,在messages文件中发现了一条信息:

xxxxx.o[2374]: segfault at7f0ed0bfbf70 ip 00007f0edd646fe7 sp 00007f0ed3603978 error 4 inlibc-2.17.so[7f0edd514000+1b6000]

由上面信息看出,系统确实是崩溃了,发生了段错误。

查看messages需要root权限,用命令:cat /var/log/messages 就可以了,还有一个命令dmesg也可以查到上面的信息。

从上面的信息,我们可以得到以下信息:

1、从libc-2.17.so[7f0edd514000+1b6000]可以看出错误发生在libc上,libc在此程序中映射的内存基址为7f0edd514000,这个信息是个坏消息,这个so上的东西太多了;

2、segfault at和error 4这两条信息可以得出是内存读出错,4的意义如下,可以对照参考:

bit2:值为1表示是用户态程序内存访问越界,值为0表示是内核态程序内存访问越界
bit1: 值为1表示是写操作导致内存访问越界,值为0表示是读操作导致内存访问越界
bit0: 值为1表示没有足够的权限访问非法地址的内容,值为0表示访问的非法地址根本没有对应的页面,也就是无效地址

4正好为用户态内存读操作访问出界。

3、7f0ed0bfbf70,00007f0edd646fe7,00007f0ed3603978这三个值:第一个值为出错的地址,用处不大;第二个值为发生错误时指令的地址,这个值在有些错误时是错误的,下面会讲一下,第三个值为堆栈指针。

除了以上信息,就是六七万行的代码。感觉没有太大的指望。

C++段错误就几类,读写错误,这个主要是参数没有控制好,这种错误比较常见,我们经常把NULL指针、未初始化或非法值的指针传递给函数,从而引出此错误;指令地址出错,这类错误主要是由虚函数,回调函数引起,最常出现的是虚函数,由于虚函数保存在类变量中,如果不小心用了非安全函数,就可能把虚数指针覆盖掉,从而影响出现错误。但指令地址出错的情况相对参数出错来讲还是要少很多的,因为用到此功能的还是需要一定的水平的,不容易犯一些低级错误。

从上面分析的第二点来看,基本上属于读写错误,但从六七万行代码找出问题,可能性不大,只能缩小范围,我决定从上面提到的三点,找到出错的函数,然后再从代码中找出所有出错函数调用的地方来定位问题。由于错误指出出错的组件为libc,而且基本上是参数出现,所以发现错误的指令地址应该是可信的,我们可以根据指令地址查出是哪个函数。指令地址为:00007f0edd646fe7 ,libc指令的基地址为:7f0edd514000,可以根据这两个值计算一下该指令的相对地址为132FE7,下面我们需要找到相对代码段地址为132FE7的地方为什么函数。

开始我想得到反汇编代码,但这个组件代码太多,看不到头,于是我找了个取巧的办法,查看导出函数和基地址,结果所以还是很多,我就用132和133进行了一下过滤,得出以下信息

[root@localhostlib64]# objdump -tT libc-2.17.so | grep 132

000000000008284fl     F .text  000000000000001b              _L_unlock_1325

0000000000082ebfl     F .text  000000000000001c              _L_lock_11322

000000000010b952l     F .text  000000000000001b              _L_unlock_132

000000000010ce62l     F .text  000000000000001b              _L_unlock_132

00000000001132e0l     F .text  00000000000001cf              ruserok2_sa

00000000000f1320l     F .text  00000000000001ce              __ecvt_r

00000000000bf370l     F .text  0000000000000132              __statfs_link_max

0000000000132080l     F .text  0000000000000068              __nss_gshadow_lookup

0000000000132f50l     F .text  0000000000000fd9              __strncmp_sse42

00000000001320f0l     F .text  00000000000000a5              __strchr_sse42

0000000000132020l     F .text  000000000000005e              __nss_aliases_lookup

00000000001321a0l     F .text  0000000000000da9              __strcmp_sse42

00000000001153b0g     F .text  0000000000000132              setnetgrent

00000000000f1320g     F .text  00000000000001ce              ecvt_r

0000000000112b50g     F .text  0000000000000132              ether_ntohost

00000000000f1320g    DF .text  00000000000001ce  GLIBC_2.2.5 ecvt_r

0000000000112b50g    DF .text  0000000000000132  GLIBC_2.2.5 ether_ntohost

00000000001153b0g    DF .text  0000000000000132  GLIBC_2.2.5 setnetgrent

[root@localhostlib64]# objdump -tT libc-2.17.so | grep 133

000000000006e3cal     F .text  000000000000001b              _L_unlock_133

0000000000075055l     F .text  0000000000000018              _L_unlock_133

000000000008286al     F .text  000000000000001f              _L_unlock_1335

000000000008305al     F .text  000000000000001b              _L_lock_13385

0000000000133f30l     F .text  000000000000019a              __strrchr_sse42

注意,我标红的部分,132f50和132fe7很接近,很大可能是这个函数出现,而且又是读地址非法,这个函数有可能会出错这个问题,__strncmp_sse42这个函数是被strncmp调用的,看到这个函数基本上可以确定应该是这个函数惹得祸。

我又写了两行代码确认了一下

strncmp(0,“1234”, 5);

strncmp(“1234”,0, 5);

分别编绎成两个应用运行,出错,messages中的错误信息如下:

Apr23 01:38:03 localhost kernel: a.out[3254]: segfault at 0 ip 00007f58a5386f7c sp00007fffb3de5d18 error 4 in libc-2.17.so[7f58a5254000+1b6000]

Apr23 01:39:34 localhost kernel: a.out[3267]: segfault at 0 ip 00007f8bb1908f80 sp00007fff61695408 error 4 in libc-2.17.so[7f8bb17d6000+1b6000]

计算了一下,出错的相对地址为:132F80和132F7C,和我们遇到的错误地址很接近,于是,把__strncmp_sse42的汇编代码打印了部分如下:

Dump of assemblercode for function __strncmp_sse42:

   0x00007ffff732ef50 <+0>: test   %rdx,%rdx

   0x00007ffff732ef53 <+3>: je     0x7ffff732ff14<__strncmp_sse42+4036>

   0x00007ffff732ef59 <+9>: cmp    $0x1,%rdx

   0x00007ffff732ef5d <+13>: je     0x7ffff732ff20<__strncmp_sse42+4048>

   0x00007ffff732ef63 <+19>: mov    %rdx,%r11

   0x00007ffff732ef66 <+22>: mov    %esi,%ecx

   0x00007ffff732ef68 <+24>: mov    %edi,%eax

   0x00007ffff732ef6a <+26>: and    $0x3f,%rcx

   0x00007ffff732ef6e <+30>: and    $0x3f,%rax

   0x00007ffff732ef72 <+34>: cmp    $0x30,%ecx

   0x00007ffff732ef75 <+37>: ja     0x7ffff732efc0 <__strncmp_sse42+112>

   0x00007ffff732ef77 <+39>: cmp    $0x30,%eax

   0x00007ffff732ef7a <+42>: ja     0x7ffff732efc0 <__strncmp_sse42+112>

           0x00007ffff732ef7c <+44>: movdqu(%rdi),%xmm1

           0x00007ffff732ef80 <+48>:movdqu (%rsi),%xmm2

   0x00007ffff732ef84 <+52>: pxor   %xmm0,%xmm0

   0x00007ffff732ef88 <+56>: pcmpeqb%xmm1,%xmm0

   0x00007ffff732ef8c <+60>: pcmpeqb%xmm2,%xmm1

   0x00007ffff732ef90 <+64>: psubb  %xmm0,%xmm1

   0x00007ffff732ef94 <+68>: pmovmskb%xmm1,%edx

   0x00007ffff732ef98 <+72>: sub    $0xffff,%edx

   0x00007ffff732ef9e <+78>: jne    0x7ffff732ff00 <__strncmp_sse42+4016>

   0x00007ffff732efa4 <+84>: sub    $0x10,%r11

   0x00007ffff732efa8 <+88>: jbe    0x7ffff732ff14 <__strncmp_sse42+4036>

   0x00007ffff732efae <+94>: add    $0x10,%rsi

   0x00007ffff732efb2 <+98>: add    $0x10,%rdi

   0x00007ffff732efb6 <+102>: nopw   %cs:0x0(%rax,%rax,1)

   0x00007ffff732efc0 <+112>: and    $0xfffffffffffffff0,%rsi

   0x00007ffff732efc4 <+116>: and    $0xfffffffffffffff0,%rdi

   0x00007ffff732efc8 <+120>: mov    $0xffff,%edx

   0x00007ffff732efcd <+125>: xor    %r8d,%r8d

   0x00007ffff732efd0 <+128>: and    $0xf,%ecx

   0x00007ffff732efd3 <+131>: and    $0xf,%eax

   0x00007ffff732efd6 <+134>: pxor   %xmm0,%xmm0

   0x00007ffff732efda <+138>: cmp    %eax,%ecx

   0x00007ffff732efdc <+140>: je     0x7ffff732f010 <__strncmp_sse42+192>

   0x00007ffff732efde <+142>: ja     0x7ffff732efe7 <__strncmp_sse42+151>

   0x00007ffff732efe0 <+144>: mov    %edx,%r8d

   0x00007ffff732efe3 <+147>: xchg   %eax,%ecx

   0x00007ffff732efe4 <+148>: xchg   %rsi,%rdi

       0x00007ffff732efe7<+151>: movdqa (%rdi),%xmm2

       0x00007ffff732efeb <+155>: movdqa(%rsi),%xmm1

   0x00007ffff732efef <+159>: lea    0xf(%rax),%r9

   0x00007ffff732eff3 <+163>: sub    %rcx,%r9

   0x00007ffff732eff6 <+166>: lea    0x4d4c3(%rip),%r10

红色部分为我自己写的测试程序出错的地址,

绿色部分为所要查的程序出错的地址,从这部分可以看到,出错的参数为strncmp的第二个参数。

四、多线程

由于用UE对源代码进行查找,还好只有335处调用,对第二个参数为常量和不被调用的代码再排除,只有20多处可疑。

现在需要再分析一下,第二个参数读非法,那么变量如果在栈中话,需要访问到栈外的空间,这样,如果是第二个参数为栈变量的话,第三个参数值应该会很大。如果在堆变量,情况就比较复杂了,由于第三个参数值都很少,最大的为128,所以又排除了栈变量出错的可能性,从上面的数据来看,出错的地址比栈小了很多,也证明了不是栈变量出错。最后只剩下8处,我对这8处仔细查了一下,感觉都没有问题,参数控制,变量长度,控制得都没有问题。

如果代码上没有问题,那么问题就比较难查了,我再进行分析一下,函数用户和变量上表面上没有问题,那么就可能出现几种可能:变量指针覆盖,内存移动。变量指针覆盖在实际中出现的概率比较大,往往使用了strcpy和memcpy之类的函数,没有进行边界值检查,把一些指针的值覆盖了,对于这种情况,我对代码进行了检查,可能性很小,而且上面给出的出错的参数值不像随意覆盖的值。如果这种情况可能性很小,对于一个变量只在单个线程被访问的情况出错的可能性也很小,于是,我又排除了6处。

剩下的两处代码在一起的,参数为一个类的内部变量,这个类可能会被多个线程访问,所以此此出现的可能性很大,我仔细看了一下,边界检查也做了,线程互斥访问控制也进行了处理,没有明显的问题,还好用到这个变量的只有一个文件,我对整个文件对这个变量进行搜索,发现了一个函数realloc。感觉可能是这个函数导致的问题,此变量存放的是一个数组,数组会不断增长,当空间不够时,就用realloc再重申请,看到这个函数后,心中一阵激动,感觉问题就在这儿,我把这几段代码仔细看了几遍,访问控制,锁住修改,动态申请,最终发现了一个问题,在使用realloc时,临界区没有保护好。

事情似乎比较明朗了,在一个线程对这个变量指针进行访问时(即用strnmcp处),线程切换到另一个线程对这个变量指针进行了realloc操作,realloc重新申请了新的空间,把老的数据移动的新的空间,然后把旧空间处的内存彻底释放掉了,然后又切换到原先的线程,执行strncmp函数,这个函数用的指针参数还是旧的空间地址,而旧的空间已经被释放了,所以出现了访问非法的错误。

我查了一下程序日志,发现出现错误时,正好需要重新申请空间,应该是这个问题导致的。

这种情况发生的概率很低,很难重现,因为realloc和strncmp函数执行时间都很短,而且realloc的概率不高,所以运行了4,5年才出现错误,但是总体来讲,还是比较幸运的,如果旧的空间没有被释放,那么接下来的各种操作全在旧的空间上,可能会导致更大的损失。

———赵海杰

20150424
---------------------
版权声明:本文为CSDN博主「zhaohaijie600」的原创文章,遵循CC 4.0 by-sa版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/zhaohaijie600/article/details/45246569

2.Linux环境下段错误的产生原因及调试方法小结

引用地址https://www.cnblogs.com/panfeng412/archive/2011/11/06/segmentation-fault-in-linux.html

最近在Linux环境下做C语言项目,由于是在一个原有项目基础之上进行二次开发,而且项目工程庞大复杂,出现了不少问题,其中遇到最多、花费时间最长的问题就是著名的“段错误”(Segmentation Fault)。借此机会系统学习了一下,这里对Linux环境下的段错误做个小结,方便以后同类问题的排查与解决。

1. 段错误是什么

一句话来说,段错误是指访问的内存超出了系统给这个程序所设定的内存空间,例如访问了不存在的内存地址、访问了系统保护的内存地址、访问了只读的内存地址等等情况。这里贴一个对于“段错误”的准确定义(参考Answers.com):

复制代码

A segmentation fault (often shortened to segfault) is a particular error condition that can occur during the operation of computer software. In short, a segmentation fault occurs when a program attempts to access a memory location that it is not allowed to access, or attempts to access a memory location in a way that is not allowed (e.g., attempts to write to a read-only location, or to overwrite part of the operating system). Systems based on processors like the Motorola 68000 tend to refer to these events as Address or Bus errors.

Segmentation is one approach to memory management and protection in the operating system. It has been superseded by paging for most purposes, but much of the terminology of segmentation is still used, "segmentation fault" being an example. Some operating systems still have segmentation at some logical level although paging is used as the main memory management policy.

On Unix-like operating systems, a process that accesses invalid memory receives the SIGSEGV signal. On Microsoft Windows, a process that accesses invalid memory receives the STATUS_ACCESS_VIOLATION exception.

复制代码

2. 段错误产生的原因

2.1 访问不存在的内存地址

复制代码

#include<stdio.h>
#include<stdlib.h>
void main()
{
        int *ptr = NULL;
        *ptr = 0;
}

复制代码

2.2 访问系统保护的内存地址

复制代码

#include<stdio.h>
#include<stdlib.h>
void main()
{
        int *ptr = (int *)0;
        *ptr = 100;
}

复制代码

2.3 访问只读的内存地址

复制代码

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void main()
{
        char *ptr = "test";
        strcpy(ptr, "TEST");
}

复制代码

2.4 栈溢出

复制代码

#include<stdio.h>
#include<stdlib.h>
void main()
{
        main();
}

复制代码

等等其他原因。

3. 段错误信息的获取

程序发生段错误时,提示信息很少,下面有几种查看段错误的发生信息的途径。

3.1 dmesg

dmesg可以在应用程序crash掉时,显示内核中保存的相关信息。如下所示,通过dmesg命令可以查看发生段错误的程序名称、引起段错误发生的内存地址、指令指针地址、堆栈指针地址、错误代码、错误原因等。以程序2.3为例:

panfeng@ubuntu:~/segfault$ dmesg
[ 2329.479037] segfault3[2700]: segfault at 80484e0 ip 00d2906a sp bfbbec3c error 7 in libc-2.10.1.so[cb4000+13e000]

3.2 -g

使用gcc编译程序的源码时,加上-g参数,这样可以使得生成的二进制文件中加入可以用于gdb调试的有用信息。以程序2.3为例:

panfeng@ubuntu:~/segfault$ gcc -g -o segfault3 segfault3.c

3.3 nm

使用nm命令列出二进制文件中的符号表,包括符号地址、符号类型、符号名等,这样可以帮助定位在哪里发生了段错误。以程序2.3为例:

复制代码

panfeng@ubuntu:~/segfault$ nm segfault3
08049f20 d _DYNAMIC
08049ff4 d _GLOBAL_OFFSET_TABLE_
080484dc R _IO_stdin_used
         w _Jv_RegisterClasses
08049f10 d __CTOR_END__
08049f0c d __CTOR_LIST__
08049f18 D __DTOR_END__
08049f14 d __DTOR_LIST__
080484ec r __FRAME_END__
08049f1c d __JCR_END__
08049f1c d __JCR_LIST__
0804a014 A __bss_start
0804a00c D __data_start
08048490 t __do_global_ctors_aux
08048360 t __do_global_dtors_aux
0804a010 D __dso_handle
         w __gmon_start__
0804848a T __i686.get_pc_thunk.bx
08049f0c d __init_array_end
08049f0c d __init_array_start
08048420 T __libc_csu_fini
08048430 T __libc_csu_init
         U __libc_start_main@@GLIBC_2.0
0804a014 A _edata
0804a01c A _end
080484bc T _fini
080484d8 R _fp_hw
080482bc T _init
08048330 T _start
0804a014 b completed.6990
0804a00c W data_start
0804a018 b dtor_idx.6992
080483c0 t frame_dummy
080483e4 T main
         U memcpy@@GLIBC_2.0

复制代码

3.4 ldd

使用ldd命令查看二进制程序的共享链接库依赖,包括库的名称、起始地址,这样可以确定段错误到底是发生在了自己的程序中还是依赖的共享库中。以程序2.3为例:

panfeng@ubuntu:~/segfault$ ldd ./segfault3
    linux-gate.so.1 =>  (0x00e08000)
    libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0x00675000)
    /lib/ld-linux.so.2 (0x00482000)

4. 段错误的调试方法

4.1 使用printf输出信息

这个是看似最简单但往往很多情况下十分有效的调试方式,也许可以说是程序员用的最多的调试方式。简单来说,就是在程序的重要代码附近加上像printf这类输出信息,这样可以跟踪并打印出段错误在代码中可能出现的位置。

为了方便使用这种方法,可以使用条件编译指令#ifdef DEBUG和#endif把printf函数包起来。这样在程序编译时,如果加上-DDEBUG参数就能查看调试信息;否则不加该参数就不会显示调试信息。

4.2 使用gcc和gdb

4.2.1 调试步骤

 1、为了能够使用gdb调试程序,在编译阶段加上-g参数,以程序2.3为例:

panfeng@ubuntu:~/segfault$ gcc -g -o segfault3 segfault3.c

2、使用gdb命令调试程序:

复制代码

panfeng@ubuntu:~/segfault$ gdb ./segfault3 
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/>...
Reading symbols from /home/panfeng/segfault/segfault3...done.
(gdb) 

复制代码

3、进入gdb后,运行程序:

复制代码

(gdb) run
Starting program: /home/panfeng/segfault/segfault3 

Program received signal SIGSEGV, Segmentation fault.
0x001a306a in memcpy () from /lib/tls/i686/cmov/libc.so.6
(gdb) 

复制代码

从输出看出,程序2.3收到SIGSEGV信号,触发段错误,并提示地址0x001a306a、调用memcpy报的错,位于/lib/tls/i686/cmov/libc.so.6库中。

4、完成调试后,输入quit命令退出gdb:

复制代码

(gdb) quit
A debugging session is active.

    Inferior 1 [process 3207] will be killed.

Quit anyway? (y or n) y

复制代码

4.2.2 适用场景

1、仅当能确定程序一定会发生段错误的情况下使用。

2、当程序的源码可以获得的情况下,使用-g参数编译程序。

3、一般用于测试阶段,生产环境下gdb会有副作用:使程序运行减慢,运行不够稳定,等等。

4、即使在测试阶段,如果程序过于复杂,gdb也不能处理。

4.3 使用core文件和gdb

在4.2节中提到段错误会触发SIGSEGV信号,通过man 7 signal,可以看到SIGSEGV默认的handler会打印段错误出错信息,并产生core文件,由此我们可以借助于程序异常退出时生成的core文件中的调试信息,使用gdb工具来调试程序中的段错误。

4.3.1 调试步骤

1、在一些Linux版本下,默认是不产生core文件的,首先可以查看一下系统core文件的大小限制:

panfeng@ubuntu:~/segfault$ ulimit -c
0

2、可以看到默认设置情况下,本机Linux环境下发生段错误时不会自动生成core文件,下面设置下core文件的大小限制(单位为KB):

panfeng@ubuntu:~/segfault$ ulimit -c 1024
panfeng@ubuntu:~/segfault$ ulimit -c
1024

3、运行程序2.3,发生段错误生成core文件:

panfeng@ubuntu:~/segfault$ ./segfault3
段错误 (core dumped)

4、加载core文件,使用gdb工具进行调试:

复制代码

panfeng@ubuntu:~/segfault$ gdb ./segfault3 ./core 
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/>...
Reading symbols from /home/panfeng/segfault/segfault3...done.

warning: Can't read pathname for load map: 输入/输出错误.
Reading symbols from /lib/tls/i686/cmov/libc.so.6...(no debugging symbols found)...done.
Loaded symbols for /lib/tls/i686/cmov/libc.so.6
Reading symbols from /lib/ld-linux.so.2...(no debugging symbols found)...done.
Loaded symbols for /lib/ld-linux.so.2
Core was generated by `./segfault3'.
Program terminated with signal 11, Segmentation fault.
#0  0x0018506a in memcpy () from /lib/tls/i686/cmov/libc.6

复制代码

从输出看出,同4.2.1中一样的段错误信息。

5、完成调试后,输入quit命令退出gdb:

(gdb) quit

4.3.2 适用场景

1、适合于在实际生成环境下调试程序的段错误(即在不用重新发生段错误的情况下重现段错误)。

2、当程序很复杂,core文件相当大时,该方法不可用。

4.4 使用objdump

4.4.1 调试步骤

1、使用dmesg命令,找到最近发生的段错误输出信息:

panfeng@ubuntu:~/segfault$ dmesg
... ...
[17257.502808] segfault3[3320]: segfault at 80484e0 ip 0018506a sp bfc1cd6c error 7 in libc-2.10.1.so[110000+13e000]

其中,对我们接下来的调试过程有用的是发生段错误的地址:80484e0和指令指针地址:0018506a。

2、使用objdump生成二进制的相关信息,重定向到文件中:

panfeng@ubuntu:~/segfault$ objdump -d ./segfault3 > segfault3Dump

其中,生成的segfault3Dump文件中包含了二进制文件的segfault3的汇编代码。

3、在segfault3Dump文件中查找发生段错误的地址:

复制代码

panfeng@ubuntu:~/segfault$ grep -n -A 10 -B 10 "80484e0" ./segfault3Dump 
121- 80483df:    ff d0                    call   *%eax
122- 80483e1:    c9                       leave  
123- 80483e2:    c3                       ret    
124- 80483e3:    90                       nop
125-
126-080483e4 <main>:
127- 80483e4:    55                       push   %ebp
128- 80483e5:    89 e5                    mov    %esp,%ebp
129- 80483e7:    83 e4 f0                 and    $0xfffffff0,%esp
130- 80483ea:    83 ec 20                 sub    $0x20,%esp
131: 80483ed:    c7 44 24 1c e0 84 04     movl   $0x80484e0,0x1c(%esp)
132- 80483f4:    08 
133- 80483f5:    b8 e5 84 04 08           mov    $0x80484e5,%eax
134- 80483fa:    c7 44 24 08 05 00 00     movl   $0x5,0x8(%esp)
135- 8048401:    00 
136- 8048402:    89 44 24 04              mov    %eax,0x4(%esp)
137- 8048406:    8b 44 24 1c              mov    0x1c(%esp),%eax
138- 804840a:    89 04 24                 mov    %eax,(%esp)
139- 804840d:    e8 0a ff ff ff           call   804831c <memcpy@plt>
140- 8048412:    c9                       leave  
141- 8048413:    c3                       ret    

复制代码

通过对以上汇编代码分析,得知段错误发生main函数,对应的汇编指令是movl $0x80484e0,0x1c(%esp),接下来打开程序的源码,找到汇编指令对应的源码,也就定位到段错误了。

4.4.2 适用场景

1、不需要-g参数编译,不需要借助于core文件,但需要有一定的汇编语言基础。

2、如果使用了gcc编译优化参数(-O1,-O2,-O3)的话,生成的汇编指令将会被优化,使得调试过程有些难度。

4.5 使用catchsegv

catchsegv命令专门用来扑获段错误,它通过动态加载器(ld-linux.so)的预加载机制(PRELOAD)把一个事先写好的库(/lib/libSegFault.so)加载上,用于捕捉断错误的出错信息。

复制代码

panfeng@ubuntu:~/segfault$ catchsegv ./segfault3
Segmentation fault (core dumped)
*** Segmentation fault
Register dump:

 EAX: 00000000   EBX: 00fb3ff4   ECX: 00000002   EDX: 00000000
 ESI: 080484e5   EDI: 080484e0   EBP: bfb7ad38   ESP: bfb7ad0c

 EIP: 00ee806a   EFLAGS: 00010203

 CS: 0073   DS: 007b   ES: 007b   FS: 0000   GS: 0033   SS: 007b

 Trap: 0000000e   Error: 00000007   OldMask: 00000000
 ESP/signal: bfb7ad0c   CR2: 080484e0

Backtrace:
/lib/libSegFault.so[0x3b606f]
??:0(??)[0xc76400]
/lib/tls/i686/cmov/libc.so.6(__libc_start_main+0xe6)[0xe89b56]
/build/buildd/eglibc-2.10.1/csu/../sysdeps/i386/elf/start.S:122(_start)[0x8048351]

Memory map:

00258000-00273000 r-xp 00000000 08:01 157 /lib/ld-2.10.1.so
00273000-00274000 r--p 0001a000 08:01 157 /lib/ld-2.10.1.so
00274000-00275000 rw-p 0001b000 08:01 157 /lib/ld-2.10.1.so
003b4000-003b7000 r-xp 00000000 08:01 13105 /lib/libSegFault.so
003b7000-003b8000 r--p 00002000 08:01 13105 /lib/libSegFault.so
003b8000-003b9000 rw-p 00003000 08:01 13105 /lib/libSegFault.so
00c76000-00c77000 r-xp 00000000 00:00 0 [vdso]
00e0d000-00e29000 r-xp 00000000 08:01 4817 /lib/libgcc_s.so.1
00e29000-00e2a000 r--p 0001b000 08:01 4817 /lib/libgcc_s.so.1
00e2a000-00e2b000 rw-p 0001c000 08:01 4817 /lib/libgcc_s.so.1
00e73000-00fb1000 r-xp 00000000 08:01 1800 /lib/tls/i686/cmov/libc-2.10.1.so
00fb1000-00fb2000 ---p 0013e000 08:01 1800 /lib/tls/i686/cmov/libc-2.10.1.so
00fb2000-00fb4000 r--p 0013e000 08:01 1800 /lib/tls/i686/cmov/libc-2.10.1.so
00fb4000-00fb5000 rw-p 00140000 08:01 1800 /lib/tls/i686/cmov/libc-2.10.1.so
00fb5000-00fb8000 rw-p 00000000 00:00 0
08048000-08049000 r-xp 00000000 08:01 303895 /home/panfeng/segfault/segfault3
08049000-0804a000 r--p 00000000 08:01 303895 /home/panfeng/segfault/segfault3
0804a000-0804b000 rw-p 00001000 08:01 303895 /home/panfeng/segfault/segfault3
09432000-09457000 rw-p 00000000 00:00 0 [heap]
b78cf000-b78d1000 rw-p 00000000 00:00 0
b78df000-b78e1000 rw-p 00000000 00:00 0
bfb67000-bfb7c000 rw-p 00000000 00:00 0 [stack]

复制代码

5. 一些注意事项

1、出现段错误时,首先应该想到段错误的定义,从它出发考虑引发错误的原因。

2、在使用指针时,定义了指针后记得初始化指针,在使用的时候记得判断是否为NULL。

3、在使用数组时,注意数组是否被初始化,数组下标是否越界,数组元素是否存在等。

4、在访问变量时,注意变量所占地址空间是否已经被程序释放掉。

5、在处理变量时,注意变量的格式控制是否合理等。

6. 参考资料列表

1、http://www.docin.com/p-105923877.html

2、http://blog.chinaunix.net/space.php?uid=317451&do=blog&id=92412

  • 4
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值