一般而言,程序编译经历下图四个阶段,链接是编译的最后一步,无论是在PC上编译代码,还是在PC上使用嵌入式gcc工具交叉编译嵌入式代码,编译过程都是如下几步
链接过程是将各式各样的.o文件链接为一个文件的过程。链接脚本描述连接器如何将这些输入文件(.o)文件映射为一个输出文件的,并且定义了输出文件的memory layout。几乎所有的链接脚本都是在做这些事情。
在使用ld的时候,通过-T选项,可以使用自己写的链接脚本完成链接过程,否则会使用默认的链接脚本。
接下来需要定义一些基础的概念以及词汇用来描述链接脚本语言。
连接器将输入文件组合为一个单独的输出文件。输入文件和输出文件的数据类型是我们熟知的object file format。每个文件被称为object file。输出文件不仅仅可以称为object file,有时也会叫他executable可执行文件,出于我们描述的目的,我们依旧称他为object file。
每一个object,有很多其他的信息,但是最重要的是section的列表。这也是着重要谈的。大多数时候,我们会把输入的Object文件里面的section称之为 input section。链接输出的单个Object文件里面的section称之为output section。
在Object文件中,每一个section有一个名字还有对应的size信息,几乎所有的section都有一个数据关联块(associated block of data)。
section可以被标记为loadable,意味着当输出文件运行起来的时候,这个section的content信息应当被加载到memory中。
section没有content信息,并且被标记为allocatable,意味着会有一块内存会被创建,但是却没有任何内容放在上面,程序运行起来的时候无需要加载任何信息到memory中。
如果section既不是loadable也不是allocatable,那么意味着它包含了一些debug的信息。
链接脚本的输出文件是一个Object文件,里面包含了多个section,包括loadable的section,也包括allocatable section。每一个section包含两个地址,分别是VMA以及LMA。
VMA是virtual memory address,LMA是load memory address。大多数两类地址是相同的,但是在嵌入式开发中不大相同,LMA是flash地址,而VMA是将flash加载到ram里面运行的ram地址。
例如下述的嵌入式C代码,显然全局变量a在编译成bin的时候是烧写进flash的,但是程序运行起来之后a的数据是需要加载进ram的,因为a是一个变量,不排除程序运行过程中代码会改变他的值。因此flash会存放他的初始值,运行的时候从ROM加载到RAM,他在ROM中的地址是LMA,在RAM中的地址是VMA。
下文会使用objdumo -h指令查看object文件里面的section,以及对应的VMA和LMA。
每个object文件拥有一个symbols列表,里面包含的symbol可以是被定义或者未被定义的。每一个symbol有名字和地址。如果是C/C++程序编译出来的.o文件。所有的函数和全局静态变量都处于defined的状态,在输入文件中引用的每个未定义的函数或全局变量都将成为一个未定义的符号。
使用 objdumo -t指令可以查看对应object的symbols。
链接脚本是text文件,空格忽略。
假设连接器的输入object里面只有代码.text段,数据.data段,未初始化的data段.bss。
假设代码希望放在0x10000,数据希望放在0x8000000。下面代码将会描述这样的链接脚本
SECTIONS
{
. = 0x10000;
.text : {*(.text)}
. = 0x8000000;
.data : {*(.data)}
.bss : {*(.bss)}
}
第一行,使用’.'给memory map定位地址。如果不适用 "."来指定开始地址,那么将会从0开始分配地址。
第二行定义了一个output scetion,名字叫’.text’,后面花括号里面的内容是其他.o文件里面作为输入的section名称,输入的section会被存放到output section中。是通配符的含义,可以匹配任意文件名 ,(.text) 意味着所有输入文件中的.text段。
因为location counter 被配置为0x10000,因此连接器会把text的内容存放到0x10000中。
后面的几行与text段同理,.data段和.bss段都是从0x8000000开始的,并且是紧紧挨在一起的。
连接器会保证每个段的对齐方式,以此作为依据来增加loaction counter。
为了深度体验连接器的工作过程,我们创建add.c文件,data.c 和main.c文件三个文件,并且写自己定义的链接脚本将这个三个.c文件链接为一个输出文件
main.c
----------------
extern int add(int a , int b);
extern int data1;
extern int data2;
int main(void){
add(data1,data2)
return 0;
}
data.c
-----------------
int data1=10;
int data2=20;
add.c
-----------------
int add(int a , int b){
return a+b;
}
使用arm gcc 生成每个.c对应的.o文件
arm-none-eabi-gcc -c main.c
arm-none-eabi-gcc -c data.c
arm-none-eabi-gcc -c add.c
使用objdump工具观察编译生成的main.o文件
arm-none-eabi-objdump -h main.o
得到
main.o: file format elf32-littlearm
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000040 00000000 00000000 00000034 2**2
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000000 00000000 00000000 00000074 2**0
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 00000000 00000000 00000074 2**0
ALLOC
3 .comment 00000080 00000000 00000000 00000074 2**0
CONTENTS, READONLY
4 .ARM.attributes 00000030 00000000 00000000 000000f4 2**0
CONTENTS, READONLY
为了更清晰的看到每个段的含义,使用
arm-none-eabi-objdump -s -d main.o
查看反汇编
Contents of section .text:
0000 00482de9 04b08de2 28309fe5 002093e5 .H-.....(0... ..
0010 24309fe5 003093e5 0310a0e1 0200a0e1 $0...0..........
0020 feffffeb 0030a0e3 0300a0e1 04d04be2 .....0........K.
0030 0048bde8 1eff2fe1 00000000 00000000 .H..../.........
Contents of section .comment:
0000 00474343 3a202847 4e552054 6f6f6c73 .GCC: (GNU Tools
0010 20666f72 2041726d 20456d62 65646465 for Arm Embedde
0020 64205072 6f636573 736f7273 20372d32 d Processors 7-2
0030 3031382d 71322d75 70646174 65292037 018-q2-update) 7
0040 2e332e31 20323031 38303632 32202872 .3.1 20180622 (r
0050 656c6561 73652920 5b41524d 2f656d62 elease) [ARM/emb
0060 65646465 642d372d 6272616e 63682072 edded-7-branch r
0070 65766973 696f6e20 32363139 30375d00 evision 261907].
Contents of section .ARM.attributes:
0000 412f0000 00616561 62690001 25000000 A/...aeabi..%...
0010 0541524d 3754444d 49000602 08010901 .ARM7TDMI.......
0020 12041401 15011703 18011901 1a011e06 ................
Disassembly of section .text:
00000000 :
0: e92d4800 push {fp, lr}
4: e28db004 add fp, sp, #4
8: e59f3028 ldr r3, [pc, #40] ; 38
c: e5932000 ldr r2, [r3]
10: e59f3024 ldr r3, [pc, #36] ; 3c
14: e5933000 ldr r3, [r3]
18: e1a01003 mov r1, r3
1c: e1a00002 mov r0, r2
20: ebfffffe bl 0
24: e3a03000 mov r3, #0
28: e1a00003 mov r0, r3
2c: e24bd004 sub sp, fp, #4
30: e8bd4800 pop {fp, lr}
34: e12fff1e bx lr
...
看到有一个需要链接的位置
ebfffffe bl 0
有一个标签,bl目前跳转的地方是0地址,后期连接器应该是需要把这个值改回来的。
总体来看main.o需要链接两个全局变量和一个函数位置。函数位置看到了 后期应该会替换,两个全局变量尚且没看到,是38
因为看到把[pc, #40]的数据和[pc, #36]的数据最后放到了r0 r1寄存器中,然后启用的调用add函数,猜测通过r0 r1传递变量。具体的看后面链接成功之后的输出.o的内容。
从二进制可以显然的看出,comment ARM.attributes存放了一些工具链相关的信息,并不需要关心。
使用objdump工具观察编译生成的data.o文件
data.o: file format elf32-littlearm
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000000 00000000 00000000 00000034 2**0
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .data 00000008 00000000 00000000 00000034 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 00000000 00000000 0000003c 2**0
ALLOC
3 .comment 00000080 00000000 00000000 0000003c 2**0
CONTENTS, READONLY
4 .ARM.attributes 00000030 00000000 00000000 000000bc 2**0
CONTENTS, READONLY
显然data.c里面没有包含任何有用的代码信息,只是使用int定义了两个全局变量,int的长度是4个字节,两个int于是占用了8个字节和.data 段的长度信息一致。
arm-none-eabi-objdump -s -d data.o
查的反汇编
data.o: file format elf32-littlearm
Contents of section .data:
0000 0a000000 14000000 ........
Contents of section .comment:
0000 00474343 3a202847 4e552054 6f6f6c73 .GCC: (GNU Tools
0010 20666f72 2041726d 20456d62 65646465 for Arm Embedde
0020 64205072 6f636573 736f7273 20372d32 d Processors 7-2
0030 3031382d 71322d75 70646174 65292037 018-q2-update) 7
0040 2e332e31 20323031 38303632 32202872 .3.1 20180622 (r
0050 656c6561 73652920 5b41524d 2f656d62 elease) [ARM/emb
0060 65646465 642d372d 6272616e 63682072 edded-7-branch r
0070 65766973 696f6e20 32363139 30375d00 evision 261907].
Contents of section .ARM.attributes:
0000 412f0000 00616561 62690001 25000000 A/...aeabi..%...
0010 0541524d 3754444d 49000602 08010901 .ARM7TDMI.......
0020 12041401 15011703 18011901 1a011e06 ................
显然的看出,data.o里面只有.data段的数据,并且长度为8个字节,小端模式,的确包含了0x0000000a 以及 0x00000014两个全局变量的数据。
使用objdump工具观察编译生成的add.o文件
arm-none-eabi-objdump -h add.o
得到
add.o: file format elf32-littlearm
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000030 00000000 00000000 00000034 2**2
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000000 00000000 00000000 00000064 2**0
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 00000000 00000000 00000064 2**0
ALLOC
3 .comment 00000080 00000000 00000000 00000064 2**0
CONTENTS, READONLY
4 .ARM.attributes 00000030 00000000 00000000 000000e4 2**0
CONTENTS, READONLY
显然add.c里面没有包含任何有用的数据信息,只有代码信息,对应.text段的长度不为零,其他为零。
arm-none-eabi-objdump -s -d add.o
得到
add.o: file format elf32-littlearm
Contents of section .text:
0000 04b02de5 00b08de2 0cd04de2 08000be5 ..-.......M.....
0010 0c100be5 08201be5 0c301be5 033082e0 ..... ...0...0..
0020 0300a0e1 00d08be2 04b09de4 1eff2fe1 ............../.
Contents of section .comment:
0000 00474343 3a202847 4e552054 6f6f6c73 .GCC: (GNU Tools
0010 20666f72 2041726d 20456d62 65646465 for Arm Embedde
0020 64205072 6f636573 736f7273 20372d32 d Processors 7-2
0030 3031382d 71322d75 70646174 65292037 018-q2-update) 7
0040 2e332e31 20323031 38303632 32202872 .3.1 20180622 (r
0050 656c6561 73652920 5b41524d 2f656d62 elease) [ARM/emb
0060 65646465 642d372d 6272616e 63682072 edded-7-branch r
0070 65766973 696f6e20 32363139 30375d00 evision 261907].
Contents of section .ARM.attributes:
0000 412f0000 00616561 62690001 25000000 A/...aeabi..%...
0010 0541524d 3754444d 49000602 08010901 .ARM7TDMI.......
0020 12041401 15011703 18011901 1a011e06 ................
Disassembly of section .text:
00000000 :
0: e52db004 push {fp} ; (str fp, [sp, #-4]!)
4: e28db000 add fp, sp, #0
8: e24dd00c sub sp, sp, #12
c: e50b0008 str r0, [fp, #-8]
10: e50b100c str r1, [fp, #-12]
14: e51b2008 ldr r2, [fp, #-8]
18: e51b300c ldr r3, [fp, #-12]
1c: e0823003 add r3, r2, r3
20: e1a00003 mov r0, r3
24: e28bd000 add sp, fp, #0
28: e49db004 pop {fp} ; (ldr fp, [sp], #4)
2c: e12fff1e bx lr
看到汇编里面
add r3, r2, r3
把r2和r3的数据相加存放在r3里面,实现了add的功能,然后用Mov把r3的数据放在了r0里面,实现return的功能?这一段汇编是实现了add功能的汇编代码。
思考:现在有三个.o文件,分别是data.o add.o main.o 其中data.o放了两个全局变量的具体取值,add.o存放了使得两个整形数相加的函数实现,main.o则是使用了data.o的数据,add.o的函数。main.o在链接之前并不知道数据的具体数值,也不知道调用函数的函数位置,因此事先会把不知道的信息空出来,等到链接的时候再合并。因此接下来要观察链接的结果。
假设,我们目标的memorymap ,数据段存放在0x80000位置,代码段存放再0x10000的位置。
于是写下如下代码
my.ld
SECTIONS {
. = 0x10000;
.text : {*(.text)}
. = 0x80000;
.data : {*(.data)}
.bss : {*(.bss)}
}
使用链接器链接
arm-none-eabi-ld add.o data.o main.o -T my.ld -o out
得到输出文件out
使用objdump观察out文件
/e/7-2018-q2/bin/arm-none-eabi-objdump.exe -h out
得到
out: file format elf32-littlearm
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000070 00010000 00010000 00010000 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .data 00000008 00080000 00080000 00020000 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .comment 0000007f 00000000 00000000 00020008 2**0
CONTENTS, READONLY
3 .ARM.attributes 00000030 00000000 00000000 00020087 2**0
CONTENTS, READONLY
看到链接后的out文件的text和data size是多个.o文件的和。
反汇编
out: file format elf32-littlearm
Contents of section .text:
10000 04b02de5 00b08de2 0cd04de2 08000be5 ..-.......M.....
10010 0c100be5 08201be5 0c301be5 033082e0 ..... ...0...0..
10020 0300a0e1 00d08be2 04b09de4 1eff2fe1 ............../.
10030 00482de9 04b08de2 28309fe5 002093e5 .H-.....(0... ..
10040 24309fe5 003093e5 0310a0e1 0200a0e1 $0...0..........
10050 eaffffeb 0030a0e3 0300a0e1 04d04be2 .....0........K.
10060 0048bde8 1eff2fe1 00000800 04000800 .H..../.........
Contents of section .data:
80000 0a000000 14000000 ........
Contents of section .comment:
0000 4743433a 2028474e 5520546f 6f6c7320 GCC: (GNU Tools
0010 666f7220 41726d20 456d6265 64646564 for Arm Embedded
0020 2050726f 63657373 6f727320 372d3230 Processors 7-20
0030 31382d71 322d7570 64617465 2920372e 18-q2-update) 7.
0040 332e3120 32303138 30363232 20287265 3.1 20180622 (re
0050 6c656173 6529205b 41524d2f 656d6265 lease) [ARM/embe
0060 64646564 2d372d62 72616e63 68207265 dded-7-branch re
0070 76697369 6f6e2032 36313930 375d00 vision 261907].
Contents of section .ARM.attributes:
0000 412f0000 00616561 62690001 25000000 A/...aeabi..%...
0010 0541524d 3754444d 49000602 08010901 .ARM7TDMI.......
0020 12041401 15011703 18011901 1a011e06 ................
Disassembly of section .text:
00010000 :
10000: e52db004 push {fp} ; (str fp, [sp, #-4]!)
10004: e28db000 add fp, sp, #0
10008: e24dd00c sub sp, sp, #12
1000c: e50b0008 str r0, [fp, #-8]
10010: e50b100c str r1, [fp, #-12]
10014: e51b2008 ldr r2, [fp, #-8]
10018: e51b300c ldr r3, [fp, #-12]
1001c: e0823003 add r3, r2, r3
10020: e1a00003 mov r0, r3
10024: e28bd000 add sp, fp, #0
10028: e49db004 pop {fp} ; (ldr fp, [sp], #4)
1002c: e12fff1e bx lr
00010030 :
10030: e92d4800 push {fp, lr}
10034: e28db004 add fp, sp, #4
10038: e59f3028 ldr r3, [pc, #40] ; 10068
1003c: e5932000 ldr r2, [r3]
10040: e59f3024 ldr r3, [pc, #36] ; 1006c
10044: e5933000 ldr r3, [r3]
10048: e1a01003 mov r1, r3
1004c: e1a00002 mov r0, r2
10050: ebffffea bl 10000
10054: e3a03000 mov r3, #0
10058: e1a00003 mov r0, r3
1005c: e24bd004 sub sp, fp, #4
10060: e8bd4800 pop {fp, lr}
10064: e12fff1e bx lr
10068: 00080000 .word 0x00080000
1006c: 00080004 .word 0x00080004
观察到如下两点
1、原先 2c 行 bl 0位置被连接器替换为了bl 10000。而0x10000刚好是add的代码地址
2、全局变量的地址,由函数首地址+0x38 和 函数首地址+0x3c获取,显然再这个例子函数首地址是0x10030,也就是到0x10068和0x1006c两个地址中获值。看到链接之后再0x10068和0x1006c这两个地址上放了存放数据的地址0x80000和0x80004。这个刚好和我们的链接脚本数据存放的位置符合。我们链接脚本中把数据放在了0x80000的位置
参考链接:ld - 链接脚本学习笔记与实践过程