【发布时间】:2021-11-23 10:35:21
【问题描述】:
我在检查 ARM Cortex A9 的向量表时偶然发现了两种类型的指令:
B _boot
和
LDR PC,_boot
有人可以向我解释一下使用 B 或 LDR 的区别吗?两个代码都应该做同样的事情,但显然必须有区别。和链接寄存器有关系吗?
感谢您的宝贵时间!
【问题讨论】:
我在检查 ARM Cortex A9 的向量表时偶然发现了两种类型的指令:
B _boot
和
LDR PC,_boot
有人可以向我解释一下使用 B 或 LDR 的区别吗?两个代码都应该做同样的事情,但显然必须有区别。和链接寄存器有关系吗?
感谢您的宝贵时间!
【问题讨论】:
彼得的回答涵盖了什么。这是对原因的尝试。为什么你看到有的代码用b,有的用ldr pc。
从简化的异常表开始:
b reset
b handler_a
b handler_b
b handler_c
reset:
mov sp,#0x8000
b .
handler_a:
b .
handler_b:
b .
handler_c:
b .
生成这个:
10000000 <_stack+0xff80000>:
10000000: ea000002 b 10000010 <reset>
10000004: ea000003 b 10000018 <handler_a>
10000008: ea000003 b 1000001c <handler_b>
1000000c: ea000003 b 10000020 <handler_c>
10000010 <reset>:
10000010: e3a0d902 mov sp, #32768 ; 0x8000
10000014: eafffffe b 10000014 <reset+0x4>
10000018 <handler_a>:
10000018: eafffffe b 10000018 <handler_a>
1000001c <handler_b>:
1000001c: eafffffe b 1000001c <handler_b>
10000020 <handler_c>:
10000020: eafffffe b 10000020 <handler_c>
地址 0x10000010 未在 b(ranch) 指令中编码。相反,一个 pc 相对偏移量是:
10000000: ea000002 -2 b 10000010 <reset>
10000004: ea000003 -1 b 10000018 <handler_a>
10000008: ea000003 +0
1000000c: ea000003 +1
10000010: e3a0d902 +2 mov sp, #32768 ; 0x8000
第一条指令用 2 作为立即数编码。对于本指令,这是以字为单位的。 pc 在执行时提前两条指令,因此 pc+2 将您带到重置处理程序 0x10000010。
对于 ldr 电脑:
ldr pc,reset_address
ldr pc,handler_a_address
ldr pc,handler_b_address
ldr pc,handler_c_address
reset_address : .word reset
handler_a_address: .word handler_a
handler_b_address: .word handler_b
handler_c_address: .word handler_c
reset:
mov sp,#0x8000
b .
handler_a:
b .
handler_b:
b .
handler_c:
b .
这给出了:
10000000 <_stack+0xff80000>:
10000000: e59ff008 ldr pc, [pc, #8] ; 10000010 <reset_address>
10000004: e59ff008 ldr pc, [pc, #8] ; 10000014 <handler_a_address>
10000008: e59ff008 ldr pc, [pc, #8] ; 10000018 <handler_b_address>
1000000c: e59ff008 ldr pc, [pc, #8] ; 1000001c <handler_c_address>
10000010 <reset_address>:
10000010: 10000020 .word 0x10000020
10000014 <handler_a_address>:
10000014: 10000028 .word 0x10000028
10000018 <handler_b_address>:
10000018: 1000002c .word 0x1000002c
1000001c <handler_c_address>:
1000001c: 10000030 .word 0x10000030
10000020 <reset>:
10000020: e3a0d902 mov sp, #32768 ; 0x8000
10000024: eafffffe b 10000024 <reset+0x4>
10000028 <handler_a>:
10000028: eafffffe b 10000028 <handler_a>
1000002c <handler_b>:
1000002c: eafffffe b 1000002c <handler_b>
10000030 <handler_c>:
10000030: eafffffe b 10000030 <handler_c>
该指令使用字节偏移进行编码:
10000000: e59ff008 ldr pc, [pc, #8]
10000004: e59ff008 ldr pc, [pc, #8]
10000008: e59ff008 +0
1000000c: e59ff008 +4
10000010: 10000020 +8 .word 0x10000020
pc+offset的地址中的word获取值的间接一级。个人计算机 = [0x10000010]
现在请注意这两种情况,因为我们使用标签和工具为我们完成工作,计算分支的偏移量、ldr pc、链接和放置处理程序的地址等。我们并不真正想做的事情如果我们可以避免它,我们自己。
现在以一个非常真实的情况为例,您从闪存启动处理器。而且您使用的是 VTOR 之前的 ARM 处理器。其中一些想要运行操作系统。因此,您可能需要一个引导加载程序的异常表(例如,像 u-boot 这样过于复杂,在某种程度上它是它自己的操作系统)。这意味着您希望 ram 位于 0x00000000。 ARM不是他们制造处理器内核的芯片供应商,芯片供应商制造芯片并做出这些决定。例如,一些芯片供应商会在 0x10000000 处映射一个闪存。为了启动并假设入口地址是 0x00000000 (芯片供应商控制的东西,但通常它是 0x00000000 用于正常启动)。因此,当他们在 ARM 内核上释放复位时,我们需要将内容 0x10000000 映射到 0x00000000。内存是它自己的银行、闪存、ram、外围设备等。芯片供应商控制所有这些,可以将其固定或使其可编程,这样就有信号和控制寄存器允许在复位时获取 0x00000000 到转到通常也映射在 0x10000000 的闪存。也许是一小部分。在启动/重置时,获取 0x00000000 会获取 b 或 ldr pc 指令(对于正常的异常表使用,您有一条指令可以从表中取出,这意味着 b 或 ldr pc)。
因此,如果有足够的闪存镜像到 0x00000000(对于成功的设计,这将是),那么这两种方法都可以进行重置。
如果您对这些芯片之一使用分支方法,那么当您分支到重置处理程序(重置标签)时,您的电脑是 0x00000010 而不是 0x10000010,因此您现在需要找到正确的地址。有些人只是将 0x10000000 到电脑上作为快速破解。或者最终会使用某个标签的 ldr pc。
然后你会从闪存运行。您有时会想要一些 ram,并将 ram 映射到地址 0x00000000(这些芯片存在,不是闻所未闻的设计)。然后开始使用它。但是你有一个问题,在正确的地址 0x00000000 处不再有异常表。这是 VTOR 之前的版本,但即使使用 VTOR,您也可能需要考虑所有这些。使用分支方法,您可能要考虑创建自己的指令,向前分支 0x10000000...如果可能的话 0xea..xxxx。我将不得不查看编码,但它是 0x10000000-8 / 4 或 0x10000000/4 - 2 或 0x04000000 - 2 并且不适合。例如,如果我们试图达到 0x8000,那将是适合指令的 0x2000 - 2。所以我们想做类似的事情
0xe59ff008
0xe59ff008
0xe59ff008
0xe59ff008
0x10000000
0x10000004
0x10000008
0x1000000C
我们自己从 0x00000000 开始写入,这样如果出现异常,我们会加载一个地址,并且该地址是闪存中的地址。
现在我们使用 ldr 方法代替。然后我们启动,我们没有使用地址 0x00000020 进入复位,而是进入所需的 0x10000020,我们不必弄乱那个地址。如果我们将本例中的第一个 0x20 字节从 0x10000000 复制到 0x00000000,那么我们的处理程序就都到位了,我们不必创建任何指令或地址,工具完成了我们只需复制工作的所有工作。
许多/大多数处理器使用带有地址的向量表,这些处理器中的 ARM 使用您提供指令的固定地址。并且 reset 是第一个,所以如果你真的构建一个入口点为 0x10000000 的二进制文件,它当然不需要有一个表,它只需要入口点和代码开始。
reset:
mov sp,0x0x8000
...
启动后,您可以在 ram 中动态构建处理程序,生成 ldr pc 指令,通过向链接器询问地址的代码(write32(0x0004,(unsigned int)handler_a);)将地址填充到表中。
其他硬件设计可以在 0x00000000 处设置闪存并在其他位置设置 ram。您可能同样希望拥有一个包含条目的表,而不仅仅是重置,并且可能希望在运行时更改其中一个。代码将链接到 0x00000000,您现在无法更改向量表,因为它在闪存中,因此您需要执行类似的操作(如果闪存位于 0x10000000):
ldr pc,a
ldr pc,b
ldr pc,c
ldr pc,d
a: .word 0x10000000
b: .word 0x10000004
c: .word 0x10000008
d: .word 0x1000000C
reset_address : .word reset
handler_a_address: .word handler_a
handler_b_address: .word handler_b
handler_c_address: .word handler_c
reset:
mov sp,#0x8000
b .
handler_a:
b .
handler_b:
b .
handler_c:
b .
创作:
00000000 <a-0x10>:
0: e59ff008 ldr pc, [pc, #8] ; 10 <a>
4: e59ff008 ldr pc, [pc, #8] ; 14 <b>
8: e59ff008 ldr pc, [pc, #8] ; 18 <c>
c: e59ff008 ldr pc, [pc, #8] ; 1c <d>
00000010 <a>:
10: 10000000 .word 0x10000000
00000014 <b>:
14: 10000004 .word 0x10000004
00000018 <c>:
18: 10000008 .word 0x10000008
0000001c <d>:
1c: 1000000c .word 0x1000000c
00000020 <reset_address>:
20: 00000030 .word 0x00000030
00000024 <handler_a_address>:
24: 00000038 .word 0x00000038
00000028 <handler_b_address>:
28: 0000003c .word 0x0000003c
0000002c <handler_c_address>:
2c: 00000040 .word 0x00000040
00000030 <reset>:
30: e3a0d902 mov sp, #32768 ; 0x8000
34: eafffffe b 34 <reset+0x4>
00000038 <handler_a>:
38: eafffffe b 38 <handler_a>
0000003c <handler_b>:
3c: eafffffe b 3c <handler_b>
00000040 <handler_c>:
40: eafffffe b 40 <handler_c>
并在启动时将 0x0000 处的四个字复制到 0x10000000,将 0x20 处的四个字复制到 0x10000010。然后,如果您想更改处理程序运行时,您可以更改 0x10000010 处的单词。
使用 VTOR 如果您在运行时不关心更改向量并且闪存位于 0x10000000,那么您可以使用 ldr pc 进行重置以使 pc 指向闪存而不是 0x00000000 处的镜像。然后在引导程序 VTOR 上指向 0x10000000。但是,如果您想动态更改地址,那么表格需要在 ram 中,并且您会回到 VTOR 之前的日子。
所以....
在指令级别,从 pc 相对偏移量加载 pc。另一个从 pc 相对偏移指向的地址加载 pc。
在系统级别,您正在处理大多数人将放在闪存入口点的异常表,然后处理在地址 0x00000000 处生成一个表(如果它不存在)。
如果您出于任何原因想要动态更改处理程序,那么您的实际表(或您至少修改的表)需要在 ram 中。
ldr pc 是最灵活的,为您添加了最多的功能,但理想情况下您必须输入更多代码。如果您没有这些问题,闪存从 0x00000000 开始,或者您不期望任何中断或异常,因此您不需要处理程序,那么 ldr pc 需要更多字节和更多类型。 b(ranch) 可以正常工作。
以上只是做每件事的一种方式,你可以用其他方式使用工具或手动做事情,只要它起作用......
【讨论】:
ldr reg, symbol 将该地址从内存中的数据加载到寄存器中。加载到 PC 是内存间接跳转。
只有当_boot 足够接近PC 相对寻址模式才能到达它时,它才会组装和链接,但如果两者都在.text 部分中,则很可能。
b symbol 设置 PC = 符号的 地址。是直接相对跳跃。
在这两种情况下都不涉及链接寄存器,因为您使用的是b 而不是bl 或blx。
ldr pc, _bootldr pc, _boot 所做的另一种方式:
ldr r0, =_boot @ "global variable" address into register
ldr r0, [r0] @ load 4 bytes from that symbol address
br r0 @ and set PC = that load result
假设您的 _boot: 标签位于某些代码前面,而不是 .word another_symbol,这不是您想要的。您将加载一些字节的机器代码并将其用作地址。 (将 PC 设置到某处可能无效。)
但如果你确实有_boot: .word foobar 之类的,那么它就是你想要的。
b _boot或者在ldr 方面相当于b _boot 是将地址从内存加载到PC 中。这意味着您需要在内存中保存该地址的单词,而不仅仅是b 编码中的直接相对位移。
但是 ARM 汇编器有一个伪指令来执行此操作:ldr pc, =_boot 将使用 PC 相对寻址模式从附近的文字池加载该标签地址到 PC。或者,您可以设置br,而不是直接进入PC。
ldr r0, =_boot @ symbol address into register
br r0 @ jump to that symbol
这并不完全等效:它不是位置独立的,因为它使用的是绝对地址,而不仅仅是一个相对分支。
【讨论】: