前言
在 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,可以快速定位到是访问空指针产生的段错误。



















