调试VIVI: 一系列莫名错误及其解决过程

  • 问题出现

近期一个项目需要将VIVI移植到我们的三星2440板上,在head.S中设置好晶振频率时钟频率之后,VIVI能跑起来了,但进入main后,初始化mtd设备时,程序挂了。

 

接上JTAG,使用ADS的AXD Debugger进行汇编调试,发现程序正运行着死循环,根据vivi.map中提供的地址信息,得知当前指令属于UndefEntryPoint异常中断处理程序,也就是说,程序发生了Undefined instruction exception异常。

 

  • 内存校验错误

由于无法知道哪里的指令发生异常,只能通过调试器单步汇编代码进行检查。在这个过程中,由于我对AXD Debugger不太熟悉,浪费了不少时间。

 

首先讲下Nand Flash Boot的机制。和Nor Flash不同,Nand Flash只提供数据访问接口,因此Nand Flash上存储的指令必须复制SDRAM中才能执行。考虑到大容量的Nor Flash成本远高于Nand Flash,普遍的做法是在板上置一片小容量的Nor Flash和一片大容量的Nand Flash,在Nor Flash写入引导代码,引导代码再将Nand Flash中的内容复制到SDRAM中执行,比起全部使用Nor Flash来说,成本可以成本不少。

 

三星的S3C2440则采用了另外一种办法,可以将Nor Flash也省掉。它的原理是CPU内置了4K的RAM,当CPU设置为Nand Boot模式时,这4K的RAM地址将被映射到0x00000000,CPU初始化时,第一件事是将Nand Flash的前4K内容读入内置的4K RAM中,之后才是执行此RAM中的指令。这就要求Boot Loader的内容必须控制在4K内,这有求太高了,现代的Boot Loader通常都带有大量的调试功能,基本上不可能做到那么小,因此Boot Loader也就分化成两部分,功能极简单的Nand Boot Loader和一般意义上的Boot Loader,比如WinCE就分为Nboot和Eboot。Nand Boot Loader只负责把Boot Loader搬运到内存中,再一个跳转就算了事。

 

可是两个Boot Loader还是太麻烦了,VIVI使用了另一种方法,启动后,首先将自己从Nand Flash中搬运到SDRAM中,再精确的跳转到下一条指令处,这样就可以省掉Nand Boot Loader。

 

就是这个搬运的过程,给我制造了不少麻烦。因为搬运后,VIVI会进行校验以确定代码是否正确复制,也就是将0x00000000开头的4K内容和目标地址的前4K内容进行比较,如果内容一致,则认为已正确搬运。这么大一个循环我当然在调试时设置了断点直接跳过,结果居然发现数据校验失败....真是太奇怪了,因为我亲自察看过两块内存,确实是完全一致的。

 

无法从代码上找到原因,只能单步执行慢慢观察,结果让我很意想不到:断点位置的内存被保护起来了,导致读出的数据是错误的。

 

  • 堆初始化错误

校验通过以后,VIVI跳转到SDRAM区域地址0x33f00000之后继续执行,进入了main函数,之后在heap_init中挂掉了。增加调试代码后,得知原因是gHeapBase初始值不对导致heap_init失败,但明明代码中的定义是"static blockhead *gHeapBase = NULL;"!没办法,只能单步调试跟踪,发现gHeapBase被莫名其妙的改为0x000000ff,真是百思不得其解。

 

后来在某一次察看vivi.map时,发现gHeapBase在地址是0x33f0d63c(0x33f0是VIVI在RAM中的起始地址),而0xd63c转为10进制为54844,正好是vivi编译后的文件长度!思路一下豁然开朗,也就是说gHeapBase的初始值并没有保存在VIVI的文件中,它本应是在内存中被初始化为0的,但因为我们的VIVI是从Nand Flash中复制到SDRAM中的,那么一定是Nand Flash的内容有问题,导致gHeapBase被赋与一个不应该的数值,马上查看Nand Flash的内容,果然文件结束后紧跟着"ff 00 00 00",这下真相大白了,烧录VIVI到Nand Flash的时候,烧录软件(我用SJF2440烧入的)虽然正确的写入VIVI,但没有处理其后应以00填充的内容,而VIVI自己没有记录文件长度,所以一股脑把这些数据复制到SDRAM中了。

 

