##问题描述:
DMA写完数据后,产生DMA中断,中断中wake_up_interruptible
唤醒等待队列(wait_event_interruptible_timeout
),唤醒后,中断上下文程序会先执行,但偶尔唤醒的进程上下文会先于中断上下文执行。某次wake_up_interruptible
唤醒等待队列后导致内核崩溃。崩溃堆栈打印pc是在__wake_up_common_lock
,大致如下:
Unable to handle kernel NULL pointer dereference at virtual address 00000000000005c8
pc : __wake_up_common_lock+0xa0/0xc8
lr : __wake_up_common_lock+0x90/0xc8
Call trace:
__wake_up_common_lock+0xa0/0xc8
__wake_up+0x14/0x20
hi3531dv200_dma_irq_handler+0x88/0x210
__handle_irq_event_percpu+0x70/0x180
handle_irq_event_percpu+0x34/0x88
handle_irq_event+0x40/0xa0
handle_fasteoi_irq+0xcc/0x1b0
generic_handle_irq+0x24/0x38
__handle_domain_irq+0x5c/0xb8
乍一看有点懵逼
##分析问题:
为什么会出现wake_up
导致内核崩溃,我只传了一个结构类型为wait_queue_head_t
的参数到该API中,并且进程已经被唤醒且调度了。怀疑极大可能是传入的参数wait_queue_head_t
数据结构有可能被破坏了。将内核vmlinux反汇编,查看反汇编部分__wake_up_common_lock
代码如下:
ffffff80080d9b00 <__wake_up_common_lock>:
ffffff80080d9b00: a9b87bfd stp x29, x30, [sp, #-128]!
ffffff80080d9b04: 910003fd mov x29, sp
ffffff80080d9b08: a90153f3 stp x19, x20, [sp, #16]
ffffff80080d9b0c: aa0003f3 mov x19, x0
ffffff80080d9b10: a90363f7 stp x23, x24, [sp, #48]
ffffff80080d9b14: 2a0203f4 mov w20, w2
ffffff80080d9b18: a9025bf5 stp x21, x22, [sp, #32]
ffffff80080d9b1c: 2a0303f7 mov w23, w3
ffffff80080d9b20: f90023f9 str x25, [sp, #64]
ffffff80080d9b24: f00047f9 adrp x25, ffffff80089d8000 <page_wait_table+0x15c0>
ffffff80080d9b28: 91172325 add x5, x25, #0x5c8
ffffff80080d9b2c: 2a0103f6 mov w22, w1
ffffff80080d9b30: f94000a6 ldr x6, [x5]
ffffff80080d9b34: f9003fa6 str x6, [x29, #120]
ffffff80080d9b38: d2800006 mov x6, #0x0 // #0
ffffff80080d9b3c: aa0403f8 mov x24, x4
ffffff80080d9b40: 9101a3a5 add x5, x29, #0x68
ffffff80080d9b44: b90053bf str wzr, [x29, #80]
ffffff80080d9b48: a905ffbf stp xzr, xzr, [x29, #88]
ffffff80080d9b4c: f90037a5 str x5, [x29, #104]
ffffff80080d9b50: f9003ba5 str x5, [x29, #112]
ffffff80080d9b54: 14000002 b ffffff80080d9b5c <__wake_up_common_lock+0x5c>
ffffff80080d9b58: aa1303e0 mov x0, x19
ffffff80080d9b5c: 941b1cab bl ffffff80087a0e08 <_raw_spin_lock_irqsave>
ffffff80080d9b60: aa0003f5 mov x21, x0
ffffff80080d9b64: 2a1403e2 mov w2, w20
ffffff80080d9b68: 910143a5 add x5, x29, #0x50
ffffff80080d9b6c: aa1803e4 mov x4, x24
ffffff80080d9b70: 2a1703e3 mov w3, w23
ffffff80080d9b74: 2a1603e1 mov w1, w22
ffffff80080d9b78: aa1303e0 mov x0, x19
ffffff80080d9b7c: 97ffff8b bl ffffff80080d99a8 <__wake_up_common>
ffffff80080d9b80: 2a0003f4 mov w20, w0
ffffff80080d9b84: aa1503e1 mov x1, x21
ffffff80080d9b88: aa1303e0 mov x0, x19
ffffff80080d9b8c: 941b1c75 bl ffffff80087a0d60 <_raw_spin_unlock_irqrestore>
ffffff80080d9b90: b94053a0 ldr w0, [x29, #80]
ffffff80080d9b94: 3717fe20 tbnz w0, #2, ffffff80080d9b58 <__wake_up_common_lock+0x58>
ffffff80080d9b98: 91172339 add x25, x25, #0x5c8
ffffff80080d9b9c: f9403fa1 ldr x1, [x29, #120]
ffffff80080d9ba0: f9400320 ldr x0, [x25]
ffffff80080d9ba4: ca000020 eor x0, x1, x0
ffffff80080d9ba8: b50000e0 cbnz x0, ffffff80080d9bc4 <__wake_up_common_lock+0xc4>
ffffff80080d9bac: a94153f3 ldp x19, x20, [sp, #16]
ffffff80080d9bb0: a9425bf5 ldp x21, x22, [sp, #32]
ffffff80080d9bb4: a94363f7 ldp x23, x24, [sp, #48]
ffffff80080d9bb8: f94023f9 ldr x25, [sp, #64]
ffffff80080d9bbc: a8c87bfd ldp x29, x30, [sp], #128
ffffff80080d9bc0: d65f03c0 ret
ffffff80080d9bc4: 97ff0a39 bl ffffff800809c4a8 <__stack_chk_fail>
崩溃指令是在__wake_up_common_lock+0xa0/0xc8(ldr x0, [x25])
处,我们看汇编代码,代码里面获得自旋锁,唤醒进程,并且释放锁流程已经处理完了,且进程也被唤醒了。崩溃的确实有点懵逼。
咱们联系上下文,看看这条汇编指令是干啥的,这个是把x25里面的值加载到x0寄存器,这也能崩?继续网上寻找x25做了啥。
str x25, [sp, #64] \\将x25值入栈保存
adrp x25, ffffff80089d8000 <page_wait_table+0x15c0> \\从page_wait_table+0x15c0获取值存入x25
add x5, x25, #0x5c8 \\x25+0x5c8存入x5
mov w22, w1
ldr x6, [x5] \\x5中的值存入x6
str x6, [x29, #120] \\x6中的值存入x29+120
```
```
ldr x1, [x29, #120] \\将x29+120值加载到x1
ldr x0, [x25] \\将x25值加载到x0
eor x0, x1, x0 \\x0 = x1^x0
cbnz x0, ffffff80080d9bc4 <__wake_up_common_lock+0xc4> \\判断异或结果是否是0,如果不等于0 则跳转__wake_up_common_lock+0xc4()
联系上下文后,再看,豁然开朗。就是进入函数后,将寄存器x25值入栈,然后从page_wait_table+0x15c0
获取值存入x25,再经过处理后存入x6,最终入栈x29(sp), #120
,然后函数主要流程基本处理完了。最后从x25取值,和入栈时的值做了一个stack_chk_fail
判断。
stack_chk_fail
这个玩意函数体中根本没有,经查证得知是内核STACK_PROTECTOR校验机制(有具体宏控制)。所以得出结论,wake_up将函数唤醒了,但是做栈校验这部分,出现了异常导致内核崩溃。也就是崩溃在ldr x0, [x25]
,原因就是x25寄存器中存放的地址被破坏了,根据内核崩溃打印应该是变成了NULL,x25是获得的页基址,__wake_up_common_lock
本身并未对x25做异常处理,但做完压栈处理后,为什么取出来后就异常了呢?结合代码分析:
init_waitqueue_head(&new_task->task.dma_write_q);
···
···
wait_event_interruptible_timeout(ptask->task.dma_write_q, 0 != ptask->task.irqraised, HZ);
···
##原因:
结构wait_queue_head_t
是定义在栈上的,当初始化完成后,调用等待队列API进入睡眠,wake_up
唤醒后即调度该进程,导致唤醒的进程立马跑完了,然后栈空间的wait_queue_head_t
结构被释放了,导致后面栈校验的时候就异常了。
##解决方案:
将栈空间的数据结构拷贝到数据结构本身,使得函数走完栈空间释放,不会影响wait_queue_head_t
结构的存亡。