LV16-07-开发工具-04-ld链接文件

本文主要是STM32开发——链接文件分析的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
Windows windows11
Ubuntu Ubuntu16.04的64位版本
VMware® Workstation 16 Pro 16.2.3 build-19376536
SecureCRT Version 8.7.2 (x64 build 2214) - 正式版-2020年5月14日
开发板 正点原子 i.MX6ULL Linux阿尔法开发板
uboot NXP官方提供的uboot,NXP提供的版本为uboot-imx-rel_imx_4.1.15_2.1.0_ga(使用的uboot版本为U-Boot 2016.03)
linux内核 linux-4.15(NXP官方提供)
STM32开发板 正点原子战舰V3(STM32F103ZET6)
点击查看本文参考资料
  • 通用
分类 网址说明
官方网站https://www.arm.com/ARM官方网站,在这里我们可以找到Cotex-Mx以及ARMVx的一些文档
https://www.st.com/content/st_com/zh.htmlST官方网站,在这里我们可以找到STM32的相关文档
https://www.stmcu.com.cn/意法半导体ST中文官方网站,在这里我们可以找到STM32的相关中文参考文档
http://elm-chan.org/fsw/ff/00index_e.htmlFatFs文件系统官网
教程书籍《ARM Cortex-M3权威指南》ARM公司专家Joseph Yiu(姚文祥)的力作,中文翻译是NXP的宋岩
《ARM Cortex-M0权威指南》
《ARM Cortex-M3与Cortex-M4权威指南》
开发论坛http://47.111.11.73/forum.php开源电子网,正点原子的资料下载及问题讨论论坛
https://www.firebbs.cn/forum.php国内Kinetis开发板-野火/秉火(刘火良)主持的论坛,现也做STM32和i.MX RT
https://www.amobbs.com/index.php阿莫(莫进明)主持的论坛,号称国内最早最火的电子论坛,以交流Atmel AVR系列单片机起家,现已拓展到嵌入式全平台,其STM32系列帖子有70W+。
http://download.100ask.net/index.html韦东山嵌入式资料中心,有些STM32和linux的相关资料也可以来这里找。
博客参考http://www.openedv.com/开源网-原子哥个人博客
http://blog.chinaaet.com/jihceng0622博主是原Freescale现NXP的现场应用工程师
cortex-m-resources这其实并不算是一个博客,这是ARM公司专家Joseph Yiu收集整理的所有对开发者有用的官方Cortex-M资料链接(也包含极少数外部资源链接)
  • STM32
STM32STM32 HAL库开发实战指南——基于F103系列开发板野火STM32开发教程在线文档
STM32库开发实战指南——基于野火霸道开发板野火STM32开发教程在线文档
  • SD卡
SD Association提供了SD存储卡和SDIO卡系统规范
点击查看相关文件下载
STM32F103xx英文数据手册STM32F103xC/D/E系列的英文数据手册
STM32F103xx中文数据手册STM32F103xC/D/E系列的中文数据手册
STM32F10xxx英文参考手册(RM0008)STM32F10xxx系列的英文参考手册
STM32F10xxx中文参考手册(RM0008)STM32F10xxx系列的中文参考手册
Arm Cortex-M3 处理器技术参考手册-英文版Cortex-M3技术参考手册-英文版
STM32F10xxx Cortex-M3编程手册-英文版(PM0056)STM32F10xxx/20xxx/21xxx/L1xxxx系列Cortex-M3编程手册-英文版
SD卡相关资料——最新版本有关SD卡的一些资料可以从这里下载
SD卡相关资料——历史版本有关SD卡的一些历史版本资料可以从这里下载,比如后边看的SD卡2.0协议
SD 2.0 协议标准完整版这是一篇关于SD卡2.0协议的中文文档,还是比较有参考价值的,可以一看

