面试-ARM架构与裸机.md 61 KB


title: 面试-ARM架构与裸机 tags: [面试, ARM, Cortex-A7, 裸机开发, IMX6ULL, 汇编] created: 2026-09-17

updated: 2026-09-17

面试-ARM架构与裸机

本模块包含30道ARM架构与裸机编程面试精选题,覆盖Cortex-A7架构、寄存器组织、工作模式、汇编指令、中断异常和裸机开发六大核心领域。每题包含答案要点、详细解答和深入追问。


一、Cortex-A7架构

Q1: Cortex-A7 MPCore的主要架构特性有哪些?

答案要点

  1. ARMv7-A架构,支持1~4核配置
  2. 8级顺序流水线,支持Thumb-2指令集
  3. LPAE支持40位物理地址,最大1TB内存
  4. 硬件虚拟化、TrustZone安全扩展
  5. NEON SIMD协处理器和VFPv4浮点运算

详细解答

Cortex-A7是ARM推出的高能效应用处理器核心,主要特性包括:

特性 说明
架构版本 ARMv7-A
流水线 8级顺序执行
指令集 ARM/Thumb-2
L1 Cache 可配置8/16/32/64KB I-Cache + D-Cache
L2 Cache 可配置0/128/256/512/1024KB
LPAE 支持40位物理地址(1TB)
虚拟化 硬件虚拟化(可选)
安全 TrustZone安全扩展
SIMD NEON协处理器,加速多媒体和信号处理
浮点 VFPv4,支持IEEE754单双精度

IMX6ULL配置了32KB L1 I-Cache、32KB L1 D-Cache和128KB L2 Cache。Cortex-A7虽然定位低功耗,但性能强于Cortex-A8,28nm工艺下可达1.2~1.6GHz,典型功耗低于100mW。

追问

  • 追问1:Cortex-A7与Cortex-A9在架构上有什么本质区别?
    • A7采用顺序执行流水线,A9采用乱序执行流水线;A7功耗更低但单核性能略弱;A7支持LPAE而A9不支持。
  • 追问2:big.LITTLE架构中Cortex-A7扮演什么角色?
    • A7作为LITTLE核负责低功耗任务(日常应用、待机),A15/A53作为大核负责高性能任务(游戏、视频编解码),通过GIC和SCU实现动态任务调度。
  • 追问3:LPAE(Large Physical Address Extension)解决了什么问题?
    • 传统32位ARM最大寻址4GB,LPAE将物理地址扩展到40位,支持最大1TB物理内存,这对服务器和高性能嵌入式场景至关重要。

Q2: Cortex-A7的8级顺序流水线与Cortex-M系列的3级流水线有什么区别?

答案要点

  1. 8级流水线吞吐率更高,但级数多意味着分支代价更大
  2. 顺序执行意味着不支持乱序执行和推测执行
  3. Cortex-M的3级流水线(取指-译码-执行)更简单,延迟更低
  4. 8级流水线需要更复杂的分支预测来减少流水线清空

详细解答

流水线是将指令执行分成多个阶段以提高吞吐率的技术。Cortex-A7的8级流水线阶段为:

取指(Fetch) → 译码(Decode) → 发射(Issue) → 执行1(EX1) → 执行2(EX2) → 访存(Memory) → 写回1(WB1) → 写回2(WB2)

Cortex-M4的3级流水线为:

取指 → 译码 → 执行

关键区别:

对比维度 Cortex-A7 (8级) Cortex-M4 (3级)
吞吐率 更高(每周期可完成更多指令) 较低
分支代价 高(清空8级流水线) 低(仅清空3级)
执行模式 顺序执行 顺序执行
流水线冒险 需要更多forwarding逻辑 较少
功耗 相对较高 极低

ARM架构中PC总是指向当前执行指令+8字节(3级流水线)或+8/12字节(更深流水线),这是因为流水线预取机制导致的。

追问

  • 追问1:为什么Cortex-A7不采用乱序执行?这对性能有什么影响?
    • 乱序执行需要复杂的重命名逻辑和调度窗口,会显著增加面积和功耗。A7定位高能效,顺序执行足以满足大多数应用场景,配合分支预测器可以缓解流水线停顿。
  • 追问2:什么是流水线冒险?Cortex-A7如何处理数据冒险?
    • 流水线冒险包括结构冒险、数据冒险和控制冒险。数据冒险通过数据前递(forwarding)和暂停(stall)解决。A7内置数据前递网络,可以将EX阶段的结果直接传递到下一条指令的EX输入,避免等待WB。

Q3: IMX6ULL的完整启动过程是怎样的?每个阶段的作用是什么?

答案要点

  1. 上电复位 → Boot ROM执行基本硬件初始化
  2. Boot ROM根据BOOT_CFG引脚选择启动介质
  3. SPL(Secondary Program Loader)初始化DDR和串口
  4. U-Boot加载Linux Kernel
  5. Kernel挂载rootfs并启动init进程

详细解答

IMX6ULL的启动分为四个阶段:

上电复位 → Boot ROM → SPL → U-Boot → Linux Kernel → Rootfs

阶段1:Boot ROM

  • 片内ROM代码,上电后自动执行
  • 初始化基本硬件(时钟、看门狗)
  • 读取BOOT_CFG[1:0]引脚选择启动介质
  • BOOT_CFG=00: SD/EMMC, BOOT_CFG=10: USB串行下载

阶段2:SPL(Secondary Program Loader)

  • 从启动介质加载到IRAM(片内RAM)
  • 初始化DDR控制器
  • 初始化串口用于调试输出
  • 加载完整U-Boot到DDR

阶段3:U-Boot

  • 初始化更多外设(网络、存储等)
  • 从启动介质加载Linux Kernel到DDR
  • 传递启动参数(命令行、设备树地址)给Kernel

阶段4:Linux Kernel

  • 解压并运行内核
  • 解析设备树(DTB)
  • 挂载rootfs
  • 启动init进程(PID=1)

追问

  • 追问1:SPL为什么要在IRAM中运行而不是DDR?
    • 上电时DDR尚未初始化,IRAM是片内SRAM,不需要初始化即可访问。SPL的职责之一就是初始化DDR,所以在SPL运行阶段DDR还不可用。
  • 追问2:Boot ROM如何保证启动安全?
    • Boot ROM支持HAB(High Assurance Boot)安全启动,通过数字签名验证加载的镜像是否经过厂商授权,防止恶意代码在启动阶段执行。
  • 追问3:什么是启动介质?IMX6ULL支持哪些?
    • 启动介质是存储启动代码的外部存储器,IMX6ULL支持SD卡、eMMC、NAND Flash、QSPI NOR Flash等。选择由BOOT_CFG引脚和eFUSE共同决定。

Q4: Cortex-A7与Cortex-M4在架构层面有哪些核心区别?

答案要点

  1. 处理器模式:A7有9种模式,M4只有2种(Thread/Handler)
  2. 中断控制器:A7使用GIC,M4使用NVIC
  3. 内存管理:A7支持MMU,M4只有MPU
  4. 指令集:A7支持ARM/Thumb-2,M4仅Thumb-2
  5. 应用场景:A7用于Linux应用处理器,M4用于RTOS实时控制

详细解答

特性 Cortex-A7 Cortex-M4
架构版本 ARMv7-A ARMv7-M
处理器模式 9种(USR/FIQ/IRQ/SVC/ABT/UND/SYS/MON/HYP) 2种(Thread/Handler)
寄存器数量 34个通用 + 8个状态 R0-R15 + xPSR
中断控制器 GIC(最多1024中断) NVIC(最多240中断)
内存管理 MMU(虚拟内存) MPU(内存保护)
指令集 ARM + Thumb-2 Thumb-2
流水线 8级顺序 3级顺序
典型应用 Linux/Android FreeRTOS/裸机

Cortex-A7的MMU支持虚拟地址到物理地址的映射,这是运行Linux等多任务操作系统的基础。M4的MPU只能进行内存区域保护,不支持地址映射。

