【问题标题】:What's the difference between "ldr pc, _boot" and "b _boot"?“ldr pc,_boot”和“b _boot”有什么区别?
【发布时间】:2021-11-23 10:35:21
【问题描述】:

我在检查 ARM Cortex A9 的向量表时偶然发现了两种类型的指令:

B _boot

LDR PC,_boot

有人可以向我解释一下使用 B 或 LDR 的区别吗?两个代码都应该做同样的事情,但显然必须有区别。和链接寄存器有关系吗?

感谢您的宝贵时间!

【问题讨论】:

    标签: assembly arm


    【解决方案1】:

    彼得的回答涵盖了什么。这是对原因的尝试。为什么你看到有的代码用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) 可以正常工作。

    以上只是做每件事的一种方式,你可以用其他方式使用工具或手动做事情,只要它起作用......

    【讨论】:

    • 哇,阅读量很大,感谢您的详细回复。我想我现在掌握了基本概念,尽管我在理解一些例子时遇到了麻烦。 ### 如果 b 。分支到当前代码(无限循环),为什么pc的偏移量是0xffffffe?这不是意味着当前 pc 16777214 的偏移量吗?为什么不使用 0? ### 你写的“执行时 pc 提前两条指令”是因为流水线吗?但是流水线阶段的数量不依赖于架构吗?那为什么要假设为 2?
    • 可以假设在开始臂的某个时刻有一个流水线,使得执行期间的 pc 提前两个,指令地址加八个。但可以肯定的是,今天,内核的构建是为了独立于它们自己的内部而兼容
    • 0xF...FFE 是负二,是吗?如果 pc 领先 2 并且您减去 2 即为负 2。因此要跳转到同一条指令,您需要在执行时减去 pc 领先的两条指令
    • 这不是假设,而是它的工作原理。拇指是相同的,即 4 个字节。但是当他们添加 thumb2 时,这让人感到困惑,我想说它是 4 个字节,而不是“提前两个”,但我必须使用一些代码来确认这一点。
    • 关于这个答案的元讨论:meta.stackoverflow.com/questions/413421/…
    【解决方案2】:

    ldr reg, symbol 将该地址从内存中的数据加载到寄存器中。加载到 PC 是内存间接跳转。
    只有当_boot 足够接近PC 相对寻址模式才能到达它时,它才会组装和链接,但如果两者都在.text 部分中,则很可能。

    b symbol 设置 PC = 符号的 地址。是直接相对跳跃。

    在这两种情况下都不涉及链接寄存器,因为您使用的是b 而不是blblx


    ldr pc, _boot

    ldr 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
    

    这并不完全等效:它不是位置独立的,因为它使用的是绝对地址,而不仅仅是一个相对分支。

    【讨论】:

    • 好吧,简而言之,当使用B程序时,程序跳转到标签_boot指定的地址,但是当使用LDR时,_boot中地址指定的内存区域的值被放入PC ,我得到正确了吗?而且B_boot和ldr pc,_boot都不会改变链接寄存器的值吧?
    • @Stone:是的,没错。
    • @old_timer:抱歉,我不明白您所说的“您可以在 VTOR 之前移动该代码”是什么意思。据我所知,您可以使用 VTOR 将矢量表位置移动到其他地方。但这与使用 B 或 LDR pc 分支到 ISR 有何关系?
    • 彼得斯的回答是关于有什么区别,这是正确的,我试图补充为什么你有时会以一种方式看待它,而以另一种方式看待它。试图不与那个答案竞争。
    • 也许我会写一个完整的答案,看看。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-07-03
    • 1970-01-01
    • 2023-01-24
    • 2021-02-22
    • 2018-06-27
    • 1970-01-01
    • 2021-10-21
    相关资源
    最近更新 更多