linux 出现 segment fault怎么解决

linux 出现 segment fault怎么解决,第1张

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 d0call *%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 e5mov%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 mov0x1c(%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:

Linux下编程(尤其是服务端程序)若由于内存越界或其他原因产生“非法 *** 作”,会导致程序悄无声息地死去,初学者往往不知道程序死掉的原因。本人也曾饱受程序死不瞑目之苦。其实“非法 *** 作”在绝大多数时候是因为“段错误”,即 SIGSEGV。而找到SIGSEGV信号抛出的位置,也就找到了程序死掉的原因。下面列出一些捕获SIGSEGV的方法。假设程序名为 myprg,其进程ID(pid)为 2032。方法一:# ./myprg // 运行程序# ps -ef | grep myprg // 找出 myprg 的 pid# gdb myprg 2032 >debug.log // 让 gdb 接管 myprg 的运行# (gdb) continue此方法利用gdb调试器捕获SIGSEGV。上例中,将gdb输出信息存入debug.log,关闭终端,gdb并不退出,继续运行直到 myprg 出错退出。gdb将捕获到出错点。方法二:# ./myprg // 运行程序# Segment fault (core dumped) // 程序死掉了,系统输出 "Segment fault"# gdb myprg core // 用 gdb 分析 core 文件此方法利用core文件,进行事后分析。但前提是保证系统会生成core文件。# ulimit -a查看当前core文件大小的限制,若为0,则需将它改为unlimited。# ulimit -S -c unlimited或修改 /etc/profile,找到 unlimit -S -c 0,将0改为unlimited,重启系统。另外,如果程序安装了 SIGSEGV 异常处理函数,那么 gdb myprg core 时显示的并不是真实的出错位置,要想看到真实的出错点,必须暂时去掉异常处理函数。

Linux应用中用backtrace和memory map信息定位段错误代码的方法 原创

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


欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/yw/7507571.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-04-06
下一篇 2023-04-06

发表评论

登录后才能评论

评论列表(0条)

保存