arm64下内核crash——非法地址
下面是在实际工作中遇到的一次内核(5.4.110)访问非法内存地址(空指针)导致出错的现场,在这里记录一下简单的分析流程为以后遇到类似的问题作为参考。
1 | [ 220.619861] Unable to handle kernel NULL pointer dereference at virtual address 0000000000000023 |
该问题初步分析是进行DMA传输时,可能将函数调用栈信息冲掉所致
dump信息说明
pc
:(Program Counter)pc指针,记录当前执行哪一条指令;存储当前CPU正在执行指令的地址lr
:(Link Register)x30
寄存器,保存函数返回地址sp
:(Stack Pointer)栈指针fp
:(Frame Pointer)x29
寄存器
根据dump出的函数调用定位具体出错的代码
最终出错代码
1 | pc : dwc_descriptor_complete+0x104/0x140 |
gdb定位具体代码
1 | $aarch64-none-linux-gnu-gdb vmlinux |
定位具体出错指令
由于出错的接口函数中只是一个普通的赋值操作,因此需要进一步确认出错时,CPU执行的汇编指令是否存在异常或者特殊性
查看dwc_descriptor_complete
接口函数的汇编实现
1 | (gdb) disassemble dwc_descriptor_complete |
- [1]: 此处为具体出错指令,意思是将寄存器X2中的值加上68后作为内存地址,并将该内存地址的数据取出,存到w1寄存器中。非法内存地址也就是X2加68(0x44)得到的,根据crash dump出的寄存器值此时
X2=ffffffffffffffdf
,0xffffffffffffffdf+0x44=0x0000000000000023
刚好是非法内存地址,也就是说出错的因为x2
寄存器的值保存错了。
以上流程中表明x2
寄存器出现FFFFFFFFFFFFFFFF
的可能性存在两种:
- dwc_descriptor_complete接口函数传参时,第二个参数是个错误的指针。这样就会使
x0
寄存器错误导致在4时,通过内存地址读取数据赋值该x2
时,出现全F的值(一个错误的指针指向了错误的内存区域所致)。- 由于在[7]处对第二个参数已经使用过(读写),因此可以证明传入的第二个参数指针是正确的。如果错误应该会在[7]处直接报错。
- dwc_descriptor_complete接口函数传参时,第二个参数是正确的。但是在4时,通过内存地址读取数据赋值给
x2
时,原来正确的数据被别的程序覆盖掉了(踩内存)
** 通过以上流程的分析我认为是在4处,读取相关内存地址中的数据时,原有的正确数据被错误数据覆盖 **
C源码:
1 | (gdb) list dwc_descriptor_complete |
通过对以上汇编代码的分析出错的原因主要是[4]
,读取内存数据(ldr x2, [x0, #48]!)时出错。该指令对应的C代码实现主要在list_for_each_entry(child, &desc->tx_list, desc_node)
接口
这样结合之前分析的出错原因,可能是别的程序写内存时覆盖了tx_list链表数据(踩内存);不过还存在一种可能就是tx_list的操作出错了,由dma驱动代码本身所造成的bug。
MMU错误信息
1 | [ 220.619861] Unable to handle kernel NULL pointer dereference at virtual address 0000000000000023 |
以上信息主要是在内核出现do_page_fault
时的一些log信息
1 | do_page_fault |
from: arch/arm64/mm/fault.c
mem_abort_decode
函数主要是解析ESR_ELx
寄存器,在内核模式下为ESR_EL1
参考:DDI0487D_a_armv8_arm.pdf —— D12.2.36 ESR_EL1, Exception Syndrome Register (EL1)
EC, bits [31:26]
, EC = 0x25, DABT (current EL),异常级别未更改的数据中止。用于数据访问产生的MMU错误、除堆栈指针未对齐引起的对齐错误和同步外部中止(包括同步奇偶校验或ECC错误)之外的对齐错误。
1 | EC == 0b010101 |
- SVC:Supervisor Call instruction
gdb调试vmlinux技巧
查看特定位置的代码
1 | (gdb) list * [函数名]或[函数名+函数内偏移] |
命令: list
或l
列出指定的函数或行
格式:
1 | list [FUNCTION/*ADDRESS] |
LINENUM
,在当前文件中围绕该行列出,FILE:LINENUM
,列出该文件中的该行,FUNCTION
,列出该函数的开头,FILE:FUNCTION
,用于区分同名的静态函数。*ADDRESS
,在包含该地址的行周围列出。
查看特定函数的汇编实现
1 | disassemble [函数名] |
命令: disassemble
反汇编内存地址部分(也可以用函数名)
格式:
1 | disassemble[/m|/r|/s] START [, END] |
/m
: 被弃用了推荐使用/s
/r
: 包含十六进制的原始指令。/s
: 包括源代码行(如果可用)。 在此模式下,输出按PC地址顺序显示,并显示所有相关源文件的文件名和内容。
查看特定地址信息
1 | (gdb) x [内存地址] |
命令:x
查看内存地址中的值
格式:
1 | x /<n/f/u> <addr> |
n
是正整数,表示需要显示的内存单元的个数,即从当前地址向后显示n个内存单元的内容,一个内存单元的大小由第三个参数u定义。f
表示addr指向的内存内容的输出格式,s对应输出字符串,此处需特别注意输出整型数据的格式:x
按十六进制格式显示变量。d
按十进制格式显示变量。u
按十六进制格式显示无符号整型。o
按八进制格式显示变量。t
按二进制格式显示变量。a
按十六进制格式显示变量。c
按字符格式显示变量。f
按浮点数格式显示变量。i
instruction以汇编指令显示。u
:就是指以多少个字节作为一个内存单元-unit,默认为4。当然u还可以用被一些字符表示,如b=1 byte, h=2 bytes,w=4 bytes,g=8 bytes.<addr>
表示内存地址。
1 | (gdb) x 0xffffffc01041b1cc #当前地址数据,表示机器码 |
参考
- DDI0487D_a_armv8_arm.pdf
- ARMv8 A64 Quick Reference —— 汇编指令