想象你开了一家小型图书馆,每天要处理大量的“取书”和“还书”操作。当你需要看一本书时,会走到书架前(内存),把书拿下来放到自己的书桌(寄存器)上;当你读完时,再把书从书桌放回书架。这个“取”与“放”的动作,就是ARM处理器中最基础也最重要的两个指令——LDR(Load,加载)和STR(Store,存储)。 整个计算机世界就像这座图书馆:寄存器是离你最近的临时工作台,内存是巨大的书架阵列。CPU 的速度可比你从书架取书快多了——它每秒能执行几十亿次操作,但每次从内存取数据,都要经历“走出工作台→在书架间找到位置→取出→走回来”这一整套流程。LDR 和 STR 就是这套流程的标准化动作,而前变址、后变址、LDM/STM 这些扩展模式,就像你优化取书路线、一次搬多本书的聪明技巧。

岗位关联

理解 LDR/STR 是读懂 ARM 汇编的敲门砖。无论你是做嵌入式开发、系统底层优化,还是 AI 芯片的算子库调优,读写内存都是最频繁的操作。掌握它,你就掌握了程序运行的本质——数据在处理器和存储器之间的循环流动。

目录
  1. 可视化演示
  2. LDR加载指令
  3. STR存储指令
  4. 前变址模式
  5. 后变址模式
  6. LDM/STM批量
  7. 非对齐访问
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

\u901a\u8fc7\u4e09\u9636\u6bb5\u4ea4\u4e92\u5c55\u793aLDR/STR\u57fa\u672c\u6570\u636e\u6d41\u52a8\u3001\u4e09\u79cd\u5bfb\u5740\u6a21\u5f0f\u5bf9\u6bd4\u4e0eLDM/STM\u6279\u91cf\u5bc4\u5b58\u5668\u64cd\u4f5c\uff0c\u8986\u76d6\u5730\u5740\u8ba1\u7b97\u4e0e\u56de\u5199\u673a\u5236\u3002

本节配有交互动效(进入视口后自动加载,约 3KB)。

LDR/STR\u57fa\u672c\u64cd\u4f5c \u5185\u5b58\u52a0\u8f7d \u5bc4\u5b58\u5668\u5b58\u50a8 Rd\u2190Mem[Rn+off] \u5730\u5740\u7531\u57fa\u5740\u52a0\u504f\u79fb\u8ba1\u7b97

LDR加载指令

图书馆管理员小林接到了一个新的取书任务:第 3 号书架第 2 层有一本书需要拿到工作台上。他记下书的位置(地址),径直走向书架,取下书,放回工作台。这个过程就是 LDR 指令的“人生”——LDR(Load Register)把内存中的数据读入寄存器,方向只能用“内存 → 寄存器”一个箭头表示,就像取书的方向只能是“书架 → 工作台”一样。

在 ARM 汇编中,LDR 的典型写法是 LDR R0, [R1],意思是:把 R1 寄存器中存储的地址所指的内存单元里的数据,加载到 R0 寄存器中。R1 在这里扮演“索书单”的角色——它不直接装数据,而是装数据所在的位置(地址)。这种“寄存器指路,目标寄存器收数据”的设计,就是寻址的精髓。

LDR 还有几个变体:LDRB 只加载一个字节,LDRH 加载半个字(16 位),LDRSB 和 LDRSH 则会在加载后进行符号扩展。好比取书时你可以只取一章(一个字节)、半本(半字),或者根据需求决定是否把“欠费标记”(符号位)一并保留。

回到图书馆的比喻:LDR 这条指令的执行,就是小林拿着索书单(R_n),走到对应的书架格子,把书取出来放到工作台(R_d)上的过程。他永远不会反向操作——不会把工作台上的书放回书架,那是 STR 的工作。

