printf 非常复杂,对于微控制器你好世界的例子来说非常糟糕,闪烁的 LED 更好,但它特定于微控制器。这足以进行链接。
两个.c
unsigned int glob;
unsigned int two ( unsigned int a, unsigned int b )
{
glob=5;
return(a+b+7);
}
一个.c
extern unsigned int glob;
unsigned int two ( unsigned int, unsigned int );
unsigned int one ( void )
{
return(two(5,6)+glob);
}
开始.s
.globl _start
_start:
bl one
b .
构建一切。
% arm-none-eabi-gcc -O2 -c one.c -o one.o
% arm-none-eabi-gcc -O2 -c two.c -o two.o
% touch start.s
% arm-none-eabi-gcc -Wall -O2 -nostdlib -nostartfiles -ffreestanding -c one.c -o one.o
% arm-none-eabi-gcc -Wall -O2 -nostdlib -nostartfiles -ffreestanding -c two.c -o two.o
% arm-none-eabi-as start.s -o start.o
% arm-none-eabi-ld -Ttext=0x10000000 start.o one.o two.o -o onetwo.elf
现在让我们看看...
arm-none-eabi-objdump -D start.o
...
00000000 <_start>:
0: ebfffffe bl 0 <one>
4: eafffffe b 4 <_start+0x4>
处理外部引用不是编译器/汇编程序的工作,因此指向一个的分支链接不完整,他们选择将其设为 0 的 bl,但他们本可以简单地将其完全未编码,这取决于工具链的作者关于如何通过目标文件在编译器、汇编器和链接器之间进行通信。
这里也一样
00000000 <one>:
0: e92d4008 push {r3, lr}
4: e3a00005 mov r0, #5
8: e3a01006 mov r1, #6
c: ebfffffe bl 0 <two>
10: e59f300c ldr r3, [pc, #12] ; 24 <one+0x24>
14: e5933000 ldr r3, [r3]
18: e0800003 add r0, r0, r3
1c: e8bd4008 pop {r3, lr}
20: e12fff1e bx lr
24: 00000000 andeq r0, r0, r0
函数二和全局变量 glob 的地址都是未知的。请注意,对于未知变量,编译器生成的代码需要全局的显式地址,因此链接器只需填写地址,glob 也是 .data 而不是 .text。
00000000 <two>:
0: e59f3010 ldr r3, [pc, #16] ; 18 <two+0x18>
4: e2811007 add r1, r1, #7
8: e3a02005 mov r2, #5
c: e0810000 add r0, r1, r0
10: e5832000 str r2, [r3]
14: e12fff1e bx lr
18: 00000000 andeq r0, r0, r0
这里也是全局在 .data 而不是这里,所以链接器必须将 .data 和其中的东西放在里面,然后填写地址。
所以在这里我们将它们链接在一起,gnu 链接器需要一个定义为 _start 的入口点标签(main 是标准引导程序所需的外部地址,我没有使用它,所以我们不会收到 main not found 错误)。因为我没有使用链接器脚本,所以 gnu 链接器按照它们在命令行中定义的顺序将项目放在二进制文件中,因为我正在控制引导,所以我需要首先为微控制器启动。我在这里也使用了非零值来进行演示......
10000000 <_start>:
10000000: eb000000 bl 10000008 <one>
10000004: eafffffe b 10000004 <_start+0x4>
10000008 <one>:
10000008: e92d4008 push {r3, lr}
1000000c: e3a00005 mov r0, #5
10000010: e3a01006 mov r1, #6
10000014: eb000005 bl 10000030 <two>
10000018: e59f300c ldr r3, [pc, #12] ; 1000002c <one+0x24>
1000001c: e5933000 ldr r3, [r3]
10000020: e0800003 add r0, r0, r3
10000024: e8bd4008 pop {r3, lr}
10000028: e12fff1e bx lr
1000002c: 1000804c andne r8, r0, ip, asr #32
10000030 <two>:
10000030: e59f3010 ldr r3, [pc, #16] ; 10000048 <two+0x18>
10000034: e2811007 add r1, r1, #7
10000038: e3a02005 mov r2, #5
1000003c: e0810000 add r0, r1, r0
10000040: e5832000 str r2, [r3]
10000044: e12fff1e bx lr
10000048: 1000804c andne r8, r0, ip, asr #32
Disassembly of section .bss:
1000804c <__bss_start>:
1000804c: 00000000 andeq r0, r0, r0
所以链接器开始放置第一个项目 start.o,它通过仅放置那里的内容粗略地计算出需要多大。这两条指令。它们占用 8 个字节,因此理论上第二项 one.o 位于 0x10000008 处。这意味着start.s中的bl编码可以使用正确的相对地址完成(_start + 8是执行时pc的值所以偏移量为零,pc+0是编码)
链接器已将 one.o 粗略地放入它正在构建的二进制文件中,它必须将地址解析为 2 和全局地址,因此它必须放置 two.o,然后找出结尾的位置这种情况下 .bss 不是 .data 因为我没有预先初始化变量。
两个的标签位于 0x10000030,因此它将 bl 二合一()编码为该相对偏移量,它还出于某种原因将 glob 置于 1000804c(我没有完全定义 ram 的位置,所以 gnu 链接器会做事像这样)。尽管有原因,这是链接器为该全局变量定义主地址的地方,并且链接器需要填写 glob 的地址,one() 和 two() 都需要填写这些地址。
因此编译器(汇编器)和链接器最终必须生成可用的二进制文件,编译器(汇编器)倾向于担心生成与位置无关的机器码并为链接器留下足够的信息,以便它拥有机器码以及它必须填写的未解析的外部列表。编译器随着时间的推移而改进,一个简单的模型是像上面为全局变量地址所做的那样具有地址位置,链接器计算绝对地址并填充它在上面,很明显,他们没有以一种可以使用绝对地址到 1 和 2 的方式对函数调用进行编码。相反,它使用 pc 相对寻址。这意味着链接器必须知道 bl 指令的机器代码编码。当前一代的 gnu 链接器知道的更多,并且可以做一些很酷的事情来解决手臂到拇指和背部的问题,这是它以前不知道的东西(您不再需要为拇指交互进行编译,链接器会处理它)。
因此,链接器获取包含数据的二进制 blob,并将它们链接到一个二进制文件中。它首先需要知道二进制文件中各种事物的实际地址。你如何告诉链接器这是链接器特定的,而不是所有 C/C++ 工具链的全局事物。 Gnu 链接器脚本本身就是一种编程语言。这些不一定是物理地址或虚拟地址,它只是代码在任何模式下(虚拟或物理)的地址空间。一旦链接器知道它的地址,它就会根据链接器规则(同样是链接器特定的)开始将这些不同的二进制 blob 放入这些地址空间中。然后它通过并解析外部/全局地址。它不在上面,但可以是一个迭代过程。例如,如果函数 two() 位于内存中的某个地址,该地址无法通过单个 pc 相关指令访问(比如我们将一个放在零附近,两个放在 0xF0000000 附近),那么编写链接器的人有两个选择,简单的选择是简单地说,它不能编码/实现那么远的分支并退出,gnu链接器已经或仍然这样做。或者另一个解决方案是链接器解决了这个问题。链接器可以在 pc 相对分支链接的范围内添加一些数据字,这些数据字是蹦床,例如加载到寄存器中的绝对地址,然后是基于寄存器的分支,或者可能是聪明的 pc 相对如果蹦床在范围内(在 0x10000000 到 0xF0000000 不起作用的情况下),则分支。如果链接器必须添加这几个字,那么这可能意味着一些二进制 blob 必须移动以为这几个字腾出空间,现在这些二进制 blob 中的所有地址现在也必须移动。因此,您必须再次遍历所有二进制 blob,解析所有填写答案的新地址,并让 pc 相对确定您是否仍然可以访问所有内容。添加这几个词可能会使某些与 pc 相关的内容现在无法访问,现在需要解决方案(错误或补丁)。
单个源文件的汇编程序本身必须经历更多的这些回转,尤其是对于像 x86 这样的寻址非常模糊的可变长度指令集。我建议自己尝试制作一个简单的汇编程序,它只支持一些指令但其中一些分支。并对指令进行解析和编码,并将其与现有的已调试汇编程序(如 gnu assembler)进行比较。
测试.s
ldr r1,locdat
nop
nop
nop
nop
nop
b over
locdat: .word 0x12345678
top:
nop
nop
nop
nop
nop
nop
over:
b top
正确答案是
00000000 <locdat-0x1c>:
0: e59f1014 ldr r1, [pc, #20] ; 1c <locdat>
4: e1a00000 nop ; (mov r0, r0)
8: e1a00000 nop ; (mov r0, r0)
c: e1a00000 nop ; (mov r0, r0)
10: e1a00000 nop ; (mov r0, r0)
14: e1a00000 nop ; (mov r0, r0)
18: ea000006 b 38 <over>
0000001c <locdat>:
1c: 12345678 eorsne r5, r4, #120, 12 ; 0x7800000
00000020 <top>:
20: e1a00000 nop ; (mov r0, r0)
24: e1a00000 nop ; (mov r0, r0)
28: e1a00000 nop ; (mov r0, r0)
2c: e1a00000 nop ; (mov r0, r0)
30: e1a00000 nop ; (mov r0, r0)
34: e1a00000 nop ; (mov r0, r0)
00000038 <over>:
38: eafffff8 b 20 <top>
该活动与链接器的工作有相似之处。您也可以根据上述文件或类似文件制作一个简单的链接器,提取二进制 blob 和其他信息,然后开始将它们放置在您想要的任何地址空间中。
其中任何一个都是相当简单的编程任务,但也很有教育意义。拥有可以生成答案的现有工具链,您可以找出哪里出错或如何获得正确答案。