这篇笔记主要是STM32CubeMX软件生成的STM32F103ZETx_FLASH.ld链接文件的学习笔记。这篇笔记按照顺序从上到下分析STM32F103ZETx_FLASH.ld文件。关于GNU链接脚本的详细语法我们可以参考这里:Documentation for binutils 2.40 (sourceware.org),这里好像还有在线的文档。【注意】生成Makefile文件的话,链接脚本是自动生成的,并且每一次重新生成的时候,链接脚本将会重新生成,也就是说,若是我们修改了链接脚本内容的话,只要我们通过STM32CubeMX更新工程,我们修改的部分都会被删除。

一、参考资料

关于链接脚本,其实在GNU的官方文档中是有相关的内容的。

GNU与GCC什么关系?这里简单了解一下吧,至少知道为什么我们找链接脚本的语法要到GNU的官网去,我查了下维基百科,是这样说的:

GNU编译器套装(英语:GNU Compiler Collection,缩写为GCC)是GNU计划制作的一种优化编译器,支持各种编程语言操作系统计算机系统结构。该编译器是以GPLLGPL许可证所发行的自由软体,也是GNU计划的关键部分,还是GNU工具链的主要组成部份之一。GCC(特别是其中的C语言编译器)也常被认为是跨平台编译器的事实标准。1985年由理查德·马修·斯托曼开始发展,现在由自由软体基金会负责维护工作。截至2019年,GCC大约有1500万行代码,是现存最大的自由程序之一。[3] 它在自由软件的发展中发挥了重要作用,不仅是一个工具,还是一个典例。

原名为GNU C语言编译器GNU C Compiler),因为它原本只能处理C语言。同年12月,新的GCC编译器可以编译C++语言。后来又为FortranPascalObjective-CJavaAdaGo等其他语言开发了前端。C和C++编译器也支持OpenMPOpenACC规范。

GCC编译器已经被移植到比其他编译器更多的平台和指令集架构上,并被广泛部署在开发自由和专有软件的工具中。GCC还可用于许多嵌入式系统,包括基于ARMPower ISA的芯片。

GCC不仅是GNU操作系统的官方编译器,还是许多类UNIX系统和Linux发行版的标准编译器。BSD家族中的大部分操作系统也在GCC发布之后转用GCC;不过FreeBSD、OpenBSD和Apple macOS已经转向了Clang编译器[4],主要是因为许可问题。[5][6][7]GCC也可以编译WindowsAndroidiOSSolarisHP-UXIBM AIXDOS系统的代码。GCC原本用C开发,后来因为LLVMClang的崛起,它更快地将开发语言转换为C++。许多C的爱好者在对C++一知半解的情况下主观认定C++的性能一定会输给C,但是Ian Lance Taylor给出了不同的意见,并表明C++不但性能不输给C,而且能设计出更好,更容易维护的程式[8][9]

二、一个完整的链接文件

点击查看 STM32F103ZETx_FLASH.ld 文件完整内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
/* Entry Point */
ENTRY(Reset_Handler)

/* Highest address of the user mode stack */
_estack = ORIGIN(RAM) + LENGTH(RAM); /* end of RAM */
/* Generate a link error if heap and stack don't fit into RAM */
_Min_Heap_Size = 0x200; /* required amount of heap */
_Min_Stack_Size = 0x400; /* required amount of stack */

/* Specify the memory areas */
MEMORY
{
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 64K
FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 512K
}

