uboot 执行printf后异常死机问题定位
环境
硬件: PowerPC T1024,SPI启动
软件: uboot 2015.01
调试软件: codeWarrior
调试工具: codeWarrior TAP
现象
移植uboot,删除Nand 和Nor Flash相关内容。在启动后,死在了某一处打印位置,打印信息不完整。
调试1:定位死机位置
使用仿真器Attach板子,发现系统是死在了0x800(Program)处,LR寄存器指向的是_serial_puts函数中调用_serial_putc打印的位置。这说明系统是在执行_serial_puts函数时死掉的。
结论: 怀疑是打印函数一层层调用,堆栈溢出,或者系统printf函数问题。
调试2:验证系统printf函数
自己在需要打印的文件内部,写了一个裸串口打印函数和dbgputs函数,发现依然会死在裸串口输出函数里面。
结论: 说明和函数调用无关系,是串口打印本身的问题。
调试3:定位串口输出异常
在串口打印函数里面点灯:
while (*pstr)
{
out_be32(&pgpio->gpdat, LED_G_ON|LED_R_ON);
while(!(readb(CONFIG_SYS_NS16550_COM1 + UART_ULSR) & ULSR_TEMT));
out_be32(&pgpio->gpdat, LED_G_ON|LED_R_OFF);
writeb(*pstr, CONFIG_SYS_NS16550_COM1 + UART_DATA);
out_be32(&pgpio->gpdat, LED_G_OFF|LED_R_ON);
pstr++;
isync();
}
发现死在了“while(!(readb(CONFIG_SYS_NS16550_COM1 + UART_ULSR) & ULSR_TEMT));”里面。怀疑是读寄存器读多了导致异常,于是在while里面加上了delay,也修改readb为指针方式直接读,问题依旧。
结论: 和读寄存器无关系。
调试4:定位中断异常指令
通过观察Program中断异常,发现此事中断向量里面的值全是0x00000000,并未初始化。所以Program并不是真的出现中断异常,它可能是其他异常中断向量未初始化(默认为0)导致的。于是在checkboard函数后面,将中断向量都初始化为“0x48000000”(死循环),观察原始异常是谁。
for(i = 0x100; i < 0xf00; i+= 0x100)
*((volatile u32*)i) = 0x48000000; //指令 b 0,死循环
结论: 系统死机后,程序停在了“0x0a00”(Decrementer)异常处。
调试5:分析Decrementer中断
搜索Decrementer中断,发现它是一个递减器,递减频率和TB寄存器一样,目的是为系统提供一种可编程的软件延时中断。通过观察SRR0寄存器(异常地址),发现它是在执行在执行while(!(readb(CONFIG_SYS_NS16550_COM1 + UART_ULSR) & ULSR_TEMT))时异常。所以之前死机的原因是:串口打印时使用轮询方式,结果轮询时间太长,导致递减器溢出中断,而此时系统还未挂载中断,所以死机。
结论: 系统进入了软延时中断,但没有挂载中断处理函数,导致系统死在了Program中断中。
调试6:分析中断挂载顺序。
发现系统在board_r.c/board_init_r中的函数执行顺序如下:
init_fnc_t init_sequence_r[] = {
724 initr_trap, //初始化后中断向量
730 board_early_init_r,
848 initr_net, //打印死机的位置
818 interrupt_init, //使能DEC中断
}
也就是说,开启中断前已经挂载了中断函数。在board_early_init_r中打印0xA00的值,发现里面是有内容的,这与之前仿真器调试时里面没有值的信息不符。还原代码复现问题,死机后再次连仿真器调试,发现虚拟地址里面0xA00值是0,但物理地址里面的值却是真实的值。
结论: 中断向量是配置了的,但是在异常时,虚拟地址中却没有找到中断向量,怀疑是物理地址0没有映射到虚拟地址0当中。
调试7:核对TLB配置
在程序中board_early_init_r、initr_net等处加死循环,用仿真器观察对应地方的MMU配置,看他们是否有改变,是否有其他物理地址映射到虚拟地址0的地方。
/* 写DEC计数器,避免产生Decrementer中断 */
void set_dec (void)
{
unsigned long val = 0xffffffff;
asm volatile ("mtdec %0"::"r" (val));
}
volatile int s_stop = 1; //volatile 告知编译器s_stop的值随时回改变,避免编译器将while(s_stop)优化成了while(1)
static int initr_net(void)
{
......
__asm__ ("nop"); //通过nop的个数,方便汇编中定位死循环的位置
__asm__ ("nop");
while(s_stop)
set_dec();
}
在死机前和死机后,通过仿真器观察MMU中条目有略微的改变,但改变的地方均和物理地址0和虚拟地址0无关。并且条目中只有TLB12是将物理地址0(DDR)映射到虚拟地址0。
结论: 和MMU配置无关。
调试8:分析cache
在调试7过程中,发现虚拟地址中,部分中断向量正常,部分不正常,物理地址中的全都正常。于是使用刷新命令,在读一次,发现虚拟地址中的内容也正常了。怀疑虚拟地址中内容不正常,和MMU配置无关,和cache有关系,是cache不一致导致。
搜索代码中cache的配置,发现cache是开启的。搜索代码,发现代码中有invalidate_icache、icache_enable、icache_disable等函数。而BSP中和cache相关的操作,只有在board_early_init_r 中Flash相关的地方,有如下操作:
/* Flush d-cache and invalidate i-cache of any FLASH data */
flush_dcache(); //同步数据cache
invalidate_icache(); //无效指令cache(和flush_icache一样),同步指令cache
这段代码和Flash操作一起被屏蔽掉了。在代码中还原此操作,异常死机问题就没有了。
结论
uboot 在initr_trap中以写数据的形式修改了中断向量,但是没有同步cache,导致指令cache中的内容未被更新。所以系统在进入DEC中断时,没有了可执行的指令,进而进入Program异常中断而死机。解决的办法是先同步dcache,确保修改的中断向量写入到内存中,再同步icache,将中断向量加载到icache中来。
系统异常中断调试总结
1、观察程序挂起后停止的中断向量位置,查找对应的中断原因。
2、如果死在了Program位置,并且中断向量未初始化,说明这里并不是第一中断现场,通过如下代码初始化中断向量后再观察。
for(i = 0x100; i < 0xf00; i+= 0x100)
*((volatile u32*)i) = 0x48000000; //死循环指令,保存第一中断现场
3、对应内核文档,定位异常的地址、原因等。通常SRR0保存了中断下条指令的位置。MSR寄存器保存了一些状态,它能够指示具体的中断原因。LR寄存器保存了最近堆栈地址等。