追问

  • 追问1:为什么Cortex-M4不需要MMU?
    • M4面向实时控制场景,通常运行RTOS或裸机程序,这些系统使用物理地址直接访问内存,不需要虚拟内存管理。MMU会增加中断延迟,不适合实时场景。
  • 追问2:GIC和NVIC在中断响应速度上有什么差异?
    • NVIC是内核集成的,中断响应延迟固定(12周期);GIC是外挂的,需要通过总线访问,延迟更高但中断数量更多。NVIC更适合对延迟敏感的实时系统。

Q5: IMX6ULL的外设地址映射是怎样的?为什么要了解地址映射?

答案要点

  1. 每个外设都有固定的物理基地址
  2. 寄存器通过基地址+偏移访问
  3. 汇编/裸机开发需要直接操作物理地址
  4. Linux驱动中需要通过ioremap映射为虚拟地址

详细解答

IMX6ULL主要外设地址映射:

// 时钟控制器
#define CCM_BASE           0x020C4000

// GPIO控制器
#define GPIO1_BASE         0x0209C000
#define GPIO2_BASE         0x020A0000
#define GPIO3_BASE         0x020A4000

// IOMUX控制器
#define IOMUXC_BASE        0x020E0000

// UART串口
#define UART1_BASE         0x02020000

// I2C
#define I2C1_BASE          0x021A0000

在裸机开发中,配置GPIO1_IO03为输出并点亮LED需要:

ldr r0, =0X020E0068    @ IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03地址
ldr r1, =0X5           @ MUX_MODE=5 (GPIO模式)
str r1, [r0]           @ 写入配置

ldr r0, =0X0209C004    @ GPIO1_GDIR地址
ldr r1, =0X0000008     @ bit3=1,设置为输出
str r1, [r0]

追问

  • 追问1:为什么裸机开发用物理地址,Linux驱动用虚拟地址?
    • 裸机没有MMU,CPU直接访问物理地址。Linux开启MMU后,所有地址都是虚拟地址,需要通过ioremap将物理地址映射为虚拟地址后才能访问。
  • 追问2:如何查找IMX6ULL的外设地址?
    • 查阅《IMX6ULL参考手册》的Chapter 2(Address Map),或查看NXP官方SDK中的寄存器定义头文件。

二、ARM寄存器

Q6: Cortex-A7的R0~R15寄存器分别有什么用途?

答案要点

  1. R0-R7:未备份寄存器,所有模式共享同一物理寄存器
  2. R8-R12:备份寄存器,FIQ模式有独立物理寄存器
  3. R13(SP):栈指针,每种模式有独立的SP
  4. R14(LR):链接寄存器,保存函数返回地址
  5. R15(PC):程序计数器,指向当前指令+8字节

详细解答

ARM架构提供16个32位通用寄存器(R0-R15):

R0-R7   : 未备份寄存器(Low Registers)
R8-R12  : 备份寄存器(High Registers)
R13(SP) : 栈指针
R14(LR) : 链接寄存器
R15(PC) : 程序计数器

R0-R7在所有模式下共享同一物理寄存器,这意味着模式切换时这些寄存器的值可能被破坏。R8-R12在FIQ模式下有独立的物理寄存器(R8_fiq-R12_fiq),其他模式共享。

R13(SP)有8个物理寄存器:User/Sys共用1个,其他7种模式各1个。每种模式的SP指向各自的栈空间,初始化SP就是设置每种模式的栈顶地址。

R14(LR)在函数调用时保存返回地址(BL指令自动写入LR),在异常发生时保存异常返回地址。

R15(PC)的值总是当前执行指令地址+8字节(ARM状态),这是因为流水线预取机制。

追问

  • 追问1:为什么FIQ模式要为R8-R12提供独立寄存器?
    • FIQ要求快速响应,独立寄存器意味着中断处理程序不需要保存/恢复R8-R12,减少压栈操作,加速中断处理。
  • 追问2:PC+8字节是怎么来的?
    • ARM的3级流水线(取指-译码-执行),当执行第N条指令时,第N+1条正在译码,第N+2条正在取指。PC指向取指地址,即当前指令地址+2条指令×4字节=+8字节。

Q7: CPSR寄存器的各位含义是什么?如何通过CPSR切换处理器模式?

答案要点

  1. N/Z/C/V:条件标志位,用于条件跳转
  2. I/F:IRQ/FIQ中断禁止位
  3. T:ARM/Thumb指令集切换
  4. M[4:0]:处理器模式控制位(5位编码)
  5. SPSR是CPSR的备份,异常退出时恢复CPSR

详细解答

CPSR(Current Program Status Register)结构:

31 30 29 28  27  26:25  24  23:20  19:16  15:10  9  8  7  6  5  4:0
 N  Z  C  V   Q  IT[1:0] J  reserved GE[3:0] IT[7:2] E  A  I  F  T  M[4:0]

关键位说明:

名称 功能
N(31) 负数 有符号运算结果为负时置1
Z(30) 运算结果为零时置1
C(29) 进位 无符号运算产生进位/借位时置1
V(28) 溢出 有符号运算产生溢出时置1
I(7) IRQ禁止 I=1禁止IRQ
F(6) FIQ禁止 F=1禁止FIQ
T(5) Thumb T=1处于Thumb状态
M[4:0] 模式 处理器模式编码

模式编码:

M[4:0] 模式
10000 User
10001 FIQ
10010 IRQ
10011 SVC
10111 Abort
11011 Undef
11111 System

切换处理器模式的汇编示例:

mrs r0, cpsr          @ 读取CPSR
bic r0, r0, #0x1f     @ 清除M[4:0]位
orr r0, r0, #0x13     @ 设置为SVC模式(10011)
msr cpsr, r0          @ 写入CPSR

追问

  • 追问1:为什么User模式不能直接切换到其他模式?
    • User是非特权模式,不能修改CPSR的M位。必须通过异常(中断/软件中断/SVC)进入特权模式,这是ARM安全模型的基础。
  • 追问2:SPSR和CPSR有什么关系?
    • SPSR是CPSR的备份寄存器。当异常发生时,硬件自动将当前CPSR保存到对应模式的SPSR中;异常退出时,通过MOVS PC, LR指令自动从SPSR恢复CPSR。

Q8: 什么是备份寄存器?R13(SP)和R14(LR)为什么需要每种模式独立一套?

答案要点

  1. 备份寄存器在不同模式下有独立的物理寄存器
  2. R13(SP)独立是为了每种模式有独立的栈空间
  3. R14(LR)独立是为了保存各自的返回地址
  4. 模式切换时自动使用对应模式的寄存器,保护用户模式数据不被破坏

详细解答

Cortex-A7的寄存器组织分为两类:

未备份寄存器(R0-R7):所有模式共享同一物理寄存器。模式切换时这些寄存器的值会被破坏,因此中断处理程序必须先保存这些寄存器(现场保护)。

备份寄存器(R8-R14):在不同模式下有不同的物理寄存器:

寄存器 User/Sys FIQ IRQ SVC ABT UND
R8-R12 共享 R8-R12_fiq 共享 共享 共享 共享
R13(SP) SP SP_fiq SP_irq SP_svc SP_abt SP_und
R14(LR) LR LR_fiq LR_irq LR_svc LR_abt LR_und

为什么R13需要独立: 每种模式有自己的栈空间。例如IRQ发生时,CPU自动切换到IRQ模式并使用SP_irq,不会破坏User模式的SP。中断处理程序在自己的栈上保存现场,返回时恢复User的SP。

为什么R14需要独立: BL指令将返回地址写入LR。如果在IRQ处理中调用了子函数(BL),LR_irq会保存子函数返回地址。如果LR不独立,IRQ模式的LR会覆盖User模式的LR,导致User模式的函数返回地址丢失。

追问

  • 追问1:为什么FIQ模式的R8-R12也是独立的?
    • FIQ设计目标是极速响应。独立的R8-R12意味着FIQ处理程序可以直接使用这些寄存器,不需要执行PUSH/POP保存恢复,减少中断延迟。
  • 追问2:如果不用独立栈会怎样?
    • 所有模式共用一个栈会导致中断嵌套时栈溢出或数据覆盖。例如IRQ中断了SVC模式的函数调用,如果共用栈,IRQ的压栈会覆盖SVC函数的局部变量。

Q9: R15(PC)为什么总是指向当前指令+8字节?这对编程有什么影响?