/* Define output sections */
SECTIONS
{
/* The startup code goes first into FLASH */
.isr_vector :
{
. = ALIGN(4);
KEEP(*(.isr_vector)) /* Startup code */
. = ALIGN(4);
} >FLASH

/* The program code and other data goes into FLASH */
.text :
{
. = ALIGN(4);
*(.text) /* .text sections (code) */
*(.text*) /* .text* sections (code) */
*(.glue_7) /* glue arm to thumb code */
*(.glue_7t) /* glue thumb to arm code */
*(.eh_frame)

KEEP (*(.init))
KEEP (*(.fini))

. = ALIGN(4);
_etext = .; /* define a global symbols at end of code */
} >FLASH

/* Constant data goes into FLASH */
.rodata :
{
. = ALIGN(4);
*(.rodata) /* .rodata sections (constants, strings, etc.) */
*(.rodata*) /* .rodata* sections (constants, strings, etc.) */
. = ALIGN(4);
} >FLASH

.ARM.extab : { *(.ARM.extab* .gnu.linkonce.armextab.*) } >FLASH
.ARM : {
__exidx_start = .;
*(.ARM.exidx*)
__exidx_end = .;
} >FLASH

.preinit_array :
{
PROVIDE_HIDDEN (__preinit_array_start = .);
KEEP (*(.preinit_array*))
PROVIDE_HIDDEN (__preinit_array_end = .);
} >FLASH
.init_array :
{
PROVIDE_HIDDEN (__init_array_start = .);
KEEP (*(SORT(.init_array.*)))
KEEP (*(.init_array*))
PROVIDE_HIDDEN (__init_array_end = .);
} >FLASH
.fini_array :
{
PROVIDE_HIDDEN (__fini_array_start = .);
KEEP (*(SORT(.fini_array.*)))
KEEP (*(.fini_array*))
PROVIDE_HIDDEN (__fini_array_end = .);
} >FLASH

/* used by the startup to initialize data */
_sidata = LOADADDR(.data);

/* Initialized data sections goes into RAM, load LMA copy after code */
.data :
{
. = ALIGN(4);
_sdata = .; /* create a global symbol at data start */
*(.data) /* .data sections */
*(.data*) /* .data* sections */

. = ALIGN(4);
_edata = .; /* define a global symbol at data end */
} >RAM AT> FLASH


/* Uninitialized data section */
. = ALIGN(4);
.bss :
{
/* This is used by the startup in order to initialize the .bss secion */
_sbss = .; /* define a global symbol at bss start */
__bss_start__ = _sbss;
*(.bss)
*(.bss*)
*(COMMON)

. = ALIGN(4);
_ebss = .; /* define a global symbol at bss end */
__bss_end__ = _ebss;
} >RAM

/* User_heap_stack section, used to check that there is enough RAM left */
._user_heap_stack :
{
. = ALIGN(8);
PROVIDE ( end = . );
PROVIDE ( _end = . );
. = . + _Min_Heap_Size;
. = . + _Min_Stack_Size;
. = ALIGN(8);
} >RAM



/* Remove information from the standard libraries */
/DISCARD/ :
{
libc.a ( * )
libm.a ( * )
libgcc.a ( * )
}

.ARM.attributes 0 : { *(.ARM.attributes) }
}

三、各部分说明

1. 入口与堆栈

1
2
3
4
5
6
7
8
/* Entry Point */
ENTRY(Reset_Handler)

/* Highest address of the user mode stack */
_estack = ORIGIN(RAM) + LENGTH(RAM); /* end of RAM */
/* Generate a link error if heap and stack don't fit into RAM */
_Min_Heap_Size = 0x200; /* required amount of heap */
_Min_Stack_Size = 0x400; /* required amount of stack */

(1)第2行:指定入口地址。

(2)第5行:RAM的结束地址

(3)第7、8行:指定堆的大小和栈的大小分别为0x200和0x400。

2. 内存布局

2.1 MEMORY

1
2
3
4
5
MEMORY
{
  <name> [(<attr>)] : ORIGIN = <origin>, LENGTH = <len>
  ...
}

(1)MEMORY关键字用于描述一个MCU ROM和RAM的内存地址分布(Memory Map),MEMORY中所做的内存描述主要用于SECTIONS中LMA和VMA的定义。

(2)name :是所要定义的内存区域的名字。

(3)attr :是可选的,并不重要,具体用法可参考GNU Linker的语法说明。

(4)origin :是其起始地址。

(5)len :为内存区域的大小,MEMORY语法中可以使用如K、M和G这样的内存单位。

【注意】ORIGIN可以写为org,而LENGTH可以写为l。

