一、kernel panic
二、mips异常机制
三、linuxkernel 对mips异常的处理
四、kernel panic 实例分析
Kernel panic
内核代码,相比用户层代码更难以调试,在内核程序开发上更要加倍小心和注意:有的BUG在内核或内核模块运行时会导致系统崩溃。当崩溃发生时,收集尽可能多的信息将有助于问题的解决。这就是内核OOPS诞生的目的。
OOPS会显示出CPU在崩溃时的状态,包括CPU寄存器和其它一些看起来难懂的信息。其实现代码在内核的arch\xxx\kernel\traps.c中。
在linux中对mips体系结构代码中,主要是通过异常处理的方式,在产生异常时将当前的寄存器状态,线程状态,加载的模块及函数调用信息,打印处理,为内核debug提供尽可能多的信息。
mips异常机制
Exception 2/3:TLB Miss Load/Write
Miss Load/Write,如果试图访问没有在MMU的TLB中映射的内存地址,会触发这个异常。在支持虚拟内存的操作系统中,这会触发内存的页面倒换,系统的Exception Handler会将所需要的内存页从虚拟内存中调入物理内存,并更新相应的TLB表项。
Exception 4/5:Address Error Load/Write
如果试图访问一个非对齐的地址,例如lw/sw指令的地址非4字节对齐,或lh/sh的地址非2字节对齐,就会触发这个异常。一般地,操作系统在Exception Handler中对这个异常的处理,是分开两次读取/写入这个地址。虽然一般的操作系统内核都处理了这个异常,最后能够完成期待的操作,但是由于会引起用户态到内核态的切换,以及异常的退出,当这样非对齐操作较多时会严重影响程序的运行效率。因此,编译器在定义局部和全局变量时,都会自动考虑到对齐的情况,而程序员在设计数据结构时,则需要对对齐做特别的斟酌。
Exception 9:Break Point
绝对断点指令。和syscall指令类似,它也是由专用指令break触发的。它指示了系统的一些异常情况,编程人员可以在某些不应当出现的异常分支里面加入这个指令,便于及早发现问题和调试。我们可以用高级语言中的assert机制来类比理解它。最常见的Break异常的子类型为0x07,它是编译器在编译除法运算时自动加入的。如果除数为0则执行一条break 0x07指令。这样,当出现被0除的情况时,系统就会抛出一个异常,并执行Coredump,以便于程序员定位除0错误的根因。
内核中的BUG()函数使用break指指令产生断点异常
Mips 通用寄存器定义
Linux kernel 对mips异常的处理
/arch/mips/kernel/trap.c
/* 异常向量初始化 */
void __init trap_init(void)
/* 设置异常向量 */
void *set_except_vector(int n, void *addr)
异常向量的处理函数:
TLBL/TLBS:
do_page_fault()
{
printk(KERN_ALERT "CPU %d Unable to handle kernel paging request at ""virtual address %0*lx, epc == %0*lx, ra == %0*lx\n",raw_smp_processor_id(), field, address, field, regs->cp0_epc, field, regs- >regs[31]);
die("Oops", regs);
}
ADEL/ADES
Arch/mips/unaligned.c
do_ade(struct pt_regs *regs)
{ …
sigbus:
die_if_kernel("Kernel unaligned instruction access", regs);
force_sig(SIGBUS, current);
…
}
BP
Arch/mips/kernel/trap.c
do_bp(struct pt_regs *regs)
{
do_trap_or_bp(regs, bcode, "Break");
}
do_trap_or_bp()
{
case BRK_BUG:
die_if_kernel("Kernel bug detected", regs);
force_sig(SIGTRAP, current);
break;
}
die_if_kernel
static inline void die_if_kernel(const char *str, const struct pt_regs *regs)
{
if (unlikely(!user_mode(regs)))
die(str, regs);}
die()
show_register
kernel panic 实例分析
epc:产生异常时的PC指针的值
ra:子程序的返回地址
cp0 cause
BadVA
我们重点看下epc的分析过程,工具链中提供了一系列的工具可以用于二进制的文件的分析,通过这些工具的使用我们可以定位到产生异常的代码:
内核需要开启CFG_KALLSYM,在产生异常时,能够打印出对应的函数及相应的偏移。函数地址加上偏移就是对应的汇编指令地址。
由于EPC和堆栈中的函数地址信息都是装载之后的地址,所以可以使用objdump -S 将相应的.o文件反汇编获取到函数在编译时的静态地址,从而确定函数地址。
addr2line 命令可以从.o文件中通过指令在代码段中的偏移来确定对应的C程序所在的行(需要在gcc编译时加上-g选项)。
实例(实际信息比这多,当前截取一部分):
我们看到当前epc指针指向的地址为:xxx_state_context+0xb0/0x5624(偏移为0xb0),使用上面步骤:
1)反汇编命令:你的编译工具链路径/objdump -S 你的内核vmlinx或模块.ko >dump.txt
2)获取函数地址:在dump.txt中查找xxx_state_context,找到该函数地址,假如为00000034
3)获取pc指针地址:00000034+0xb0=0x000000e4
4)定位到具体函数某一行,命令:
你的编译工具链路径/addr2line 0x000000e4 -e 你的内核vmlinx或模块.ko -f xxx_state_context
后续
1)找到产生异常的代码仅仅只是开始,因为产生异常的原因有很多,kernel panic打印处理的系统信息能提供的仅仅是当前的一些状态,更具体的分析需要结合代码进行分析。
2)假如编译工具链中有gdb的话,可以更快捷的定位到具体代码,执行命令:
#你的编译工具链gdb路径/gdb 你的内核vmlinx或模块.ko
(gdb) b *xxx_state_context+0xb0
该指令将同样可以定位到具体文件的某一行,有兴趣的朋友可以试一下。
3)该文章提到的具体命令用法,请自行查阅文档。