答案要点

  1. ARM的3级流水线导致PC预取
  2. 当执行第N条指令时,PC指向第N+2条指令
  3. ARM状态每条指令4字节,所以PC=当前地址+8
  4. Thumb状态每条指令2字节,PC=当前地址+4

详细解答

ARM处理器采用3级流水线:取指(Fetch)→ 译码(Decode)→ 执行(Execute)。

时刻T1: 取指指令1  译码-    执行-
时刻T2: 取指指令2  译码指令1  执行-
时刻T3: 取指指令3  译码指令2  执行指令1

当指令1正在执行时,指令2正在译码,指令3正在取指。PC寄存器存储的是当前取指的地址,即指令3的地址。由于指令1和指令3之间差2条指令,每条指令4字节,所以:

PC = 当前执行指令地址 + 2 × 4 = 当前地址 + 8

编程影响:

  • 读取PC值时要意识到它不是当前指令的地址
  • 跳转时写入PC的值会跳转到目标地址+8的位置(因此B指令的目标编码是相对偏移)
  • 在汇编中计算地址偏移时要考虑这个偏移量

    ldr r0, =loop      @ r0中保存的是loop标号的实际地址
    ldr pc, r0         @ 跳转到loop,但实际执行的是loop+8处的指令
    

追问

  • 追问1:Cortex-A7的8级流水线中PC偏移是多少?
    • 虽然A7是8级流水线,但架构上仍然保持PC=当前地址+8的约定(ARM状态)。这是为了向后兼容,软件不需要感知底层流水线深度。
  • 追问2:Thumb状态下的PC偏移为什么是+4?
    • Thumb指令是16位(2字节)编码,3级流水线下PC指向当前+2条指令=当前+4字节。Thumb-2混合了16位和32位指令,但架构上PC偏移保持+4。

Q10: SPSR寄存器的作用是什么?异常返回时如何恢复CPSR?

答案要点

  1. SPSR是CPSR的备份寄存器,异常发生时自动保存CPSR
  2. User和Sys模式没有SPSR(非异常模式)
  3. 异常返回时通过MOVS PC, LRLDMFD SP!, {PC}^恢复
  4. 恢复CPSR的同时也会恢复中断禁止位和处理器模式

详细解答

SPSR(Saved Program Status Register)结构与CPSR完全相同。每种异常模式(FIQ/IRQ/SVC/ABT/UND/MON/HYP)都有自己的SPSR。

异常发生时的自动操作

  1. 当前CPSR的值被保存到对应模式的SPSR中
  2. CPSR的M[4:0]被设置为目标模式
  3. CPSR的I/F位可能被设置(禁止中断)
  4. PC的值(返回地址)被保存到对应模式的LR中

异常返回时的恢复

@ 方法1:MOVS指令(自动从SPSR恢复CPSR)
MOVS PC, LR

@ 方法2:LDMFD出栈恢复(^表示恢复CPSR)
LDMFD SP!, {PC}^

@ 方法3:手动恢复
MRS R0, SPSR      @ 读取SPSR
MSR CPSR, R0      @ 恢复CPSR
BX LR             @ 跳转回被中断的代码

MOVS PC, LR指令的特殊行为:不仅将LR的值写入PC(实现跳转),还会自动将当前模式的SPSR复制到CPSR(恢复处理器状态)。这是ARM架构的关键设计。

追问

  • 追问1:为什么User和Sys模式没有SPSR?
    • User和Sys不是异常模式,不会被异常打断。它们是"被动"执行的模式,只有通过异常才能切换出去。因此不需要保存CPSR。
  • 追问2:如果在异常处理中又发生了嵌套异常,SPSR会怎样?
    • 新的异常会将当前异常的CPSR保存到新异常模式的SPSR中。返回时先恢复外层异常的CPSR,再恢复到最初的被中断代码。这就是异常嵌套的上下文保存机制。

三、ARM工作模式

Q11: Cortex-A7有哪9种处理器模式?各模式的用途是什么?

答案要点

  1. User(USR):非特权用户模式,大多数程序运行于此
  2. FIQ:快速中断模式,有专用寄存器加速中断处理
  3. IRQ:普通中断模式
  4. Supervisor(SVC):超级管理员模式,操作系统使用
  5. Abort(ABT):数据访问终止模式,处理内存异常
  6. Undef(UND):未定义指令终止模式
  7. System(SYS):特权级用户模式
  8. Monitor(MON):TrustZone安全扩展模式
  9. Hyp(HYP):虚拟化扩展模式

详细解答

模式 编码 特权级 用途 独立寄存器
User 10000 非特权 用户程序 SP, LR, CPSR
FIQ 10001 特权 快速中断 R8-R14, SPSR
IRQ 10010 特权 普通中断 SP, LR, SPSR
SVC 10011 特权 系统调用/复位 SP, LR, SPSR
ABT 10111 特权 内存访问异常 SP, LR, SPSR
UND 11011 特权 未定义指令 SP, LR, SPSR
SYS 11111 特权 特权级用户任务 共享User寄存器
MON 10110 特权 安全世界切换 SP, LR, SPSR
HYP 11010 特权 虚拟化管理 SP, LR, ELR_Hyp

模式切换规则:

  • 软件可以通过修改CPSR的M位任意切换(特权模式下)
  • 异常发生时硬件自动切换到对应异常模式
  • User模式不能直接切换到其他模式,必须通过异常

追问

  • 追问1:System模式和User模式有什么区别?
    • 两者共享相同的寄存器(R0-R15, CPSR),但System是特权模式。System模式用于操作系统执行特权任务(如OS任务调度),同时复用User模式的寄存器,避免不必要的寄存器备份。
  • 追问2:Monitor模式在TrustZone中的作用是什么?
    • ARM TrustZone将世界分为Secure World和Normal World。Monitor模式是切换两个世界的枢纽,通过SMC(Secure Monitor Call)指令进入Monitor模式,保存当前世界状态后切换到另一个世界。

Q12: 什么是特权模式和非特权模式?特权模式有什么特权?

答案要点

  1. User是唯一的非特权模式,其他8种都是特权模式
  2. 特权模式可以访问系统所有资源
  3. 特权模式可以修改CPSR切换模式
  4. 非特权模式对某些寄存器和内存区域的访问受限

详细解答

ARM架构的安全模型基于特权级别:

级别 模式 可以做什么
非特权 User 执行普通程序,不能修改CPSR,不能访问受保护的内存区域
特权 其他8种 完全访问所有资源,修改CPSR,控制中断,切换模式

特权模式的具体特权:

  1. 修改CPSR:可以切换处理器模式、使能/禁止中断
  2. 访问所有内存:不受MPU/MMU限制(在特权级)
  3. 执行特权指令:如MRS/MSR访问CPSR/SPSR
  4. 控制协处理器:访问系统控制寄存器
  5. 切换安全世界:Monitor模式可以切换Secure/Normal World

操作系统内核运行在特权模式(通常是SVC模式),用户应用程序运行在User模式。当用户程序需要系统服务时,通过SVC指令(软件中断)切换到SVC模式执行系统调用。

追问

  • 追问1:Linux内核运行在什么模式?
    • Linux内核主要运行在SVC模式。中断处理进入IRQ模式,系统调用通过SVC异常进入SVC模式。内核代码运行在特权模式下,可以访问所有硬件资源。
  • 追问2:如果用户程序尝试修改CPSR会怎样?
    • CPU会触发Undefined Instruction异常,因为MRS/MSR指令在User模式下执行是未定义行为。这是ARM硬件保护机制的一部分。

Q13: 为什么每种异常模式都需要独立的栈空间?

答案要点

  1. 异常发生时CPU自动切换到对应模式
  2. 独立栈防止异常处理破坏被中断程序的栈数据
  3. 中断嵌套时每层异常需要独立的栈空间
  4. 不同异常可以有不同优先级的栈大小

详细解答

以IRQ中断为例说明为什么需要独立栈:

User模式运行 → SP=0x87F00000 (User栈)
    ↓
IRQ中断发生 → CPU自动切换到IRQ模式,使用SP_irq
    ↓
IRQ处理程序压栈 → 使用SP_irq指向的栈空间
    ↓
返回User模式 → SP恢复为0x87F00000