2.2 实例分析

1
2
3
4
5
6
/* Specify the memory areas */
MEMORY
{
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 64K
FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 512K
}

这一部分给出地址的划分区间。

(1)RAM和FLASH表示内存名称;

(2)后边的括号表示权限(x:可运行;r:可读;w:可写);

(3)ORIGIN表示起始地址,LENGTH表示这段内存区域的长度。可以看到这里RAM是可运行可读可写,而FLASH只可运行可读。我们知道STM32的FLASH是可以写的,但这里的权限只针对链接脚本,并不代表代码执行。

我们可以在这里增加一个由malloc使用的MALLOC段,放在外部SRAM上,地址0x68000000:

1
2
3
4
5
6
7
/* Specify the memory areas */
MEMORY
{
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 64K
FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 512K
MALLOC(rw) : ORIGIN = 0x68000000, LENGTH = 512K
}

3. 段的定义

3.1 SECTIONS

3.1.1 框架

1
2
3
4
5
6
SECTIONS
{
  <sections−command>
  <sections−command>
  ...
}

(1)SECTIONS关键字用于定义output section(输出段)的相应input section(输入段)、LMA和VMA,是整个连接脚本中最为重要的部分。注:output section是实际存储在内存中的“段”,而input section是其构成成员,如.data为数据段,由所有全局变量构成(默认情况下);.text为代码段,由所有函数构成(默认情况下)。

3.1.2 command

主要的部分是 sections−command ,SECTIONS{ }只是属于框架。 sections−command 的语法如下:

1
2
3
4
5
6
7
8
9
10
11
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] [,]

