链接脚本
链接脚本
程序编译的几个阶段
一般程序编译都会经过下面几个阶段,不管是PC上还是单片机上
目标文件结构
常见段 | 存放内容 |
---|---|
text | 代码 |
rodata | 全局常量、字符串常量 |
bss | 未初始化的全局变量和局部静态变量 |
data | 已初始化的全局变量和局部静态变量 |
heap | 动态分配的数据,new、malloc |
stack | 局部变量 |
LMA和VMA
- VMA( Virtual Memory Address,虚拟内存地址): 程序在运行时访问内存的地址,即变量和函数在程序执行期间所在的内存地址。对于 MCU(微控制器),VMA 通常就是程序运行时的物理地址,因为 MCU 通常没有复杂的内存管理单元(MMU)。
- LMA( Load Memory Address,加载内存地址): 程序在加载到内存时的实际存放地址,即程序或数据最初被存储的位置。对于 MCU,通常是指代码或初始化数据在 Flash 等非易失性存储器中的存放地址。
[!NOTE]
在MCU开发中,使用链接脚本指定每个section的LMA和VMA,以控制代码和数在存储中的布局:
- 将初始化数据从FLASH加载到SRAM:
data
段的初始化值存储在FLASH(LMA),程序运行时需要复制到SRAM(VMA)- 设置未初始化数据的位置:
bss
段的变量在程序启动时被清零,直接放在SRAM,其LMA和VMA都在SRAM
MCU与现代CPU(如PC或MPU)的区别
- MCU:
- 缺少MMU: 大多数 MCU 没有内存管理单元,物理地址和逻辑地址是一致的
- 直接物理寻址: 程序访问的地址就是实际的物理地址
- 启动代码负责初始化: 复制
.data
段,清零.bss
段。 - 固定的内存布局: 程序和数据的物理地址在编译时确定。
- 开发者需要明确指定 LMA 和 VMA,因为程序加载和运行在相同的物理地址空间
- 有限的内存保护: 一些高级 MCU 提供 MPU(Memory Protection Unit),但功能有限
- 缺乏权限控制: 程序通常可以访问所有内存区域
- 现代CPU:
- 拥有 MMU: 具备复杂的内存管理单元,可以进行虚拟内存地址到物理内存地址的转换
- 支持虚拟内存: 进程运行在各自的虚拟地址空间中,地址由 MMU 映射到物理内存
- 操作系统和 MMU 负责地址映射,开发者通常不需要直接处理物理地址
- 动态加载: 操作系统的加载器根据可执行文件的元数据加载程序。
- 地址重定位: 支持动态链接库和地址重定位,物理地址在运行时确定。
- 支持虚拟内存和分页: 提供内存保护、进程隔离和内存扩展功能 , 每个进程有独立的虚拟地址空间
- 严格的内存保护: 操作系统和 MMU 管理内存访问权限,防止非法访问
- 用户态和内核态: 进程运行在受限的用户态,系统资源受保护
链接脚本
链接过程是将各式各样的.o文件链接为一个文件的过程。链接脚本描述连接器如何将这些输入文件(.o)文件映射为一个输出文件的,并且定义了输出文件的memory layout。几乎所有的链接脚本都是在做这些事情。
在使用ld的时候,通过-T选项,可以使用自己写的链接脚本完成链接过程,否则会使用默认的链接脚本。
链接器,它是编译过程中的最后一步,负责将各种.o文件链接为一个可执行文件或库文件。链接器需要解决两个问题:一是如何将输入文件中的section映射到输出文件中的section,并确定它们在内存中的位置和大小;二是如何解析输入文件中引用的未定义的symbol,并将它们与输出文件中定义的symbol进行匹配
section和symbol,它们是object file format中最重要的两个概念。每个object file都包含一个section列表和一个symbol列表。section是object file中存放代码或数据的区域,每个section都有一个名字、一个大小、一个数据块(除了.bss section)和一些属性(如可读、可写、可执行等)。symbol是object file中定义或引用的标识符,每个symbol都有一个名字、一个地址、一个类型(如函数、变量等)和一些属性(如全局、局部、弱等)。链接器需要根据section和symbol的信息来组合输入文件和生成输出文件
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。
实践链接器过程
准备三个c文件
main.c
----------------
extern int add(int a , int b);
extern int data1;
extern int data2;
//使用汇编调用系统调用结束程序,否则将运行到意外的内存地址,导致段错误
void exit()
{
asm( "movq $66,%rdi \n\t"
"movq $60,%rax \n\t"
"syscall \n\t");
}
int main(void){
add(data1,data2);
exit();
}
data.c
-----------------
int data1=10;
int data2=20;
add.c
-----------------
int add(int a , int b){
return a+b;
}
编译每一个c文件生成.o文件
#gcc -fno-builtin -fno-stack-protector -c *.c
gcc -c *.c
观察.o文件内容
main.o
使用objdump查看main.o文件
objdump -h main.o
main.o: file format elf64-x86-64
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000049 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000000 0000000000000000 0000000000000000 00000089 2**0
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 0000000000000000 0000000000000000 00000089 2**0
ALLOC
3 .comment 0000002c 0000000000000000 0000000000000000 00000089 2**0
CONTENTS, READONLY
4 .note.GNU-stack 00000000 0000000000000000 0000000000000000 000000b5 2**0
CONTENTS, READONLY
5 .note.gnu.property 00000020 0000000000000000 0000000000000000 000000b8 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
6 .eh_frame 00000058 0000000000000000 0000000000000000 000000d8 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
可以看到.data和.bss都为0,.text代码段存在数据
查看反汇编
objdump -s -d main.o
main.o: file format elf64-x86-64
Contents of section .text:
0000 f30f1efa 554889e5 48c7c742 00000048 ....UH..H..B...H
0010 c7c03c00 00000f05 905dc3f3 0f1efa55 ..<......].....U
0020 4889e58b 15000000 008b0500 00000089 H...............
0030 d689c7e8 00000000 b8000000 00e80000 ................
0040 0000b800 0000005d c3 .......].
Contents of section .comment:
0000 00474343 3a202855 62756e74 75203131 .GCC: (Ubuntu 11
0010 2e342e30 2d317562 756e7475 317e3232 .4.0-1ubuntu1~22
0020 2e303429 2031312e 342e3000 .04) 11.4.0.
Contents of section .note.gnu.property:
0000 04000000 10000000 05000000 474e5500 ............GNU.
0010 020000c0 04000000 03000000 00000000 ................
Contents of section .eh_frame:
0000 14000000 00000000 017a5200 01781001 .........zR..x..
0010 1b0c0708 90010000 1c000000 1c000000 ................
0020 00000000 1b000000 00450e10 8602430d .........E....C.
0030 06520c07 08000000 1c000000 3c000000 .R..........<...
0040 00000000 2e000000 00450e10 8602430d .........E....C.
0050 06650c07 08000000 .e......
Disassembly of section .text:
0000000000000000 <exit>:
0: f3 0f 1e fa endbr64
4: 55 push %rbp
5: 48 89 e5 mov %rsp,%rbp
8: 48 c7 c7 42 00 00 00 mov $0x42,%rdi
f: 48 c7 c0 3c 00 00 00 mov $0x3c,%rax
16: 0f 05 syscall
18: 90 nop
19: 5d pop %rbp
1a: c3 ret
000000000000001b <main>:
1b: f3 0f 1e fa endbr64
1f: 55 push %rbp
20: 48 89 e5 mov %rsp,%rbp
23: 8b 15 00 00 00 00 mov 0x0(%rip),%edx # 29 <main+0xe>
29: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # 2f <main+0x14>
2f: 89 d6 mov %edx,%esi
31: 89 c7 mov %eax,%edi
33: e8 00 00 00 00 call 38 <main+0x1d>
38: b8 00 00 00 00 mov $0x0,%eax
3d: e8 00 00 00 00 call 42 <main+0x27>
42: b8 00 00 00 00 mov $0x0,%eax
47: 5d pop %rbp
48: c3 ret
可以看到一个需要链接的地方
18: e8 00 00 00 00 call 1d <main+0x1d>
data.o
objdumo -h data.o
data.o: file format elf64-x86-64
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000000 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .data 00000008 0000000000000000 0000000000000000 00000040 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 0000000000000000 0000000000000000 00000048 2**0
ALLOC
3 .comment 0000002c 0000000000000000 0000000000000000 00000048 2**0
CONTENTS, READONLY
4 .note.GNU-stack 00000000 0000000000000000 0000000000000000 00000074 2**0
CONTENTS, READONLY
5 .note.gnu.property 00000020 0000000000000000 0000000000000000 00000078 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
可以看到.data段存在数据,.text代码段为空
objdump -s -d data.o
data.o: file format elf64-x86-64
Contents of section .data:
0000 0a000000 14000000 ........
Contents of section .comment:
0000 00474343 3a202855 62756e74 75203131 .GCC: (Ubuntu 11
0010 2e342e30 2d317562 756e7475 317e3232 .4.0-1ubuntu1~22
0020 2e303429 2031312e 342e3000 .04) 11.4.0.
Contents of section .note.gnu.property:
0000 04000000 10000000 05000000 474e5500 ............GNU.
0010 020000c0 04000000 03000000 00000000 ................
反汇编后不存在代码
add.o
objdump -h add.o
add.o: file format elf64-x86-64
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000018 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .data 00000000 0000000000000000 0000000000000000 00000058 2**0
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 0000000000000000 0000000000000000 00000058 2**0
ALLOC
3 .comment 0000002c 0000000000000000 0000000000000000 00000058 2**0
CONTENTS, READONLY
4 .note.GNU-stack 00000000 0000000000000000 0000000000000000 00000084 2**0
CONTENTS, READONLY
5 .note.gnu.property 00000020 0000000000000000 0000000000000000 00000088 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
6 .eh_frame 00000038 0000000000000000 0000000000000000 000000a8 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
和main.o一样,.data和.bss无数据,.text存在数据
objdump -s -d add.o
add.o: file format elf64-x86-64
Contents of section .text:
0000 f30f1efa 554889e5 897dfc89 75f88b55 ....UH...}..u..U
0010 fc8b45f8 01d05dc3 ..E...].
Contents of section .comment:
0000 00474343 3a202855 62756e74 75203131 .GCC: (Ubuntu 11
0010 2e342e30 2d317562 756e7475 317e3232 .4.0-1ubuntu1~22
0020 2e303429 2031312e 342e3000 .04) 11.4.0.
Contents of section .note.gnu.property:
0000 04000000 10000000 05000000 474e5500 ............GNU.
0010 020000c0 04000000 03000000 00000000 ................
Contents of section .eh_frame:
0000 14000000 00000000 017a5200 01781001 .........zR..x..
0010 1b0c0708 90010000 1c000000 1c000000 ................
0020 00000000 18000000 00450e10 8602430d .........E....C.
0030 064f0c07 08000000 .O......
Disassembly of section .text:
0000000000000000 <add>:
0: f3 0f 1e fa endbr64
4: 55 push %rbp
5: 48 89 e5 mov %rsp,%rbp
8: 89 7d fc mov %edi,-0x4(%rbp)
b: 89 75 f8 mov %esi,-0x8(%rbp)
e: 8b 55 fc mov -0x4(%rbp),%edx
11: 8b 45 f8 mov -0x8(%rbp),%eax
14: 01 d0 add %edx,%eax
16: 5d pop %rbp
17: c3 ret
可以看到在汇编中存在
14: 01 d0 add %edx,%eax
链接脚本
假定将代码段放在0x10000,数据段放在0x80000
SECTIONS {
. = 0x10000;
.text : {*(.text)}
. = 0x80000;
.data : {*(.data)}
.bss : {*(.bss)}
}
链接
ld -e main add.o data.o main.o -T my.ld -o hello.out
-e main:指定从main开始执行,这里将main函数的名字改成其它名字也可以,从main开始执行是glibc规定的,但是这里并没有使用glibc,故可以随意指定开始函数
观察输出文件
objdump -h hello.out
hello.out: file format elf64-x86-64
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000061 0000000000010000 0000000000010000 00001000 2**0
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .eh_frame 00000078 0000000000010068 0000000000010068 00001068 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .note.gnu.property 00000020 00000000000100e0 00000000000100e0 000010e0 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
3 .data 00000008 0000000000080000 0000000000080000 00002000 2**2
CONTENTS, ALLOC, LOAD, DATA
4 .comment 0000002b 0000000000000000 0000000000000000 00002008 2**0
CONTENTS, READONLY
可以看到数据段和代码段都存在数据
反汇编
objdump -s -d hello.out
hello.out: file format elf64-x86-64
Contents of section .text:
10000 f30f1efa 554889e5 897dfc89 75f88b55 ....UH...}..u..U
10010 fc8b45f8 01d05dc3 f30f1efa 554889e5 ..E...].....UH..
10020 48c7c742 00000048 c7c03c00 00000f05 H..B...H..<.....
10030 905dc3f3 0f1efa55 4889e58b 15c3ff06 .].....UH.......
10040 008b05b9 ff060089 d689c7e8 b0ffffff ................
10050 b8000000 00e8beff ffffb800 0000005d ...............]
10060 c3 .
Contents of section .eh_frame:
10068 14000000 00000000 017a5200 01781001 .........zR..x..
10078 1b0c0708 90010000 1c000000 1c000000 ................
10088 78ffffff 18000000 00450e10 8602430d x........E....C.
10098 064f0c07 08000000 1c000000 3c000000 .O..........<...
100a8 70ffffff 1b000000 00450e10 8602430d p........E....C.
100b8 06520c07 08000000 1c000000 5c000000 .R..........\...
100c8 6bffffff 2e000000 00450e10 8602430d k........E....C.
100d8 06650c07 08000000 .e......
Contents of section .note.gnu.property:
100e0 04000000 10000000 05000000 474e5500 ............GNU.
100f0 020000c0 04000000 03000000 00000000 ................
Contents of section .data:
80000 0a000000 14000000 ........
Contents of section .comment:
0000 4743433a 20285562 756e7475 2031312e GCC: (Ubuntu 11.
0010 342e302d 31756275 6e747531 7e32322e 4.0-1ubuntu1~22.
0020 30342920 31312e34 2e3000 04) 11.4.0.
Disassembly of section .text:
0000000000010000 <add>:
10000: f3 0f 1e fa endbr64
10004: 55 push %rbp
10005: 48 89 e5 mov %rsp,%rbp
10008: 89 7d fc mov %edi,-0x4(%rbp)
1000b: 89 75 f8 mov %esi,-0x8(%rbp)
1000e: 8b 55 fc mov -0x4(%rbp),%edx
10011: 8b 45 f8 mov -0x8(%rbp),%eax
10014: 01 d0 add %edx,%eax
10016: 5d pop %rbp
10017: c3 ret
0000000000010018 <exit>:
10018: f3 0f 1e fa endbr64
1001c: 55 push %rbp
1001d: 48 89 e5 mov %rsp,%rbp
10020: 48 c7 c7 42 00 00 00 mov $0x42,%rdi
10027: 48 c7 c0 3c 00 00 00 mov $0x3c,%rax
1002e: 0f 05 syscall
10030: 90 nop
10031: 5d pop %rbp
10032: c3 ret
0000000000010033 <main>:
10033: f3 0f 1e fa endbr64
10037: 55 push %rbp
10038: 48 89 e5 mov %rsp,%rbp
1003b: 8b 15 c3 ff 06 00 mov 0x6ffc3(%rip),%edx # 80004 <data2>
10041: 8b 05 b9 ff 06 00 mov 0x6ffb9(%rip),%eax # 80000 <data1>
10047: 89 d6 mov %edx,%esi
10049: 89 c7 mov %eax,%edi
1004b: e8 b0 ff ff ff call 10000 <add>
10050: b8 00 00 00 00 mov $0x0,%eax
10055: e8 be ff ff ff call 10018 <exit>
1005a: b8 00 00 00 00 mov $0x0,%eax
1005f: 5d pop %rbp
10060: c3 ret
MCU上使用链接脚本
MEMORY {
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
RAM (rwx): ORIGIN = 0x20000000, LENGTH = 128K
}
SECTIONS {
.text : {
*(.text)
*(.rodata)
} > FLASH
__text_end = .;
.data : AT(__text_end){
*(.data)
} > RAM
.bss (NOLOAD):
{
*(.bss)
} > RAM AT>RAM
}
MEMORY 块
MEMORY
块定义了链接器可用来放置段的内存区域
FLASH
区域:- 属性:
rx
(可读、可执行) - 起始地址:
0x08000000
- 长度:
512K
- 属性:
RAM
区域:- 属性:
rwx
(可读、可写、可执行) - 起始地址:
0x20000000
- 长度:
128K
- 属性:
SECTIONS 块
SECTIONS
块定义了程序中不同段在内存中的布局
.text段
.text : {
*(.text)
} > FLASH
内容: 包含所有输入文件中的 .text
段(*(.text)
) 和.rodata
段(*(.rodata)
)
VMA分配:
.text
段被放置在FLASH
区域。这意味着它的 VMA(运行时地址)从FLASH
中的当前地址计数器(.
)开始
LMA:
- 由于未指定
AT
属性,LMA 默认为 VMA。因此,.text
段的 LMA 和 VMA 都在FLASH
中 - 代码不需要复制到 RAM;它直接在
FLASH
中运行
.data段
.data : AT(__text_end){
*(.data)
} > RAM
内容: 包含所有输入文件中的 .data
段(*(.data)
)
VMA分配:
.data
段被放置在RAM
区域。这意味着它的 VMA(运行时地址)从RAM
中的当前地址计数器(.
)开始
LMA分配:
.data
段的 LMA 被设置为__text_end
,即.text
段在FLASH
中的结束地址.data
段的初始化数据紧接在.text
段之后存储在FLASH
中
:::color1
在运行时,这些变量需要位于 RAM 中,以便被读取和修改,但是初始值配存储与PFLASH中,故在MCU启动时, 初始值从其在 FLASH
中的 LMA 复制到其在 RAM
中的 VMA
:::
.bss段
.bss (NOLOAD):
{
*(.bss)
} > RAM AT>RAM
内容: 包含所有输入文件中的 .bss
段(*(.bss)
),.bss
段包含未初始化的全局和静态变量,这些变量被初始化为零
NOLOAD
属性告诉链接器,该段不应在输出文件中占用空间
VMA分配:
.bss
段被放置在RAM
区域。其 VMA 从RAM
中的当前地址计数器开始
LMA分配(AT>RAM):
- LMA 也被分配到
RAM
区域。由于 LMA 区域与 VMA 区域相同,LMA 和 VMA 相等
MEMORY块
MEMORY
{
name [(attr)] : ORIGIN = origin, LENGTH = len
…
}
- attr 字符是一个可选的属性列表,用于指定是否对链接器脚本中未显式映射的输入段使用特定的内存区域
R
只读段W
可写段X
可执行段A
可分配段I
已初始化段L
类似于I
!
反转后面所有属性
origin
是内存区域起始地址的数值表达式。表达式的计算结果必须为常量,并且不能包含任何符号len
是内存区域的字节大小的表达式。与原始表达式一样,表达式必须仅为数值,并且必须计算为常量
output section描述
section [address] [(type)] :
[AT(lma)]
[ALIGN(section_align) | ALIGN_WITH_INPUT]
[SUBALIGN(subsection_align)]
[constraint]
{
output-section-command
output-section-command
…
} [>region] [AT>lma_region] [:phdr :phdr …] [=fillexp]
- Output Section Type: 输出段类型
- Output Section LMA: 输出段LMA —加载地址
- Forced Output Alignment: 强制输出对齐
- Forced Input Alignment: 强制输入对齐
- Output Section Constraint: 输出段限制
- Output Section Region: 输出段区域
- Output Section Phdr: 输出段phdr
- Output Section Fill: 输出段填充
Output Section LMA
每个段有一个虚拟地址(VMA)和一个加载地址(LMA),加载地址由 AT
或 AT>
关键字指定
AT
关键字把一个表达式当作自己的参数。这将指定段的实际加载地址
关键字 AT>
使用内存区域的名字作为参数
AT(lma)
用于明确指定输出段的加载地址(LMA)
AT>lma_region
用于将输出段的加载地址分配到**指定的内存区域, 由链接器根据内存区域自动计算实际的加载地址 **
[!NOTE]
如果没有为可分配段使用 AT 和 AT>,链接器会使用下面的方式尝试来决定加载地址:
- 如果段有一个特定的VMA地址,则LMA也使用该地址。
- 如果段为不可分配的则LMA被设置为它的VMA