解决方法有2种,一是修改烧录程序一劳永逸,而更快速的方法是16进制编辑器直接修改VIVI映像文件,填充00直到块对齐再烧录。

 

  • arch_udelay()发生异常

堆初始化后,在mtd_dev_init时又挂了,增加调试代码,知道是调用arch_udelay()时发生了异常,很奇怪的在计算CPU频率的时候触发异常,代码如下:

/* * cpu clock = (((mdiv + 8) * FIN) / ((pdiv + 2) * (1 << sdiv))) * FIN = Input Frequency (to CPU) */ static inline unsigned long get_cpu_clk(void) { unsigned long val = MPLLCON; return (((GET_MDIV(val)+8)*FIN*2)/((GET_PDIV(val)+2)*(1<<GET_SDIV(val)))); }

 

你能看出代码有什么问题吗?我看不出来,简单计算一下数值,连除0错误都不是,只知道直接返回一个值是没问题的,那就姑且先跳过吧。

 

  • printk()发生异常

这下严重了,printk可是我们调试重要道具啊,连它都出问题,没得玩了,一步一步往下查吧。

 

调用vsnprintf异常,继续跟进。

调用number异常,原来是数值转换出了问题,继续跟进。

do_div异常,WTF?这是个宏...

/* We're not 64-bit, but... */ #define do_div(n,base) / ({ / int __res; / __res = ((unsigned long)n) % (unsigned int)base; / n = ((unsigned long)n) / (unsigned int)base; / __res; / })

简单点说,这里的除法运算又出了问题,仍然不是除0错误!

  • 编译器问题?

那会是什么问题呢?这么简单的代码,一个最基本的除法,连浮点都不是,还能有啥问题?

 

查资料得知,原来ARM是没有除法指令的!所有ARM的除法都是编译器提供的软件算法计算出来的,这下有点头绪了。

arm-linux-gcc在编译程序时,调用子程序__aeabi_uidiv实现除法,使用objdump(arm-linux-objdump -d vivi-elf > test.out),察看其汇编代码:

33f09e58 <__aeabi_uidiv>: 33f09e58: e2512001 subs r2, r1, #1 ; 0x1 33f09e5c: 012fff1e bxeq lr 33f09e60: 3a000074 bcc 33f0a038 <__aeabi_uidiv+0x1e0> 33f09e64: e1500001 cmp r0, r1 33f09e68: 9a00006b bls 33f0a01c <__aeabi_uidiv+0x1c4> 33f09e6c: e1110002 tst r1, r2 33f09e70: 0a00006c beq 33f0a028 <__aeabi_uidiv+0x1d0> 33f09e74: e16f3f10 clz r3, r0 33f09e78: e16f2f11 clz r2, r1 33f09e7c: e0423003 sub r3, r2, r3

 

单步执行跟踪进去,到0x33f09e74处,发生了Undefined instruction exception异常!

 

clz这是什么指令?ARM官网上有介绍:

 

4.4.5. CLZ
计算前导零数目。

语法 CLZ{cond} Rd, Rm


其中:
cond
是一个可选的条件代码(请参阅条件执行)。

Rd
是目标寄存器。 Rd 不能为 r15。

Rm
是操作数寄存器。 Rm 不能为 r15。

用法:
CLZ 指令可计算 Rm 中的值的前导零的数目,并会将结果存入 Rd。如果不设置源寄存器中的位,则结果为 32;如果设置位 31,则结果将为零。

条件标记
此指令不更改标记。

体系结构:
此 ARM 指令可用于 ARMv5 及更高版本。
此 32 位 Thumb 指令可用于 ARMv6T2 和 ARMv7。
此指令无 16 位 Thumb 版本。

再查CPU资料:

ARM1 V1
ARM2 V2
ARM2aS、ARM3 V2a
ARM6、ARM600、ARM610 V3
ARM7、ARM700、ARM710 V3
ARM7TDMI、ARM710T、ARM720T、ARM740T V4T
Strong ARM、ARM8、ARM810 V4
ARM9TDMI、ARM920T、ARM940T V4T
ARM9E-S V5TE
ARM10TDMI、ARM1020E V5TE
ARM11、ARM1156T2-S、ARM1156T2F-S、ARM1176JZ-S、ARM11JZF-S V6

