测试环境
- TI-DM8167 + Linux 2.6.34
内核部分环境
内核配置选项
Kernel hacking --->
--> [*] Kernel debugging
--> [*] Verbose user fault messages # CONFIG_DEBUG_USER
设置 user_debug
arch/arm/kernel/traps.c 代码修改。
#ifdef CONFIG_DEBUG_USER
-unsigned int user_debug;
+unsigned int user_debug = 0xff;
static int __init user_debug_setup(char *str)
{
get_option(&str, &user_debug);
return 1;
}
__setup("user_debug=", user_debug_setup);
#endif
arch/arm/mm/fault.c 代码片段。
static void
__do_user_fault(struct task_struct *tsk, unsigned long addr,
unsigned int fsr, unsigned int sig, int code,
struct pt_regs *regs)
{
struct siginfo si;
#ifdef CONFIG_DEBUG_USER
if (user_debug & UDBG_SEGV) {
printk(KERN_DEBUG "%s: unhandled page fault (%d) at 0x%08lx, code 0x%03x\n",
tsk->comm, sig, addr, fsr);
show_pte(tsk->mm, addr);
show_regs(regs);
}
#endif
user_debug 可以通过 bootcmd 参数进行设置,这里直接修改默认值。
do_page_fault 添加调试打印
arch/arm/mm/fault.c 代码修改。
static int __kprobes
do_page_fault(unsigned long addr, unsigned int fsr, struct pt_regs *regs)
{
struct task_struct *tsk;
struct mm_struct *mm;
int fault, sig, code;
+ if (strncmp(current->comm, "sig_test", strlen("sig_test")) == 0) {
+ printk("addr 0x%x fsr %x\n", addr, fsr);
+ }
if (notify_page_fault(regs, fsr))
return 0;
应用部分环境
测试一:非法地址访问
测试代码:
$ cat sig_test.c
```
int main(int argc, char const *argv[])
{
*(int *)0x5a = 0xa5; // 这里用 0x5a / 0xa5 为了数值区分
return 0;
}
```
交叉编译
$ arm-linux-uclibcgnueabi-gcc sig_test.c -o sig_test
反汇编测试程序
$ arm-linux-uclibcgnueabi-objdump -d sig_test
```
000083c4 <main>:
83c4: e52db004 push {fp} ; (str fp, [sp, #-4]!)
83c8: e28db000 add fp, sp, #0 ; 0x0
83cc: e24dd00c sub sp, sp, #12 ; 0xc
83d0: e50b0008 str r0, [fp, #-8]
83d4: e50b100c str r1, [fp, #-12]
83d8: e3a0305a mov r3, #90 ; 0x5a
83dc: e3a020a5 mov r2, #165 ; 0xa5
83e0: e5832000 str r2, [r3] ; 注意这是异常点
83e4: e3a03000 mov r3, #0 ; 0x0
83e8: e1a00003 mov r0, r3
83ec: e28bd000 add sp, fp, #0 ; 0x0
83f0: e8bd0800 pop {fp}
83f4: e12fff1e bx lr
```
测试运行
# mount -t nfs -o nolock 172.9.21.107:/tftpboot/netra_rootfs /mnt
# /mnt/sig_test
```
# do_page_fault 打印
addr 0x5a fsr 817
# __do_user_fault 打印
sig_test: unhandled page fault (11) at 0x0000005a, code 0x817
pgd = d3d34000
[0000005a] *pgd=93d2d031, *pte=00000000, *ppte=00000000
Pid: 77, comm: sig_test policy 0 priority: 0
CPU: 0 Not tainted (2.6.34 #67)
PC is at 0x83e0
LR is at 0x4004a9c4
pc : [<000083e0>] lr : [<4004a9c4>] psr: 20000010
sp : bea6ee20 ip : 40057f30 fp : bea6ee2c
r10: bea6ee30 r9 : 000083c4 r8 : 00008300
r7 : bea6ef60 r6 : 00000001 r5 : bea6eeb4 r4 : 40057ed0
r3 : 0000005a r2 : 00000000 r1 : bea6eeb4 r0 : 00000001
Flags: nzCv IRQs on FIQs on Mode USER_32 ISA ARM Segment user
Control: 10c5387d Table: 93d34019 DAC: 00000015
Segmentation fault
```
注意这里的 PC is at 0x83e0
和上面 sig_test 反汇编 83e0: e5832000 str r2, [r3]
是对应的,也就是说 e5832000 指令时段错误的触发点。
原理分析
*(int *)0x5a = 0xa5;
对应以下汇编指令。
83d8: e3a0305a mov r3, #90 ; 0x5a
83dc: e3a020a5 mov r2, #165 ; 0xa5
83e0: e5832000 str r2, [r3] ; 注意这是异常点
对 0x5a 地址进行写值操作触发缺页异常,内核执行 do_page_fault
进行缺页异常处理。
do_page_fault 执行过程
do_page_fault
-> __do_page_fault
-> find_vma # 非法地址,返回 NULL
-> __do_user_fault
-> force_sig_info # 向进程发送 SIGSEGV 信号,触发段错误
signal 处理过程
do_notify_resume
-> do_signal
-> get_signal_to_deliver
-> do_coredump if sig_kernel_coredump
-> do_group_exit -> do_exit # 进程退出
测试二:栈溢出
测试代码:
$ cat sig_test.c
```
int main(int argc, char const *argv[])
{
int *i;
i = (int *)&i;
while (1) {
*i-- = 0; // 栈向下增长
}
return 0;
}
```
交叉编译
$ arm-linux-uclibcgnueabi-gcc sig_test.c -o sig_test
反汇编测试程序
$ arm-linux-uclibcgnueabi-objdump -d sig_test
```
000083c4 <main>:
83c4: e52db004 push {fp} ; (str fp, [sp, #-4]!)
83c8: e28db000 add fp, sp, #0 ; 0x0
83cc: e24dd014 sub sp, sp, #20 ; 0x14
83d0: e50b0010 str r0, [fp, #-16]
83d4: e50b1014 str r1, [fp, #-20]
83d8: e24b3008 sub r3, fp, #8 ; 0x8
83dc: e50b3008 str r3, [fp, #-8]
83e0: e51b3008 ldr r3, [fp, #-8]
83e4: e3a02000 mov r2, #0 ; 0x0
83e8: e5832000 str r2, [r3]
83ec: e2433004 sub r3, r3, #4 ; 0x4
83f0: e50b3008 str r3, [fp, #-8]
83f4: eafffff9 b 83e0 <main+0x1c>
```
测试运行
# mount -t nfs -o nolock 172.9.21.107:/tftpboot/netra_rootfs /mnt
# /mnt/sig_test
```
... ...
addr 0xbed30ffc fsr 817
addr 0xbed2fffc fsr 817
... ...
addr 0xbe532ffc fsr 817
addr 0xbe531ffc fsr 817
sig_test: unhandled page fault (11) at 0xbe531ffc, code 0x817
pgd = d3d30000
[be531ffc] *pgd=93d2c031, *pte=00000000, *ppte=00000000
Pid: 77, comm: sig_test policy 0 priority: 0
CPU: 0 Not tainted (2.6.34 #73)
PC is at 0x83e8
LR is at 0x4004a9c4
pc : [<000083e8>] lr : [<4004a9c4>] psr: 20000010
sp : bed31e18 ip : 40057f30 fp : bed31e2c
r10: bed31e30 r9 : 000083c4 r8 : 00008300
r7 : bed31f60 r6 : 00000001 r5 : bed31eb4 r4 : 40057ed0
r3 : be531ffc r2 : 00000000 r1 : bed31eb4 r0 : 00000001
Flags: nzCv IRQs on FIQs on Mode USER_32 ISA ARM Segment user
Control: 10c5387d Table: 93d30019 DAC: 00000015
sig_test/77: potentially unexpected fatal signal 11.
```
注意这里的 PC is at 0x83e8
和上面 sig_test 反汇编 83e8: e5832000 str r2, [r3]
是对应的,也就是说 e5832000 指令时段错误的触发点,访问地址 0xbe531ffc 出现异常。
原理分析
# ulimit -s
```
8192
```
Linux 系统进程默认栈空间大小为 8M ,而上面地址 0xbed30ffc - 0xbe531ffc == 0x7FF000
因此是栈溢出导致段错误。
__do_page_fault 处理过程
__do_page_fault
-> expand_stack
-> expand_downwards
-> acct_stack_growth
{
/* Stack limit test */
if (size > ACCESS_ONCE(rlim[RLIMIT_STACK].rlim_cur))
return -ENOMEM;
/*
* Overcommit.. This must be the final test, as it will
* update security statistics.
*/
if (security_vm_enough_memory_mm(mm, grow))
return -ENOMEM;
}
当栈部分出现缺页异常时,系统会通过调用expand_stack
进行扩展,扩展过程中会做一些安全监测,主要是栈大小限制判断和虚拟内存过量分配判断。
实际问题排查过程中,遇到类似 unhandled page fault (11) at 0xbe531ffc, code 0x817
的打印基本上可以怀疑是栈溢出导致异常的。具体原因定位的话,对于单线程的进程,进程起来之前后,通过 cat /proc/<pid>/maps
获取进程栈的其实地址,通过 getrlimit
系统调用获取进程的栈大小,最后计算一下就可以了。对于多线程,要根据线程库类型对于 linux thread 排查方法与单线程进程类似,但对于线程不停创建退出的场景就不太容易定位了;对于 NPTL 线程库暂时不太清楚。当然,可以暴力点,直接在内核中加打印。