我们从使用的角度来讲解其语法:(假设有一个全局变量myData,我们用#pragma section命令将其定义为.myData段(input section))

(1)我们首先可以定义output section的名字,随便什么都可以,比如.my_data;

(2)然后我们可以定义其构成成员,*(.myData);

(3)接下来我们就要指定 .my_data 的LMA和VMA了,有4种方法:

1
2
3
4
5
6
7
[<address>] + [AT(<lma>)]

[<address>] + [AT><lma region>]

[><region>] + [AT><lma region>]

[><region>] + [AT(<lma>)]

但是要注意这些用法的不同:[\<address>] 和 [AT(<lma>)]必须指定具体的地址,而\ [><region>] 和\ [AT><lma region>]只需指定内存空间,具体地址紧接着上一个output section的末尾地址。

经过以上步骤,我们得出如下section定义:(这里只列出2种)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
SECTIONS
{
  .my_data ( 0xD0004000 ) : AT ( 0x80000020 )
  {
    *(.myData)
  }
  ...
}
/* 或者 */
SECTIONS
{
  .my_data :
  {
    *(.myData)
  } > ram AT> rom
  ...

}

3.2 段的作用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* Define output sections */
SECTIONS
{
/* 中间部分省略...*/
.text :
{
. = ALIGN(4);
*(.text) /* .text sections (code) */
*(.text*) /* .text* sections (code) */
*(.glue_7) /* glue arm to thumb code */
*(.glue_7t) /* glue thumb to arm code */
*(.eh_frame)

KEEP (*(.init))
KEEP (*(.fini))

. = ALIGN(4);
_etext = .; /* define a global symbols at end of code */
} >FLASH
/* 中间部分省略...*/
}

这里就是链接脚本的核心功能,我们先不考虑每个段的功能,先了解一下链接脚本到底用来干什么。因为一个工程内可能有多个.c文件,比如代码里面一共3个文件:分别是 main.c 、usart.c 、 start.s 。很明显一个主程序,一个串口驱动,一个启动文件。在实际编译中,会把它们分别编译为 main.o 、usart.o 、 start.o ,假如每个.o文件中都包含.text、.data、.bss段,但是我们知道,程序最后烧入MCU是只有一个文件,这里就涉及到段的合并,也就是上面的命令所实现的功能,把每个.o文件中的相同段合一。

  • 一般的程序中包含常见的几个段:text(存放程序),rodata(存放被初始化的数据),data(表示初始化不为0的变量),bss(表示初始化值为默认的全局变量)。
  • text,rodata放在flash中,而data中的初始化值作为rodata放在flash中,变量在ram中占有空间,bss占ram空间。

3.2 isr_vector

1
2
3
4
5
6
7
/* The startup code goes first into FLASH */
.isr_vector :
{
. = ALIGN(4);
KEEP(*(.isr_vector)) /* Startup code */
. = ALIGN(4);
} >FLASH

这一部分表示中断向量表被链接到段 .isr_vector 内。

(1)点( . ) :表示当前虚拟内存地址(location counter)

(2)ALIGN(4) :表示四字节对齐。

(3)KEEP :这个命令来保存所有文件中的 .isr_vector 内容,即使它们没有被调用。也就是在保存中断向量前做四字节对齐,保存后做四字节对齐。

(3)>FLASH :内容存在上面MEMORY中定义的FLASH中。

3.3 text

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* The program code and other data goes into FLASH */
.text :
{
. = ALIGN(4);
*(.text) /* .text sections (code) */
*(.text*) /* .text* sections (code) 所有代码段*/
*(.glue_7) /* glue arm to thumb code ARM与THUMB相互调用生成的段 */
*(.glue_7t) /* glue thumb to arm code ARM与THUMB相互调用生成的段*/
*(.eh_frame) /* 用于链接异常生成的段 */

KEEP (*(.init)) /* 构造析构相关 */
KEEP (*(.fini)) /* 构造析构相关 */

. = ALIGN(4);
_etext = .; /* define a global symbols at end of code 把当前虚拟地址赋值给_etext,这是一个全局变量 */
} >FLASH

第一个冒号左边的.text就是合并后的.text段,它是由符合花括号内规则的所有内容合并得到的,在刚才的例子里就是 main.o 、usart.o 、 start.o 中的.text段。* 号代表通配符,这里它没有前缀和后缀,也没有 [ ] 内容修饰,所以它代表所有匹配文件。

  • . = ALIGN(4); :是指4字节对齐。
  • . :小数点表示当前的地址位置。
  • eh_frame :段可以自定义,由于编译obj过程中不会生成用户自定义的段,因此在源码中需要指定需要特殊处理的段。
  • 结尾的 >FLASH指上面花括号内的内容都放在第二部分中定义的FLASH空间中。

剩下的暂时就不说了,详细的语法结构,我们可以看ld.pdf (sourceware.org)的3.6 SECTIONS Command一节,有超级详细的说明。

四、多个链接脚本

我们的工程中只能有一个链接脚本吗?一开始我以为只能有一个,后来发现,其实可以有多个的,最终他们都会汇集到一起。比如,后边我们学习SRAM的时候,可能会使用链接脚本来定义一个段,然后预存数组到指定的地址。我们直接在STM32CubeMX生成的链接脚本中添加也是没问题的,但是就一个坏处,那就是每次STM32CubeMX更新工程的时候,这个脚本都会重新生成,我们添加的东西也都不复存在,所以我们可以自己新建一个链接脚本,然后在编译的时候一起编译就好啦。

1. 新建链接脚本 mine.ld

1
2
3
4
5
6
7
8
9
10
11
12
13
SECTIONS
{
.sram :
{
. = ALIGN(4);
__SRAM_SYMBOLS = .; /* create a global symbol at ccmram start */
*(.sram)
*(.sram*)

. = ALIGN(4);
__SRAM_SYMBOLS = .; /* create a global symbol at ccmram end */
} >SRAM AT> FLASH
}

2. 修改Makefile

我们需要把我们自己的链接脚本也添加到Makefile中:

1
2
3
4
5
6
#######################################
# LDFLAGS
#######################################
# link script
LDSCRIPT = STM32F103ZETx_FLASH.ld mine.ld

这样我们那就不用担心更新工程的时候把我们添加的内容清理掉啦。