s3c2440学习之路-012-1 Undefined未定义中断
硬件平台:jz2440
软件平台:Ubuntu16.04 arm-linux-gcc-3.4.5
源码位置: https://github.com/lian494362816/C/tree/master/2440/013_excption/001_undef
承接上篇博客 s3c2440学习之路-012-0 异常中断基础知识 接下来让我们看看第一个异常/中断相关的实例。
其实这里应该叫做未定义异常,不过网上大多都是叫做未定义中断,所以后面的属于异常的叫法我都会换成中断,免得给人感觉怪怪的
文章目录
1 未定义中断的原理
1.1 ARM的指令组成
ARM的指令是由32位组成的,是有一定的组成格式的,如果不符合组成格式的话,那就这条指令就无法被识别,就是未定义指令了。指令的[31]~[28] 是条件位,当条件位为1110B时,就表明该指令一定背执行。这里特别指出[31] ~[28]是因为后面的例子种将会使用到。
1.2 执行未定中断的过程
当发现未定义指令时ARM会做什么呢,如同s3c2440学习之路-012-0 异常中断基础知识 的1.4 小节所说的, 程序会自动跳到0x4的位置去执行代码。
具体的执行过程如下:
-
执行某条命令,发现不符合ARM的命令格式,产生未定义异常
-
发生异常时硬件的处理,即进入异常
2.1将返回地址保存在LR(R14)寄存器
2.2将CPSR复制到SPSR(SPSR 就是专门弄来备份CPSR的)
2.3设置CPSR的模式位(设置CPSR的bit0~bit4)
2.4设置PC值为0x4(对应未定义异常向量的地址) -
发生异常时软件的处理,即处理异常然后回到异常发生时的位置继续处理
3.1 设置SP(因为需要通过压栈来保存寄存器和代码跳转)
3.2 保存现场(将通用的寄存器保存起来)
3.3 将SPSR赋值给CPSR
3.4 清除中断标志位(未定义异常无中断标志可清除)
3.5 恢复现场(将通用的寄存器恢复回去)
3.6 将LR直接赋值给PC(这里不需要做差值)
我们把重点放在软件的处理上,接下来通过源码来分析,这样可以更好的体会到未定义中断的处理过程。
2 源码分析
2.1 进入未定义中断
start.s
ldr pc, =sdram_addr
sdram_addr:
bl uart0_init
bl print_hello
.word 0xdeadc0de
bl print_hello
ldr pc, =main /* abs jump to main */
loop:
b loop
ldr pc, =sdram_addr 是为了让代码跳到SDRAM上去执行,如果不清楚请看s3c2440学习之路-011代码重定位
接下来就是初始化串口,然后执行print_hello函数,这个函数非常简单,就是打印"hello"。
void print_hello(void)
{
printf("\n");
printf("hello\n");
}
接下来就是重头戏,.word 0xdeadc0de, 是一条未定指令(0xdeadc0de,C零DE, 不是C欧DE, 有点像Dead Code 死代码),它的组成不符合ARM指令格式,因此会产生未定义中断。回顾前面所说的,最终硬件会把PC赋值成0x4, 程序会跑到0x4的地址去执行。
2.2 未定义中断的软件处理
start.s
.text
.global _start
_start:
b _reset
ldr pc, _undef_addr
_undef_addr: .word _undef
_undef:
/*set bank sp, sdram end address is 0x34000000
because sdram size is 64M,
and sdram start address is 0x30000000 */
ldr sp, =0x34000000
/* store register */
stmdb sp!, {r0-r12, lr}
mrs r0, cpsr
ldr r1, =undef_string
bl printf_undef
mov r0, #4
bl led_off
/* resume register */
ldmia sp!, {r0-r12, pc}^ /*^ means copy spsr to cpsr */
undef_string:
.string "Undefinf Excption!"
.align 4
_reset:
/* stop watch dog */
ldr r0, =0x53000000
mov r1, #0
str r1, [r0]
程序开始会执行2条指令,第一条是b reset, 所处的地址是0x0, 也就是上电后自动执行的第一条指令。第二条指令是ldr pc, _undef_addr, 所处的地址是0x4, 也就是发生未定义中断时硬件会自动跳到这里的地址。要弄懂这条命令的意思,就需要弄懂ldr 汇编指令的2种用法。
ldr r0, =0xA
ldr r0, 0xB
上面2条ldr指令,一条是带"=",一条不带"="。 第1条指令的意思是r0=0xA, 是直接赋值的意思,而第2条指令是获取0xB地址上的值,再赋值给r0。如果把0xB当做指针的话,第2条指令就等价于r0=*0xB。一个是直接赋值,一个是取它地址里面的值,在赋值。
在回到ldr pc, _undef_addr, 先获取_undef_addr地址处的数值,在赋值给pc。通过反汇编查看可知,_undef_addr的值为0x30000008, 而0x30000008地址上的值为0x3000000C ,因此ldr pc, _undef_addr 就等价于ldr pc, =0x3000000C。
(这里起始地址为0x30000000是因为链接脚本的原因, 实际的存储地址需要减去0x30000000)
代码跳到0x3000000C的位置,也就是_undef: 标号的位置。接下来代码就会按照之前所说的流程来处理。
2.2.1 设置SP
ldr sp, =0x34000000, 重新设置栈,原因有2个:
- 在发生未定义中断前,ARM就进入了未定义模式,此时sp(r13)寄存器是此模式下私有的寄存器, 不理解请看 s3c2440学习之路-012-0 异常中断基础知识
- 后续需要跳转到C函数和压栈操作,这些都依赖sp寄存器
2.2.2 保存现场
stmdb sp!, {r0-r12, lr}, stmdb是用来存储多个寄存器的指令。意思是以sp的数值为基础(0x340000000),依次把r0-r12, lr寄存器入栈保存起来。如果不懂麻烦上网查查,这里不做过深的解释。
2.2.3 打印数值和点亮led灯
mrs r0, cpsr
ldr r1, =undef_string
bl printf_undef
mov r0, #4
bl led_off
/* resume register */
ldmia sp!, {r0-r12, pc}^ /*^ means copy spsr to cpsr */
undef_string:
.string "Undefinf Excption!"
.align 4
通过mrs命令把cpsr 的数值读给r0, 把字符串"Undefinf Excption!" 传递给r1, 然后调用printf_undef 函数。
printf_undef 函数就是先打印传进来的字符串,然后输出cpsr的数值。
如果不清楚为什么要把值传给r0, r1 请看s3c2440学习之路-003 汇编给C传参数 点亮不同led灯
void printf_undef(unsigned int cpsr_status, char *string)
{
printf("%s\n", string);
printf("cpsr:0x%x\n", cpsr_status);
}
最终的实验结果就是, 先打印hello, 然后输出Undefinf Excption! ,cpsr的数值为0x600000db
0xdb=1101 1011b, 最低5位为11011b 也就是Undefinded模式。从这里可以看出,ARM确实进入了未定义模式。
2.2.4 恢复现场
ldmia sp!, {r0-r12, pc}^, 一条指令就搞定了。ldmia 对应前面的stmdb 命令,一个是把寄存器入栈存起来,一个是把寄存器出栈取出来。这里有2小点要注意
- 保存的时候是stmdb sp!, {r0-r12, lr}, 而取出时是ldmia sp!, {r0-r12, pc}^, 最后面一个是保存lr, 一个是取出pc, 就等价于把lr 赋值给pc了
- ldmia sp!, {r0-r12, pc}^ 最后还有一个"^" 符号, 意思是把spsr赋值给cpsr
这么简单的一条指令就完成了3个动作:
- 把之前保存的寄存器r0-r12恢复
- spsr赋值给cpsr
- lr 赋值给pc ,代码就回到了发生未定义中断前的位置了
2.3 总结
通过2.2 节 可以看出,对于未定义中断,整个软件部分的处理流程就如同1.2 小节说所的。
这里还有一个小疑问,问什么程序的一开始就是2条跳转指令呢?
2条跳转指令的地址分别位于0x0, 0x4, 这是ARM在发生复位操作和未定义中断时会去访问的地址。这里拿未定义中断来说,软件的处理有很多步骤需要完成,因此一条指令是无法完成的,所以在0x4的位置直接执行跳转ldr pc, _undef_addr, 跳转到其地方来完成,避免占用到0x0~0x1C的位置。(0x0 ~ 0x1C是ARM不同异常时的向量地址,不过目前我的程序只需要0x0 和0x4 不被占用)
因此,2440 uboot 的开头就是一堆的跳转指令,而且是按照下面的异常向量表来写的。
2440 uboot的start.S 开头部分代码
3 遗留问题
3.1 bl print_hello
sdram_addr:
bl uart0_init
bl print_hello
.word 0xdeadc0de
bl print_hello
ldr pc, =main /* abs jump to main */
loop:
b loop
在执行 .word 0xdeadc0de 前先执行了bl print_hello。但是发现如果把bl print_hello 去掉后,就不会出现未定异常了,换句话说.word 0xdeadc0de 指令被忽视了。
由此做了一个小测试,在word 0xdeadc0de 后面打印了cpsr的值,分2种情况
- 去掉106 行的bl print_hello,bl printf_cpsr的值为:0x200000d3
- 保留106 行的bl print_hello,bl printf_cpsr的值为:0x600000d3
唯一的不同就是最高的拿一个字节,一个是0x2, 一个是0x6。查看手册可以得到, 0x2表示进为,0x6 表示溢出位,为何0x2不会被执行,而0x6会被执行,这里暂时不清楚,不过区别点就在这里。我们这里的原因就是word 0xdeadc0de 指令被忽视了,没有执行。
为了保证指令被执行,需要将0xdeadc0de 修改为0xeeadc0de , 这样最高位就是1110b, 一定会被执行。这样即使去掉106行的去掉106 行的bl print_hello, .word 0xeeadc0de 也一定会被执行,一定会产生未定义中断。