1. 为什么寻址方式值得单独拎出来讲
1.1 从一条指令的困惑说起
刚接触ARM汇编那会儿,我盯着一条指令看了半天:
LDR R0, [R1, #4]当时脑子里全是问号——这个方括号是什么意思?#4是加到R1上还是加到R0上?为什么有时候写[R1],有时候写[R1, #4],还有时候写[R1, #4]!?后来才明白,这些方括号里的花样,就是ARM的寻址方式。说白了,寻址方式就是告诉CPU去哪里找操作数。你可以把它理解成快递地址的写法:有的写“某某路100号”,有的写“某某路100号3单元”,还有的写“某某路100号3单元,到了之后把门牌号改成101”。地址写法不同,快递员(CPU)跑的路和干的事就不一样。
ARM处理器之所以要设计这么多种寻址方式,核心原因就一个:在有限的指令编码空间里,尽可能高效地支持高级语言的各种数据访问模式。ARM指令是32位定长编码,不像x86那样指令长度可变,所以每条指令能塞进去的信息量是有限的。寻址方式的设计,本质上是在“指令编码复杂度”和“编程灵活性”之间找平衡。
1.2 九种寻址方式的分类逻辑
ARM的九种寻址方式,如果按“操作数从哪里来”来分,可以归成三大类:
- 立即寻址:操作数直接写在指令里,CPU不用去内存或寄存器找
- 寄存器寻址:操作数在寄存器里,CPU直接读寄存器
- 内存寻址:操作数在内存里,CPU需要根据地址去内存取。这一类下面又细分成七种,包括寄存器间接寻址、基址加偏移寻址、前索引寻址、后索引寻址、基址加变址寻址、多寄存器寻址、堆栈寻址
这九种寻址方式覆盖了从最简单的常量操作到复杂的数据块搬运的所有场景。你写C语言里的数组访问、指针解引用、结构体成员读取、函数调用时的参数传递,编译成ARM汇编之后,底层用的就是这些寻址方式的组合。
1.3 学寻址方式到底有什么用
有人可能会问:现在都用C、C++、Rust写代码了,谁还看汇编?学寻址方式是不是浪费时间?
我的看法是:你不一定要手写汇编,但一定要能看懂汇编。原因有三个:
第一,调试崩溃和性能瓶颈时,汇编是最后的真相。你写的高级代码经过编译器优化后,实际执行的指令序列可能和你想象的完全不一样。一个看似简单的数组遍历,编译器可能用了多寄存器寻址一次加载8个数据;一个你以为会走缓存的指针访问,编译器可能生成了带偏移的基址寻址。看不懂寻址方式,你就看不懂编译器到底干了什么。
第二,嵌入式开发中,启动代码和中断向量表必须用汇编写。这些代码里大量使用寄存器间接寻址和堆栈寻址。不理解这些寻址方式,启动流程就是一团黑。
第三,理解寻址方式能帮你写出对编译器更友好的代码。比如你知道ARM支持基址加偏移寻址,那么用结构体数组比用指针链表更容易生成高效指令;你知道多寄存器寻址一次能搬多个寄存器,那么把频繁访问的变量放在连续的寄存器里就有意义。
我个人的经验是:花一个下午把九种寻址方式彻底搞明白,后面看任何ARM汇编代码都不会再发怵。这个时间投入的回报率极高。
2. 九种寻址方式逐个拆解
2.1 立即寻址:操作数就写在指令里
立即寻址是最简单的一种。操作数直接以常量的形式出现在指令中,CPU从指令编码里就能拿到数据,不需要访问寄存器或内存。
MOV R0, #0x1234 ; 把十六进制数0x1234放入R0 ADD R1, R2, #100 ; R1 = R2 + 100这里的#0x1234和#100就是立即数。#是ARM汇编中立即数的前缀符号,用来和地址、寄存器区分开。
但ARM的立即寻址有一个非常重要的限制:并不是所有32位常量都能直接作为立即数。ARM指令是32位定长的,指令里要留出空间给操作码和寄存器编号,留给立即数的位数通常只有12位。为了扩大立即数的表示范围,ARM采用了一种“8位数值+4位循环右移”的编码方式。也就是说,一个合法的ARM立即数,必须能表示成“一个8位数字循环右移偶数位”的形式。
举个例子:
#0xFF是合法的,因为8位可以表示0xFF,移位0次#0x104是合法的,因为0x41循环右移30位可以得到0x104#0x101是不合法的,因为没有任何8位数字经过偶数位循环右移能得到0x101
那如果确实需要一个不合法的立即数怎么办?编译器通常有两种处理方式:一是把常量放在文字池(Literal Pool)里,然后用内存寻址去加载;二是用多条指令拼出来,比如先MOV低16位,再ORR高16位。
实操心得:手写汇编时如果遇到“immediate out of range”的报错,先别急着换指令,算一下这个数能不能表示成8位循环右移的形式。很多时候换个写法就能解决,比如用
MOV加ADD组合代替一个大的立即数。
2.2 寄存器寻址:操作数在寄存器里
寄存器寻址就是操作数存放在寄存器中,指令直接指定寄存器编号。
MOV R0, R1 ; R0 = R1 ADD R2, R3, R4 ; R2 = R3 + R4这种寻址方式执行速度最快,因为寄存器就在CPU内部,不需要访问内存。ARM有16个通用寄存器(R0-R15),其中R13通常用作堆栈指针SP,R14用作链接寄存器LR,R15是程序计数器PC。
寄存器寻址的一个关键点是寄存器编号的编码。在32位ARM指令中,每个寄存器编号占4位,所以一条指令最多能直接指定3个寄存器(两个源操作数加一个目标寄存器)。这也是为什么ARM指令通常采用三操作数格式:ADD Rd, Rn, Rm。
注意:R15(PC)虽然可以作为通用寄存器使用,但在现代ARM架构中,直接读写PC会导致流水线刷新,性能损失很大。除非你在写非常底层的跳转代码,否则不要用PC作为普通寄存器。
2.3 寄存器间接寻址:寄存器里存的是地址
寄存器间接寻址是指寄存器中存放的不是数据本身,而是数据在内存中的地址。指令通过这个地址去内存取数据。
LDR R0, [R1] ; R0 = *R1,把R1指向的内存内容加载到R0 STR R2, [R3] ; *R3 = R2,把R2的值存储到R3指向的内存方括号[]在ARM汇编中表示“内存访问”,里面的寄存器是地址寄存器。这种寻址方式等价于C语言里的指针解引用。
寄存器间接寻址的典型应用场景是遍历数组。假设R1指向数组首元素,每次循环后给R1加4(假设是32位整数数组),就能依次访问每个元素。
MOV R1, #0x1000 ; R1指向数组首地址 MOV R2, #0 ; 循环计数器 LOOP: LDR R0, [R1] ; 加载当前元素 ADD R0, R0, #1 ; 元素值加1 STR R0, [R1] ; 写回内存 ADD R1, R1, #4 ; 指针后移4字节 ADD R2, R2, #1 ; 计数器加1 CMP R2, #10 ; 比较是否到10 BNE LOOP ; 没到就继续这段代码就是典型的“指针遍历”模式。寄存器间接寻址的优点是灵活,地址可以在运行时动态计算;缺点是每次访问都要先读寄存器拿地址,再访问内存,比寄存器寻址多一步。
2.4 基址加偏移寻址:最常用的内存访问方式
基址加偏移寻址是在寄存器间接寻址的基础上,给地址加上一个偏移量。这个偏移量可以是立即数,也可以是另一个寄存器。
LDR R0, [R1, #4] ; R0 = *(R1 + 4) LDR R2, [R3, R4] ; R2 = *(R3 + R4) LDR R5, [R6, #-8] ; R5 = *(R6 - 8)这种寻址方式在访问结构体成员和数组元素时特别有用。比如一个结构体有多个字段,每个字段偏移固定,就可以用同一个基址寄存器加上不同的立即数偏移来访问。
; 假设R0指向一个结构体,字段偏移如下: ; +0: id (4字节) ; +4: value (4字节) ; +8: flag (4字节) LDR R1, [R0, #0] ; 读取id LDR R2, [R0, #4] ; 读取value LDR R3, [R0, #8] ; 读取flag基址加偏移寻址的偏移量范围是12位有符号立即数,也就是-4095到+4095。如果偏移超出这个范围,编译器会先把偏移加载到寄存器,然后用寄存器偏移的形式。
实操心得:写结构体时,把最常访问的字段放在偏移量小的位置,这样编译器可以用一条指令直接访问。如果字段偏移超过4095,就需要额外指令来构造地址,性能会下降。
2.5 前索引寻址:先算地址再访问,顺便更新基址
前索引寻址是基址加偏移寻址的变体,特点是在访问内存之前先更新基址寄存器。语法上是在方括号后面加一个感叹号!。
LDR R0, [R1, #4]! ; 先执行R1 = R1 + 4,然后R0 = *R1注意这里的执行顺序:CPU先计算R1 + 4,把这个结果写回R1,然后用新的R1值去访问内存。所以这条指令执行后,R1的值已经变了。
前索引寻址的典型应用是遍历数组时自动递增指针。对比一下用普通基址加偏移寻址遍历数组:
; 方式一:普通基址加偏移 MOV R1, #0x1000 MOV R2, #0 LOOP1: LDR R0, [R1, R2, LSL #2] ; 用R2作为索引 ADD R2, R2, #1 CMP R2, #10 BNE LOOP1 ; 方式二:前索引寻址 MOV R1, #0x1000 MOV R2, #0 LOOP2: LDR R0, [R1], #4 ; 这是后索引,下面会讲 ...前索引寻址把“更新指针”和“访问内存”合并成一条指令,减少了指令数量。在循环中使用前索引寻址,可以省掉单独的指针递增指令。
注意:前索引寻址的
!表示“写回基址寄存器”。如果基址寄存器是R15(PC),写回会导致跳转,这是非常危险的操作。另外,如果基址寄存器和目标寄存器相同,比如LDR R0, [R0, #4]!,执行后R0的值会被内存内容覆盖,基址更新就丢失了。这种写法虽然合法,但容易让人困惑,建议避免。
2.6 后索引寻址:先访问内存再更新基址
后索引寻址和前索引寻址正好相反:先访问内存,然后再更新基址寄存器。语法上偏移量写在方括号外面。
LDR R0, [R1], #4 ; 先执行R0 = *R1,然后R1 = R1 + 4执行顺序是:CPU先用R1的当前值访问内存,把结果加载到R0;然后计算R1 + 4,写回R1。所以这条指令执行后,R0拿到的是旧地址的内容,R1指向下一个位置。
后索引寻址在遍历数组时特别自然,因为它的语义就是“取当前值,然后移动到下一个”。用后索引寻址重写前面的数组遍历:
MOV R1, #0x1000 MOV R2, #0 LOOP: LDR R0, [R1], #4 ; 取当前元素,R1自动加4 ADD R0, R0, #1 ; 元素值加1 STR R0, [R1, #-4] ; 写回原位置(因为R1已经加了4) ADD R2, R2, #1 CMP R2, #10 BNE LOOP这里有个细节:因为后索引寻址在加载后已经把R1加了4,所以写回时要用[R1, #-4]来回到原来的位置。这是后索引寻址的一个小陷阱,不熟悉的话很容易写错。
实操心得:后索引寻址最适合“读-处理-写回”的模式,但写回时要注意地址已经偏移了。如果觉得容易搞混,可以在循环里用两个指针,一个读一个写,或者干脆用前索引寻址。
2.7 基址加变址寻址:偏移量来自另一个寄存器
基址加变址寻址是基址加偏移寻址的扩展,偏移量不是立即数,而是来自另一个寄存器。而且这个寄存器还可以在参与计算前进行移位操作。
LDR R0, [R1, R2] ; R0 = *(R1 + R2) LDR R3, [R4, R5, LSL #2] ; R3 = *(R4 + R5 * 4) LDR R6, [R7, R8, ASR #1] ; R6 = *(R7 + R8 >> 1)支持的移位操作包括:
LSL #n:逻辑左移n位,相当于乘以2的n次方LSR #n:逻辑右移n位ASR #n:算术右移n位,保留符号位ROR #n:循环右移n位RRX:带进位循环右移1位
基址加变址寻址最典型的应用是数组索引访问。在C语言中,array[i]编译成ARM汇编时,如果i是变量,通常就会生成LDR R0, [Rbase, Rindex, LSL #2]这样的指令。其中LSL #2是因为int类型占4字节,索引要乘以4才能得到字节偏移。
; 假设R0 = 数组首地址,R1 = 索引i LDR R2, [R0, R1, LSL #2] ; R2 = array[i]这种寻址方式把“索引乘以元素大小”和“基址加偏移”合并成一条指令,效率很高。如果元素大小不是2的幂次方,比如结构体大小是12字节,那就不能直接用移位,需要先用乘法指令算出偏移。
注意:基址加变址寻址的移位量是有限的,LSL和LSR支持0-31位,ASR支持1-32位,ROR支持1-31位。如果需要的移位量超出范围,就要分多条指令完成。
2.8 多寄存器寻址:一次搬多个寄存器
多寄存器寻址是ARM非常有特色的一种寻址方式,它允许一条指令同时加载或存储多个寄存器。语法上用花括号列出寄存器列表。
LDMIA R0!, {R1-R4} ; 从R0指向的内存连续加载4个字到R1-R4,R0自动更新 STMIA R5!, {R1-R4} ; 把R1-R4的值连续存储到R5指向的内存,R5自动更新LDM是Load Multiple,STM是Store Multiple。后面的IA表示Increase After,即每次传输后地址递增。ARM支持四种地址模式:
| 模式 | 含义 | 地址变化时机 |
|---|---|---|
| IA | Increment After | 每次传输后地址增加 |
| IB | Increment Before | 每次传输前地址增加 |
| DA | Decrement After | 每次传输后地址减少 |
| DB | Decrement Before | 每次传输前地址减少 |
多寄存器寻址最大的优势是高效的数据块搬运。比如保存和恢复寄存器现场:
; 保存现场 STMDB SP!, {R0-R12, LR} ; 把R0-R12和LR压入堆栈 ; 恢复现场 LDMIA SP!, {R0-R12, PC} ; 从堆栈恢复R0-R12和PC,实现返回这里STMDB SP!表示堆栈向下增长(Decrement Before),LDMIA SP!表示从堆栈弹出(Increment After)。这是ARM函数调用标准中保存和恢复寄存器的标准写法。
实操心得:多寄存器寻址的寄存器列表顺序不影响传输顺序,传输顺序永远是从低地址到高地址,对应寄存器编号从小到大。比如
{R3, R1, R5}和{R1, R3, R5}效果完全一样。但为了代码可读性,建议按编号从小到大写。
2.9 堆栈寻址:多寄存器寻址的特化
堆栈寻址其实是多寄存器寻址的一种特殊应用,专门用于堆栈操作。ARM的堆栈可以是满堆栈或空堆栈,可以是递增堆栈或递减堆栈,组合起来有四种模式:
| 模式 | 全称 | 含义 |
|---|---|---|
| FA | Full Ascending | 满递增:SP指向最后一个压入的元素,压栈时地址增加 |
| FD | Full Descending | 满递减:SP指向最后一个压入的元素,压栈时地址减少 |
| EA | Empty Ascending | 空递增:SP指向下一个空位,压栈时地址增加 |
| ED | Empty Descending | 空递减:SP指向下一个空位,压栈时地址减少 |
ARM架构默认使用**满递减(FD)**堆栈,也就是STMDB SP!和LDMIA SP!的组合。在汇编中可以直接写PUSH和POP,它们会被汇编器翻译成对应的多寄存器指令。
PUSH {R0-R3} ; 等价于 STMDB SP!, {R0-R3} POP {R0-R3} ; 等价于 LDMIA SP!, {R0-R3}堆栈寻址在函数调用、中断处理、局部变量分配中无处不在。理解堆栈寻址的关键是搞清楚SP指向哪里以及地址变化方向。满递减堆栈的SP总是指向最后一个有效数据,压栈时先减SP再存数据,弹栈时先取数据再增SP。
注意:如果在一个工程中混用了不同的堆栈模式,会导致灾难性的后果。比如启动代码用FD模式,某个汇编函数用FA模式,堆栈就会错乱。所以一定要在工程初期就确定堆栈模式,并在所有汇编代码中保持一致。
3. 寻址方式在真实代码中的组合应用
3.1 函数调用的完整寻址过程
一个标准的ARM函数调用,从调用者到被调用者再到返回,涉及多种寻址方式的配合。下面用一个简单的例子来拆解:
; 调用者代码 MOV R0, #10 ; 立即寻址:第一个参数 MOV R1, #20 ; 立即寻址:第二个参数 BL add_func ; 跳转到add_func,LR保存返回地址 ; 被调用者代码 add_func: PUSH {R2, LR} ; 堆栈寻址:保存R2和返回地址 ADD R2, R0, R1 ; 寄存器寻址:R2 = R0 + R1 MOV R0, R2 ; 寄存器寻址:返回值放在R0 POP {R2, PC} ; 堆栈寻址:恢复R2,PC从堆栈恢复实现返回这个过程里:
- 参数传递用了立即寻址和寄存器寻址
- 保存现场用了堆栈寻址(多寄存器寻址的特化)
- 实际计算用了寄存器寻址
- 返回用了堆栈寻址,通过恢复PC实现跳转
3.2 数组遍历的多种寻址方式对比
同一个数组遍历,可以用不同的寻址方式实现,性能和代码量各有差异。假设要计算一个长度为N的整数数组之和:
; 方式一:基址加变址寻址 MOV R0, #0 ; 累加和 MOV R1, #0 ; 索引 MOV R2, #N ; 数组长度 LDR R3, =array ; 数组首地址 LOOP1: LDR R4, [R3, R1, LSL #2] ; 基址加变址 ADD R0, R0, R4 ADD R1, R1, #1 CMP R1, R2 BNE LOOP1 ; 方式二:后索引寻址 MOV R0, #0 MOV R1, #N LDR R3, =array LOOP2: LDR R4, [R3], #4 ; 后索引,自动递增 ADD R0, R0, R4 SUBS R1, R1, #1 BNE LOOP2 ; 方式三:多寄存器寻址(每次处理4个) MOV R0, #0 MOV R1, #N LDR R3, =array LOOP3: LDMIA R3!, {R4-R7} ; 一次加载4个 ADD R0, R0, R4 ADD R0, R0, R5 ADD R0, R0, R6 ADD R0, R0, R7 SUBS R1, R1, #4 BNE LOOP3方式一最直观,但每次循环要额外维护索引寄存器;方式二省掉了索引寄存器,但写回时要注意地址偏移;方式三效率最高,但要求数组长度是4的倍数,且需要更多寄存器。
实操心得:在性能敏感的代码中,如果数组长度较大且对齐,优先考虑多寄存器寻址。实测下来,多寄存器寻址比单寄存器加载能减少30%以上的循环开销,因为指令数量少了,流水线利用率更高。
3.3 结构体访问中的偏移计算
结构体访问是基址加偏移寻址的典型场景。假设有这样一个结构体:
struct student { int id; // 偏移0 int score; // 偏移4 char name[20]; // 偏移8 int age; // 偏移28 };访问不同字段时,编译器生成的指令不同:
; R0指向student结构体 LDR R1, [R0, #0] ; 读取id,偏移0 LDR R2, [R0, #4] ; 读取score,偏移4 ADD R3, R0, #8 ; name数组首地址,偏移8 LDR R4, [R0, #28] ; 读取age,偏移28注意name是字符数组,访问时通常用ADD算出首地址,然后用字节加载指令LDRB逐个读取。如果偏移超过4095,比如一个很大的结构体,编译器会先把偏移加载到寄存器,然后用寄存器偏移形式。
注意:结构体的字段顺序会影响访问效率。把最常访问的字段放在前面(偏移小),可以用一条指令直接访问;把不常访问的大字段放在后面,即使偏移超出立即数范围,影响也小。
4. 常见问题与排查技巧实录
4.1 立即数非法报错怎么排查
问题现象:汇编时报错“immediate out of range”或“invalid constant”。
排查思路:
- 把立即数转成二进制,看是否能表示成8位有效位加偶数位循环右移
- 如果不能,考虑用
MOV加ORR组合,或者把常量放到文字池 - 检查是否可以用移位操作代替乘法,比如
#1024可以写成#1, LSL #10
速查表:
| 立即数 | 是否合法 | 原因 |
|---|---|---|
| 0xFF | 合法 | 8位可表示 |
| 0x104 | 合法 | 0x41循环右移30位 |
| 0x101 | 非法 | 无法用8位循环右移表示 |
| 0xFFFFFFFF | 非法 | 超出12位编码范围 |
| 0x80000000 | 合法 | 0x2循环右移2位 |
4.2 前索引和后索引写反了怎么办
问题现象:数组遍历结果不对,或者指针越界。
排查思路:
- 检查方括号的位置:
[R1, #4]!是前索引,[R1], #4是后索引 - 前索引是“先更新地址再访问”,后索引是“先访问再更新地址”
- 如果写回时地址不对,检查是否因为后索引已经偏移了地址
避坑技巧:在循环中同时有读和写时,建议用前索引寻址,因为地址更新发生在访问之前,写回时用同一个基址寄存器加偏移即可。后索引寻址更适合只读遍历。
4.3 多寄存器寻址的寄存器列表顺序问题
问题现象:保存和恢复的寄存器值对不上。
排查思路:
- 多寄存器寻址的传输顺序永远是从低地址到高地址,对应寄存器编号从小到大
- 寄存器列表的书写顺序不影响传输顺序
- 如果保存和恢复用了不同的寄存器列表,检查列表内容是否一致
速查表:
| 保存指令 | 恢复指令 | 是否正确 |
|---|---|---|
| STMDB SP!, {R0-R3, LR} | LDMIA SP!, {R0-R3, PC} | 正确 |
| STMDB SP!, {R0-R3, LR} | LDMIA SP!, {R0-R3, LR} | 错误,LR不会恢复PC |
| STMDB SP!, {R3, R1, R2} | LDMIA SP!, {R1-R3} | 正确,顺序不影响 |
4.4 堆栈模式不匹配导致崩溃
问题现象:函数调用后程序跑飞,或者中断处理返回后死机。
排查思路:
- 检查所有汇编代码中的堆栈操作是否使用同一种模式
- 检查C编译器的堆栈模式设置和汇编代码是否一致
- 用调试器观察SP的变化,确认压栈和弹栈的地址变化方向一致
避坑技巧:在工程初期就确定堆栈模式(通常用FD满递减),并在链接脚本和编译选项中明确指定。如果必须混用,在汇编代码中用注释标明当前使用的模式。
4.5 基址加变址寻址的移位量超范围
问题现象:汇编报错“shift amount out of range”。
排查思路:
- LSL和LSR支持0-31位,ASR支持1-32位,ROR支持1-31位
- 如果需要的移位量超出范围,先用
MOV指令单独移位,再用寄存器偏移 - 检查是否可以用乘法指令代替移位
速查表:
| 移位类型 | 合法范围 | 超范围处理 |
|---|---|---|
| LSL | 0-31 | 用MOV单独移位 |
| LSR | 0-31 | 用MOV单独移位 |
| ASR | 1-32 | 用MOV单独移位 |
| ROR | 1-31 | 用MOV单独移位 |
5. 从寻址方式看ARM的设计哲学
5.1 定长指令下的灵活取舍
ARM指令是32位定长的,这意味着每条指令能携带的信息量是固定的。寻址方式的设计,本质上是在这个约束下做取舍。比如基址加偏移寻址的偏移量只有12位,这是为了给操作码和寄存器编号留出空间。如果偏移量需要更大,就要用寄存器偏移,多占一个寄存器编号。
这种取舍的结果是:ARM指令的编码效率很高,但单条指令的能力有限。复杂操作需要多条指令组合完成。这和x86的设计哲学正好相反——x86指令长度可变,单条指令能做很复杂的事,但编码效率低,译码复杂。
5.2 寻址方式对编译器的影响
编译器在生成ARM汇编时,会尽量利用寻址方式的特性来减少指令数量。比如:
- 数组索引访问优先用基址加变址寻址,把索引乘以元素大小合并到移位操作里
- 结构体成员访问优先用基址加偏移寻址,把字段偏移直接编码到指令里
- 函数调用保存现场优先用多寄存器寻址,一次保存多个寄存器
理解这些模式,就能理解编译器为什么生成某些指令序列。反过来,写代码时如果顺着这些模式来,编译器就能生成更高效的代码。
5.3 寻址方式与性能的关系
不同寻址方式的性能差异主要来自两个方面:指令数量和内存访问次数。
- 立即寻址和寄存器寻址不需要访问内存,速度最快
- 寄存器间接寻址和基址加偏移寻址需要访问内存,但地址计算在指令内部完成,不需要额外指令
- 多寄存器寻址一次访问多个内存位置,虽然单次访问慢,但总体指令数量少,适合批量操作
在性能敏感的代码中,减少内存访问次数和指令数量是优化的关键。选择合适的寻址方式,往往比微调算法更有效。
我个人的体会是:寻址方式的选择不是孤立的,它和寄存器分配、指令调度、流水线优化紧密相关。真正写好ARM汇编,需要把这些因素放在一起考虑。但作为起点,先把九种寻址方式搞清楚,后面的路就好走多了。