LR 寄存器在 ARM 中主要的两种用途
在 ARM 架构中,LR(Link Register) 是一个非常重要的寄存器,具有多种用途,但其最主要的用途可以归纳为以下两种:
1. 存储函数的返回地址
在 ARM 调用约定中,LR 主要用于存储子程序调用的返回地址。
1.1 函数调用(子程序调用)中的作用
当一个函数调用(使用 BL
或 BLX
指令)时,处理器自动将当前指令的地址(即函数返回的地址)保存在 LR 寄存器中。这个返回地址是函数执行完毕后,需要跳回的地方。
BL
(Branch with Link)指令: 将当前指令的地址(即调用指令的下一条指令的地址)保存到 LR 中,并跳转到目标函数的起始地址。BLX
(Branch with Link and Exchange)指令: 类似BL
,但还会交换指令集(从 ARM 模式切换到 Thumb 模式,或反之)。
1.2 函数返回(返回地址的使用)
当函数执行完毕并准备返回时,程序通过 MOV PC, LR
或 BX LR
指令,将 LR 寄存器的值(即返回地址)加载到 PC(Program Counter) 中,从而跳转回到调用函数的地方。
MOV PC, LR
:将 LR 寄存器中的返回地址直接移动到程序计数器 PC。BX LR
:根据 LR 寄存器的低位判断是 ARM 还是 Thumb 模式,然后跳转到对应的返回地址。
1.3 示例
BL func ; 调用 func 函数,LR 保存返回地址
...
func:
; 执行函数体
MOV PC, LR ; 使用 LR 中的返回地址返回
2. 存储异常返回地址
在 ARM 中,异常发生时,LR 也有一个非常关键的用途,那就是 存储异常的返回地址。
2.1 异常模式下的作用
当发生中断、异常或系统调用时,LR 被用来存储当前程序计数器(PC)值的拷贝,以便在处理完异常后能够返回到发生异常的地方。
- 在 FIQ(Fast Interrupt Request) 和 IRQ(Interrupt Request) 等模式下,LR 保存的返回地址会指向触发中断或异常的指令地址,允许在中断处理完后继续执行被中断的指令。
- 在 异常模式(如 Supervisor Mode,SVC)下,LR 保存的是异常返回的地址,也就是返回到异常发生之前的程序状态。
2.2 使用方式
当发生中断或异常时,LR 会自动保存当前的 PC 值,在异常处理程序完成后,通过将 LR 的值加载到 PC 中,可以返回到中断或异常发生时的指令位置。
- 异常返回: 通过
MOV PC, LR
或BX LR
指令,将 LR 中保存的返回地址加载到 PC,恢复到正常程序流。
2.3 示例:异常返回
SVC_Handler: ; 异常处理程序
...
MOV PC, LR ; 恢复程序计数器,返回到异常发生的位置
在特定的异常模式下(例如 IRQ 模式、FIQ 模式等),LR 会有专门的名称:
- R14_svc:在 SVC 模式下的链接寄存器,存储异常返回地址。
- R14_irq:在 IRQ 模式下的链接寄存器。
- R14_fiq:在 FIQ 模式下的链接寄存器。
总结
在 ARM 中,LR 寄存器的两种主要用途为:
- 函数调用的返回地址: 存储函数调用后的返回地址,用于函数返回时跳转回调用点。
- 异常的返回地址: 存储异常发生时的返回地址,用于异常处理完成后恢复程序执行。
这使得 LR 寄存器在程序流控制、异常处理和中断管理中都扮演着至关重要的角色。
细说程序计数器 R15的值
在 ARM 架构中,程序计数器(PC) 也就是 R15,是一个特殊的寄存器,用于存储当前正在执行的指令的地址。R15 不仅用于控制程序流,还有一些特殊的行为和特性,以下是详细解释:
1. R15(PC)的基本用途
- 程序计数器指向当前正在执行的指令的地址,也就是下一条将要执行的指令的地址。
- 在 ARM 体系结构中,R15 被直接与程序流控制相关联,修改 R15 的值意味着改变程序的执行路径。
2. R15(PC)值的特殊特性
-
指向下一条指令:R15 总是包含当前正在执行的指令的地址 + 8 字节偏移(ARM 状态)或 4 字节偏移(Thumb 状态)。
-
在 ARM 模式下,R15 会自动加上偏移量,这个偏移是由于指令预取队列的存在。ARM 体系结构设计允许指令在实际执行之前就被预取到指令缓存中。
- ARM 模式(32-bit): 当前正在执行的指令的地址 + 8 字节。
- Thumb 模式(16-bit): 当前正在执行的指令的地址 + 4 字节。
这就是为什么我们不能直接从 R15 读取到正在执行的指令地址,而是需要调整偏移来获取准确的指令地址。
3. 修改 R15 的值
R15 作为程序计数器,修改它的值直接影响程序的执行流。常见的修改方式包括:
-
跳转指令(Branch)
B
(Branch):通过修改 R15 来实现无条件跳转。BL
(Branch with Link):跳转并保存返回地址(通过 LR 寄存器)。BX
(Branch and Exchange):跳转并交换指令集(ARM/Thumb)。
-
函数调用返回
- 在函数调用(通过
BL
)或异常处理完成后,修改 R15 为返回地址以恢复执行。
- 在函数调用(通过
-
异常和中断返回
- 当发生中断或异常时,LR 寄存器会保存异常发生时的地址,而通过
MOV PC, LR
或BX LR
来恢复到异常发生点。
- 当发生中断或异常时,LR 寄存器会保存异常发生时的地址,而通过
4. R15(PC)在不同模式下的行为
ARM 处理器支持多种模式,包括用户模式和特权模式(如中断模式、异常模式等)。在这些模式下,R15(PC)具有不同的行为:
-
在用户模式下:
- R15 直接指向程序计数器的当前地址,表示下一条将要执行的指令。
-
在特权模式下(如 FIQ、IRQ、Supervisor 模式等):
-
R15 被用来存储返回地址。在发生异常时,R15 会保存当前的指令地址(即异常发生时的地址),以便异常处理完成后可以恢复执行。
-
例如,在 IRQ 模式下,程序计数器 R15 会指向当前正在执行的指令地址加上偏移量(如加 8 字节),而在异常返回时,LR(链接寄存器)保存的是该返回地址,处理完成后通过
MOV PC, LR
恢复到原来的地址。
-
5. R15(PC)在汇编中的使用
在汇编代码中,程序通常通过 B
、BL
、BX
等指令来修改 R15 的值,实现跳转和函数调用。
5.1 分支跳转
B target ; 无条件跳转到 target 地址
BL target ; 跳转到 target 地址,并将返回地址存储到 LR
5.2 函数返回
MOV PC, LR ; 使用 LR 中存储的返回地址返回
5.3 异常返回
BX LR ; 使用 LR 恢复程序流,返回到异常发生点
6. 总结
- R15(PC) 是 ARM 中控制程序流的关键寄存器,存储当前正在执行的指令的地址(加上偏移)。
- 修改 R15 的值直接影响程序的执行路径,可以用于跳转、函数调用、异常返回等操作。
- 在 ARM 和 Thumb 模式下,R15 会加上偏移量来配合预取指令队列的存在,使得我们通常会看到 R15 的值比实际执行的指令地址大一些。
PC+8的问题
在 ARM 架构中,PC
寄存器显示的值与实际正在执行的指令地址之间有一个固定的偏移量,通常是 8 字节。这意味着,PC
寄存器的值总是比当前正在执行的指令地址大 8 字节。
这个偏移的原因:
ARM 处理器的 PC
寄存器(程序计数器)实际上指向的是当前指令的 地址+8。这个偏移量是 ARM 架构设计的一部分,具体原因如下:
-
流水线处理: ARM 处理器通常是 3-阶段流水线 或更高效的流水线架构。在这种架构下,处理器需要一些时间来取指、解码和执行指令。因此,
PC
在执行指令时通常已经提前指向下一条指令的地址。 -
取指时的延迟: ARM 处理器在取指(fetch)时,通常会先取下一条指令。因此,
PC
显示的地址并不是当前正在执行的指令地址,而是当前指令地址的 偏移 8 字节。
举个例子:
假设当前执行的指令地址为 0x1000,即 PC
应该指向 0x1000。
- 实际上,在 ARM 中,
PC
寄存器的值通常会显示为 0x1008,即当前指令地址加 8 字节。
0x1000: MOV R0, #1 ; 执行当前指令
0x1004: ADD R1, R0, R0
0x1008: BL func ; 例如这条是分支指令
在这个例子中,指令 BL func
的地址是 0x1008,这时 PC
寄存器的值会显示为 0x1008,但是实际上 CPU 在执行的是 0x1000 地址的指令。
总结:
在 ARM 中,PC
寄存器的值通常比实际执行的指令地址大 8 字节,因为它指向的地址是下一条指令的地址。这是 ARM 处理器流水线和取指过程的一个特性。
Thumb 模式:
- 在 Thumb 模式(16 位模式)下,
PC
会指向当前执行指令的地址 加 4 字节,因为 Thumb 指令集每条指令占用 2 字节,因此两条指令会占用 4 字节。
举例说明
为了更清楚地说明 PC(程序计数器)和 LR(链接寄存器)是如何在 ARM 架构中工作的,将从头到尾分析一个详细的例子。这个例子将展示在 用户模式 下执行程序时,如何通过指令的执行、函数调用以及中断处理时,PC
和 LR
的值变化。
场景:
- 执行一段简单的程序,使用
BL func
进行函数调用。 - 详细说明每条指令执行时 PC 和 LR 的变化。
程序代码:
start:
MOV R0, #5 ; 0x1000
MOV R1, #10 ; 0x1004
BL func ; 0x1008
MOV R2, #20 ; 0x100C
B end ; 0x1010
func:
MOV R3, #15 ; 0x2000
MOV PC, LR ; 0x2004
end:
NOP ; 0x1014
执行分析:
假设我们正在 ARM 模式 下执行该程序,并且通过流水线机制和指令预取的行为来分析 PC
和 LR
的值。
1. 执行 MOV R0, #5
(0x1000)
- 当前执行的指令地址:
PC = 0x1000
- 在 ARM 模式下,
PC
显示的是当前执行指令地址 加 8 字节。因此,PC = 0x1000 + 8 = 0x1008
(即下一条指令的地址)。
2. 执行 MOV R1, #10
(0x1004)
- 当前执行的指令地址:
PC = 0x1004
- 由于流水线机制,
PC
会指向下一条指令的地址 加 8,因此PC = 0x1004 + 8 = 0x100C
。
3. 执行 BL func
(0x1008)
BL
指令作用: 跳转到func
函数的起始地址,并将返回地址(PC + 4
)保存到 LR 中。- PC 会修改为
func
函数的起始地址,例如PC = 0x2000
(func
的地址)。 - LR 会保存跳过
BL func
之后的地址,即LR = 0x100C
。
- PC 会修改为
此时的寄存器值:
- PC = 0x2000(跳转到
func
函数的地址)。 - LR = 0x100C(
BL
指令后面要跳回的位置)。
4. 执行 MOV R3, #15
(0x2000)
- 当前执行的指令地址:
PC = 0x2000
- 由于流水线机制,
PC
会自动向前推进,指向下一条指令的地址 加 8 字节,因此PC = 0x2000 + 8 = 0x2008
。
5. 执行 MOV PC, LR
(0x2004)
MOV PC, LR
:将 LR 中保存的返回地址加载到 PC 中。LR
中的值是0x100C
,所以PC = 0x100C
。- 程序返回到
MOV R2, #20
,即跳回到函数调用后的位置。
此时的寄存器值:
- PC = 0x100C(恢复到
MOV R2, #20
处,继续执行)。 - LR = 0x100C(保持返回地址,直到下一次调用)。
6. 执行 MOV R2, #20
(0x100C)
- 当前执行的指令地址:
PC = 0x100C
- 由于流水线机制,
PC
会自动向前推进,指向下一条指令的地址 加 8 字节,因此PC = 0x100C + 8 = 0x1014
。
7. 执行 B end
(0x1010)
- 当前执行的指令地址:
PC = 0x1010
B end
指令跳转到end
标签,PC
会被修改为跳转目的地的地址,假设end
标签的地址为0x1014
。
此时的寄存器值:
- PC = 0x1014(跳转到
end
处)。
寄存器值汇总:
步骤 | 操作 | PC 地址 | LR 地址 |
---|---|---|---|
执行 MOV R0, #5 | 当前指令地址 | 0x1000 | 未改变 |
执行 MOV R1, #10 | 当前指令地址 | 0x1004 | 未改变 |
执行 BL func | 跳转到 func 函数 | 0x2000 | 0x100C |
执行 MOV R3, #15 | 当前指令地址 | 0x2000 | 0x100C |
执行 MOV PC, LR | 恢复到 MOV R2, #20 处 | 0x100C | 0x100C |
执行 MOV R2, #20 | 当前指令地址 | 0x100C | 0x100C |
执行 B end | 跳转到 end 标签 | 0x1014 | 0x100C |
总结:
- PC 的值:在 ARM 模式下,
PC
总是指向当前执行的指令地址 加 8 字节,因为 ARM 处理器采用指令预取机制,使得PC
显示为当前指令的地址加上 8 字节的偏移量。 - LR 的值:在
BL
指令执行时,LR
保存的是跳过BL
指令后的地址,即PC + 4
。这使得LR
能够在函数调用返回时提供正确的返回地址。
为了更清楚地展示中断时 PC 和 LR(以及其他寄存器)的变化,我们将通过一个 中断处理 的例子来分析。
假设我们有一个简单的场景,其中一个中断触发后,ARM 处理器进入中断处理程序(ISR),并在中断服务完成后返回。
中断的基本流程:
- 当一个外部中断(如外部设备产生的 IRQ)发生时,处理器会保存当前的执行状态(包括
PC
和LR
等寄存器)并跳转到 中断向量表 中的入口地址,进入中断处理程序。 - 中断服务例程(ISR)执行完后,通过
BX LR
或者MOV PC, LR
指令将 LR 中保存的返回地址加载回 PC,恢复到中断发生前的程序位置。
程序代码:
为了简单起见,我们假设在这个示例中,外部中断发生后,程序会跳转到一个名为 ISR_Handler
的中断服务程序。以下是我们假设的代码,展示中断发生时的寄存器变化:
start:
MOV R0, #0 ; 0x1000 设定初始值
MOV R1, #10 ; 0x1004
MOV R2, #20 ; 0x1008
; 中断处理发生前的代码...
B main_loop ; 0x100C
main_loop:
NOP ; 0x1010 主循环
NOP ; 0x1014
; 模拟外部中断发生
NOP ; 0x1018
ISR_Handler:
; 处理中断的代码
MOV R3, #99 ; 0x2000 保存数据
MOV PC, LR ; 0x2004 中断返回
执行流程分析:
1. 程序初始化:
程序从 start
标签开始执行。处理器在运行时使用 PC
寄存器指向当前指令的地址。
- PC = 0x1000 执行
MOV R0, #0
- PC = 0x1004 执行
MOV R1, #10
- PC = 0x1008 执行
MOV R2, #20
- PC = 0x100C 执行
B main_loop
,跳转到main_loop
标签。
此时,程序进入主循环,继续执行 main_loop
中的指令。
2. 中断发生:
在运行过程中,假设外部设备触发了一个中断(IRQ)。当中断触发时,处理器会保存当前执行状态,并跳转到 中断向量表 中的中断处理程序入口。通常情况下,ARM 处理器会使用 IRQ 中断向量。
- 中断发生时:
- 当前
PC
(执行B main_loop
后的地址)会被 保存到 LR(链接寄存器)中。 - 处理器会跳转到中断向量表中定义的 ISR(中断服务例程)入口。假设入口地址是
0x2000
。
- 当前
此时,寄存器的变化如下:
- LR 保存当前地址:
LR = 0x1018
(即main_loop
中断发生时的地址,下一条指令地址)。 - PC 跳转到中断处理程序:
PC = 0x2000
(进入ISR_Handler
)。
3. 中断服务例程 ISR 执行:
当执行 ISR_Handler
中的指令时:
- PC = 0x2000 执行
MOV R3, #99
,中断服务例程的处理代码。 - PC = 0x2004 执行
MOV PC, LR
,将保存的返回地址从 LR 恢复到 PC。
4. 中断返回:
- 在
MOV PC, LR
执行时,LR
中的值会被加载回PC
,恢复到中断发生前的程序状态。 - LR = 0x1018(即
main_loop
中断发生时的地址)。 - PC = 0x1018,程序跳回到中断发生时的地址,继续执行
main_loop
后的指令。
最终寄存器值:
- PC = 0x1018(恢复到中断前的执行地址)。
- LR = 0x1018(LR 在进入 ISR 时保存了当前的
PC
值,之后恢复时保持不变)。
寄存器值汇总:
步骤 | 操作 | PC 地址 | LR 地址 |
---|---|---|---|
执行 MOV R0, #0 | 当前指令地址 | 0x1000 | 未改变 |
执行 MOV R1, #10 | 当前指令地址 | 0x1004 | 未改变 |
执行 MOV R2, #20 | 当前指令地址 | 0x1008 | 未改变 |
执行 B main_loop | 跳转到 main_loop | 0x100C | 未改变 |
执行 NOP (在主循环) | 当前指令地址 | 0x1010 | 未改变 |
执行 NOP (在主循环) | 当前指令地址 | 0x1014 | 未改变 |
中断发生 | 保存当前 PC + 4 到 LR ,跳转到中断入口 | 0x2000 | 0x1018 |
执行 MOV R3, #99 | 当前指令地址 | 0x2000 | 0x1018 |
执行 MOV PC, LR | 恢复 PC (跳回中断前) | 0x1018 | 0x1018 |
执行 NOP (恢复后) | 当前指令地址 | 0x1018 | 0x1018 |
总结:
-
当中断发生时,
PC
会指向当前指令的地址加 4(ARM 的中断向量表会根据中断类型将PC
跳转到合适的处理程序)。同时,LR
会保存当前PC + 4
(中断前的下一条指令地址),以便在中断结束后恢复。 -
在 ISR 中,处理器执行完中断服务后,通过
MOV PC, LR
将保存的返回地址(即LR
中的值)加载到PC
寄存器中,从而跳回中断发生前的执行位置,恢复程序的正常执行。