Mis*_*ère 4 c compiler-construction embedded microcontroller linker
我已经在C和C++,现在相当长的时间编程,所以我熟悉的链接过程的用户:预处理器扩展中的每个.c文件所有原型和宏,然后分别编译成自己的对象文件和所有目标文件以及静态库都链接到可执行文件中.
但是我想更多地了解这个过程:链接器如何链接目标文件(它们包含什么?)?将声明但未定义的函数与其他文件中的定义匹配(如何?)?转换为程序存储器的确切内容(上下文:微控制器)?
理想情况下,我正在寻找基于以下简单示例的流程正在进行的详细逐步描述.因为它似乎没有在任何地方说出来,以这种方式回答谁的名声和荣耀.
main.c中
#include "otherfile.h"
int main(void) {
otherfile_print("Foo");
return 0;
}
Run Code Online (Sandbox Code Playgroud)
otherfile.h
void otherfile_print(char const *);
Run Code Online (Sandbox Code Playgroud)
otherfile.c
#include "otherfile.h"
#include <stdio.h>
void otherfile_print(char const *str) {
printf(str);
}
Run Code Online (Sandbox Code Playgroud)
old*_*mer 10
printf是非常复杂的,对于微控制器问候世界的例子来说非常糟糕,闪烁的LED更好,但这是微控制器特有的.这足以链接.
two.c
unsigned int glob;
unsigned int two ( unsigned int a, unsigned int b )
{
glob=5;
return(a+b+7);
}
Run Code Online (Sandbox Code Playgroud)
one.c
extern unsigned int glob;
unsigned int two ( unsigned int, unsigned int );
unsigned int one ( void )
{
return(two(5,6)+glob);
}
Run Code Online (Sandbox Code Playgroud)
的start.s
.globl _start
_start:
bl one
b .
Run Code Online (Sandbox Code Playgroud)
建立一切.
% 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
Run Code Online (Sandbox Code Playgroud)
现在让我们看......
arm-none-eabi-objdump -D start.o
...
00000000 <_start>:
0: ebfffffe bl 0 <one>
4: eafffffe b 4 <_start+0x4>
Run Code Online (Sandbox Code Playgroud)
它不是编译器/汇编程序的工作来处理外部引用,所以分支链接到一个是不完整的,他们选择使它成为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
Run Code Online (Sandbox Code Playgroud)
函数2和全局变量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
Run Code Online (Sandbox Code Playgroud)
在这里,全局在.data中不在这里,因此链接器必须将.data及其中的内容放入其中然后填写地址.
所以这里我们将它们全部链接在一起,gnu链接器需要一个定义的入口点标签_start(main是标准bootstrap所需的extern地址,我没有使用它,所以我们没有得到一个主要的未找到错误).因为我没有使用链接器脚本,所以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
Run Code Online (Sandbox Code Playgroud)
所以链接器开始放置第一个项目start.o,它大致通过放置那里的东西来确定需要多大的东西.那两条指示.它们需要8个字节,所以理论上第二项是one.o接下来是0x10000008.这意味着可以完成start.s中bl的编码,以使用正确的相对地址(_start + 8,这是执行时pc的值,因此偏移为零,pc + 0是编码)
链接器大致将one.o放入它正在构建的二进制文件中,它必须将地址解析为两个全局,因此它必须放置两个.o然后找出在这种情况下放置的结尾.不是.data,因为我没有预先启动变量.
两个标签位于0x10000030,所以它对一个()中的bl二进行编码以获得该相对偏移量,由于某种原因,它还将glob放置在1000804c(我没有完成定义ram所在的位置,因此gnu链接器将执行此类操作) .尽管有这个原因,这就是链接器为该全局变量定义了home的地方,并且链接器填充了需要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-relative达到,现在无法访问,现在需要一个解决方案(错误或补丁).
对于单个源文件的汇编程序本身,必须经历更多这些回转esp,对于像x86这样的可变长度指令集,其中寻址是一个很大的模糊.我建议你自己尝试制作一个只支持一些指令但只支持其中一些指令的简单汇编程序.并解析和编码指令并将其与现有的调试汇编程序(如gnu汇编程序)进行比较.
test.s
ldr r1,locdat
nop
nop
nop
nop
nop
b over
locdat: .word 0x12345678
top:
nop
nop
nop
nop
nop
nop
over:
b top
Run Code Online (Sandbox Code Playgroud)
正确的答案是
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>
Run Code Online (Sandbox Code Playgroud)
与该活动和链接器的工作有相似之处.你也可以根据上面的文件或类似方法设计一个简单的链接器,提取二进制blob和其他信息,然后开始将它们放在你想要的任何地址空间中.
任何一个都是相当简单的编程任务,但相当有教育意义.拥有可以产生答案的现有工具链,您可以找出出错的地方或如何得到正确的答案.