一、访问信息
1、MOV指令,目的操作数只能是寄存器,源操作数可以是寄存器或立即数
这个指令后面可以加S,表示是否更新APSR,注意这个指令不区分sign和unsign,所有短位的数例如short,被移动到寄存器后都只会进行0扩展,不会进行符号扩展
2、LDR和STR,用于访问内存,不能直接从一个立即数地址load数据,必须先把这个立即数load到一个寄存器中,再用这个寄存器里的值访问内存。这两个指令有sign和unsign版,还有byte和half版,例如LDRSH,就是sign,half,unsign就不用写LDRUH了,直接写LDRH就行
二、算术运算
1、基本加减乘除
ADD,SUB可以加S,代表更新APSR
两个特殊的ADC,SBC,好像有点复杂,没咋讲就不看了
乘法除法,乘法就是MUL,结果是32比特的,除法有无符号数除法和有符号数除法,分别是UDIV和SDIV
2、三种奇怪的操作数
LDRB R0, [R1, #0x3]
这个的意思是load的byte是R1寄存器中存的地址再加3的地址
LDR R3, [R0, R2, LSL #2]
这个的意思是load的地址是R0+(R2<<2),这里LSL是逻辑左移
LDR R0, [R1], #4
这个是先把R1寄存器里的地址load进来,然后再把R1寄存器里的地址+4
3、关于伪汇编
LDR R0, =0x12345678
这条汇编由于后面的立即数已经有32bit了,然而一条指令最多也就32bit,没地方存了,所以这条汇编实际上是假的,但是我们可以这么写,汇编器/编译器会帮我们把这条伪汇编实现,通过在代码段存一些立即数和label,然后在指令中存offset,用pc+offset的方式访问内存中的地址
LDR R0, [PC, #offset]
...
DCD 0x12345678
4、Literal
接上面伪汇编的实现方法,这种在代码段中插数据的方法叫做Literal,我们也可以自己做
看下面这个例子,DCD是分配一个字的长度,DCB是分配一个字节或多个字节的长度,这里定义MY_NUMBER是0x2000ABCC,HELLO_TEXT是“Hello\n”,并且以0作为终结符,所以这个字符串实际上是hello和\n一共6字节,再加上终结符0,一共7字节。然后前面的ALIGN意思是,下面的代码以4字节对齐,这样可以避免一些存储和运行上的麻烦。注意这些label并不会出现在二进制机器码中,这只是汇编器为了方便程序员阅读从而添加的标记,实际的二进制码只会存这个32位的数,DCD、DCB也不会出现,这些只是为了让汇编器理解这行的内容。
汇编指令部分,前两条是把MY_NUMBER,也就是0x2000ABCC这个值复制到R3,然后把这个地址对应的内容复制到R4,后两条是先把HELLO_TEXT也就是“Hello\n”,的地址复制到R0,然后再从R0这个地址load一个byte到R2,这里一个byte就是首字母H。
LDR R3, =MY_NUMBER ;
LDR R4, [R3]
LDR R0, =HELLO_TEXT
LDRB R2, [R0]
…
ALIGN 4 ;
MY_NUMBER DCD 0x2000ABCC
HELLO_TEXT DCB “Hello\n”, 0
三、控制
1、关于指令长
因为有2字节的指令也有4字节的指令,那么机器怎么取指呢
机器可以通过前两字节,也就是hw1来判断这是一个两字节的指令还是一个4字节的指令,如果是4字节的指令,机器就会把后两字节取出来。但是在模拟的时候会发现机器每次都会取4字节的指令,这是因为机器做了一定的优化。
2、如何实现跳转
通过修改PC的值实现跳转,那么是怎么修改的呢,ARM汇编指令集存的是当前PC到要跳转的地方的偏移,这个offset可正可负。
3、跳转指令
有四个基本的
1)B <label>,无条件的跳转到label处
2)B<cc> <label>,条件跳转到label处,cc是条件
3)BL <label>,干两件事,第一,把当前PC压入LR寄存器,为什么是当前PC,因为PC已经在fetch这条BL指令之后自加了,当前PC就是下一条指令的地址,第二,跳转到label处。
4)BX Rm,这也是干两件事,先根据Rm寄存器中的LSB,也就是最低位判断mode,是1就说明要用thumb mode,如果是0就说明是要用ARM mode。(为什么我们可以把地址的最低位用来做这个标记,因为ARM的处理器要求arm指令必须字对齐,thumb指令必须半字对齐,所以指令一定是存在偶地址的,最后一位一定是0,我们就可以拿这位做这个标记。)确定mode之后,还原出原来的地址,然后跳转到这个地址。
5)BLX Rm,这个就是前两条指令的结合,要干三件事了,一个是把PC存到LR里,然后在干BX Rm干的事。
4、关于模式的改变
1)b label不支持模式转变,处理器默认你b之前和b之后是一个mode
2)只有bx才支持模式转变,所以bx会要求操作数是一个寄存器,这样方便他判断LSB
3)很多时候需要修改一个地址的LSB,要么是你存的时候要改,例如BL的时候要根据你当前的mode改LSB,还有的时候是取的时候要改,例如你确认该次跳转要进行模式转变,那么在bx之前,把要跳转的地址load到寄存器的时候,你要进行LSB的修改,例如你知到你要跳转到thumb mode,那就可以把你要跳转的地址和1按位或,1二进制就是前面全0,最后一位是1,按位或之后前面都不变,最后一位一定变成1。这样就可以确保LSB是1了。
5、条件后缀
首先是APSR状态寄存器可以通过cmp指令更新
然后就是这张表
6、对于栈的操作
Thumb指令集支持简单的push和pop,而且可以一条指令push或pop多个值
首先,cortex-M的栈是从高地址向低地址增长的,这样我们就可以开始理解push和pop了
Push多个寄存器的值到栈里的时候,遵循寄存器号越小,栈内地址越低这一原则,例如push{R0,LR},R0会在低地址,LR会在高地址,所以逻辑上是LR先被push进去,也就是说我写的这条指令中寄存器的排列顺序与逻辑上的push顺序没有关系。
Pop多个值到多个寄存器里的时候,也遵循寄存器号越小,栈内地址越低这一原则,例如栈内是这样,我要pop到R0和R3,那么我写的顺序是不影响的,最终都是658那个地址的数去R0,65C那个地址的数去R3,也就是大地址到大号码寄存器,小地址到小号码寄存器。
四、过程
这里梳理一下函数调用的过程
1、首先要把r0-r3空出来,也就是说如果我当前过程中有用r0-r3存东西,我需要保护他,要么把它压入栈中,要么把他存在Callee saved寄存器里。前者因为要访问栈,也就是内存访问,所以会慢,但是能存多个,后者是寄存器访问,所以会快,但是只能存有限个。R0-r3这四个寄存器是Caller saved寄存器,剩下r4-r11是Callee saved寄存器。
2、然后我需要传参,参数要存在r0-r3上,按顺序存,且需要按字对齐,这会出现问题,第一,如果参数太多放不下,就需要把剩下的放到栈上。第二,这四个寄存器两两成对,也就是说,如果我的参数是一个32位,一个64位的,一个32位的。我把第一个32位的参数存在r0之后,不能把64位的存在r1和r2,因为r0和r1是一对,r2和r3是一对,我想存这个64位的数就必须存在r2和r3上,r1就只能空出来了,也不能存第三个32位的数,因为要遵循按顺序存放的原则,这第三个参数就只能通过栈传递了。注意栈上也要遵循按字对齐,举个例子,栈上要传一个byte(8bit)和一个short(16bit),这俩不能放在一行,是放在两行的,也就是这个意思
e -> [ e | | | ]
f -> [ f | | ]
3、然后现场保护完了,参数传完了,我需要跳转了,要用BL,也就是把等会调用完回来要执行的指令的地址压到LR里,这个时候如果我已经在一个函数里了,也就是说我本来LR就存了上一次调用要回去的地址,那就需要先把这个LR压到栈里,再调用BL,这样保证到时候能回来。这里必要时也可以用BLX,同时也要注意先前提BL和BLX时的细节
4、然后就执行函数里的指令,这时候如果要用到Callee saved寄存器就需要把用到的寄存器里的值压入栈中保存,在跳转回去之前再pop出来,执行完之后如果函数有返回值,那么就需要传递返回值给原过程,传返回值的规则如下
为什么能是Indeterminate
5、执行完函数里的指令之后,需要跳转回来了,通常是BX LR,同样需要注意先前提到的细节。回来之后要从栈上拿信息,栈顶的可能是返回值,拿完之后如果是过程调用过程,还有可能要pop先前压进去的上一个LR,然后如果先前压进去了r0-r3,那也要pop出来,之后就可以正常执行后面的指令了。