如果IRQ和User共用一个栈,会发生:

  1. IRQ处理程序的压栈操作会覆盖User栈的局部变量
  2. 如果发生中断嵌套,栈会混乱
  3. User模式的SP被破坏,函数返回时栈帧错误

独立栈的好处:

  • 每种异常有自己的栈空间,互不干扰
  • 可以根据需要分配不同大小的栈(如IRQ栈可以较小)
  • 中断嵌套时每层异常有自己的栈帧
  • 异常返回时SP自动恢复到异常前的值

初始化SP的示例代码(在start.S中):

_start:
    @ 设置SVC模式栈
    mrs r0, cpsr
    bic r0, r0, #0x1f
    orr r0, r0, #0x13     @ SVC模式
    msr cpsr, r0
    ldr sp, =0x87F00000   @ SVC栈顶

    @ 设置IRQ模式栈
    mrs r0, cpsr
    bic r0, r0, #0x1f
    orr r0, r0, #0x12     @ IRQ模式
    msr cpsr, r0
    ldr sp, =0x87D00000   @ IRQ栈顶

追问

  • 追问1:如何确定每种模式栈的大小?
    • 根据该模式下最坏情况的栈使用量。IRQ栈通常需要处理中断嵌套(如4层嵌套,每层保存17个寄存器×4字节=68字节,总计约272字节+局部变量)。SVC栈需要考虑系统调用深度。
  • 追问2:栈的增长方向是什么?
    • ARM架构使用满递减栈(Full Descending),即SP指向最后一个入栈的值,栈从高地址向低地址增长。PUSH指令先减SP再存数据,POP指令先读数据再加SP。

Q14: ARM的三种异常(IRQ/FIQ/SVC)在处理流程上有什么区别?

答案要点

  1. IRQ:可被FIQ打断,中断处理程序通常用汇编跳转到C函数
  2. FIQ:不可被IRQ打断(更快),有独立R8-R12,适合关键实时任务
  3. SVC:软件触发,用于系统调用,操作系统入口

详细解答

三种异常的处理流程对比:

特性 IRQ FIQ SVC
触发方式 外部中断引脚 外部快速中断引脚 SVC指令(软件中断)
可否被中断 可被FIQ打断 不可被其他中断打断 可被IRQ/FIQ打断
延迟 较高 极低
独有寄存器 SP, LR, SPSR R8-R14, SP, LR, SPSR SP, LR, SPSR
保存现场 需要保存R0-R12 仅需保存R0-R7 仅需保存R0-R3
典型用途 外设中断(GPIO/UART/Timer) 高速数据传输(DMA) 系统调用(OS API)

IRQ处理流程:

1. 保存R0-R12到IRQ栈
2. 读取GIC_IAR获取中断号
3. 切换到SVC模式(允许中断嵌套)
4. BL调用C语言中断处理函数
5. 切换回IRQ模式
6. 写GIC_EOIR确认中断
7. 恢复R0-R12
8. MOVS PC, LR返回

FIQ处理流程类似,但:

  • 不需要保存R8-R12(独立寄存器)
  • 不需要切换模式(FIQ本身就是最高优先级)
  • 处理程序可以直接写在异常向量表偏移0x1C处,减少一次跳转

追问

  • 追问1:为什么FIQ放在异常向量表的最后?
    • 异常向量表中每个异常占4字节(一条跳转指令),FIQ放在0x1C(最后)。如果FIQ处理程序较短,可以直接写在0x1C处,不需要跳转,节省一个取指周期。
  • 追问2:SVC和IRQ有什么本质区别?
    • SVC是同步异常(由指令触发),发生在指令执行阶段;IRQ是异步异常(由外部信号触发),可能在任何时刻发生。SVC用于主动请求操作系统服务,IRQ用于响应外部事件。

Q15: 什么是处理器模式切换?用户模式如何进入特权模式?

答案要点

  1. 模式切换通过修改CPSR的M[4:0]位实现
  2. 特权模式可以直接切换到任何模式
  3. User模式只能通过异常(SVC/IRQ/FIQ等)进入特权模式
  4. 异常返回时通过SPSR恢复原来的CPSR,自动回到之前的模式

详细解答

模式切换有两种方式:

方式1:软件切换(特权模式下)

mrs r0, cpsr
bic r0, r0, #0x1f     @ 清除模式位
orr r0, r0, #0x13     @ 设置SVC模式
msr cpsr, r0

方式2:异常切换(任何模式下)

User模式 → SVC指令异常 → 自动进入SVC模式
User模式 → IRQ中断 → 自动进入IRQ模式
SVC模式 → IRQ中断 → 自动进入IRQ模式

异常发生时的硬件自动操作:

  1. 将当前CPSR保存到新模式的SPSR
  2. 修改CPSR的M位为新模式
  3. 可能修改I/F位(禁止中断)
  4. 将返回地址保存到新模式的LR

异常返回时的硬件自动操作:

  1. 将SPSR的值恢复到CPSR
  2. PC的值恢复为LR中的返回地址

这就是ARM异常处理的核心机制:通过硬件自动保存/恢复CPSR,实现模式切换和返回的无缝衔接。

追问

  • 追问1:为什么不设计一条直接切换模式的指令?
    • 模式切换涉及安全问题。如果允许任意切换,恶意代码可以轻松获取特权。通过异常机制切换,操作系统可以在异常处理中进行权限检查,控制模式切换。
  • 追问2:Sys模式和SVC模式都可以运行OS任务,怎么选择?
    • Sys模式通常用于OS执行需要访问User模式寄存器的任务(如任务调度),SVC模式用于系统调用处理。Sys模式复用User寄存器,减少上下文切换开销。

四、ARM汇编指令

Q16: MOV、MRS、MSR三条数据传输指令有什么区别?分别用在什么场景?

答案要点

  1. MOV:寄存器间或立即数到寄存器的数据传输
  2. MRS:将特殊寄存器(CPSR/SPSR)读取到通用寄存器
  3. MSR:将通用寄存器写入特殊寄存器(CPSR/SPSR)
  4. 只有MRS/MSR可以访问CPSR/SPSR,MOV只能操作通用寄存器

详细解答

指令 语法 功能 场景
MOV MOV Rd, Rn Rd = Rn 寄存器间复制
MOV MOV Rd, #immed Rd = immediate 加载立即数
MRS MRS Rd, CPSR Rd = CPSR 读取处理器状态
MRS MRS Rd, SPSR Rd = SPSR 读取保存的状态
MSR MSR CPSR, Rd CPSR = Rd 修改处理器状态
MSR MSR SPSR, Rd SPSR = Rd 恢复保存的状态

使用示例:

@ MOV:数据传输
MOV R0, #10           @ R0 = 10
MOV R1, R0            @ R1 = R0

@ MRS/MSR:切换处理器模式
mrs r0, cpsr          @ 读取当前CPSR
bic r0, r0, #0x1f     @ 清除模式位[4:0]
orr r0, r0, #0x13     @ 设置SVC模式(10011)
msr cpsr, r0          @ 写入CPSR,完成模式切换

@ MSR:使能/禁止中断
mrs r0, cpsr
bic r0, r0, #0x80     @ 清除I位,使能IRQ
msr cpsr, r0

注意:MOV不能直接操作CPSR/SPSR,必须通过MRS/MSR。这是ARM架构的安全设计,防止普通指令意外修改处理器状态。

追问

  • 追问1:为什么不能用MOV直接写CPSR?
    • CPSR包含处理器模式、中断禁止位等关键控制信息。如果允许任意指令修改CPSR,会导致不可预测的行为。MRS/MSR是专门用于访问特殊寄存器的指令,程序员必须明确意图。
  • 追问2:LDR伪指令和MOV有什么区别?
    • MOV R0, #0xFF只能加载8位立即数(循环右移偶数位),LDR R0, =0x12345678可以加载任意32位常量。LDR伪指令会被汇编器转换为从文字池加载或MOV+ORR组合。

Q17: LDR和STR指令如何操作内存?字节、半字、字操作有什么区别?

答案要点

  1. LDR从内存加载数据到寄存器,STR从寄存器存储数据到内存
  2. ARM状态默认按字(32位)操作
  3. LDRB/STRB按字节(8位)操作,LDRH/STRH按半字(16位)操作
  4. 需要地址对齐,未对齐访问可能触发异常