LDR Rd, [Rn]\text{LDR}\ R_d,\ [R_n]
LDR 指令把内存中 R_n 指向的数据复制到 R_d。
  1. 目标寄存器R_d 是数据的目的地,好比工作台上放书的位置。
    Rd←Mem[Rn]R_d \leftarrow \text{Mem}[R_n]
  2. 源地址寄存器R_n 存的是内存地址,好比索书单上书架编号。
    addr=Rn\text{addr} = R_n
  3. 方向标记箭头指向 R_d,表示数据流动方向是内存到寄存器,反向则非法。
    Memory→Rd\text{Memory} \rightarrow R_d

STR存储指令

书看完了,工作台上堆满了书,小林要把其中一本放回书架。他拿起书,走到对应的书架位置,放了回去。这就是 STR(Store Register)的故事——STR 把寄存器中的数据存储到内存中,方向与 LDR 完全相反:“寄存器 → 内存”。

STR 的语法同样直观:STR R0, [R1],意思是将 R0 寄存器中的数据,存储到 R1 寄存器所指的内存地址中。R0 是“手中的书”,R1 是“索书单”。要特别注意的是,LDR 和 STR 的方向不能搞反——就像你不能把“从书架取书”说成“把书放回书架”,LDR 永远不会把数据从寄存器写到内存,STR 也永远不会把数据从内存读到寄存器。

