ARM汇编指令
学习arm汇编的主要目的是为了编写arm启动代码,启动代码启动以后,引导程序到c语言环境下运行。换句话说启动代码的目的是为了在处理器复位以后搭建c语言最基本的需求。因此启动代码的主要任务有:
- 初始化异常向量表;
- 初始化各工作模式的栈指针寄存器;
- 开启arm内核中断允许;
- 将工作模式设置为user模式;
- 完成上述工作后,引导程序进入c语言主函数执行;
因此汇编指令的学习主要是围绕这几个目的展开,主要学习跟上述目的相关的指令。
- mov指令:加载12位立即数到寄存器或转移一个寄存器的值到另外一个寄存器
mov r0, #2 ;加载立即数2到寄存器r0,MOV{S}<c> <Rd>, #<const>
mov r1, r0 ;将r0寄存器的值加载到r1,MOV{S}<c> <Rd>, <Rm>
-
- 大多数指令的格式为opcode rd, rn ,rm,其中,rd是目标寄存器,rn是第一操作数寄存器。同理:
- add指令常用的两种方式
ADD{S}<c> <Rd>, <Rn>, #<const>
ADDS}<c> <Rd>, <Rn>, <Rm>{, <shift>}
类似的还有
- sub指令
SUB{S}<c> <Rd>, <Rn>, #<const>
SUB{S}<c> <Rd>, <Rn>, <Rm>{, <shift>}
以上四条指令都有立即数作为第二操作数的情况,那么是什么立即数呢?准确的说这里所指的是12位立即数imm12。先说怎么判断某数是不是12位立即数,12位立即数的条件是:
- 如果某个数的数值范围是0~255之间,那么这个数一定是立即数;
- 把某个数展开成2进制,这个数的最高位1至最低位1之间的二进制数序列的位数不能超过8位;
- 这个数的二进制序列的右边必须为偶数个连续的 0
例如:0x234 = 0000 0000 0000 0000 0000 0010 0011 0100
最高位1至最低位1之间的二进制数序列:1000 1101没有超过8位
末尾1的右边有2个0,所以0x234是立即数
0x3f4 = 0000 0000 0000 0000 0000 0011 1111 0100
最高位1至最低位1之间的二进制数序列:1111 1101 从第一个1开始到最后一个1之间没有超过8位
末尾1的右边有2个0,所以0x3f4是立即数
0x132 = 0000 0000 0000 0000 0000 0001 0011 0010
最高位1至最低位1之间的二进制数序列:1001 1001 从第一个1开始到最后一个1之间没有超过8位
末尾1的右边有1个0,不满足第二条,所以0x132不是立即数
0x7f8 = 0000 0000 0000 0000 0000 0111 1111 1000
最高位1至最低位1之间的二进制数序列:1111 1111从第一个1开始到最后一个1之间没有超过8位
末尾1的右边有3个0,不满足第二条,所以0x7f8不是立即数
0xfab4 = 0000 0000 0000 0000 1111 1010 1011 0100
最高位1至最低位1之间的二进制数序列:0011 1110 1010 1101 从第一个1开始到最后一个1之间超过8位,不满足条件1,所以这个数不是立即数
这是因为ARM中将这 12bits 分为 8bit 常数(0~255)和 4bit 循环右移位值(0~15)
8bit 常数范围(0~255),位移的步进值是以2为单位(即实际位移 2 * rotate 位),可以表示循环有以(0~30)偶数位: 0、2、4、6、8、10、12、14、16、18、20、22、24、26、28、30。在实际存储这个数值的时候,要想办法把这个数压缩到这12位中去。压缩的方法就是找一个数,这个数必须是一个8bit数,之后循环右移2 * rotate位。如果能找打这个数,那么待保存的数就是立即数,否则就不是。
那么如果我们就是希望把一个非立即数存进rn,又该怎么做呢?
- ldr寄存器加载指令:
LDR{<c>}{<q>} <Rt>, <label> ;如ldr r0, =0x2FAB4
ldr指令多用于从ram中将一个32位的字数据传送到目的寄存器中
LDR<c> <Rt>, [<Rn>{, #+/-<imm12>}] 如:
LDR R0,[R1,#8] ;将内存地址为R1+8的字数据读入寄存器R0,这里的#8作为12位立即数是可以省略的
LDR<c> <Rt>, [<Rn>], #+/-<imm12> 如:
ldr r0, [r1], #8 ;将内存地址R1的字数据读入r0,之后r1+8
LDR<c> <Rt>, [<Rn>, #+/-<imm12>]! 如:
LDR R0,[R1,#8] ! ;将存储器地址为R1+8的字数据读入寄存器R0,并将新地址R1+8写入R1。
- bic指定位清零指令:
BIC{S}<c> <Rd>, <Rn>, #<const>;将rn中的字数据const为1的比特清零,把结果放入rd
- orr指定位置位(置1)指令:
ORR{S}<c> <Rd>, <Rn>, #<const>
- 汇编指令的s后缀,几乎所有的汇编指令都可以在指令后面加上s后缀,s后缀的含义是在指令执行过程中会更新cpsr寄存器的N,V,C,Z位
N:在结果是有符号的二进制补码情况下,如果结果为负数,则N=1;如果结果为非负数,则N=0
Z:如果结果为0,则Z=1;如果结果为非零,否则Z=0
C(无符号溢出):是针对无符号数最高有效位向更高位进位时C=1;减法中运算结果的最高有效位从更高位借位时C=0
V(有符号溢出):该位是针对有符号数的操作,会在下面两种情形变为1,两个最高有效位均为0的数相加,得到的结果最高有效位为1;两个最高有效位均为1的数相加,得到的结果最高有效位为0;除了这两种情况以外V位为0
例如:
mov r0, #0xFFFFFFFF
adds r1, r0, #1
上面的操作会导致Z,C置位,这是因为结果为0,并且从无符号数角度来看,已经从最高位向更高位进位了
而
mov r0. #0x7FFFFFFF
adds r1, r0, #1
会造成N位和C位置位,这是因为计算结果0x80000000最为位为1,代表负数,并且 从有符号角度来看,把一个整数加成了负数。
- 更新N,V,C,Z位有什么用呢?几乎所有的arm指令都可以在指令之后可选地增加执行条件
例如:movcs r0, #100;表示只有在C位置位的情况下才能把100加载入r0,这样的话就可以非常方便地实现指令的有条件执行。
例如:实现两个unsigned long long类型变量的求和,
unsigned long long l1 = 0x00000000FFFFFFFF;
unsigned long long l2 = 0x000000000000003;
很明显结果为0x0000000100000002
我们用r0装l1的高4字节,r1装l1的低四字节:
用r2装l2的高4字节,r3装l2的低四字节
用r4装结果的高4字节,r5装结果的低四字节
第6~7行:装入被加数
第8~9行:装入加数
第10~11行:清空装结果的变量
第12行:先加低四字节,这里因为考虑到进位问题,所以要更新N,V,C,Z位
第13行:之前的进位导致更高位数据的丢失,必须把这一位补回来,要不要补就看之前时候进位,进位标志是cs
练习:从r0, r1代表的两个有符号数中找到较大值放入r2寄存器
- CMP比较指令用于比较两个寄存器的值或者比较一个寄存器和立即数的值,其原理是对待比较的两个数求差,看结果是否为0,这个指令会无条件修改N,V,C,Z位。
如:
mov r0, #100
cmp r0, #100
会导致Z置位,从条件码表可知,只要Z置位就是两数相等。
- 跳转指令b
b指令类似c语言的goto语句,能够实现无条件跳转。跳转时需要一个lable,表示要跳转到什么地方去
配合之前的条件码,就可以实现一些较为复杂的操作,例如实现从0加到100的和
这就是循环实现的机制
- 事实上,程序跳转工作更多的是为了实现类似函数的功能,此时lable就是函数的函数名,其实lable本身代表的就是待跳转那一行指令的地址,b指令本质上就是把待跳转那行的地址装入pc寄存器,但是函数在调用完毕之后要回到调用处的下一行指令处执行,为了能够回到调用的下一行,需要使用bl指令。bl和b之间的区别就在于bl会在lr寄存器中保存回来的地址。如:
以上代码虽然实现了函数调用的程序流程,但是还存在两个问题:一是调用完毕以后r0, r1寄存器的值被max_of_two_numbers函数修改了,二是如果出现了函数的嵌套调用,那么lr寄存器的值就会被修改,而无法回到最开始的地方。要解决这个问题,就必须在每次函数调用前保护现场,在函数调用完毕以后恢复现场。而实现这个功能就需要使用栈这个数据结构。
- 栈的实现类型:
2440实现保护和恢复现场使用的栈是数组栈,即用一段连续的内存空间为栈提供空间。从数组栈的具体实现来看入栈的方式有四种做法:
- 空增:先写入数据,再让栈指针自增;
- 空减:先写入数据,再让栈指针自减;
- 满增:先让栈指针自增,再写入数据;
- 满减:先让栈指针自减,再写入数据。
arm体系采用的方案是满减,但是在进行操作之前,我们必须告诉2440栈底的位置,这里我们把栈底设置为0x40001000,从地址0x40000000开始的0x1000这段内存空间对应的是2440内部的一段ram,总共4k。实际能够使用的内存空间为[0x40000000~0x40000FFF],设置栈底指针寄存器: ldr sp =0x40001000
- 入栈保护指令stmfd(STMDB)
STMFD<c> <Rn>{!}, <registers>
其中Rn表示栈底指针寄存器,< registers >表示需要入栈保护的寄存器,!表示入栈之后sp自动自减。如:
stmfd sp!, {r0, r1, r2, r3-r12, lr}
- 出栈恢复指令ldmfd(LDM/LDMIA/)
LDMFD<c> <Rn>{!}, <registers>
中Rn表示栈底指针寄存器,< registers >表示需要入栈保护的寄存器,!表示出栈之后sp自动自增。如:
ldmfd sp!, {r0, r1, r2, r3-r12, lr}
- 在汇编中调用c语言编写的函数
设有c语言定义的函数void func_c(void);在汇编代码中调用该函数,只需用import声明函数名即可,之后就可以使用bl指令调用该函数,注意,既然是调函数,就一定要保护现场
- 向c函数传参
向c函数传参的方法很简单,如果参数个数小于等于4个,就直接用r0~r3传参,c函数返回值通过r0寄存器返回:
设有c函数:
int add_c(int a, int b, int c, int d)
{
return a + b + c + d;
}
如果参数个数大于4个,从第五个参数开始就需要通过栈来传参
- 在c语言中调用汇编编写的函数类似,不过在汇编中用export声明函数,同时需要在c语言中用extern声明函数,按照标准,调用者负责保护现场和恢复现场
传参方法于此类似
- 切换arm内核的工作模式
切换工作方式的思路很简单,由于内核的工作模式是由cpsr寄存器的低5位来设置的,那么就可以先把cpsr读出来,更改低5位之后再设置进去。这里读取cpsr使用mrs指令,写cpsr寄存器用msr指令,需要注意的是在keil环境下写cpsr需要写成: msr cpsr_c r0;将r0的值写入到cpsr寄存器
学习了这些指令,最终就可以编写我们自己的启动代码了
preserve8
area reset, code, readonly
code32
entry
b start
nop
nop
nop
nop
nop
b do_interrput
nop
start
ldr sp, =0x40001000
mrs r0, cpsr
bic r0, r0, #0x1F
orr r0, r0, #0x12
bic r0, r0, #(0x01 << 7)
msr cpsr_c, r0
ldr sp, =0x40001000
sub sp, sp, #1024
mrs r0, cpsr
bic r0, r0, #0x1F
orr r0, r0, #0x10
msr cpsr_c, r0
ldr sp, =0x40001000
sub sp, sp, #2048
import main
b main
do_interrput
sub lr, lr, #4
stmfd sp!, {r0-r12, lr}
import interrupt_handle
bl interrupt_handle
ldmfd sp!, {r0-r12, pc}^
end
代码
1. start.s
preserve8area reset, code, readonlycode32entry;ldr r0, =0x40000fff;装立即数; mov r0, #0xffffffff
; bic r0, r0, #1;将最后一位清零
; bic r0, r0, #(1 << 5);将第5位清零
; bic r0, r0, #(1 << 7);将第7位清零; mov r0, #0x0
; orr r0, r0, #1;将最后一位置1
; orr r0, r0, #(1 << 5);将第5位置1
; orr r0, r0, #(1 << 7);将第7位置1; mov r0, #10 ;两个数比较大小
; mov r1, #20
; cmp r0, r1
; movge r2, r0 ;大的给r2
; movle r2, r1 ;小的给r2; mov r0, #10 ;三个数比较大小
; mov r1, #20
; mov r2, #30 ; cmp r0, r1
; movge r3, r0
; movle r3, r1; cmp r3, r2
; movle r3, r2 ; mov r0, #1;对无符号longlong进行相加操作判断溢出
; mov r1, #0
; mov r2, #0xffffffff
; mov r3, #0
;
; adds r4, r0, r2
; mov r5, #0
; addcs r5, #1;cs为判断低四字节是否溢出; mov r0, #1;实现b
; mov r1, #0
; b foo
; mov r2, #0xffffffff
; mov r3, #0
;foo
; adds r4, r0, r2 ;add后加s,影响CPSR寄存器中的NZCV位,之后指令可加不同的条件分支
; mov r5, #0
; addcs r5, #1;cs为判断低四字节是否溢出 ; mov r0, #0x10;两个数字比大小
; mov r1, #0x20
; cmp r0, r1
; bge great
; ble less;
;great
; mov r2, r0
; b finished
;less
; mov r2, r1; mov r0, #0x10 ;判断三个数的最大值
; mov r1, #0x20
; mov r2, #0x30;
; cmp r0, r1
; bge tmpgreat
; ble tmpless
;tmpgreat
; mov r3, r0
; b continue
;tmpless
; mov r3, r1;continue
; cmp r3, r2
; bge finished
; mov r3, r2; mov r0, #0 ;1到100求和
; mov r1, #0;
;loop
; cmp r1, #100
; bgt finished
; add r0, r0, r1
; add r1, #1
; b loop; ldr sp, =0x40001000;初始化满减栈
; mov r0, #0
; mov r1, #0
; stmfd sp!, {r0-r12};保存现场,入栈保护,防止数据被修改
; bl foo ;bl会在b指令跳转时,将下一行要执行指令的地址放入lr
; ldmfd sp!, {r0-r12}; 恢复现场,出栈;
;loop
; cmp r1, #100
; bgt finished
; add r0, r0, r1
; add r1, #1
; b loop
;
;foo
; mov r0, #1
; mov r1, #2
; add r2, r0, r1
; bx lr ;(跳转回原函数)mov pc, lr; ldr sp, =0x40001000 ;入栈保护,嵌套调用汇编函数
; mov r0, #1
; mov r1, #2
; stmfd sp!, {r0-r12, lr}
; bl max_of_two_numbers
; ldmfd sp!, {r0-r12, lr};
;foo
; mov r0, #1
; mov r1, #2
; add r2, r0, r1
; bx lr ;(跳转回原函数)mov pc, lr;
;max_of_two_numbers; mov r0, #3
; mov r1, #4
; stmfd sp!, {r0-r12, lr};不加lr,第二次lr会将第一次lr的值覆盖
; bl foo
; ldmfd sp!, {r0-r12, lr}
; cmp r0, r1
; movge r2, r0
; movle r2, r1
; bx lr; mov r0, #1
; mov r1, #2
; mov r2, #3
; stmfd sp!, {r0-r12, lr} ;汇编start.s调用main.c中的c语言函数,传参利用r0、r1、r2、r3传参
; import add_c ;汇编调c中函数用import声明c中的函数,c中调汇编函数在汇编中用export声明汇编的函数
; bl add_c
; ldmfd sp!, {r0-r12, lr} ; mov r0, #1 ;编写函数实现求三个数中的最大值
; mov r1, #2 ;汇编给c中函数传参用r0-r3,其他的通过入栈出栈传参,返回值r0
; mov r2, #3
; stmfd sp!, {r0-r12, lr} ;汇编start.s调用main.c中的c语言函数,传参利用r0、r1、r2、r3传参
; import max_of_three_numbers ;调用函数时要入栈保护,保护现场恢复现场
; bl max_of_three_numbers
; ldmfd sp!, {r0-r12, lr} ; mov r0, #1 ;编写函数实现传参五个数进入main.c
; mov r1, #2
; mov r2, #3
; mov r3, #4
; mov r4, #5;
; stmfd sp!, {r0-r12, lr} ;汇编start.s调用main.c中的c语言函数,传参利用r0、r1、r2、r3传参
; import access_five_numbers ;汇编调c语言函数用import
; stmfd sp!, {r4}
; bl access_five_numbers
; ldmfd sp!, {r4}
; ldmfd sp!, {r0-r12, lr} ; ldr sp, =0x40001000;初始化满减栈
; import main
; export foo
; b main;
;foo
; add r0, r0, r1
; bx lr ;(跳转回原函数)mov pc, lr; ldr sp, =0x40001000
; import main
; export sum_c
; b main;
;sum_c
; mov r2, r0 ;1到100求和
; mov r1, #0
; mov r0, #0;
;loop
; cmp r1, r2
; bxgt lr
; add r0, r0, r1
; add r1, #1
; b loopldr pc, =startldr pc, =undifine_handlerldr pc, =swi_handlerldr pc, =prefetch_handlerldr pc, =abort_handlernopldr pc, =IRQ_handler ldr pc, =FIQ_handlerundifine_handlerb undifine_handlerswi_handlerimport swi_functionstmfd sp!, {r0-r12, lr}ldr r0, [lr, #-4] ;取到swi中的值,当执行到swi这一行时, pc指向swi的下一行,lr放着swi下一行高四个字节的地址bic r0, r0, #0xff000000 ;取swi中的0x80给到swi——function函数进行传参bl swi_functionldmfd sp!, {r0-r12, pc}^ ;^将spsr恢复到cpsr;ldmfd sp!, {r0-r12, lr};bx lrprefetch_handlerb prefetch_handlerabort_handlerb abort_handlerIRQ_handlerb IRQ_handlerFIQ_handlerb FIQ_handlerstartldr sp, =0x40001000;初始化满减栈mrs r0, cpsr ;模式切换msr读取cpsrbic r0, r0, #0x1f;所有低五位清零orr r0, r0, #0x12;置位成IRQ模式10010msr cpsr_c, r0;转换模式为usr ;写cpsr,设置cpsr,写入m域ldr r0, =0x40001000;初始化满减栈sub r0, r0, #1024mov sp, r0mrs r0, cpsrbic r0, r0, #0x1f;所有低五位清零orr r0, r0, #0x10;置位成usr模式10000msr cpsr_c, r0;转换模式为usrldr r0, =0x40001000sub r0, r0, #2048mov sp, r0swi #0x80import mainb mainfinishedb finished;类似于while(1)end
2. main.c
//extern int add_c(int a, int b, int c);
//extern int max_of_three_numbers(int a, int b, int c);
//extern int access_five_numbers(int a, int b, int c, int d, int e);
//extern int foo(int a, int b);
//extern int sum_c(int a);
//int add_c(int a, int b, int c)
//{
// return a + b + c;
//}
//
//int max_of_three_numbers(int a, int b, int c)
//{
// int max = a;
// max = a > b ? a : b;
// max = max > c ? max : c;
//
// return max;
//}
//
//int access_five_numbers(int a, int b, int c, int d, int e)
//{
// return a + b + c + d + e;
//}
extern void swi_function(int n);void swi_function(int n)
{while (n--){}}int main(void)
{
// int ret = foo(10, 20);
// int sum = sum_c(100);while (1){}}
1. add(s)<c>
s : 影响CPSR寄存器中的NZCV位,之后指令可加不同的条件分支
c :
movge >=
movgt>
b
bl : 保存下一条指令的地址
bx
arm满减栈
函数调用者实现出栈入栈
传参:两种方法一样