这是一个非常简单的例子,
.globl _start
_start:
adr r0,_start
ldr r1,_TEXT_BASE
...
_TEXT_BASE: .word _start
组装、链接然后拆卸时:
00008000 <_start>:
8000: e24f0008 sub r0, pc, #8
8004: e59f101c ldr r1, [pc, #28] ; 8028 <_TEXT_BASE>
...
00008028 <_TEXT_BASE>:
8028: 00008000 andeq r8, r0, r0
这就是你的答案。adr 指令基于您的电脑在执行时包含 0x8008 的假设。ldr 将提取一个链接时间值,无论您身在何处,该值都是相同的。
例如,如果这段代码实际上位于地址 0x20000000,那么当第一条指令(adr 是伪指令,在反汇编中它是 8 的子指令)时,adr 现在被执行,你得到一个 0x20000008-8 = 0x20000000 和您将其与不匹配的 0x8000 进行比较。如果您在 0x8000 运行代码,则 0x8008-8 = 0x8000 并且两者匹配。
只需阅读代码并查找 adr 指令(或做我所做的,然后尝试并检查编译器/工具的输出,和/或如果没有显示答案,则在硬件上运行它)。
编辑:
使用具有各种前缀的 gnu 工具,但是这段代码很简单,几乎不需要关心。arm-none-eabi- 或 arm-none-linux-gnueabi-。
假设程序集文件名是 foo.s
arm-none-eabi-as foo.s -o foo.o
arm-none-eabi-ld -T memmap foo.o -o foo.elf
arm-none-eabi-objdump -D foo.elf
我所说的 memmap 是链接器脚本,在这种情况下,您可以使用命令行 -Ttext=0x8000。而且我喜欢为我的交叉编译二进制文件使用 .elf 扩展名,并不是每个人都这样做。
00008000 <_start>:
8000: e24f0008 sub r0, pc, #8
8004: e59f101c ldr r1, [pc, #28] ; 8028 <_TEXT_BASE>
...
00008028 <_TEXT_BASE>:
8028: 00008000 andeq r8, r0, r0
_start 是一个 gnu 工具,链接器需要/希望这个标签知道代码的入口点在哪里。所以它不是真正的 uboot 东西,虽然 uboot 可能也很关心,但它绝对是 gnu 链接器的东西。
0x8000 对于基于 ARM 的 linux 程序作为入口点来说并不是一个不常见的地址,你会看到 linux 作为一个内核从这样的地址开始,但是你可以设置你的系统和二进制文件真的是任意的。
这里发生的事情并没有真正的 ARM 特定或魔法。在任何平台上都有相同的想法,您只需提出正确的说明即可。
arm 中的程序计数器提前两条指令,这是一条 32 位 arm 指令,因此执行指令时的程序计数器假定为该指令的地址加 8。由于这个 adr 是 _start 处的第一条指令,这意味着指令在链接/编译时位于地址 0x8000,因此从 0x8008 到 0x8000 指令被编码为 r0 = pc-8; 另一个信息是链接器在标签_TEXT_BASE 之后提供_start 地址。所以另一个步骤是将该值加载到 r1 中。
这仅在您基于代码实际存在于闪存中的假设和事实进行操作时才有效,以便处理器在地址 0x8000 处看到闪存中的该指令。0x8000 和 0x8000 比较是相等的,所以程序在 ram 中做一个自己的副本,然后它会跳转到这个副本所在的开头,这次当它通过代码的副本时,一个寄存器包含一些地址除了 0x8000 之外,比较失败,如果您从闪存运行原始版本或从 ram 运行副本,这只是一个检测器。目的是制作副本并运行基于 ram 的副本。需要采取其他预防措施来确保代码可以在两个地址上运行(位置无关代码)。
如果您碰巧知道闪存地址是 0x00008000 并且碰巧知道例如 ram 地址是 0x20000000,您可以改为将 pc 与 0x10000 进行比较,或者可以将 pc 的值与 0xFF000000 进行比较
and r0,pc,#0xFF000000
beq stack_setup
但我认为这是更通用的代码,因此使用了额外的指令和精确的比较。
同样,这种类型的技巧相当普遍,检测闪存或 ram 副本等。使用的确切指令以及如何让链接器为您填写内容是特定于目标的。
在这种情况下,闪存可能位于某个非零地址,而 0x8000 是 ram 副本。我不知道。