目前更新到5.3节,请在http://dl.dbank.com/c02ackpwp6下载5.3节的全部文档
ARM7芯片有2种汇编语言指令集,一种叫做ARM指令集,字长为32bits,另一种叫THUMB指令集,字长为16bits。这两种指令集各有优缺点,它们可以单独使用也可以混合在一起使用,在ARM7芯片上,我们将只使用ARM指令集,在后续的Cortex芯片上我们将使用THUMB指令集的改良版——THUMB2指令集。
本小节只介绍本操作系统中使用到的一些汇编语言,对它们的介绍也仅限于本操作系统使用到的部分用法,并非全面,更详细的信息请读者自行查阅附录中的参考文档2。
另外我再补充一下我观点,以前看到一些同学说在学习芯片,请教如何使用汇编语言编程,总是抠这方面的问题。我觉得如果我们学习芯片的目的只是做开发项目,那么就没有必要学习汇编语言,可以把更多的精力放在学习芯片的功能特性上。一个完备的芯片产品甚至不需要底层软件工程师了解太多的芯片硬件外设特性,有封装好的驱动库函数可以直接调用。这次如果不是编写操作系统,我对汇编语言也仅仅是了解一点。汇编语言了解一点即可,,在某些极少数情况下可能会使用到汇编语言定位问题,但这也是极少的情况。
在操作系统中我们使用了下面几条指令:
u MOV/MOVS
MOV是英文单词Move的缩写,“搬移”的意思,将数据搬移进寄存器,指令格式为:
MOV 目的寄存器, 源寄存器
MOV指令将源寄存器中的数据搬移到目的寄存器中,寄存器间数据搬移可以使用MOV指令,如:
MOV R0, R1
MOV R14, PC
意为:
R0 = R1
R14 = PC + 8
注意,ARM7有两级流水线,如果读取PC寄存器的话,就会多读取2条指令的长度,也就是8个字节,目的寄存器为PC+8。
MOVS指令与MOV指令的格式、功能是一样的,除此之外,如果目的寄存器是PC的话,MOVS会将当前模式下的SPSR写入到CPSR中。本操作系统从SVC模式返回USR模式时就需要使用MOVS指令恢复USR模式的CPSR。例如,在中断模式下有下面的指令:
MOVS PC, R14
意为:
CPSR = SPSR
PC = R14
u ADD
ADD指令顾名思义,就是英文Add“加”的意思,指令格式为:
ADD 目的寄存器, 源寄存器, 立即数
ADD指令将源寄存器中的数据和立即数相加的结果保存到目的寄存器中,执行加法操作时可以使用ADD指令,如:
ADD R14, R14, #0x40
意为:
R14 = R14 + 0x40
u SUB/SUBS
SUB是英文单词Subtract的缩写,意为“减”,指令格式为:
SUB 目的寄存器, 源寄存器, 立即数
SUB指令将源寄存器中的数据减去立即数,所得的结果存入到目的寄存器中,执行减法操作时可以使用SUB指令,如:
SUB R14, R14, #4
意为:
R14 = R14 – 4
SUBS指令中的S标志与MOVS指令中的S标志作用类似,如果目的寄存器是PC的话,SUBS会将当前模式下的SPSR写入到CPSR中。本操作系统从IRQ中断模式返回USR模式时就需要使用SUBS指令恢复USR模式的CPSR,如:
SUBS PC, R14, #4
意为:
PC = R14 – 4
CPSR = SPSR
u AND
AND指令顾名思义,就是英文And“与”操作的意思,指令格式为:
AND 目的寄存器, 源寄存器1, 源寄存器2
AND指令将源寄存器1中的数据和源寄存器2中的数据做与操作,结果存入目的寄存器中,执行与操作时可以使用AND指令,如:
AND R0, R0, R1
意为:
R0 = R0 & R1
u ORR
ORR对应的英文是Or,“或”操作的意思,指令格式为:
ORR 目的寄存器, 源寄存器1, 源寄存器2
ORR指令将源寄存器1中的数据和源寄存器2中的数据做或操作,结果存入目的寄存器中,执行或操作时可以使用ORR指令,如:
ORR R0, R0, R1
意为:
R0 = R0 | R1
u LDR
LDR是英文Load Register的缩写,“加载寄存器”的意思,将内存中的数据存入寄存器中,指令格式为下面2种格式:
LDR 目的寄存器, [源寄存器]
LDR 目的寄存器, =常量
第一种格式将源寄存器中数据指向的内存地址中的数据存入目的寄存器,第二种格式将常量值存入目的寄存器,为寄存器赋值时可以使用LDR指令,如:
LDR R14, [R0]
LDR R0, =gpstrCurTaskSpAddr
第一条指令意为:
R14 = *R0
第二条指令中gpstrCurTaskSpAddr是一个全局变量,第二条指令意为:
R0 = &gpstrCurTaskSpAddr
从上述介绍来看,LDR指令与MOV指令似乎具有相同的功能,都可以为寄存器赋值。这两条指令的部分功能确实是一样的,但它们也有各自应用的特点。要说明这两者之间的区别,我们还需要进一步了解ARM7的指令结构,以下是有关ARM7机器码的一些知识,可以了解一下,但如果你只是做软件开发则不会有太多用处,了解即可。
ARM7内核采用的是RISC精简指令集,所有的ARM指令都是32bits的,在这32bits里既包含了指令的指令码,也包含了指令需要运算的数据,以MOV指令为例,通过MOV指令的32bits可以识别出这是一个MOV指令,又可以在这32bits里找到源寄存器和目的寄存器。我们来看一下MOV指令的机器码格式:
图 6 MOV指令的机器码格式
28~31bits(cond)是条件码,就是表明这条语句里是否有大于、等于、非零等的条件判断,这4bits共有16种状态,分别为:
二进制码 |
指令符号 |
含义 |
二进制码 |
指令符号 |
含义 |
0000 |
EQ |
相等 |
0001 |
NE |
不等 |
0010 |
CS/HS |
进位/无符号数大于等于 |
0011 |
CC/LO |
清进位/无符号数小于 |
0100 |
MI |
减/负数 |
0101 |
PL |
加/正数或0 |
0110 |
VS |
溢出 |
0111 |
VC |
没溢出 |
1000 |
HI |
无符号数大于 |
1001 |
LS |
无符号数小于等于 |
1010 |
GE |
有符号数大于等于 |
1011 |
LT |
有符号数小于 |
1100 |
GT |
有符号数大于 |
1101 |
LE |
有符号数小于等于 |
1110 |
AL |
任何条件 |
1111 |
- |
未定义 |
表 1 汇编语言条件码
指令与条件码可以有多种组合,比如MOV指令可以有MOV、MOVEQ、MOVLT等多种形式。前面我们说过状态寄存器里有NZCV的状态标志,当执行一条指令时,芯片就会将这条指令的条件码与状态寄存器中的状态标志做比较,如果状态寄存器中的状态标志满足这条指令的条件码时,则执行这条语句,如果不满足则不执行这条指令。状态寄存器中的状态标志是受某些指令影响的,因此在使用有条件码的指令进行判断前,必然会有其它指令配合使用,先修改状态寄存器中的状态标志,例如:
CMP R1, #0
BEQ GETNEXTTASKSP
第一条指令“CMP”是一个“比较”指令,如果R1等于0,那么它就将状态寄存器中的Z置为1,表示结果为真,否则,将状态寄存器中的Z置为0,表示结果为假。第二条指令其实是一条“B”指令,是“跳转”指令,B之后的“EQ”就是条件码,从表1中可以知道,条件是“相等”时才执行。
当R1等于0时,CMP指令就将Z置为1,执行BEQ时满足条件,就执行了跳转。如果R1不等于0,CMP指令就将Z置为0,执行BEQ时不满足条件,就不执行跳转。
同理,只有当状态寄存器中的标志为相等时,MOVEQ指令才会执行,这时其功能与MOV指令相同。而MOVLT指令则是当状态寄存器中的标志为有符号数,并且处于小于状态时才会执行的MOV指令。MOV指令的条件码是AL,因此MOV指令可以不管任何条件都去执行。其它指令也可以与条件码组合使用,具体情况请查阅参附录中的参考文档2。
25bit(I)是用来区别shifer_operand域是采用立即数寻址方式还是寄存器寻址方式,该bit为0表示寄存器寻址方式,为1表示立即数寻址方式,这就涉及到了指令的寻址方式。
寻址方式的出现不是为了使指令能有多种写法,而是受指令长度限制被迫产生的产物。以MOV指令为例,如果采用立即数寻址,立即数的长度不可能超过shifer_operand域的长度(MOV指令可以采用移位的方式装下部分更长的立即数,这些不在讨论之内),因此我们就不可能使用
MOV R0, #0x12345678
这条指令。立即数#0x12345678是32bits数据,已经超过了shifer_operand域所能装下的最长12bits数据,如果把0x12345678全部被存到指令中,那么该指令中将无法存储条件码等其它指令信息,因此,这条指令在编译时就会报错。
为了解决这个问题,芯片设计人员就设计了寄存器寻址方式,在ARM7中每种模式有16个通用寄存器,2的4次方等于16,因此只需要用4bits就可以为每个寄存器分配一个编号,R0~R15寄存器分别对应0~15的编号。4bits的寄存器编号完全可以存入shifer_operand域。采用寄存器寻址时,指令先查到寄存器的编号,然后再从寄存器中取出使用的数据,这样就解决了MOV指令受指令长度的限制而无法操作长立即数的问题。
从上述描述的过程来看采用寄存器寻址方式必须先将数据放入一个寄存器中,然后才能使用MOV指令采用寄存器寻址。对比立即数寻址方式,它增加了指令的执行时间,也增加了代码,还多用了一个寄存器,但它的优点是可以操作长的数据。
除了上面这两种寻址方式外,ARM7还有多种其它寻址方式,每种寻址方式都有其自身的特点,适用不同的场景,这里不介绍了。
21~24bits(opcode)是指令码,用来表明这条指令是什么指令,例如,MOV指令的指令码是0b1101,看到0b1101,芯片就将这条指令当做MOV指令来解析。
20bit(S)就是指令中S标志的体现,该bits为0表示指令不带S,为1表示指令带S,功能见上述指令介绍。
16~19bits(SBZ)手册中没查到是什么意思,SBZ应该是should be zero的意思,对比了几条指令发现该域果然全是0,应该是保留位。
12~15bits(Rd)是指令中的目的寄存器,存放寄存器的4bits编号。
0~11bits(shifter_operand),指令的操作数。
下面我找了4条指令,将MOV指令做一个对比:
指令 |
机器码 |
指令格式 |
|||||||
cond |
00 |
I |
opcode |
S |
SBZ |
Rd |
shifer_operand |
||
MOV R1, #0x64 |
E3A01064 |
1110 |
00 |
1 |
1101 |
0 |
0000 |
0001 |
000001100100 |
条件码为1110适用任何条件 |
|
立即数方式 |
MOV的指令码 |
指令没有S标志 |
|
目的寄存器为R1 |
源操作数为立即数0x64 |
||
MOVS PC, R14 |
E1B0F00E |
1110 |
00 |
0 |
1101 |
1 |
0000 |
1111 |
000000001110 |
条件码为1110适用任何条件 |
|
寄存器方式 |
MOV的指令码 |
指令有S标志 |
|
目的寄存器为R15 |
源操作数为寄存器R14 |
||
MOVLT R3, #0x1 |
B3A03001 |
1011 |
00 |
1 |
1101 |
0 |
0000 |
0011 |
000000000001 |
LT的条件码为1011 |
|
立即数方式 |
MOV的指令码 |
指令没有S标志 |
|
目的寄存器为R3 |
源操作数为立即数1 |
||
MOVEQ R0, R1 |
01A00001 |
0000 |
00 |
0 |
1101 |
0 |
0000 |
0000 |
000000000001 |
EQ的条件码为0000 |
|
寄存器方式 |
MOV的指令码 |
指令没有S标志 |
|
目的寄存器为R0 |
源操作数为寄存器R1 |
表 2 MOV指令汇编格式对比
LDR指令可以将32bits数据一次装入寄存器中,这里不再详细说明了,请读者自行参考文档。
u STR
STR是英文Store Register的缩写,“存储寄存器”的意思,将数据从寄存器存入内存。STR指令与LDR指令功能相反,指令格式为:
STR 源寄存器, [目的寄存器]
STR指令将源寄存器中的数据存入目的寄存器中数据所指向的内存地址,将寄存器中的数据存入内存时可以使用STR指令,如:
STR R1, [R0]
意为:
*R0 = R1
u LDM
LDM对应的英文是Load Multiple,LDM指令是LDR指令的增强版,可以将多个连续的内存数据存入一组寄存器中,这条指令在堆栈操作中经常使用,在介绍这条指令前我们先了解一下堆栈。
堆栈是分配在内存中的一部分空间,但堆和栈是2个概念,用户调用malloc等函数申请的内存就是从堆中申请的,这块内存使用完需要由用户自行释放,堆是由用户管理的。当发生函数调用时,程序会自动将父函数的寄存器存入内存,这部分内存就叫做栈,当子函数返回父函数时,程序会从栈中取出保存的寄存器数值,再恢复到寄存器中,这样就完成了一次函数调用,栈是由程序自动管理的。
栈有空满之分,栈有增减之分。根据栈指针不同的操作方式,可以将栈分为4种。栈指针指向栈顶的元素,即最后一个入栈的元素,此时栈指针指向的栈空间是用过的,是满的,这种栈叫做满(Full)栈。栈指针指向与栈顶元素相邻的下一个元素,此时栈指针指向的栈空间是没用过的,是空的,这种栈叫做空(Empty)栈。当向栈存储数据时,栈指针是向着内存地址减少的方向移动的,这种栈叫做递减(Descending)栈。当向栈存储数据时,栈指针是向着内存地址增长的方向移动的,这种栈叫做递增(Ascending)栈。综合栈的空满和增减特性,栈可以分为FD ED FA EA这4种类型,我们所使用的ARM7芯片是FD类型。
图 8 B指令的机器码格式
BL指令的0~23bits存放的是要跳转的相对地址,由于指令所在地址必须是4字节对齐的,因此跳转的地址最低2bits必然是0,因此BL指令0~23bits保存的是省略这最低2bits的地址,如果补全了这2bits,BL指令就可以表示26bits的跳转地址。在这26bits中需要使用1bit表示向前跳还是向后跳,那么剩下的25bits就可以表示32MByts的范围了,225=32M,因此,我们在很多文档上可以看到B跳转指令只能跳转到±32MBytes范围内的说明,就是这个原因。
上面这个BL指令要跳转的相对地址是0x2E2(BL指令0~23bits),补充2个最低位后,跳转的相对地址为0xB88,由于ARM7有2级流水线,所以跳转到的指令需要多加8个字节,BL要跳转的实际地址为0x00080398+0xB88+8=0x00080F28。
这条BL指令执行下面的操作:
LR = 0x0008039C
PC = 0x00080F28
在操作系统中我们没有使用BL指令,就是因为我们不知道我们所调用的函数是否会超出BL指令的跳转范围,但我们可以看到编译器编译出的很多程序都是使用BL指令调用函数的,编译器之所以不怕跳转超出±32MBytes的范围,是因为编译器在编译时就知道了程序所需要跳转的范围,它会为±32MBytes之内的跳转分配BL指令,保证BL指令不会超出范围。在这里以BL指令为例,介绍一下B指令的相关知识。
u BX
BX是英文Branch and Exchange的缩写,“跳转并改变状态”的意思,BX指令除了可以实现跳转,还可以改变芯片运行的指令集,可以在ARM指令集与THUMB指令集之间切换,这里我们只使用了它的跳转功能,格式为:
BX 目的寄存器
BX指令跳转到目的寄存器中存储的地址,由于寄存器可以存放32bits数据,因此BX指令可以实现芯片全空间跳转。
u MRS
MRS是英文Move PSR to general-purpose register的缩写,“将PSR寄存器的内容保存到通用寄存器中”的意思,就是将CPSR或SPSR寄存器的内容保存到R寄存器中,格式为:
MRS R0, SPSR
意为:
R0 = SPSR
u MSR
MSR是英文Move general-purpose register to PSR的缩写,“将通用寄存器的内容保存到PSR寄存器中”的意思,就是将R寄存器的内容保存到CPSR或SPSR寄存器中,格式为:
MSR SPSR, R0
意为:
SPSR = R0
u NOP
NOP是NO Operation 的缩写,意为空指令,执行该指令时芯片什么也不做,空闲一个指令周期。
在ARM7芯片上,我们有了上述的汇编知识就足够编写操作系统了。
指令先介绍到这里,我们再来看看汇编的函数如何来写。汇编函数使用“.func”作为函数的开始,使用“.end”标志着函数的结束,例如
.func TaskOccurSwi
TaskOccurSwi:
SWI
BX R14
.endfunc
这就是用汇编语言写的一个函数——TaskOccurSwi。
在这里我们使用的是GNU编译器,在GNU的编译器中“@”代表注释符,与C语言中的//是一样的效果,它之后的所有语句都被认为是注释,例如:
@CMP R1, #0
这条语句不能执行,编译器根本就不会将它编译进来。