2.1 指令集架构 ISA
应用程序的硬件执行环境
在冯诺依曼架构的计算机中, CPU 的运行可以当作是一个死循环, 不断取指执行. 在最开始设计硬件的时候, 有一套指令集, 声明如何使用这些硬件资源, 外设等. 操作系统对这些指令进行封装, 提供程序员或者用户使用.
操作系统本身也是一个程序, 只是这个程序拥有一些特权指令.
程序的运行
我们在使用 C 语言写代码时, 需要进行一个编译, 这个编译其实是把源代码先编译成汇编代码, 然后汇编成机器码. 机器码才是机器读得懂的语言, 汇编和源代码都是给我们人看的.
程序启动时, 将机器码从硬盘拷贝到内存中, 进行一系列的准备操作.
CPU 有一个特殊的寄存器, 叫程序计数器 PC, 记录 下一条 指令在内存的 位置 . 程序运行时不停地读取 PC 计数器内容, 然后取指执行.
ARM 汇编语言
ARM 的寄存器
ARM64 有 31 个 64 位通用寄存器, X0 到 X30.
X0 通常保存函数的返回值或者是调用函数的第一个参数, X1 到 X7 通常是保存调用函数的第二个到第八个参数.
X8 保存系统调用调用号.
X9 到 X15 是临时寄存器.
X16 到 X18 与平台相关.
X19 到 X28 作为被调用者保存.
X29 保存帧指针.
X30 是链接寄存器, 通常保存调用函数后的下一条指令, 也叫返回地址. 指示函数调用完成后应该执行什么指令.
也有 W0 到 W30, 是 X 寄存器的低 32 位.
此外还有
用于浮点计算, SIMD 和加密操作
32 个 128 位寄存器
32 位形式
64 位形式
常用的汇编指令
数据处理
存取指令
寻址方式
基址寻址方式
直接根据地址找数据
LDR W0, [X1]
表示将 X1 指向的数据赋值给 W0 寄存器.
变址寻址方式
对地址运算后, 用运算后的地址找数据
LDR W0, [X1, #12]
表示将 X1 地址+#12 的结果指向的数据赋值给 W0 寄存器
前变址寻址方式
与变址寻址类似, 但是会对 X1 进行更新. 前变址寻址先更新 X1, 再去找数据.
LDR W0, [X1, #12]!
表示先给 X1 加上#12, 再将得到的 X1 指向的数据赋值给 W0 寄存器
后变址寻址方式
先找数据, 然后再更新地址
LDR W0, [X1], #12
先将 X1 指向的数据赋值给 W0 寄存器, 再更新 X1 的值
条件分支与条件码
根据前一条指令的状态进行跳转
一些单字的助记
b branch, 跳转到某分支
ne not equal
c compare
z zero
比如上一条指令是类似 i--的操作, 这一条要判断 i 是否为 0 或者小于 0 之类的, 这时候就需要根据前一条指令的状态进行跳转了.
有一个特殊的寄存器叫状态寄存器, 会保存前一条指令的状态.
根据当前指令的状态进行跳转
函数的调用
简单写一下这样的代码
int s(int n) {
return n * n;
}
int cube(int n) {
return n * s(n);
}
使用gcc -S call.c
编译成汇编代码.
s:
sub sp, sp, #16
str w0, [sp, 12]
ldr w0, [sp, 12]
mul w0, w0, w0
add sp, sp, 16
ret
cube:
stp x29, x30, [sp, -32]!
mov x29, sp
str w0, [sp, 28]
ldr w0, [sp, 28]
bl s
mov w1, w0
ldr w0, [sp, 28]
mul w0, w1, w0
ldp x29, x30, [sp], 32
ret
这里省去了一些东西, 跟你编译出来的东西不太一样是正常的.
arm 和 x86 的程序执行都是类似的, 都会维护一个栈(还有一个内核栈, 不过这里不讨论). 栈里会存放一些数据, 也可能存放一些函数地址.
对于 arm 来说, 它有一个 sp 寄存器用来指向栈顶, 还有一个 X29 寄存器用来指向当前帧基址, 在两者之间的就是一个栈帧. 不过 X29 是可选的.
注意这个栈是从高地址向低地址走的, 每次 push sp 寄存器的值都会减少, pop sp 寄存器的值会增加.
在需要使用局部变量时, 栈顶就会分配一块空间, 来保存这些数据. 这里 cube 的stp x29, x30, [sp, -32]!
就是分配了 32 字节的空间, 并保存 x29, x30, 为了继续指向栈顶, sp-32 字节. 此时 x29 保存的是上一个栈帧的基址, x30 保存的是上一个栈帧的返回地址.
注意 x29 和 x30 都只有 64 位, 8 个字节大小. 因此这里还有 16 字节的空间是没有使用的.
在保存好上一个栈帧的状态后, 设置栈帧基址为 sp 当前的值.
这里可能是没有优化的原因, 将 w0 存在了 sp+28 的地方, 然后又从 sp+28 读回 w0 作为调用 s 的参数.
bl s 就表示调用 s 函数了. bl 这里隐式地将下一条指令保存到 X30 寄存器中, 之后调用完 s 就会执行 X30 里的指令了, 这里存的是mov w1, w0
.
在 s 函数中由于没有调用其它函数, 因此没有对 x29 和 x30 进行操作. 像这种没有调用其它函数的叫 叶子函数 .
s 里分配了 16 字节空间来存 w0, 然后又读出来进行运算, 最后存回 w0. 最后回收分配的空间, 执行 ret 返回了.
在执行 ret 后调用 X30 寄存器的指令, 计算完保存至 w0 恢复 X29 和 X30 并 ret.