Lecture 02: ARM 汇编基础
Contents
- 为什么学习ARM/ISA汇编
- 从C到汇编
- 理解arm汇编
- 理解机器执行
1 为什么学习汇编和指令集架构?
1.令人困惑的应用表现
2.指令集架构ISA(Instruction Set Architecture)
- CPU向软件(应用程序和操作系统)提供的接口。
- 理解软件在CPU上的运行(OS设计,程序调试)。
- 操作系统包含体系结构相关的汇编代码。
- 操作系统启动代码(栈没有设置)
- 部分操作C语言无法表达。(e.g.获取系统状态,刷新TLB)
- 部分场景下汇编更加高效(e.g.
memcpy
)
2.2 从C语言到汇编
1.为什么硬件不能直接运行C
- 硬件设计
(1)高级语言表达能力很强
(2)硬件理解高级语言复杂度过高难以高效设计。 - 机器指令
(1)格式相对固定
(2)功能相对简单
(3)二进制编码
2.编译过程
二进制文件难以理解->汇编较为适合阅读。
2.3 理解arm汇编
- 在完成程序编写后,程序被储存在磁盘中。
- OS加载程序,将其放入内存,CPU中的PC指向当前需要执行的第一个汇编指令。每执行一个指令,PC=PC+4.
- 数据刚开始存储在磁盘。后来会加载到内存当中。CPU中具有特殊的存储单元:寄存器,用于临时存储数据。可以用load/store指令来搬运数据。
2.4 常用汇编
2.4.1 数据搬运
2.4.2 算术指令
2.4.3 移位指令
2.4.4 逻辑运算指令
2.4.5 Modified Register
- z=z*48分解成z=z*3与z=z*16,这样可以采用右移,减少运行时间。(浮点乘法消耗时间远大于位移)
- Modified register 优势
- 对操作数进行移位/位扩展。
2.4.6 访存指令
2.4.7 内存结构
1.CPU视角下的内存:
- 内存可以被视为一个很大的字节数组。
- 数组每个元素可以由唯一的地址来索引。
2.内存地址
- 内存数组的名称计为M。M[addr]为addr开始的内存单元的内容。addr为内存数组的索引。内存单元大小由上下文决定。
- addr的具体格式由寻址模式决定。
3.寻址模式
(1)基地址模式(索引寻址)
- \([r_b]\)
(2)基地址+偏移量
- \([r_b,\text{offset}]\)
(3)前索引寻址(寻址操作前更新基地址)
- \([r_b,\text{offset}]!\), \(r_b+=\text{offset}\),寻址\(M[r_b]\)
(4)后索引寻址(寻址操作后更新基地址)
- \([r_b],\text{offset}\) 寻址\(M[r_b]\);\(r_b+=\text{offset}\).
(5)offset可以是
- 立即数 #imm
- 64位通用寄存器\(r_i\)
- 修改过的寄存器。例如:移位运算
lsl #3
,位扩展sxtw
(4)example:
2.4.8 条件码,分支指令
标签:.L3
,.L1
分支指令 .cbz
,bne
1.条件码
- 一组标识位的统称。
- 由PSTATE寄存器维护。
- N(Negative),Z(zero),C(carry),V(overflow)
- 条件码保留之前相关指令的执行状态,其中有
- 带有s后缀的算术/逻辑指令(
subs
,adds
) - 比较指令
2.条件码的设置
- 第一类:通过s后缀数据处理指令隐式设置。
adds Rd, Rn, Op2
相当于t=a+b
。
- C:运算产生进位时设置。
- Z:当t=0时被设置。
- N:当t<0时被设置。
- V:当运算产生有符号溢出时被设置。
(a>0 && b>0 && t<0) || (a<0 && b<0 && t>=0)
- 第二类:通过比较指令cmp显式设置。
cmp src1, src2
计算src1-src2
,不存储结果,只改变条件码。
- C:运算不产生借位时设置。
- Z:当操作数相等时被设置。
- N:当src1<src2时被设置。
- V:当运算产生有符号溢出时被设置。
3.跳转条件
4.跳转指令
- 直接分支指令
- 标签对应地址作为跳转目标
- 无条件分支指令:
b <label>
- 有条件分支指令:
bcond <label>
, bcond=beq,bne,ble,... - 间接分支指令
- 寄存器中地址作为跳转目标。
br reg
。
2.5 函数调用
2.5.1 函数调用=无条件跳转
2.5.2 基本概念
- 术语:
- Caller 调用者
- Callee 被调用者
2.5.3 函数调用与返回指令
函数调用bl <label>
,返回ret
函数调用
- 指令:
bl <label>
直接调用函数blr Rn
间接调用,调用函数指针。
- 功能:
- 将返回地址存储在链接寄存器x30
- 跳转到被调用者的入口地址。
返回指令
- 指令
ret
不区分直接调用和间接调用。
- 功能
- 跳转到返回地址X30
2.5.4 多级函数调用
- [ ]一级:cube调用square
- cube中的bl指令将返回地址保存在LR(X30)中
- square中的ret指令返回到LR(X30)记录的地址
- [ ]二级:cube调用square,square调用foo
- LR首先存储了square返回cube的地址
- 嵌套调用时发生覆盖:LR(X30)存储foo返回square的地址
2.5.5 函数栈帧
-
栈桢:函数在运行期间使用的一段内存
-
生命周期:从被调用到返回前
-
作用:存放其局部状态,包括:
(1) 存放返回地址
(2) 存放上一个栈桢的位置
(3) 存放局部变量 -
多级函数调用
-
例如,A调用B、B调用C
-
程序执行中存在多个未返回的函数
-
函数栈桢按照调用顺序排列
(1) 先被调用者后返回,后被调用者先返回
(2) 栈:先进后出,后进先出 -
CPU中的另一个特殊寄存器SP
SP: Stack Pointer
指向栈顶(低地址)
2.5.6 函数调用返回过程中栈的变化
2.5.7 帧指针FP:X29寄存器
- 栈桢回溯
- 栈桢大小不一
- 如何找到上一个栈桢(如调试)
(1) 保存x29(上一个栈桢的SP)
(2) 将当前SP写入x29(让callee能保存)
2.5.8 函数的调用,返回与栈
2.6 函数参数与返回值
2.6.1 寄存器传递数据
- x0-x7寄存器传递前8个参数
- x0作为返回值
2.6.2 传递数据
- 调用者压到栈上的数据
- 第8个之后的参数
- 按声明顺序从右到左
Why? 因为参数的数量无法确定。而编译器读取参数时只是读取sp指针以上的内存。因此可以得到每个参数的地址。反之则会导致每个参数的内存地址无法确定。 - 所有数据对齐到8字节
- 被调用者通过SP+
举例说明:
void proc(long a1, long *a1p,int a2, int *a2p,short a3, short *a3p,char a4, char *a4p,char a5, char *a5p)
{*a1p += a1;*a2p += a2;*a3p += a3;*a4p += a4;*a5p += a5;
}void caller(long *n)
{proc (1,0x2000,3,0x4000,5,0x6000,7,0x8000,9,0xA000);
}
查看caller的汇编语言:
00000000000000a0 <_caller>:a0: ff c3 00 d1 sub sp, sp, #48a4: fd 7b 02 a9 stp x29, x30, [sp, #32]a8: fd 83 00 91 add x29, sp, #32ac: a0 83 1f f8 stur x0, [x29, #-8]b0: e9 03 00 91 mov x9, spb4: 28 01 80 52 mov w8, #9b8: 28 01 00 39 strb w8, [x9]bc: 08 00 94 d2 mov x8, #40960c0: 28 05 00 f9 str x8, [x9, #8]c4: 20 00 80 d2 mov x0, #1c8: 01 00 84 d2 mov x1, #8192cc: 62 00 80 52 mov w2, #3d0: 03 00 88 d2 mov x3, #16384d4: a4 00 80 52 mov w4, #5d8: 05 00 8c d2 mov x5, #24576dc: e6 00 80 52 mov w6, #7e0: 07 00 90 d2 mov x7, #32768e4: 00 00 00 94 bl 0xe4 <_caller+0x44>e8: fd 7b 42 a9 ldp x29, x30, [sp, #32]ec: ff c3 00 91 add sp, sp, #48f0: c0 03 5f d6 ret
2.7 寄存器保存
2.7.1 通用寄存器保存
- 不同函数共享同一批通用寄存器
- 因此能够通过寄存器传递参数和返回值
- 然而,不同的函数对通用寄存器的使用会存在冲突 — 覆盖
- 避免冲突的思路
- 函数在使用某个寄存器之前保存该寄存器的值,返回前恢复
- 保存在哪:函数栈桢中
- 效率问题:有时候可能无需保存
如:一个函数内不调用其他函数
编译器会尽可能减少冗余保存的代码
2.7.2 寄存器使用约定
-
调用者保存的寄存器包括 X9~X15
-
调用者在调用前按需(仅考虑自己是否需要)进行保存
调用者在被调用者返回后恢复这些寄存器的值 -
被调用者可以随意使用
这些寄存器调用后的值可能发生改变 -
被调用者保存的寄存器包括 X19~X28
-
被调用者在使用前进行保存
-
被调用者在返回前进行恢复
-
调用者视角:这些寄存器的值在函数调用前后不会改变
2.7.3 举例理解
0000000000000000 <square>:0: 1b007c00 mul w0, w0, w04: d65f03c0 ret0000000000000008 <cube>:8: a9be7bfd stp x29, x30, [sp, #-32]! // 开辟栈帧,保留调用者的栈帧x29,保存返回地址x30c: 910003fd mov x29, sp // 当前帧的栈顶地址写入x2910: f9000bf3 str x19, [sp, #16] //被调用者使用前保存调用者或之前保留的数据14: 2a0003f3 mov w19, w0 // 使用数据参数18: 94000000 bl 0 <square>1c: 1b137c00 mul w0, w0, w1920: f9400bf3 ldr x19, [sp, #16] // 从内存中恢复数据24: a8c27bfd ldp x29, x30, [sp], #32 // 返回弹栈28: d65f03c0 ret
2.8 局部变量
2.8.1 函数局部变量存放在函数栈桢中
- 为什么不直接把局部变量存储在寄存器?
- 寄存器数量有限
- 数组和结构体等复杂数据结构
- 局部变量可能需要寻址 (如&a)
2.8.2 局部变量
- 局部变量的分配
- 在分配栈帧时被一起分配
- 局部变量的释放
- 在返回前释放栈帧时释放
- 局部变量通过SP相对地址引用
- (例如
ldr x1, [sp, #8]
)
2.8.3 小结