Lec 02 arm汇编语言基础

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.编译过程
link
hex
二进制文件难以理解->汇编较为适合阅读。

2.3 理解arm汇编

  1. 在完成程序编写后,程序被储存在磁盘中。
  2. OS加载程序,将其放入内存,CPU中的PC指向当前需要执行的第一个汇编指令。每执行一个指令,PC=PC+4.
  3. 数据刚开始存储在磁盘。后来会加载到内存当中。CPU中具有特殊的存储单元:寄存器,用于临时存储数据。可以用load/store指令来搬运数据。
    general reg
    vector reg

2.4 常用汇编

2.4.1 数据搬运

mov

2.4.2 算术指令

arith

2.4.3 移位指令

mov

2.4.4 逻辑运算指令

logic

2.4.5 Modified Register

  1. z=z*48分解成z=z*3与z=z*16,这样可以采用右移,减少运行时间。(浮点乘法消耗时间远大于位移)
    alt
  2. Modified register 优势
    alt
  3. 对操作数进行移位/位扩展。
    alt

2.4.6 访存指令

alt

2.4.7 内存结构

1.CPU视角下的内存:

  • 内存可以被视为一个很大的字节数组。
  • 数组每个元素可以由唯一的地址来索引。

2.内存地址

  • 内存数组的名称计为M。M[addr]为addr开始的内存单元的内容。addr为内存数组的索引。内存单元大小由上下文决定。
  • addr的具体格式由寻址模式决定。

3.寻址模式
(1)基地址模式(索引寻址)

  • [rb]

(2)基地址+偏移量

  • [rb,offset]

(3)前索引寻址(寻址操作前更新基地址)

  • [rb,offset]!rb+=offset,寻址M[rb]

(4)后索引寻址(寻址操作后更新基地址)

  • [rb],offset 寻址M[rb];rb+=offset.

(5)offset可以是

  • 立即数 #imm
  • 64位通用寄存器ri
  • 修改过的寄存器。例如:移位运算lsl #3,位扩展sxtw

(4)example:

alt

2.4.8 条件码,分支指令

alt

标签:.L3,.L1
分支指令 .cbz,bne

alt

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.跳转条件
alt

4.跳转指令

  • 直接分支指令
  • 标签对应地址作为跳转目标
  • 无条件分支指令:b <label>
  • 有条件分支指令:bcond <label>, bcond=beq,bne,ble,...
  • 间接分支指令
  • 寄存器中地址作为跳转目标。br reg

alt

2.5 函数调用

2.5.1 函数调用=无条件跳转

alt

2.5.2 基本概念

  • 术语:
  1. Caller 调用者
  2. Callee 被调用者

alt

2.5.3 函数调用与返回指令

函数调用bl <label>,返回ret

alt

函数调用

  • 指令:
  1. bl <label> 直接调用函数
  2. blr Rn 间接调用,调用函数指针。
  • 功能:
  1. 将返回地址存储在链接寄存器x30
  2. 跳转到被调用者的入口地址。

返回指令

  • 指令
  1. ret 不区分直接调用和间接调用。
  • 功能
  1. 跳转到返回地址X30

alt

2.5.4 多级函数调用

alt

  • [ ]一级: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
    指向栈顶(低地址)

alt

2.5.6 函数调用返回过程中栈的变化

alt

alt

2.5.7 帧指针FP:X29寄存器

  • 栈桢回溯
  • 栈桢大小不一
  • 如何找到上一个栈桢(如调试)
    (1) 保存x29(上一个栈桢的SP)
    (2) 将当前SP写入x29(让callee能保存)
    alt

2.5.8 函数的调用,返回与栈

alt

2.6 函数参数与返回值

2.6.1 寄存器传递数据

  1. x0-x7寄存器传递前8个参数
  2. x0作为返回值

alt

2.6.2 传递数据

  • 调用者压到栈上的数据
  • 第8个之后的参数
  • 按声明顺序从右到左
    Why? 因为参数的数量无法确定。而编译器读取参数时只是读取sp指针以上的内存。因此可以得到每个参数的地址。反之则会导致每个参数的内存地址无法确定。
  • 所有数据对齐到8字节
  • 被调用者通过SP+

alt

举例说明:

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, #48
a4: fd 7b 02 a9 stp x29, x30, [sp, #32]
a8: fd 83 00 91 add x29, sp, #32
ac: a0 83 1f f8 stur x0, [x29, #-8]
b0: e9 03 00 91 mov x9, sp
b4: 28 01 80 52 mov w8, #9
b8: 28 01 00 39 strb w8, [x9]
bc: 08 00 94 d2 mov x8, #40960
c0: 28 05 00 f9 str x8, [x9, #8]
c4: 20 00 80 d2 mov x0, #1
c8: 01 00 84 d2 mov x1, #8192
cc: 62 00 80 52 mov w2, #3
d0: 03 00 88 d2 mov x3, #16384
d4: a4 00 80 52 mov w4, #5
d8: 05 00 8c d2 mov x5, #24576
dc: e6 00 80 52 mov w6, #7
e0: 07 00 90 d2 mov x7, #32768
e4: 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, #48
f0: c0 03 5f d6 ret

2.7 寄存器保存

2.7.1 通用寄存器保存

  • 不同函数共享同一批通用寄存器
  • 因此能够通过寄存器传递参数和返回值
  • 然而,不同的函数对通用寄存器的使用会存在冲突 — 覆盖
  • 避免冲突的思路
  • 函数在使用某个寄存器之前保存该寄存器的值,返回前恢复
  • 保存在哪:函数栈桢中
  • 效率问题:有时候可能无需保存
    如:一个函数内不调用其他函数
    编译器会尽可能减少冗余保存的代码

2.7.2 寄存器使用约定

alt

  • 调用者保存的寄存器包括 X9~X15

  • 调用者在调用前按需(仅考虑自己是否需要)进行保存
    调用者在被调用者返回后恢复这些寄存器的值

  • 被调用者可以随意使用
    这些寄存器调用后的值可能发生改变

  • 被调用者保存的寄存器包括 X19~X28

  • 被调用者在使用前进行保存

  • 被调用者在返回前进行恢复

  • 调用者视角:这些寄存器的值在函数调用前后不会改变

alt

2.7.3 举例理解

0000000000000000 <square>:
0: 1b007c00 mul w0, w0, w0
4: d65f03c0 ret
0000000000000008 <cube>:
8: a9be7bfd stp x29, x30, [sp, #-32]! // 开辟栈帧,保留调用者的栈帧x29,保存返回地址x30
c: 910003fd mov x29, sp // 当前帧的栈顶地址写入x29
10: f9000bf3 str x19, [sp, #16] //被调用者使用前保存调用者或之前保留的数据
14: 2a0003f3 mov w19, w0 // 使用数据参数
18: 94000000 bl 0 <square>
1c: 1b137c00 mul w0, w0, w19
20: 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 小结

alt
alt
alt

posted @   木木ちゃん  阅读(156)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 【.NET】调用本地 Deepseek 模型
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
点击右上角即可分享
微信分享提示