这篇文章主要介绍汇编、计算机组成原理等基本内容,属于基础篇。方便其他人快速查阅。
每一段的基础介绍都参杂一些个人理解

基于ARM的体系结构

冯诺依曼体系

定义

冯诺依曼体系:数据和指令都存储再一个存储器中的计算机。
这种结构的计算机系统由中央处理器单元(CPU)和一个存储器组成。

存储器拥有数据和指令,可以根据所给的地址对它进行读写。
CPU有几个可以存放内部使用值的内部寄存器。
程序计数器器(PC):存放指令再存储器中地址的寄存器。完成一个取指令操作后,PC = PC + 1.这里的1指的是一个单位,也就是一条指令的长度 ÷ 寻址粒度。因为X86的指令长度不一样,所以每次增加的量会变化。

流程

CPU先从存储器中取出指令,然后对指令进行译码,最后执行。PC并不直接决定机器下一步要做什么,它只是间接地指向了存储器中的指令。
因此,只要改变了存储器中的指令,就能改变CPU所做的事情。

冯诺依曼体系认为:程序只是一种(特殊)的数据,它可以像数据一样被处理,因此可以和数据一起被存储在同一个存储器中。数据总线和地址总线共用。

哈佛体系

定义

哈佛体系为数据和程序分别提供了独立的存储器(即将冯诺依曼体系里的数据和程序指令拆开)。

流程

CPU先从程序存储器中取出指令,译码,拿到数据存储器的地址,然后执行。

它的主要特点是将程序和数据存储在不同的存储空间中,即程序存储器和数据存储器是两个独立的存储器,每个存储器独立编址、独立访问。与两个存储器相对应的是系统的4条总线:程序的数据总线与地址总线,数据的数据总线与地址总线。这种分离的程序总线和数据总线允许在一个机器周期内同时获得指令字(来自程序存储器)和操作数(来自数据存储器),从而提高了执行速度,使数据的吞吐率提高了1倍。又由于程序和数据存储器在两个分开的物理空间中,因此取指和执行能完全重叠。CPU首先到程序指令存储器中读取程序指令内容,解码后得到数据地址,再到相应的数据存储器中读取数据,并进行下一步的操作(通常是执行)。

总结

上面的这两种体系都属于存储器体系,前者是将程序指令和数据放在一块,后者是拆分。
哈佛体系的速度快是显而易见的,但我认为这两者都有点理想模型的意思。在实际的使用中,都允许存在一些例外。

拿哈佛体系来说,它效率高,但设计复杂。如果我们要动态加载程序,那么就需要从外存中读取一段程序然后加载到RAM,如果这个程序在数据内存中,我们就需要一种机制将数据内存再传输到程序内存中,这就增加了设备复杂度。到RAM,如果这个程序在数据内存中,我们就需要一种机制将数据内存再传输到程序内存中,这就增加了设备复杂度。此外,此外,,绝对不允许CPU/DSP读取程序内存来当作数据,这也是不现实的,程序一般都有只读数据区和静态数据区,烧写程序的时候会当作程序的一部分烧进ROM中,如果为了把程序和数据分开,而需要设置两块ROM,再把其中一块ROM和RAM通过复用器接起来,结构就太复杂了就太复杂了,更不用说每次烧写都要分开烧两块ROM。实际上即使是DSP通常也允许从程序内存的总线上读取一些数据。

ARM寄存器

arm指令集分为arm指令和thumb指令等,在IDA中对于一个函数或地址判断是否是ARM还是thrumb看调用的地址是否为奇数,如果为奇数则为thrumb指令,如果是4字节对齐则为ARM指令