STR 同样有字节和半字变体:STRB 存储一个字节,STRH 存储 16 位半字。此外,STR 在偏移量应用上非常灵活,可以用立即数偏移(如 STR R0, [R1, #4],往 R1 地址往后 4 个字节的位置存数据),也可以用寄存器偏移(如 STR R0, [R1, R2],往 R1 加上 R2 得到的地址处存数据),这好比把书放到“第 3 排书架往右数 2 格”的位置。

用图书馆的话说:STR 就是你手中的书(R_d)按照索书单(R_n),放在某个书架(地址 = R_n + 偏移量)上的过程。你永远不会让这个过程反向发生——从书架上“拿”书是 LDR 的活儿。

STR Rd, [Rn,#offset]\text{STR}\ R_d,\ [R_n, \#\text{offset}]
STR 把 R_d 的数据写入 R_n + offset 对应的内存单元。
  1. 数据源寄存器R_d 持有要存储的数据,是“书”。
    Mem[Rn+offset]←Rd\text{Mem}[R_n + \text{offset}] \leftarrow R_d
  2. 基址寄存器R_n 提供起始地址,是“索书单”的基础。
    base=Rn\text{base} = R_n
  3. 偏移量offset 可以是立即数或寄存器,表示在基址基础上偏移的位置。
    addr=Rn+offset\text{addr} = R_n + \text{offset}

前变址模式

小林发现,每天整理书架时,他要按照入库单的顺序,把一本本书放到连续的位置上:第 1 本书放 0 号位,第 2 本放 1 号位,第 3 本放 2 号位……如果每放一本书都要重新算一遍位置,效率太低了。于是他学了个聪明办法:先算出新位置,放好书,然后更新自己的索书单——把索引加 1,下次直接按新索书单找位置。这就是前变址(Pre-indexed)模式。

前变址的写法是 LDR R0, [R1, #4],含义是:先用 R1 的值加上 4 得到有效地址,再对这个地址进行访问。如果你希望访问完之后,R1 也跟着更新成这个新地址,就在后面加一个感叹号:LDR R0, [R1, #4]!。带 ! 的写法叫“前变址回写”,意思是“用新索书单替换旧索书单”。

前变址最典型的应用是数组遍历。假设你有一个整数数组,基址存在 R1 中,你想依次读取第 0、1、2 个元素。每次取完一个元素后,地址自动加 4(一个字的大小),R1 自动指向下一个元素。这样循环体里只需要一条 LDR 指令,简洁高效。

前变址模式就像小林“先走到新位置,再把书放下,同时更新索书单”。下次再操作时,他不需要重新计算,直接按照更新后的索书单就能找到正确的位置。这种模式在遍历数组、访问结构体字段时特别高效——一条指令同时完成了“找位置”和“更新索书单”两件事。

Rd←Mem[Rn+offset];Rn←Rn+offset (若加!)R_d \leftarrow \text{Mem}[R_n + \text{offset}];\quad R_n \leftarrow R_n + \text{offset}\ (\text{若加}!)
前变址先算新地址,再访问数据;加 ! 时回写基址寄存器。
  1. 计算有效地址基址 + 偏移,得到要访问的地址。
    addr=Rn+offset\text{addr} = R_n + \text{offset}
  2. 访问数据用上一步算出的地址访问内存。
    Rd←Mem[addr]R_d \leftarrow \text{Mem}[\text{addr}]
  3. 回写基址(可选)如果指令带 !,把新地址写回 R_n。
    Rn←addrR_n \leftarrow \text{addr}

后变址模式

有时候,小林的整理方式不一样:他先按手里的索书单把书放到当前位置,放完之后,再更新索书单指向下一个位置。这个顺序与前变址正好相反,叫做后变址(Post-indexed)模式。就好比你在一张便签上写下“当前放到第 3 格”,放好书之后,再把便签改成“第 4 格”。下一次按便签找的时候,已经是新的位置了。

后变址的写法是 LDR R0, [R1], #4,注意没有感叹号,但偏移量写在了方括号外面。含义是:先用 R1 的值作为有效地址访问内存,访问结束后 R1 自动加上 4。基址寄存器的更新是强制发生的,不需要 ! 来标记。

后变址的一个经典应用场景是栈操作。栈的本质就是一段连续内存,栈指针 SP(Stack Pointer)指向栈顶。压栈操作(相当于把数据存入栈)可以用 STR R0, [SP], #-4 来实现:先把 R0 的值存到当前 SP 指向的位置,然后 SP 自动减去 4,指向新的栈顶。出栈则用 LDR R0, [SP], #4:先从当前 SP 位置取数据,再把 SP 加上 4。

用图书馆比喻:后变址是“先按现在的索书单放好书,再算好下次的位置并更新索书单”。它与前变址不同——前变址是先更新索书单再找位置,后变址是先找位置放好书再更新索书单。这个顺序差异在栈操作中至关重要:栈指针必须“先用后移”或者“先移后用”,顺序搞反就会覆盖数据。

Rd←Mem[Rn];Rn←Rn+offsetR_d \leftarrow \text{Mem}[R_n];\quad R_n \leftarrow R_n + \text{offset}
后变址先访问数据,再更新基址寄存器。
  1. 访问当前地址直接使用 R_n 当前值作为地址访问内存。
    Rd←Mem[Rn]R_d \leftarrow \text{Mem}[R_n]
  2. 更新基址寄存器访问完成后,R_n 自动加上偏移量。
    Rn←Rn+offsetR_n \leftarrow R_n + \text{offset}

LDM/STM批量

小林收到一个紧急任务:要把一整排书架上的 10 本书全部搬到工作台上。如果他一本一本搬,效率太低。聪明的他找来了一辆小推车,一次把 10 本书全推过来——这就是 LDM/STM 批量传输指令的智慧。LDM(Load Multiple)一次从内存加载多个寄存器的值,STM(Store Multiple)一次把多个寄存器的值存储到内存。

LDM 的写法如 LDMIA R0!, {R1-R4},含义是:从 R0 指向的地址开始,连续读取 4 个字,分别存入 R1、R2、R3、R4。后面的 IA 表示 "Increment After",即每次访问后地址自动增加 4。R0 后的感叹号表示基址寄存器会被更新。STM 的写法类似:STMIA R0!, {R1-R4}。

批量传输最大的价值是现场保护。当发生中断或函数调用时,CPU 需要保存当前所有寄存器的值(好比离开图书馆前把桌面文件归档),以便处理完回来还能继续原来的工作。STM 一次就能把所有需要保存的寄存器推入栈中,LDM 再一次性恢复,大大减少了指令条数。

小推车的比喻完美诠释了 LDM/STM:单条 LDR/STR 就像一本一本地搬书,而 LDM/STM 则是一车把所有书推走。在中断处理和函数调用的场景中,寄存器里的数据就是“桌面上的重要文件”,必须快速归档(STM)并快速恢复(LDM),批量指令正是为此而生。

LDMIA Rn!, {Ra,Rb,…}\text{LDMIA}\ R_n!,\ \{R_{a}, R_{b}, \ldots\}
LDMIA 从 R_n 指向的连续内存中加载多个寄存器,并更新 R_n。
  1. 寄存器列表花括号内列出要加载/存储的寄存器。
    {Ra,Rb,…}\{R_{a}, R_{b}, \ldots\}
  2. 连续访问从基址开始逐字访问,每次地址加 4。
    addrk+1=addrk+4\text{addr}_{k+1} = \text{addr}_k + 4
  3. 基址更新带 ! 时,R_n 更新为最后访问的地址。
    Rn←addrlastR_n \leftarrow \text{addr}_{\text{last}}

非对齐访问

小林注意到一个奇怪的现象:大多数书架上的书都对齐摆放,每本书占一个格子,书从格子开头开始放。但偶尔有一本书放歪了,占了两个格子的边界。对于处理器来说,内存也有类似的“对齐”规则——如果一个 32 位字的地址是 4 的倍数(即地址最低 2 位为 0),就说它是对齐的;否则就是非对齐的。

在 ARM 架构中,早期的处理器严格禁止非对齐访问,一旦遇到非对齐地址,直接引发异常(类似图书馆管理员发现书放错位置,立即上报处理)。新一些的 ARM 处理器增加了非对齐访问支持:硬件自动把一次非对齐的字访问拆成两次对齐访问,对程序员透明——好比管理员自己悄悄把歪书扶正,不影响其他人。

但是,并非所有指令都支持非对齐访问。LDM、STM、LDRD、STRD 等指令要求地址必须对齐,如果地址非对齐,无论处理器是否支持非对齐访问,都会引发异常。此外,LDRH/STRH(半字访问)要求地址是 2 的倍数。这是因为批量传输的硬件逻辑太复杂,拆分会严重影响效率。

回到图书馆比喻:对齐的书整整齐齐排成一条线,管理员一眼就能找到;非对齐的书横跨格子边界,要么系统支持自动处理(硬件拆分成两次格子内的操作),要么就上报异常让上级解决。对于嵌入式开发者来说,尽量保证地址对齐是避免踩坑的最好策略——就像你总希望图书馆的书都放得整整齐齐。

addr mod 4=0(字对齐)\text{addr} \bmod 4 = 0 \quad (\text{字对齐})
字对齐要求地址是 4 的倍数,半字对齐要求是 2 的倍数。
  1. 对齐判定地址对字大小取模,结果为 0 则对齐。
    aligned=(addr mod 4=0)\text{aligned} = (\text{addr} \bmod 4 = 0)
  2. 硬件拆分非对齐访问被拆分为多次对齐访问。
    Memread(addr)→split into aligned accesses\text{Mem}_{\text{read}}(\text{addr}) \rightarrow \text{split into aligned accesses}

AI 应用场景

场景一:DMA 控制器中的描述符搬运

阶段:嵌入式系统底层驱动开发

解决的问题:DMA(直接内存访问)控制器的描述符表包含多个字段(源地址、目的地址、长度、控制标志等)。如果逐字段读取效率极低,还容易因非对齐访问触发异常。用 LDR/STR 配合 LDM/STM 可以一次将整个描述符载入或存储。

具体案例:在 AI 推理芯片的驱动代码中,CPU 需要将计算任务描述符写入内存,DMA 才能执行搬运。通过 STM 一条指令把描述符的 4 个字段一次性写入,再用 LDR 验证回读,确保数据正确。这种模式在 NVDLA 等 NPU 驱动中很常见。

场景二:AI 算子库中的张量数据搬运

阶段:AI算子计算加速

解决的问题:AI 模型计算涉及大量张量数据在内存与寄存器之间的搬运。在 ARM Cortex-A 系列处理器上编写 NEON 优化代码时,需要先用 LDR 把张量数据从内存加载到寄存器,计算完再用 STR 存回内存。理解 LDR/STR 的寻址模式,直接影响数据搬运效率。

具体案例:在实现一个 3×3 卷积算子时,需要反复加载图像像素块到寄存器。用前变址模式 LDR Q0, [R1, #16]! 可以一次加载 16 字节并自动更新基址,使得内层循环只有一条 LDM 指令就能完成数据加载,显著提升算子性能。

场景三:RTOS 上下文切换中的现场保护

阶段:实时操作系统任务调度

解决的问题:实时操作系统(RTOS)的多任务调度需要保存和恢复每个任务的寄存器现场。用 STM 一次保存所有寄存器到任务栈,用 LDM 从任务栈恢复,确保任务切换时数据不丢失。

具体案例:FreeRTOS 的上下文切换代码(PendSV_Handler)正是使用 STMDB SP!, {R4-R11} 保存任务现场,用 LDMIA SP!, {R4-R11} 恢复现场。这一对批量访问指令是整个 RTOS 任务切换的基础。

Python 演示

python104 行
"""模拟 ARM LDR/STR 指令行为演示前变址与后变址模式下寄存器和内存的变化""" class ARMSimulator:    def __init__(self):        # 内存用字典表示,键为地址        self.memory = {}        # 寄存器组,R0-R15        self.regs = [0] * 16        self.free_lists = {}     def ldr(self, rd, rn, offset=0, pre=False, writeback=False):        """LDR: 从内存加载到寄存器        pre=True时为前变址,pre=False时为后变址        writeback=True时更新基址寄存器"""        addr = self.regs[rn] + offset if pre else self.regs[rn]        self.regs[rd] = self.memory.get(addr, 0)        if pre and writeback:            self.regs[rn] = addr        elif not pre:            # 后变址强制更新基址            self.regs[rn] = self.regs[rn] + offset        return self.regs[rd]     def str(self, rd, rn, offset=0, pre=False, writeback=False):        """STR: 从寄存器存储到内存        pre=True时为前变址,pre=False时为后变址"""        addr = self.regs[rn] + offset if pre else self.regs[rn]        self.memory[addr] = self.regs[rd]        if pre and writeback:            self.regs[rn] = addr        elif not pre:            self.regs[rn] = self.regs[rn] + offset        return addr     def ldm(self, rn, reg_list, writeback=True):        """LDM: 批量加载多个寄存器        从R0指向的地址开始,连续读取reg_list中每个寄存器"""        addr = self.regs[rn]        for reg in reg_list:            self.regs[reg] = self.memory.get(addr, 0)            addr += 4        if writeback:            self.regs[rn] = addr        return self.regs[rn]     def stm(self, rn, reg_list, writeback=True):        """STM: 批量存储多个寄存器        将reg_list中每个寄存器的值连续写入内存"""        addr = self.regs[rn]        for reg in reg_list:            self.memory[addr] = self.regs[reg]            addr += 4        if writeback:            self.regs[rn] = addr        return self.regs[rn]  def main():    sim = ARMSimulator()    # 初始化内存:地址 100 处放值 42    sim.memory[100] = 42    sim.memory[104] = 7    sim.memory[108] = 99     # 1. 基本 LDR:R1 = 100,LDR R0, [R1]    sim.regs[1] = 100    val = sim.ldr(0, 1)    print("LDR R0, [R1]  -> R0 =", val)     # 2. STR:R0 = 42,STR R0, [R1]    sim.regs[0] = 42    sim.regs[1] = 200    addr = sim.str(0, 1)    print("STR R0, [R1]  -> 内存[", addr, "] =", sim.memory[200])     # 3. 前变址:LDR R0, [R1, #4]!    sim.regs[1] = 100    val = sim.ldr(0, 1, offset=4, pre=True, writeback=True)    print("LDR R0, [R1, #4]! -> R0 =", val, ", R1 =", sim.regs[1])     # 4. 后变址:LDR R0, [R1], #4    sim.regs[1] = 100    val = sim.ldr(0, 1, offset=4, pre=False)    print("LDR R0, [R1], #4 -> R0 =", val, ", R1 =", sim.regs[1])     # 5. STM 批量存储 + LDM 批量加载    sim.regs[0] = 10    sim.regs[1] = 20    sim.regs[2] = 30    base_addr = 300    sim.regs[3] = base_addr    sim.stm(3, [0, 1, 2])    print("STM R3!, {R0-R2} -> 内存[300]=10, [304]=20, [308]=30")    sim.regs[3] = base_addr    sim.regs[0] = sim.regs[1] = sim.regs[2] = 0    sim.ldm(3, [0, 1, 2])    print("LDM R3!, {R0-R2} -> R0 =", sim.regs[0], ", R1 =", sim.regs[1], ", R2 =", sim.regs[2])  if __name__ == "__main__":    main()
目的:用 Python 模拟 ARM LDR/STR 指令及前变址/后变址、LDM/STM 批量传输的行为,帮助理解汇编指令的底层语义。
  1. class ARMSimulator:建立指令模拟器类,内存用字典、寄存器用列表,相当于图书馆的管理系统。
  2. def ldr(self, rd, rn, offset=0, pre=False, writeback=False):模拟 LDR 指令,pre=True 时先算地址,反之先访问再更新基址。
  3. addr = self.regs[rn] + offset if pre else self.regs[rn]前变址用“基址+偏移”作地址,后变址直接用当前基址。
  4. self.regs[rd] = self.memory.get(addr, 0)从内存取数据存入目标寄存器,未初始化地址按 0 处理。
  5. def stm(self, rn, reg_list, writeback=True):批量存储,把多个寄存器依次写入连续内存。
  6. sim.ldr(0, 1, offset=4, pre=True, writeback=True)演示前变址回写:加载 R1+4 处数据后,R1 自动更新为 R1+4。
  7. sim.ldr(0, 1, offset=4, pre=False)演示后变址:先取 R1 处数据,再让 R1 加 4。

常见误区

误区我以为 LDR 和 STR 都是双向的,可以互相替换使用,反正都是内存和寄存器之间传数据。写代码时经常随手互换,出错了也不知道为什么。

正解LDR 的方向永远是“内存 → 寄存器”(取书到工作台),STR 的方向永远是“寄存器 → 内存”(把书放回书架)。单向性是硬件设计决定的,指令编码中已经写死了方向。你不可能让 LDR 把寄存器数据写到内存,就像你不可能让“取书”动作变成“放书”。

为何常见:我们平时编程用高级语言,赋值语句 a = b 看起来是双向的“把 b 给 a”,容易形成“数据搬运是双向”的直觉。但汇编指令是硬件的直接映射,方向是绝对的单向。

误区前变址和后变址的区别就是有没有感叹号,写的时候经常搞混。我以为加了感叹号就是后变址,不加就是前变址。

正解前变址([R1, #4])是“先算新地址再访问”,后变址([R1], #4)是“先访问再算新地址”。感叹号只表示“是否回写基址寄存器”,而非决定前/后变址。判断方法很简单:偏移量写在方括号内就是前变址,写在方括号外就是后变址。

为何常见:感叹号像“语法装饰品”,容易让人误以为是模式开关。实际上前变址也可以不带感叹号(访问但不回写),后变址的基址更新是强制的。关键在于偏移量的书写位置而非感叹号。

误区我认为 `LDM R0!, {R1-R4}` 和 `LDM R0!, {R4-R1}` 效果一样,反正都是把 R1 到 R4 四个寄存器加载一遍。

正解ARM 规定 LDM/STM 寄存器列表中的编号是按从低到高依次处理的,编号最低的寄存器对应最低的内存地址。写 {R4-R1} 并不会“倒序加载”,ARM 汇编器通常要求列表按升序书写,而且某些汇编器直接禁止倒序写法。这与栈操作中“后进先出”的顺序配合时非常容易出错。

为何常见:数学直觉告诉我们 {1,2,3,4} 和 {4,3,2,1} 的元素相同,但在汇编指令中,集合的书写顺序直接影响硬件行为——先处理编号小的寄存器。

误区既然新处理器支持非对齐访问,那我可以放心地在所有代码里使用非对齐地址,不用管什么对齐不对齐。

正解虽然较新的 ARM 处理器(ARMv7 及以上)支持非对齐访问,但 LDM、STM、LDRD、STRD 等批量指令仍然要求严格对齐。非对齐的 LDM 会直接引发异常。此外,某些 DMA 控制器和外设也要求对齐。代码在支持非对齐的 CPU 上正常,不代表换到别的平台也正常。

为何常见:理论课讲“处理器支持非对齐访问”时没强调例外情况,学习者容易把“部分支持”理解成“全部支持”。

误区`LDR R0, [R1, #4]` 我以为是从 R1 指向的位置往后数 4 个元素(如果数组是 int 类型,就是 4 个 int),但实际上……

正解ARM 汇编中偏移量的单位是字节,不是元素个数。如果 R1 指向一个 int 数组,想取下一个 int 元素,偏移量应写成 #4(4 字节),而不是 #1。这与 C 语言中 ptr + 1 自动按元素大小偏移的语义完全不同——汇编是裸的硬件视角,一切按字节算。

为何常见:C 语言的指针算术具有“类型感知”能力,p + 1 自动跳过一个元素。学习者从 C 转到汇编时,容易把这种类型感知的偏移习惯带过来。

面试题

Q1. 在 ARM 汇编中,LDR R0, [R1] 指令的含义是什么?

单选题 · easy

  • A.将 R0 的值存储到 R1 指向的内存
  • B.将 R1 指向的内存数据加载到 R0← 正确答案
  • C.将 R0 和 R1 的值相加
  • D.将 R1 的值存储到 R0 指向的内存

答案:B。LDR 是加载指令,方向是“内存 → 寄存器”。R1 提供内存地址,该地址处的数据被读入 R0,好比从书架上取书放到工作台。A 是 STR 的语义,C 是 ADD,D 方向完全反了。

Q2. 关于 STR 指令,下列说法正确的是?

单选题 · easy

  • A.STR 将内存数据加载到寄存器
  • B.STR 将寄存器数据存储到内存← 正确答案
  • C.STR 可以同时双向传输数据
  • D.STR 与 LDR 功能完全相同

答案:B。STR 的方向是“寄存器 → 内存”,把寄存器中的值写入指定地址,相当于把书从工作台放回书架。A 是 LDR 的功能,C 和 D 都错误,因为 LDR 和 STR 都是单向的且功能不同。

Q3. 指令 LDR R0, [R1, #4]! 执行后,R1 的值会怎样变化?

单选题 · mid

  • A.R1 保持不变
  • B.R1 更新为 R1 + 4← 正确答案
  • C.R1 更新为 R0 的值
  • D.R1 清零

答案:B。这是前变址回写模式。[R1, #4]! 先用 R1+4 作为有效地址访问,然后因为带有感叹号,R1 会被回写为新地址 R1+4,相当于“先算出新位置再更新索书单”。

Q4. 指令 LDR R0, [R1], #4 执行后,下列说法正确的是?

单选题 · mid

  • A.先访问 R1+4 地址,然后 R1 不变
  • B.先访问 R1+4 地址,然后 R1 更新为 R1+4
  • C.先访问 R1 地址,然后 R1 更新为 R1+4← 正确答案
  • D.先访问 R1 地址,然后 R1 不变

答案:C。这是后变址模式,偏移量在方括号外。先以 R1 当前值为地址访问内存,访问结束后 R1 自动加上偏移量 4。后变址的基址更新是强制的,不需要感叹号,好比“先按旧索书单取书,再更新索书单”。

Q5. 以下哪些指令属于 ARM 的批量数据传输指令?

多选题 · mid

  • A.LDMIA← 正确答案
  • B.STMDB← 正确答案
  • C.LDRH
  • D.STRB

答案:AB。LDM(Load Multiple)和 STM(Store Multiple)系列是批量指令,一次操作多个寄存器。LDMIA 和 STMDB 都是批量指令。LDRH 是半字加载、STRB 是字节存储,都是单寄存器指令。

Q6. ARM 中“字”(Word)的大小是多少字节?

单选题 · easy

  • A.1 字节
  • B.2 字节
  • C.4 字节← 正确答案
  • D.8 字节

答案:C。ARM 架构中一个字是 32 位,即 4 字节。字对齐要求地址是 4 的倍数,半字(Halfword)是 2 字节,要求地址是 2 的倍数。这解释了为什么访问 int 数组元素时偏移量要加 4。

Q7. 在 ARMv7 架构中,以下哪种情况一定会引发对齐异常?

单选题 · hard

  • A.LDR 访问非对齐地址
  • B.LDRH 访问奇数地址
  • C.LDM 访问非对齐地址← 正确答案
  • D.STRB 访问任意地址

答案:C。LDM/STM 等批量指令要求严格对齐,地址非对齐时必定引发异常。普通 LDR 在支持非对齐访问的处理器上可以正常执行(由硬件拆分),LDRH 要求地址为 2 的倍数,访问奇数地址也可能异常。STRB 只写一个字节,不存在对齐问题。

Q8. 关于 LDM/STM 指令的作用,下列说法正确的有?

多选题 · mid

  • A.一次操作可以加载或存储多个寄存器← 正确答案
  • B.常用于中断处理中的现场保护← 正确答案
  • C.只能操作 R0-R3 四个寄存器
  • D.访问的内存地址必须是连续的← 正确答案

答案:ABD。LDM/STM 从连续内存地址存取多个寄存器(A、D 正确),寄存器列表可以是 R0-R15 的任意组合。现场保护是典型应用(B 正确)。C 错误,寄存器列表不限于 R0-R3。

Q9. 执行 STMDB SP!, {R0-R3} 指令后,SP 的值如何变化?(假设 SP 初始指向地址 100)

单选题 · hard

  • A.SP 变为 100
  • B.SP 变为 116
  • C.SP 变为 84← 正确答案
  • D.SP 变为 96

答案:C。STMDB 是“Decrement Before“模式——先减地址再存储。4 个寄存器共 16 字节,SP 先减去 16 得到 84,然后从 84 开始依次存储 R0 到 R3,最终 SP 停在 84。这就是栈操作中”向下增长”的压栈行为。

Q10. 在 C 语言中访问 int arr[10],若 arr 的起始地址在 R0 中,要读取 arr[2],应该使用哪条指令?

单选题 · mid

  • A.LDR R1, [R0, #2]
  • B.LDR R1, [R0, #8]← 正确答案
  • C.LDR R1, [R0, #4]
  • D.LDR R1, [R0], #2

答案:B。int 在 ARM 中是 4 字节,arr[2] 距起始地址偏移 2×4=8 字节。前变址格式 [R0, #8] 计算 R0+8 作为有效地址,加载该处的值到 R1,相当于“跳过 2 个 int 再取第 3 个”。A 只偏移 2 字节,C 是 arr[1],D 是后变址且偏移量错误。