1. 段错误是什么
一句话来说,段错误是指访问的内存超出了系统给这个程序所设定的内存空间,例如访问了不存在的内存地址、访问了系统保护的内存地址、访问了只读的内存地址等等情况。
2. 段错误的原因
段错误,英文segmentation fault
- 段错误的定义
- segementation fault (often shortened to segfault) or access violation
- often raised by hardware with memory protection, notifying an operation system(OS) the software has attempted to access a restricted area of memory.
段错误常见于提供低级别的内存访问机制(如C语言)的语言程序中,通常由非法访问导致,即错误地使用指针访问虚拟内存。
原因列举
- 对空指针赋值——由内存管理硬件所导致
- 尝试去访问一个不存在的内存地址(超出进程分配的地址)
- 尝试去访问程序无权限访问的地址(如进程上下文中的内核结构(kernel structures))
- 尝试去访问只读的内存(如代码段)
对应着程序中的情况分别为:
- 引用或者赋值一个为初始化的指针(野指针(wild pointer),随机指向一个内存地址)
- 引用或者赋值一个已经被free的指针(dangling pointer,指向一个已经被freed/deallocated/deleted的内存地址)
- A buffer overflow
- A stack overflow
3. 段错误信息的获取
3.1 dmesg
dmesg可以在应用程序crash掉时,显示内核中保存的相关信息。如下所示,通过dmesg命令可以查看发生段错误的程序名称、引起段错误发生的内存地址、指令指针地址、堆栈指针地址、错误代码、错误原因等。
3.2 -g
使用gcc编译程序的源码时,加上-g参数,这样可以使得生成的二进制文件中加入可以用于gdb调试的有用信息。
3.3 nm
使用nm命令列出二进制文件中的符号表,包括符号地址、符号类型、符号名等,这样可以帮助定位在哪里发生了段错误。
3.4 ldd
使用ldd命令查看二进制程序的共享链接库依赖,包括库的名称、起始地址,这样可以确定段错误到底是发生在了自己的程序中还是依赖的共享库中。
4. 段错误的调试方法
4.1 使用gcc和gdb
4.1.1 调试步骤
为了能够使用gdb调试程序,在编译阶段加上-g参数,以程序segfault3.c为例。
root@twq2018:~/segfault$ gcc -g -o segfault3 segfault3.c
root@twq2018:~/segfault$ gdb ./segfault3
(gdb) run
Starting program: /home/twq/segfault/segfault3
Program received signal SIGSEGV, Segmentation fault.
0x001a306a in memcpy () from /lib/tls/i686/cmov/libc.so.6
(gdb)
从输出看出,程序segfault3.c收到SIGSEGV信号,触发段错误,并提示地址0x001a306a、调用memcpy报的错,位于/lib/tls/i686/cmov/libc.so.6库中。
4.1.2 适用场景
1、仅当能确定程序一定会发生段错误的情况下使用。
2、当程序的源码可以获得的情况下,使用-g参数编译程序。
3、一般用于测试阶段,生产环境下gdb会有副作用:使程序运行减慢,运行不够稳定,等等。
4、即使在测试阶段,如果程序过于复杂,gdb也不能处理。
4.2 使用gdb和core文件
在4.1节中提到段错误会触发SIGSEGV信号,通过man 7 signal,可以看到SIGSEGV默认的handler会打印段错误出错信息,并产生core文件,由此我们可以借助于程序异常退出时生成的core文件中的调试信息。
使用gdb工具来调试程序中的段错误,用bt命令查看backtrace以检查发生程序运行到哪里, 来定位core dump的文件->行.。
4.2.1 调试步骤
1、在一些Linux版本下,默认是不产生core文件的,首先可以查看一下系统core文件的大小限制:
root@twq2018:~/segfault$ ulimit -c
0
2、可以看到默认设置情况下,本机Linux环境下发生段错误时不会自动生成core文件,下面设置下core文件的大小限制(单位为KB),其中使用ulimit -c unlimited来设置无限大,则任意情况下都会产生core文件。
root@twq2018:~/segfault$ ulimit -c 1024
root@twq2018:~/segfault$ ulimit -c
1024
3、运行程序segfault3.c,发生段错误生成core文件:
root@twq2018:~/segfault$ ./segfault3
段错误 (core dumped)
3、加载core文件,使用gdb工具进行调试:
root@twq2018:~/segfault$ gdb ./segfault3 ./core
4.2.2 适用场景
1、适合于在实际生成环境下调试程序的段错误(即在不用重新发生段错误的情况下重现段错误)。
2、当程序很复杂,core文件相当大时,该方法不可用。
4.3 使用objdump
4.3.1 调试步骤
1、使用dmesg命令,找到最近发生的段错误输出信息:
root@twq2018:~/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生成二进制的相关信息,重定向到文件中:
root@twq2018:~/segfault$ objdump -d ./segfault3 > segfault3Dump
其中,生成的segfault3Dump文件中包含了二进制文件的segfault3的汇编代码。
3、在segfault3Dump文件中查找发生段错误的地址:
root@twq2018:~/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.3.2 适用场景
1、不需要-g参数编译,不需要借助于core文件,但需要有一定的汇编语言基础。
2、如果使用了gcc编译优化参数(-O1,-O2,-O3)的话,生成的汇编指令将会被优化,使得调试过程有些难度。