详细解答

指令 操作大小 功能
LDR 32位(字) 从Rn+offset加载32位到Rd
LDRB 8位(字节) 从Rn+offset加载8位到Rd(高24位清零)
LDRH 16位(半字) 从Rn+offset加载16位到Rd(高16位清零)
STR 32位(字) 将Rd的32位存储到Rn+offset
STRB 8位(字节) 将Rd的低8位存储到Rn+offset
STRH 16位(半字) 将Rd的低16位存储到Rn+offset

使用示例:

@ 读取GPIO1_GDIR寄存器(地址0x0209C004)
ldr r0, =0x0209C004    @ r0 = 寄存器地址
ldr r1, [r0]           @ r1 = *0x0209C004 (32位)

@ 写入GPIO1_DR寄存器
ldr r0, =0x0209C000    @ r0 = GPIO1_DR地址
ldr r1, =0x00000000    @ r1 = 要写入的值
str r1, [r0]           @ *0x0209C000 = 0

@ 按字节读取
ldrb r2, [r0]          @ 读取0x0209C000处的1个字节

@ 带偏移的基址寻址
ldr r3, [r0, #4]       @ r3 = *(r0 + 4)
str r4, [r0, #8]       @ *(r0 + 8) = r4

LDR伪指令 vs LDR指令的区别:

  • LDR R0, =0x0209C004(伪指令):将地址值加载到R0
  • LDR R0, [R1](指令):从R1指向的内存加载数据到R0

追问

  • 追问1:什么是地址对齐?未对齐访问会怎样?
    • 字访问地址必须4字节对齐(地址是4的倍数),半字必须2字节对齐。未对齐访问可能触发Data Abort异常,或由处理器自动拆分为多次对齐访问(性能下降)。
  • 追问2:为什么GPIO寄存器必须用volatile修饰?
    • *(volatile unsigned int *)0x0209C004中的volatile告诉编译器每次都从内存重新读取,而不是使用缓存的值。硬件寄存器的值可能随时变化,必须每次直接访问。

Q18: B、BL、BX、BLX四条跳转指令有什么区别?

答案要点

  1. B:无条件跳转,不保存返回地址
  2. BL:跳转并保存返回地址到LR(用于函数调用)
  3. BX:间接跳转,跳转到Rm指定的地址并切换指令集
  4. BLX:结合BL和BX,跳转到Rm并保存返回地址+切换指令集

详细解答

指令 功能 LR变化 指令集切换
B label PC = label 不变 不切换
BL label PC = label, LR = PC+4 写入返回地址 不切换
BX Rm PC = Rm 不变 根据Rm[0]切换
BLX Rm PC = Rm, LR = PC+4 写入返回地址 根据Rm[0]切换

使用场景:

@ B指令:用于死循环或跳转到不需要返回的地方
loop:
    b loop              @ 死循环

b main                  @ 跳转到main,不再返回

@ BL指令:用于函数调用
bl function             @ 调用function,LR保存返回地址
@ function执行完毕后通过 MOV PC, LR 返回

@ BX指令:用于返回或切换指令集
bx lr                   @ 从函数返回
bx r0                   @ 跳转到R0指定的地址

@ BLX指令:调用Thumb代码
ldr r0, =thumb_func
blx r0                  @ 跳转到thumb_func并切换到Thumb状态

BL指令是函数调用的核心:它将返回地址(下一条指令的地址)保存到LR,子函数通过MOV PC, LR返回。

追问

  • 追问1:BL指令保存的返回地址是+4还是+8?
    • BL保存的是下一条指令的地址(当前指令地址+4),而不是PC的值(+8)。这是因为BL是在译码阶段将PC-4写入LR,对于32位ARM指令,下一条指令地址=当前+4。
  • 追问2:B指令的跳转范围是多少?
    • B指令的编码中偏移量是24位有符号数,左移2位后是26位,跳转范围为±32MB。超过这个范围需要使用LDR PC, =label间接跳转。

Q19: PUSH和POP指令的工作原理是什么?堆栈增长方向是怎样的?

答案要点

  1. PUSH将多个寄存器压入栈(SP先减后存)
  2. POP从栈中恢复多个寄存器(先取后加SP)
  3. ARM使用满递减栈(Full Descending)
  4. SP指向最后一个入栈的值,栈从高地址向低地址增长

详细解答

PUSH/POP是多寄存器加载/存储指令的别名:

@ PUSH等价于STMFD(满递减存储)
PUSH {R0-R3, R12}     @ 先减SP,再存寄存器

@ 等价汇编
STMFD SP!, {R0-R3, R12}
STMDB SP!, {R0-R3, R12}  @ DB = Decrement Before

堆栈增长示意:

初始状态: SP = 0x80000000

PUSH {R0} → SP = 0x7FFFFFFC, *0x7FFFFFFC = R0
PUSH {R1} → SP = 0x7FFFFFF8, *0x7FFFFFF8 = R1
PUSH {R2} → SP = 0x7FFFFFF4, *0x7FFFFFF4 = R2

POP {R2}  → R2 = *0x7FFFFFF4, SP = 0x7FFFFFF8
POP {R1}  → R1 = *0x7FFFFFF8, SP = 0x7FFFFFFC
POP {R0}  → R0 = *0x7FFFFFFC, SP = 0x80000000

栈帧结构(函数调用时):

高地址
    +----------------+
    | 参数5~参数n    |  ← 调用者压栈
    +----------------+
    | 返回地址(LR)   |  ← BL指令自动保存
    +----------------+
    | 旧的FP         |  ← 函数序言压栈
    +----------------+
    | R4-R11         |  ← 保存被调用者寄存器
    +----------------+
    | 局部变量       |  ← 栈上分配
    +----------------+
低地址 ← SP指向这里

ATPCS(ARM-Thumb Procedure Call Standard)规定使用FD(满递减)栈。

追问

  • 追问1:为什么选择满递减栈而不是其他类型?
    • 满递减栈(FD)的SP指向最后一个有效数据,入栈时先减后存。这种设计与ARM的LDM/STM指令配合最优,且与x86架构一致,成为事实标准。
  • 追问2:PUSH指令中寄存器编号小的在高地址还是低地址?
    • 编号小的在低地址。PUSH {R0, R3}中R0在低地址,R3在高地址。POP时按相反顺序恢复,POP {R0, R3}先恢复R3(高地址),再恢复R0(低地址)。

Q20: AND、ORR、BIC、EOR四条逻辑运算指令分别用在什么场景?

答案要点

  1. AND:按位与,用于提取特定位(掩码操作)
  2. ORR:按位或,用于设置特定位
  3. BIC:位清除,用于清除特定位
  4. EOR:按位异或,用于翻转特定位

详细解答

指令 功能 公式 典型场景
AND 按位与 Rd = Rn & Rm 读取/清除特定位
ORR 按位或 Rd = Rn | Rm 设置特定位
BIC 位清除 Rd = Rn & (~Rm) 清除特定位
EOR 按位异或 Rd = Rn ^ Rm 翻转特定位

使用示例:

@ AND:读取bit3的状态
ldr r0, =0x0209C000    @ GPIO1_DR地址
ldr r1, [r0]           @ 读取DR寄存器
and r2, r1, #0x8       @ 提取bit3
cmp r2, #0             @ 判断bit3是否为0
beq led_off            @ 如果为0,LED亮(低电平有效)

@ ORR:设置bit3(输出高电平)
orr r1, r1, #0x8       @ 设置bit3为1
str r1, [r0]

@ BIC:清除bit3(输出低电平)
bic r1, r1, #0x8       @ 清除bit3为0
str r1, [r0]

@ EOR:翻转bit3
eor r1, r1, #0x8       @ 翻转bit3
str r1, [r0]

寄存器配置中的经典用法:

@ 设置处理器模式为SVC
mrs r0, cpsr
bic r0, r0, #0x1f      @ BIC清除低5位(模式位)
orr r0, r0, #0x13      @ ORR设置为SVC模式(10011)
msr cpsr, r0

追问

  • 追问1:为什么用BIC清除位而不是AND?
    • AND R0, R0, #0xFFFFFFE0BIC R0, R0, #0x1F效果相同,但BIC更直观。BIC的语义是"清除Rn中Rm为1的位",AND的语义是"保留Rn中Rm为1的位"。BIC在汇编中更常见。
  • 追问2:如何用逻辑指令实现位域提取?
    • 先AND提取目标位域,再LSR右移到最低位。例如提取bit[7:4]:AND R0, R0, #0xF0LSR R0, R0, #4

五、中断与异常

Q21: ARM的异常向量表是什么?放在内存的什么位置?

答案要点

  1. 异常向量表是一组跳转指令,每个异常占4字节
  2. 位于内存固定地址,可以重定位(VBAR寄存器)
  3. 7个标准异常+预留位置,共32字节
  4. 每个异常的处理程序入口在向量表中有固定偏移

详细解答

ARM异常向量表布局:

偏移 地址(默认) 异常类型
0x00 0x00000000 Reset
0x04 0x00000004 Undefined Instruction
0x08 0x00000008 SVC (SWI)
0x0C 0x0000000C Prefetch Abort
0x10 0x00000010 Data Abort
0x14 0x00000014 Reserved
0x18 0x00000018 IRQ
0x1C 0x0000001C FIQ

向量表的每个条目通常是一条跳转指令:

.section .vectors, "ax"
.global _vectors
_vectors:
    ldr pc, =reset_handler       @ 0x00: Reset
    ldr pc, =undef_handler       @ 0x04: Undefined
    ldr pc, =svc_handler         @ 0x08: SVC
    ldr pc, =prefetch_handler    @ 0x0C: Prefetch Abort
    ldr pc, =data_abort_handler  @ 0x10: Data Abort
    nop                           @ 0x14: Reserved
    ldr pc, =irq_handler         @ 0x18: IRQ
    ldr pc, =fiq_handler         @ 0x1C: FIQ

Cortex-A7的向量表可以重定位到任意地址,通过配置VBAR(Vector Base Address Register)寄存器:

ldr r0, =0x87800000    @ 新的向量表地址
mcr p15, 0, r0, c12, c0, 0  @ 写入VBAR

IMX6ULL默认向量表在0x00000000(Boot ROM地址),Linux启动后会重定位到高地址。

追问

  • 追问1:为什么FIQ放在0x1C(最后)?
    • FIQ处理程序可以不使用跳转指令,直接从0x1C开始写代码,节省一个取指周期。其他异常必须跳转(占4字节),FIQ可以利用这4字节直接执行关键代码。
  • 追问2:VBAR是什么?什么时候使用?
    • VBAR是ARMv7引入的向量基址寄存器,允许将向量表重定位到任意对齐地址。Linux启动时会将VBAR设置到内核异常向量表地址,避免与用户空间地址冲突。

Q22: GIC(通用中断控制器)的架构是怎样的?CPU如何响应中断?

答案要点

  1. GIC分为Distributor和CPU Interface两部分
  2. Distributor负责中断优先级仲裁和分发
  3. CPU Interface负责将中断传递给CPU核心
  4. CPU通过读取GIC_IAR获取中断号,写入GIC_EOIR确认中断

详细解答

GIC架构:

外设中断源 → Distributor → CPU Interface → CPU核心
                        ↑
                   优先级仲裁
                   中断屏蔽
                   中断使能

GIC主要组件:

组件 功能 关键寄存器
Distributor 全局中断管理 GICD_ISENABLER, GICD_IPRIORITYR
CPU Interface CPU级中断控制 GICC_IAR, GICC_EOIR, GICC_CTLR

中断响应流程:

@ 1. 读取GIC_IAR获取中断号
ldr r0, =0x01C8400C      @ GICC_IAR地址
ldr r1, [r0]             @ r1 = 中断号 + CPU ID

@ 2. 保存现场
push {r0-r12, lr}

@ 3. 调用C语言中断处理函数
bl system_irqhandler     @ 传入中断号

@ 4. 恢复现场
pop {r0-r12, lr}

@ 5. 写入GIC_EOIR确认中断
ldr r0, =0x01C84010      @ GICC_EOIR地址
str r1, [r0]             @ 写入中断号确认

GIC中断优先级:

  • 每个中断有独立的优先级(0-255,0最高)
  • 高优先级中断可以抢占低优先级中断
  • 相同优先级中断按中断号顺序响应

追问

  • 追问1:GIC和NVIC在中断数量上有什么差异?
    • GIC支持最多1024个中断(SPI+PPI+SGI),NVIC在Cortex-M4上最多支持240个中断。GIC更适合复杂SoC,NVIC适合微控制器。
  • 追问2:什么是SGI、PPI、SPI?
    • SGI(Software Generated Interrupt):软件触发,用于核间通信;PPI(Private Peripheral Interrupt):每个CPU私有,如本地定时器;SPI(Shared Peripheral Interrupt):所有CPU共享,如UART、GPIO中断。

Q23: 中断处理程序应该做什么?不应该做什么?

答案要点

  1. 应该:读取中断状态、处理中断事件、清除中断标志、确认中断
  2. 不应该:做耗时操作、使用printf、等待事件、分配内存
  3. 中断处理应尽量短小,耗时工作延迟到下半部处理
  4. 中断上下文不能睡眠,不能调用可能睡眠的函数

详细解答

中断处理程序的职责:

应该做

  1. 保存必要的寄存器(现场保护)
  2. 读取中断状态寄存器,判断中断源
  3. 执行必要的硬件操作(如读取数据、清除标志)
  4. 清除中断标志位
  5. 写入EOI(End of Interrupt)确认中断
  6. 恢复寄存器(恢复现场)

不应该做

  1. 耗时操作:中断处理程序占用CPU时间,阻塞其他中断和任务
  2. 使用printf:printf涉及串口操作和锁,可能死锁或长时间阻塞
  3. 等待事件:中断上下文不能睡眠,等待会导致系统死锁
  4. 分配内存:kmalloc在中断上下文中可能睡眠
  5. 持有互斥锁:中断可能打断持有同一锁的代码,导致死锁
  6. 复杂计算:应延迟到进程上下文执行

中断处理最佳实践:

// 简短的上半部(中断上下文)
irqreturn_t gpio_irq_handler(int irq, void *dev_id)
{
    // 1. 读取中断状态
    u32 status = readl(gpio_base + GPIO_ISR);

    // 2. 清除中断标志(写1清零)
    writel(status, gpio_base + GPIO_ISR);

    // 3. 调度下半部处理
    tasklet_schedule(&gpio_tasklet);

    return IRQ_HANDLED;
}

// 下半部(进程上下文,可以睡眠)
void gpio_tasklet_handler(unsigned long data)
{
    // 耗时处理在这里执行
    process_gpio_event(data);
}

追问

  • 追问1:什么是上半部和下半部?
    • 上半部(Top Half)是中断处理程序本身,运行在中断上下文,必须快速执行。下半部(Bottom Half)是延迟处理的工作,运行在进程上下文,可以使用tasklet/workqueue等机制。
  • 追问2:中断上下文和进程上下文有什么区别?
    • 中断上下文不能睡眠(因为没有关联的进程,睡眠后无法唤醒),不能访问用户空间内存。进程上下文是内核代表进程执行代码,可以睡眠,可以访问用户空间。

Q24: 什么是中断嵌套?ARM处理器如何支持中断嵌套?

答案要点

  1. 中断嵌套是指高优先级中断打断低优先级中断的处理
  2. ARM通过CPSR的I/F位和GIC优先级实现嵌套
  3. 每层中断有自己的栈空间和返回地址
  4. 中断嵌套增加系统复杂度,需要仔细管理栈和上下文

详细解答

中断嵌套示意:

User模式执行 → IRQ1发生 → 进入IRQ模式处理IRQ1
                        → IRQ2发生(更高优先级)→ 进入IRQ模式处理IRQ2
                        → IRQ2处理完成 → 返回继续处理IRQ1
                        → IRQ1处理完成 → 返回User模式

ARM支持中断嵌套的机制:

  1. 优先级控制:GIC可以为每个中断设置优先级,高优先级中断可以抢占低优先级中断

  2. 中断禁止位

    • CPSR.I=1禁止IRQ,CPSR.F=1禁止FIQ
    • 中断处理程序可以选择是否禁止中断来控制嵌套
  3. 模式切换:IRQ处理时切换到IRQ模式,允许再次进入IRQ中断

  4. 栈空间:每层中断在IRQ栈上保存上下文,互不干扰

嵌套中断处理的关键代码:

irq_handler:
    @ 1. 保存R0-R12到IRQ栈
    push {r0-r12}

    @ 2. 切换到SVC模式(允许中断嵌套)
    cps #0x13

    @ 3. 调用C中断处理函数
    bl system_irqhandler

    @ 4. 切换回IRQ模式
    cps #0x12

    @ 5. 恢复R0-R12
    pop {r0-r12}

    @ 6. 确认中断
    ldr r0, =0x01C84010
    str r1, [r0]

    @ 7. 返回
    subs pc, lr, #4

追问

  • 追问1:为什么IRQ处理时要切换到SVC模式?
    • 切换到SVC模式后CPSR.I=0,允许其他IRQ中断,实现中断嵌套。如果不切换,IRQ模式下I=1(禁止IRQ),无法嵌套。
  • 追问2:中断嵌套过深会导致什么问题?
    • 栈溢出:每层嵌套都消耗栈空间,嵌套过深可能导致栈溢出。中断延迟:高优先级中断可能长时间得不到响应。解决方案:合理分配栈大小,限制嵌套深度。

Q25: 异常向量表的每个条目为什么通常是一条跳转指令而不是实际的处理代码?

答案要点

  1. 向量表每个条目只有4字节(一条ARM指令的空间)
  2. 4字节不足以存放完整的中断处理程序
  3. 使用跳转指令跳转到实际的处理代码(可以放在任意地址)
  4. 也可以直接写短小的处理代码(如FIQ处理)

详细解答

向量表布局限制:

偏移0x00: 4字节 → Reset处理
偏移0x04: 4字节 → Undefined处理
偏移0x08: 4字节 → SVC处理
偏移0x0C: 4字节 → Prefetch Abort处理
偏移0x10: 4字节 → Data Abort处理
偏移0x14: 4字节 → 保留
偏移0x18: 4字节 → IRQ处理
偏移0x1C: 4字节 → FIQ处理

典型的向量表条目:

@ 每个条目是一条跳转指令(4字节)
vectors:
    ldr pc, =reset_handler       @ 4字节
    ldr pc, =undef_handler       @ 4字节
    ldr pc, =svc_handler         @ 4字节
    ldr pc, =prefetch_handler    @ 4字节
    ldr pc, =data_abort_handler  @ 4字节
    nop                           @ 4字节
    ldr pc, =irq_handler         @ 4字节
    ldr pc, =fiq_handler         @ 4字节

ldr pc, =label伪指令会被汇编器转换为:

ldr pc, [pc, #offset]  @ 从文字池加载地址
 nop                    @ 对齐填充

FIQ的特殊之处:如果FIQ处理程序很短(小于4字节×5=20字节),可以直接写在0x1C处,不需要跳转。

追问

  • 追问1:为什么不能在向量表中写超过4字节的代码?
    • 向量表的每个条目是固定4字节,如果写了超过4字节的代码,会覆盖下一个异常的向量表条目,导致异常处理混乱。
  • 追问2:文字池(literal pool)是什么?
    • 文字池是汇编器在代码段附近创建的一个数据区域,存放常量地址。ldr pc, =label需要从文字池加载label的地址,汇编器自动在合适的位置放置文字池。

六、裸机开发

Q26: 裸机程序的完整启动流程是怎样的?需要做哪些初始化?

答案要点

  1. 上电复位,CPU从固定地址开始执行
  2. 设置处理器模式(通常为SVC模式)
  3. 初始化栈指针(SP)
  4. 初始化数据段(从Flash复制到RAM)
  5. 清零BSS段
  6. 跳转到main函数

详细解答

裸机启动流程(以IMX6ULL为例):

Boot ROM → 加载SPL → 加载程序到DDR → 跳转到_start
    ↓
_start:
    1. 设置SVC模式
    2. 初始化各模式栈
    3. 初始化.data段(如果有)
    4. 清零.bss段
    5. 跳转到main()

启动文件(start.S)示例:

.global _start

_start:
    /* 1、设置处理器为SVC模式 */
    mrs r0, cpsr
    bic r0, r0, #0x1f
    orr r0, r0, #0x13     @ SVC模式
    msr cpsr, r0

    /* 2、设置SVC模式栈指针 */
    ldr sp, =0x87F00000   @ DDR顶部

    /* 3、设置IRQ模式栈指针 */
    mrs r0, cpsr
    bic r0, r0, #0x1f
    orr r0, r0, #0x12     @ IRQ模式
    msr cpsr, r0
    ldr sp, =0x87D00000   @ IRQ栈

    /* 4、跳转到main函数 */
    b main

链接脚本(imx6ul.lds):

SECTIONS {
    . = 0x87800000;       @ DDR起始地址

    .text : {
        *(.text)
    }

    .rodata : {
        *(.rodata*)
    }

    .data : {
        *(.data)
    }

    __bss_start = .;
    .bss : {
        *(.bss)
        *(COMMON)
    }
    __bss_end = .;
}

追问

  • 追问1:为什么链接地址设置为0x87800000?
    • IMX6ULL的DDR起始地址是0x80000000,0x87800000是DDR中靠后的位置,前面的空间可以用于存放设备树、内核镜像等。这是正点原子开发板的约定地址。
  • 追问2:BSS段为什么要清零?
    • BSS段存放未初始化的全局变量和静态变量,C语言标准规定它们的初始值为0。编译器不会将BSS段存储在二进制文件中(节省空间),启动代码必须手动清零。

Q27: 链接脚本的作用是什么?如何编写一个裸机程序的链接脚本?

答案要点

  1. 链接脚本定义代码和数据在内存中的布局
  2. 指定程序入口点(ENTRY)和代码起始地址
  3. 分配.text/.data/.bss等段的地址
  4. 定义BSS段边界符号供启动代码使用

详细解答

链接脚本的核心要素:

ENTRY(_start)              /* 指定程序入口 */

SECTIONS {
    . = 0x87800000;        /* 代码起始地址 */

    .text : {               /* 代码段 */
        *(.text)
    }

    .rodata : {             /* 只读数据段 */
        *(.rodata*)
    }

    .data : {               /* 已初始化数据段 */
        *(.data)
    }

    __bss_start = .;        /* BSS段起始地址符号 */
    .bss : {                /* 未初始化数据段 */
        *(.bss)
        *(COMMON)
    }
    __bss_end = .;          /* BSS段结束地址符号 */
}

各段说明:

内容 存储位置
.text 可执行代码 Flash/ROM(启动后复制到RAM)
.rodata 只读常量(字符串、const数组) Flash/ROM
.data 已初始化全局变量 RAM(初始值从Flash复制)
.bss 未初始化全局变量 RAM(启动时清零)

编译命令:

# 编译
arm-linux-gnueabihf-gcc -g -c led.s -o led.o

# 链接(使用链接脚本)
arm-linux-gnueabihf-ld -T imx6ul.lds led.o -o led.elf

# 生成二进制文件
arm-linux-gnueabihf-objcopy -O binary led.elf led.bin

追问

  • 追问1:ENTRY(_start)的作用是什么?
    • ENTRY告诉链接器程序的入口点。当程序被加载执行时,CPU从_start标号开始执行。没有ENTRY时,链接器默认从地址0开始执行。
  • 追问2:为什么.data段需要从Flash复制到RAM?
    • 已初始化的全局变量需要可读写的存储空间(RAM),但初始值需要持久保存(Flash)。启动时将初始值从Flash复制到RAM,变量就可以在RAM中读写。

Q28: 如何用汇编语言操作IMX6ULL的GPIO寄存器来点亮LED?

答案要点

  1. 使能GPIO时钟(CCM_CCGR寄存器)
  2. 配置引脚复用(IOMUXC_SW_MUX_CTL寄存器)
  3. 配置IO属性(IOMUXC_SW_PAD_CTL寄存器)
  4. 设置GPIO方向(GPIO_GDIR寄存器)
  5. 控制输出电平(GPIO_DR寄存器)

详细解答

点亮LED的完整汇编代码:

.global _start

_start:
    /* 1、使能所有时钟(CCM_CCGR0~CCGR6) */
    ldr r0, =0X020C4068    @ CCGR0地址
    ldr r1, =0XFFFFFFFF    @ 所有位置1
    str r1, [r0]           @ 写入CCGR0

    ldr r0, =0X020C406C    @ CCGR1
    str r1, [r0]
    ldr r0, =0X020C4070    @ CCGR2
    str r1, [r0]
    ldr r0, =0X020C4074    @ CCGR3
    str r1, [r0]
    ldr r0, =0X020C4078    @ CCGR4
    str r1, [r0]
    ldr r0, =0X020C407C    @ CCGR5
    str r1, [r0]
    ldr r0, =0X020C4080    @ CCGR6
    str r1, [r0]

    /* 2、设置GPIO1_IO03复用为GPIO(ALT5) */
    ldr r0, =0X020E0068    @ IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03
    ldr r1, =0X5           @ MUX_MODE = 5 (GPIO)
    str r1, [r0]

    /* 3、配置IO属性 */
    ldr r0, =0X020E02F4    @ IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03
    ldr r1, =0X10B0        @ 100MHz速度,R0/6驱动能力
    str r1, [r0]

    /* 4、设置GPIO1_IO03为输出 */
    ldr r0, =0X0209C004    @ GPIO1_GDIR
    ldr r1, =0X0000008     @ bit3=1,输出
    str r1, [r0]

    /* 5、输出低电平(点亮LED) */
    ldr r0, =0X0209C000    @ GPIO1_DR
    ldr r1, =0             @ 全部输出低
    str r1, [r0]

loop:
    b loop                 @ 死循环

GPIO配置流程总结:

时钟使能 → 引脚复用 → IO属性配置 → GPIO方向设置 → 输出控制
(CCGR)    (MUX)      (PAD)        (GDIR)         (DR)

追问

  • 追问1:为什么IMX6ULL的GPIO配置比STM32复杂?
    • IMX6ULL的IO复用更灵活(每个IO可以复用9种功能),需要配置MUX和PAD两个寄存器。STM32的GPIO配置封装在HAL库中,看起来简单但底层操作类似。
  • 追问2:为什么要一次性使能所有外设时钟?
    • 这是裸机实验的简化做法,为了省去查找每个外设时钟对应CCGR位的步骤。实际产品中应该只使能需要的时钟以降低功耗。

Q29: 裸机程序编译过程中,编译、链接、objcopy各步骤的作用是什么?

答案要点

  1. 编译(gcc -c):将源文件编译为目标文件(.o),不链接
  2. 链接(ld -T):将多个.o文件链接为可执行文件(.elf)
  3. objcopy:将.elf转换为纯二进制文件(.bin)用于烧录
  4. 每个步骤都有特定的作用,不能跳过

详细解答

编译流程:

led.s → [gcc -c] → led.o → [ld -T] → led.elf → [objcopy] → led.bin

各步骤详解:

步骤1:编译

arm-linux-gnueabihf-gcc -g -c led.s -o led.o
  • -g:生成调试信息(GDB使用)
  • -c:只编译不链接
  • 输出:目标文件(.o),包含机器码和符号表,但地址未最终确定

步骤2:链接

arm-linux-gnueabihf-ld -T imx6ul.lds led.o -o led.elf
  • -T imx6ul.lds:指定链接脚本
  • 将所有.o文件的代码段、数据段按链接脚本布局
  • 解析符号引用(函数调用、全局变量地址)
  • 输出:可执行文件(.elf),包含调试信息和符号表

步骤3:格式转换

arm-linux-gnueabihf-objcopy -O binary led.elf led.bin
  • -O binary:输出纯二进制格式
  • 去除ELF头、段表、符号表等元数据
  • 输出:.bin文件,可以直接烧录到Flash

步骤4(可选):反汇编查看

arm-linux-gnueabihf-objdump -D led.elf > led.dis
  • 生成反汇编文件,用于调试和验证

追问

  • 追问1:.elf和.bin有什么区别?
    • .elf是可执行与可链接格式,包含段表、符号表、调试信息等元数据,不能直接烧录。.bin是纯二进制镜像,可以直接烧录到Flash或通过串口下载。
  • 追问2:为什么需要调试信息(-g选项)?
    • 调试信息包含源码行号与机器码的对应关系、变量名与内存地址的映射等。GDB使用这些信息实现源码级调试(单步执行、查看变量值)。发布时可以用strip去除调试信息。

Q30: 裸机开发中常见的程序不运行问题有哪些?如何排查?

答案要点

  1. 启动模式引脚配置错误(BOOT_CFG)
  2. 链接脚本地址错误(代码在错误的内存位置执行)
  3. 栈指针未初始化(SP指向无效地址)
  4. 时钟未使能(外设寄存器无法访问)
  5. 硬件问题(电源、复位、晶振)

详细解答

常见问题排查清单:

问题现象 可能原因 排查方法
程序完全不运行 BOOT_CFG引脚错误 检查启动模式引脚电平
程序完全不运行 链接地址错误 检查链接脚本的.地址
程序完全不运行 SP未初始化 检查start.S中SP设置
指令异常(Undefined) 代码不在正确的执行区域 用objdump检查反汇编
数据异常(Data Abort) 地址越界或权限错误 检查MMU配置
LED不亮 GPIO时钟未使能 检查CCGR寄存器
LED不亮 GPIO复用未配置 检查IOMUXC寄存器
串口乱码 波特率错误 检查UBIR/UBMR配置
程序跑飞 堆栈溢出 增大栈空间

调试技巧:

  1. LED指示法:在关键位置翻转LED,观察程序执行到哪一步

    @ 在start.S中插入LED翻转,验证程序启动
    ldr r0, =0X0209C000
    ldr r1, [r0]
    eor r1, r1, #0x8       @ 翻转bit3
    str r1, [r0]
    
  2. 串口打印法:初始化串口后打印调试信息

    // 在main函数入口打印
    uart_init();
    printf("Program started!\n");
    
  3. JTAG/SWD调试:使用OpenOCD+GDB单步调试

    openocd -f interface/stlink.cfg -f target/stm32f1x.cfg
    arm-none-eabi-gdb led.elf
    (gdb) target remote :3333
    (gdb) break _start
    (gdb) continue
    

追问

  • 追问1:如何判断是链接地址问题还是时钟问题?
    • 如果程序完全不运行(LED无反应),很可能是链接地址或启动模式问题。如果程序运行但LED不亮,可能是时钟或GPIO配置问题。可以用JTAG连接查看PC寄存器值判断。
  • 追问2:IMX6ULL的Data Abort异常通常是什么原因?
    • Data Abort通常由以下原因触发:访问未映射的地址(MMU配置错误)、访问无权限的内存区域、未对齐的内存访问。检查MMU页表和地址映射配置。

附录:高频考点速查

考点 出现频率 对应题号
Cortex-A7 vs Cortex-M4 ⭐⭐⭐⭐⭐ Q1, Q4
处理器模式 ⭐⭐⭐⭐⭐ Q11, Q12, Q15
CPSR寄存器 ⭐⭐⭐⭐⭐ Q7, Q10
寄存器组织(R0-R15) ⭐⭐⭐⭐⭐ Q6, Q8, Q9
异常向量表 ⭐⭐⭐⭐ Q21, Q25
GIC中断控制器 ⭐⭐⭐⭐ Q22
中断处理规范 ⭐⭐⭐⭐ Q23, Q24
ARM汇编指令 ⭐⭐⭐⭐ Q16, Q17, Q18, Q19, Q20
裸机启动流程 ⭐⭐⭐⭐ Q26, Q28
链接脚本 ⭐⭐⭐ Q27
GPIO配置步骤 ⭐⭐⭐ Q28
编译工具链 ⭐⭐⭐ Q29
调试排查 ⭐⭐⭐ Q30

最后更新: 2026-09-17 来源: [[01-Cortex-A7架构精讲]] | [[02-ARM汇编与启动流程]] | [[08-裸机实验总结]]