前言
在 Linux 系统中,运行一个应用程序,突然提示段错误,并停止运行
# ./crash.out
Segmentation fault
如果这个时候操作系统能多提示点错误信息,那将会缩短我们 debug 的时间。
core dump 就是一个办法,可以查看我之前写的一篇文章《core dump》。
今天我们来介绍另外一种方法,那就是内核编译选项 DEBUG_USER。
DEBUG_USER
该编译选项位于 > Kernel hacking > arm Debugging
,含义是:详细的用户故障信息。
当用户程序由于异常而崩溃时,内核可以打印一条简短的消息来说明问题所在。这有时对调试很有帮助,但对生产系统没有任何作用。大多数人应该在这里说 N。
此外,你需要在内核命令行上传递 user_debug=N 来启用此特性。N 由以下和组成:1 - 未定义的指令事件
2 - 系统调用
4 - 无效的数据中止
8 - sigsecv 故障
16 - SIGBUS 故障
测试
实验环境:ARM Linux 32bit,qemu
crash.c
#include <stdio.h>
#include <unistd.h>
int main()
{
int *p = NULL;
printf("*p = %d\n", *p);
return 0;
}
Makefile
CC=/home/liyongjun/project/board/buildroot/Vexpress_2/host/bin/arm-linux-gcc
crash:
${CC} crash.c -o crash.out -g -no-pie
cp:
cp crash.out ~/tftp
clean:
rm *.out
设置内核的命令行参数,添加 user_debug=0xff
,
# cat /proc/cmdline
console=ttyAMA0,115200 rootwait root=/dev/mmcblk0 user_debug=0xff
将编译好的程序拷贝到 ARM Linux,为方便调试,关闭地址随机化,参考《ASLR 和 PIE》,然后执行代码
# tftp -gr crash.out 192.168.31.223
# chmod +x crash.out
# echo 0 > /proc/sys/kernel/randomize_va_space
# ./crash.out
8<--- cut here ---
crash.out: unhandled page fault (11) at 0x00000000, code 0x017
[00000000] *pgd=611d0831, *pte=00000000, *ppte=00000000
CPU: 0 PID: 131 Comm: crash.out Not tainted 6.1.44 #1
Hardware name: ARM-Versatile Express
PC is at 0x10444
LR is at 0x76eb8868
pc : [<00010444>] lr : [<76eb8868>] psr: 60080010
sp : 7efffcf8 ip : 7efffd80 fp : 7efffd04
r10: 76ffece0 r9 : 7efffe7c r8 : 00000000
r7 : 00011ed8 r6 : 00011ed8 r5 : 00000001 r4 : 7efffe74
r3 : 00000000 r2 : 7efffe7c r1 : 7efffe74 r0 : 00000001
Flags: nZCv IRQs on FIQs on Mode USER_32 ISA ARM Segment user
Control: 10c5387d Table: 61b74059 DAC: 00000055
Segmentation fault
程序执行出现段错误,显示 PC 位于 0x10444
我们去 host 主机,反汇编,查看 0x10444 附近的指令是什么
$ /home/liyongjun/project/board/buildroot/Vexpress_2/host/bin/arm-linux-objdump -d crash.out
...
0001042c <main>:
1042c: e92d4800 push {fp, lr}
10430: e28db004 add fp, sp, #4
10434: e24dd008 sub sp, sp, #8
10438: e3a03000 mov r3, #0
1043c: e50b3008 str r3, [fp, #-8]
10440: e51b3008 ldr r3, [fp, #-8]
10444: e5933000 ldr r3, [r3]
10448: e1a01003 mov r1, r3
1044c: e3000504 movw r0, #1284 @ 0x504
10450: e3400001 movt r0, #1
10454: ebffffad bl 10310 <printf@plt>
10458: e3a03000 mov r3, #0
1045c: e1a00003 mov r0, r3
10460: e24bd004 sub sp, fp, #4
10464: e8bd8800 pop {fp, pc}
PC 位于 0x10444,说明代码在执行前一条指令(0x10440)时出问题了,该指令是 ldr 指令,ldr 指令的作用是将内存地址中的数据存入寄存器中,而内存地址的值为[fp, #-8]
,再往前两条指令
10438: e3a03000 mov r3, #0
1043c: e50b3008 str r3, [fp, #-8]
作用是将立即数 0 赋值给 r3,然后再将 r3 的值赋值给 [fp, #-8]
,所以 0x10440 处 [fp, #-8]
的值为 0,从 0 地址处取数据,超出了该应用程序所能访问的段的范围,于是发生了段错误。
objdump -S
或者我们直接使用 objdump -S 选项,来显示 C 源码和汇编的混合代码
int main()
{
1042c: e92d4800 push {fp, lr}
10430: e28db004 add fp, sp, #4
10434: e24dd008 sub sp, sp, #8
int *p = NULL;
10438: e3a03000 mov r3, #0
1043c: e50b3008 str r3, [fp, #-8]
printf("*p = %d\n", *p);
10440: e51b3008 ldr r3, [fp, #-8]
10444: e5933000 ldr r3, [r3]
10448: e1a01003 mov r1, r3
1044c: e3000504 movw r0, #1284 @ 0x504
10450: e3400001 movt r0, #1
10454: ebffffad bl 10310 <printf@plt>
return 0;
10458: e3a03000 mov r3, #0
1045c: e1a00003 mov r0, r3
10460: e24bd004 sub sp, fp, #4
10464: e8bd8800 pop {fp, pc}
出错指令对应的 C 代码为 printf("*p = %d\n", *p);
,结合前面指针 p 的值为 NULL,可以快速定位到是访问空指针产生的段错误。