原来如此,S3C2440内核为ARM920T,仅支持到V4T指令,是不支持clz指令的。

 

至此,已经知道是编译器配置问题,arm-linux-gcc生成了不支持的指令。

 

  • 最终诊断

 arm-linux-gcc可以通过-m参数生成编译目标的指令和优化代码,有-mcpu,-mtune,-march三种方式:

 

-mcpu=name
This specifies the name of the target ARM processor. GCC uses this name to
determine what kind of instructions it can emit when generating assembly code.
Permissible names are: arm2, arm250, arm3, arm6, arm60, arm600, arm610, arm620,
arm7, arm7m, arm7d, arm7dm, arm7di, arm7dmi, arm70, arm700, arm700i, arm710,
arm710c, arm7100, arm7500, arm7500fe, arm7tdmi, arm7tdmi-s, arm8, strongarm,
strongarm110, strongarm1100, arm8, arm810, arm9, arm9e, arm920, arm920t,
arm922t, arm946e-s, arm966e-s, arm968e-s, arm926ej-s, arm940t, arm9tdmi,
arm10tdmi, arm1020t, arm1026ej-s, arm10e, arm1020e, arm1022e, arm1136j-s,
arm1136jf-s, mpcore, mpcorenovfp, arm1176jz-s, arm1176jzf-s, xscale, iwmmxt,
ep9312.

 

-mtune=name
This option is very similar to the -mcpu= option, except that instead of speci-
fying the actual target processor type, and hence restricting which instruc-
tions can be used, it specifies that GCC should tune the performance of the
code as if the target were of the type specified in this option, but still
choosing the instructions that it will generate based on the cpu specified by a
-mcpu= option. For some ARM implementations better performance can be obtained
by using this option.

 

-march=name
This specifies the name of the target ARM architecture. GCC uses this name to
determine what kind of instructions it can emit when generating assembly code.
This option can be used in conjunction with or instead of the -mcpu= option.
Permissible names are: armv2, armv2a, armv3, armv3m, armv4, armv4t, armv5,
armv5t, armv5te, armv6, armv6j, iwmmxt, ep9312.

 

看样子我们只需要在编译时加上-mcpu=arm920t就可以解决这个问题了,但结果仍不如人意,照样出现了异常,用objdump看,还是生成了clz代码。仔细一想,明白了,除法的实现函数__aeabi_uidiv是链接的时候加入vivi的,gcc的库链接错了!

 

看看Makefile,原来默认链接库的目录是/usr/local/arm/4.3.2/lib/gcc/arm-none-linux-gnueabi/4.3.2/,但此目录下还有个子目录armv4t/,修改链接目录到/usr/local/arm/4.3.2/lib/gcc/arm-none-linux-gnueabi/4.3.2/armv4t/,重新编译生成,使用objdump再看代码:

 

33f09e58 <__aeabi_uidiv>: 33f09e58: e2512001 subs r2, r1, #1 ; 0x1 33f09e5c: 012fff1e bxeq lr 33f09e60: 3a000036 bcc 33f09f40 <__aeabi_uidiv+0xe8> 33f09e64: e1500001 cmp r0, r1 33f09e68: 9a000022 bls 33f09ef8 <__aeabi_uidiv+0xa0> 33f09e6c: e1110002 tst r1, r2 33f09e70: 0a000023 beq 33f09f04 <__aeabi_uidiv+0xac> 33f09e74: e311020e tst r1, #-536870912 ; 0xe0000000 33f09e78: 01a01181 lsleq r1, r1, #3 33f09e7c: 03a03008 moveq r3, #8 ; 0x8 33f09e80: 13a03001 movne r3, #1 ; 0x1 33f09e84: e3510201 cmp r1, #268435456 ; 0x10000000 33f09e88: 31510000 cmpcc r1, r0

 

该死的clz指令总算彻底消失了,VIVI终于顺顺利利跑起来了。

  • 附:

一个方便调试的宏,和printk使用方式一样,只是在显示调试内容前先输出文件行号和函数名

#define debug(...)  (printk("%s@%s, line %u:",__func__, __FILE__,__LINE__), printk(" "__VA_ARGS__))

你可能感兴趣的:(exception,汇编,assembly,Flash,performance,编译器)