寻址方式:9种

  • 立即寻址:MOV R0, #1234 地址码部分为立即数
  • 寄存器寻址:MOV R0, R1
  • 寄存器间接寻址: LDR R0, [R1]
  • 寄存器移位寻址: MOV R0, R1, LSL #2
  • 多寄存器寻址:LDMIA R0, {R1,R2,R3,R4}. 指令后缀IA表示每次执行完加载操作后R0寄存器的值自增1.R1=[R0], R2=[R0+#4],R3=[R0+#8],R4=[R0+#12]
  • 基址寻址
  • 堆栈寻址: STMFD SP!, {R1-R7, LR} 将R1-R7,LR入栈。多用于保存子程序“现场”
  • 块拷贝寻址:
  • 相对寻址

R7: 栈帧指针(Frame Pointer).指向前一个保存的栈帧(stack frame)和链接寄存器(link register, lr)在栈上的地址

LR

链接寄存器。它与子程序调用密切相关,用于存放子程序的返回地址,它是ARM程序实现子程序调用的关键所在

  • 保存子程序返回地址;
  • 当异常发生时,LR中保存的值等于异常发生时PC的值减4(或者减2),因此在各种异常模式下可以根据LR的值返回到异常发生前的相应位置继续执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
1 int main()
2 {
3 int k, i = 1, j = 2;
4 addsub(i, j);
5 k = 3;
6 return 0;
7 }

8 int addsub(inta, int b)
9 {
10 int c;
11 c = a+ b;
12 return c;
}

对于上面的程序,编译器会将第4行编译为指令:BL addsub,将第12行编译为指令:MOV pc, lr

在这里,关键指令BL addsub会完成2件事情:

  1. 将子程序的返回地址(也就是第5行代码在内存中的位置)保存到寄存器LR中;
  2. 跳转到子程序addsub的第1条指令处。

这样就完成了子程序的调用。

如果子程序又调用了孙子程序(LR又会变为BL孙子程序的下一行),所以需要先将当前的LR入栈(一般是在子程序的入口出),等到子程序返回时POP出来就可以了

SP

堆栈寄存器

按照后进先出(LIFO)原则组织起来的连续存储区域。用于程序保存或恢复数据,或用于子程序调用及中断响应时保护与恢复现场。SP是堆栈指针寄存器,存放着当前堆栈栈顶地址。
一般情况下,对SP有影响的指令,对SP的操作都是隐式的,就是说SP并不出现在指令操作数当中。

1
2
PUSH AX;累加器AX的内容压栈保存,(SP)=(SP)-2
POP BX;堆栈内保存的累加器AX的内容弹出到BX寄存器中,(SP)=(SP)+2

常见汇编指令

LDR

LDR 从基址寄存器Rn 的和立即数的偏移之和 得出内存地址的位置,从内存地址里面Load(装载)一个字的值写到 Rt 寄存器。

从存储器(确切地说是地址空间)中装载数据到通用寄存器。

此外,ARM中还存在一个LDR伪指令,它和LDR是不同的东西。区别在于它可以在立即数前增加=,表示把某一地址写到寄存器中

1
2
LDR r0, 0x12345678  # LDR指令 
LDR r0, = 0x12345678 # LDR伪指令

它和MOV的作用类似,但ARM是RISC结构,数据从内存到CPU之间的移动只能通过L/S指令来完成,也就是ldr/str指令。mov只能在寄存器之间移动数据,或者把立即数移动到寄存器中。

mov指令限制了立即数的长度为8位,也就是不能超过512。而ldr伪指令没有这个限制。如果使用ldr伪指令时,后面跟的立即数没有超过8位,那么在实际汇编的时候该ldr伪指令是被转换为mov指令的。

STR

STR(立即数) 指令计算出从基址寄存器的数值和立即数的偏移(通过这,得出内存(memory)地址),然后从一个寄存器(register)读取它的值(store)到内存(memory)中

说人话就是,STR{条件} 源寄存器,<存储器地址>

1
2
3
4
STR R0,[R1],#8             ;将R0中的字数据写入以R1为地址的存储器中,并将新地址R18写入R1
STR R0,[R1,#8] ;将R0中的字数据写入以R18为地址的存储器中。”
STR R1, [R0] ;将r1寄存器的值,传送到地址值为r0的(存储器)内存中
STR R1, [R0, #var_c] ; 将R1的值赋给var_c

B\BNE\BEQ\BL\BLX\BX

首先了解一下CPSR程序状态寄存器:它用于在用户编程时存储标志,包括条件码标志,中断禁止位,当前处理器模式以及其他状态和控制信息。

CPSR

  • N标志位:运算结果的最高位反映在该标志位。对于有符号二进制补码,结果为负数时N=1,结果为正数或零时N=0
  • Z标识位:Z=1表示运算的结果为零;Z=0表示运算的结果不为零。CMP指令的结果可以直接影响Z
  • C标志位:加法指令中(包括比较指令CMN),当结果产生了进位,则C=1,表示无符号运算发生溢出;其他情况C=0。减法指令中(包括比较指令CMP),当运算中发生借位,则C=0,表示无符号运算数发生进位;其他情况下C=1。对于包含移位操作的非加减运算指令,C中包含最后一次溢出的位的数值。对于其他非加减运算指令,C位的值通常不受影响
  • V标志位:对于加减运算指令,当操作数和运算结果为二进制的补码表示的带符号数时,V=1表示符号位溢出;通常其他指令不影响V位。

B

B 指令是最简单的跳转指令。一旦遇到一个 B 指令,ARM 处理器将立即跳转到给定的目标地址,从那里继续执行。注意存储在跳转指令中的实际值是相对当前PC 值的一个偏移量,而不是一个绝对地址,它的值由汇编器来计算(参考寻址方式中的相对寻址)。它是 24 位有符号数,左移两位后有符号扩展为 32 位,表示的有效偏移为 26 位(前后32MB 的地址空间)

相对跳转指令,跳转的位置是相对于当前PC值的偏移量

BEQ

如果CPSR的Z标识位为为1,则跳转。

BNE

如果CPSR的Z标志位为0, 则跳转

BL

将下一个指令的地址复制到 r14(链接寄存器)中,跳转

BLX

将处理器的状态从 ARM 更改为 Thumb,或从 Thumb 更改为 ARM,将下一个指令的地址复制到r14中,跳转

BX

将处理器的状态从 ARM 更改为 Thumb,或从 Thumb 更改为 ARM,跳转

示例1

源码:

1
2
3
4
JNIEXPORT jstring JNICALL Java_com_example_lizhongquan_hellojni_JniUtils_getJniString
(JNIEnv *env, jclass) {
return env->NewStringUTF("Hello World");
}

汇编:

getJniString

首先,R0 = env

  • LDR R1, [R0]:R1 = env
  • LDR.W R2, [R1, #0x29c] : 等价于R2=R1+0x29c。也就是获得env中指定偏移地址的方法在内存中的位置
  • ADR R1, aHelloWorld: R1指向hello world对应的地址。ADR是指获取对于当前PC的相对地址。
  • BX R2 这时候R0 = env, R1 = hello world, R2是env中的指定方法。也就是说调用env->NewStringUTF(“hello world”)

示例2

源码:

1
2
3
4
5
extern "C"
JNIEXPORT jint JNICALL encrypt(JNIEnv *env, jclass type, jint key) {

return key + 5;
}

汇编:

encrypt

R0 = env, R1 = type, R2 = key.

  • ADDS R0,R2,#5 : key+5,结果放入R0
  • BX LR: 回调,值为R0

未完待续