GCC链接脚本.ld 文件详解
关于eclipse搭建stm32开发环境的内容请移步:【windows下基于Eclipse搭建stm32开发环境】
关于eclipse + stm32 + hex和bin文件下载请移步:【windows下基于Eclipse搭建stm32开发环境(2)】hex和bin文件下载到单片机
~~ 原创文章,转载请注明出处 !
一、链接脚本的配置:
当我们用eclipse插件创建一个stm32工程的时候,会发现其中有三个.ld文件
这就是GCC的链接器的链接脚本文件,如下图所示:
那么链接文件是在哪里引用的 呢?看下一工程配置:
因为这里配置了链接脚本文件的路径和文件名,所以编译工程的时候才会正确的找到链接脚本。
其中mem.ld和sections.ld就是对应我们stm32的链接脚本文件,libs.ld是一个空文件,后续扩展使用,可以不管
二、mem.ld文件讲解
打开mem.ld文件:
1、MEMORY命令:
是GCC链接器支持语法中的内存块配置命令,一个连接脚本最多一个’MEMORY’命令。
MEMORY语法如下:
MEMORY
{
NAME [(ATTR)] : ORIGIN = ORIGIN, LENGTH = LEN
...
}
NAME是内存区域的名字,可任意编写,尽在链接脚本中有实际意义。
区域名存储在一个单独的名字空间中,它不会和符号名,文件名,节名产生冲突,每一块内存区域必须有一个唯一的名字。
ATTR字符串是属性列表,它指出是否为一个没有在连接脚本中进行显式映射地输入段使用一个特定的内存区域。
如果你没有为某些输入段指定一个输出段,连接器会创建一个跟输入段同名的输出段。如果你定义了区域属性,连接器会使用它们来为它创建的输出段选择内存区域。
ATTR字符串必须包含下面字符中的一个,且必须只包含一个:
‘R’:只读节。
‘W’:可读写节。
‘X’:可执行节。
‘A’:可分配节。
‘I’:已初始化节。
‘L’:同‘I’
‘!’:对前一个属性值取反。
A、 如果一个未映射节匹配了上面除’!'之外的一个属性,它就会被放入该内存区域。 '!'属性对该测试取反,所以只有当它不匹配上面列出的行何属性时,一个未映射节才会被放入到内存区域。
ORIGIN
是内存区域地始地址(可以是表达式)。
在内存分配执行之前,这个表达式必须被求值产生一个常数,所以不可以使用任何节相关的符号。可简写为’org’或’o’(不可以写为 ‘ORG’)
LEN
内存区域长度(以字节为单位)的表达式。这个表达式在分配执行前也必须被求得为一个常数值。所以不可以使用任何节相关的符号。可简写为‘len’或’l’。
在下面的例子中,我们指定两个可用于分配的内存区域:
一个从0开始,有256kb长度,另一个从0x4000000开始,有4mb长度。连接器会把那些没有进行显式映射且是只读或可执行的节放到’rom’内存区域。并会把另外的没有被显式映射地节放入到’ram’内存区域。
MEMORY
{
rom(rx) :ORIGIN=0, LENGTH=256K
ram (!rx):org =0x40000000,l=4M
}
一旦你定义了一个内存区域,你也可以指示连接器把指定的输出段放入到这个内存区域中,这可以通过使用’>REGION’输出段属性。
比如,如果你有一个名为’mem’的内存区域,你可以在输出段定义中使用’>mem’。
如果没有为输出段指定地址,连接器就会把地址设置为内存区域中的下一个可用的地址。
如果总共的映射到一个内存区域的输出段对于区域来说太大了,连接器会提示一条错误信息。
了解了MEMORY命令的用法后,mem.ld文件的代码也很容易看懂了,就是定义了两个存储区,一个只读的Flash,和一个可读写的RAM
三、sections.ld文件讲解:
接下来打开sections.ld文件
这里有一个PROVIDE命令,如下图所示:
该命令定义了4个符号:
_Main_Stack_Size,_Main_Stack_Limit,_Heap_Begin,_Heap_Limit
分别表示栈的最大最小值,和堆的起始地址
1、PROVIDE关键字语法:
想象某些情况下, 一个符号被引用到的时候只在连接脚本中定义,而不在任何一个被连接进来的目标文件中定义.这种做法比较明智.比如, 传统的连接器定义了一个符号’etext’. ANSI C 需要用户能够把’etext’作为一个函数使用,这时不会产生错误.
‘PROVIDE’关键字可以被用来定义一个符号,比如’etext’, 这个定义只在它被引用到的时候有效,而在它被定义的时候无效.
语法是 :
`PROVIDE(SYMBOL = EXPRESSION)'.
下面是一个关于使用’PROVIDE’定义’etext’的例子:
SECTIONS
{
.text :
{
*(.text)
_etext = .;
PROVIDE(etext = .);
}
}
在这个例子中, 如果程序定义了一个’_etext’(带有一个前导下划线), 连接器会给出一个重定义错误. 如果,程序定义了一个’etext’(不带前导下划线), 连接器会默认使用程序中的定义. 如果程序引用了’etext’但不定义它,连接器会使用连接脚本中的定义
总结下来就是:
PROVIDE定义的符号,允许C语言中重定义,重定义后优先使用C中的定义
也就是说_Main_Stack_Size,_Main_Stack_Limit,_Heap_Begin,_Heap_Limit这四个变量我们可以在C程序中重定义
2、ENTRY命令:
接下来是一个ENTRY命令,如下图所示
运行一个程序时第一个被执行到的指令称为"入口点",默认是start,可以使用’ENTRY’连接脚本命令来设置入口点.参数是一个符号名:
ENTRY(SYMBOL)
有多种不同的方法来设置入口点.连接器会尝试以下顺序来设置入口点, 如果成功了,就会停止.
- ‘-e‘入口命令行选项;
- 连接脚本中的`ENTRY(SYMBOL)'命令;
- 如果定义了start, 就使用start的值;
- 使用’.text’节的首地址;
- 地址`0’
因为STM32的.text段的头部是向量表,0地址存放是的栈顶地址,不是程序入口,所以需要重新来指定入口地址,其实对于STM32来说入口地址只要是写在复位中断中就可以,链接脚本中的声明主要是为了调试和仿真
3、SECTIONS命令:
接下来就是【段】命令了,这也是链接脚本中最重要的部分
段命令为sections{} ,段中又包含多个【节】
SECTIONS命令告诉连接器如何把输入节映射到输出节, 并如何把输入节放入到内存中.isr_vector节。
4、ALTGN命令:
5、FILL命令:
用‘FILL’命令来为当前节设置填充样式。它后面跟有一个括号中的表达式。任何未指定的节内内存区域(比如,因为输入节的对齐要求而造成的裂缝)会以这个表达式的值进行填充。
一个’FILL’语句会覆盖到它本身在节定义中出现的位置后面的所有内存区域;通过引入多个‘FILL’语句,可以在输出节的不同位置拥有不同的填充样式。
如何在未被指定的内存区域填充’0x90’:
FILL(0x90909090)
‘FILL’命令跟输出节的‘=FILLEXP’属性相似,但它只影响到节内跟在‘FILL’命令后面的部分,而不是整个节。
如果两个都用到了,那‘FILL’命令优先
6、ABSOLUTE命令:
7、KEEP()命令:
如下图所示,keep命令,主要作用是防止垃圾收集机制把这两个重要的节排除在外,另外呢也保证了向量表在段中的位置处于最顶端
与STM32单片机不同的是,飞思卡尔出品的单片机虽然也有类似向量表的东西,但是有所不同,以.cfmconfig为段名作为标识,因为我们是开发STM32,所以没有用到这个段,可以忽略
那么(.isr_vector)是啥意思呢?*
首先搜索一下,看看工程中哪里定义的:
找到.isr_vector定义的地方,猛的一看,好像看不懂,这里出现一个__attribute__
8、补充一下__attribute__机制的知识:
attribute 机制是GUN C的一大特色,主要作用是设置函数属性、变量属性、和类型属性
attribute 书写特征是:
attribute 前后都有两个下划线,并切后面会紧跟一对原括弧,括弧里面是相应的__attribute__ 参数。
attribute 语法格式为:attribute ((attribute-list))
函数属性(Function Attribute)
noreturn
noinline
always_inline
pure
const
nothrow
sentinel
format
format_arg
no_instrument_function
section
constructor
destructor
used
unused
deprecated
weak
malloc
alias
warn_unused_result
nonnull
类型属性(Type Attributes)
aligned
packed
transparent_union,
unused,
deprecated
may_alias
变量属性(Variable Attribute)
aligned
packed
attribute 的常参数介绍:
8.1、 attribute 参数: aligned
指定对象的对齐格式(以字节为单位),如:
1. struct S {
2.
3. short b[3];
4.
5. } __attribute__ ((aligned (8)));
6.
7.
8. typedef int int32_t __attribute__ ((aligned (8)));
该声明将强制编译器确保(尽它所能)变量类 型为struct S 或者int32_t 的变量在分配空间时采用8字节对齐方式。
8.2、attribute 参数: packed
使用该属性对struct 或者union 类型进行定义,设定其类型的每一个变量的内存约束。就是告诉编译器取消结构在编译过程中的优化对齐(使用1字节对齐),按照实际占用字节数进行对齐,是GCC特有的语法。
下面的例子中,packed_struct 类型的变量数组中的值将会紧紧的靠在一起,但内部的成员变量s 不会被“pack” ,如果希望内部的成员变量也被packed 的话,unpacked-struct 也需要使用packed 进行相应的约束。
struct unpacked_struct
{
char c;
int i;
};
struct packed_struct
{
char c;
int i;
struct unpacked_struct s;
}__attribute__ ((__packed__));
在GCC下:struct my{ char ch; int a; sizeof(int)=4;sizeof(my)=8(非紧凑模式)
8.3、attribute 参数: at
绝对定位,可以把变量或函数绝对定位到Flash中,或者定位到RAM。
1)、定位到flash中,一般用于固化的信息,如出厂设置的参数,上位机配置的参数,ID卡的ID号,flash标记等等
const u16 gFlashDefValue[512] __attribute__((at(0x0800F000))) = {0x1111,0x1111,0x1111,0x0111,0x0111,0x0111};
//定位在flash中,其他flash补充为00
const u16 gflashdata__attribute__((at(0x0800F000))) = 0xFFFF;
2)、定位到RAM中,一般用于数据量比较大的缓存,如串口的接收缓存,再就是某个位置的特定变量
u8 USART2_RX_BUF[USART2_REC_LEN] attribute ((at(0X20001000)));//接收缓冲,最大USART_REC_LEN个字节,起始地址为0X20001000.
注意:
1)、绝对定位不能在函数中定义,局部变量是定义在栈区的,栈区由MDK自动分配、释放,不能定义为绝对地址,只能放在函数外定义。
2)、定义的长度不能超过栈或Flash的大小,否则,造成栈、Flash溢出。
8.4、attribute 参数: section
提到section,就得说RO RI ZI了,在ARM编译器编译之后,代码被划分为不同的段,RO Section(ReadOnly)中存放代码段和常量,RW Section(ReadWrite)中存放可读写静态变量和全局变量,ZI Section(ZeroInit)是存放在RW段中初始化为0的变量。
于是本文的大体意思就清晰了,attribute((section(“section_name”))),其作用是将作用的函数或数据放入指定名为"section_name"对应的段中。
1)、编译时为变量指定段:
1.
2. /* in RO section */
3. const int descriptor[3] __attribute__ ((section ("descr"))) = { 1,2,3 };
4. /* in RW section */
5. long long rw[10] __attribute__ ((section ("RW")));
6. /* in ZI section */
7. long long altstack[10] __attribute__ ((section ("STACK"), zero_init));
2)、编译时为函数指定段
下面例子的意思是Function_Attributes_section_0被放到只读段new_section,而不是代码段.text
void Function_Attributes_section_0 (void) __attribute__ ((section ("new_section")));
void Function_Attributes_section_0 (void)
{
static int aStatic =0;
aStatic++;
}
第二个例子:
#pragma arm section code="foo"
int f2()
{
return 1;
} // into the 'foo' area
__attribute__ ((section ("bar"))) int f3()
{
return 1;
} // into the 'bar' area
int f4()
{
return 1;
} // into the 'foo' area
#pragma arm section
8.5、attribute 参数: used 和 unused
#define __attribute_used__ __attribute__((__used__))
表示该函数或变量可能不使用,这个属性可以避免编译器产生警告信息。
#define __attribute_unused__ __attribute__((__unused__))
向编译器说明这段代码有用,即使在没有用到的情况下编译器也不会警告!
8.5、attribute 参数: noreturn
该属性通知编译器函数从不返回值。当遇到类似函数还未运行到return语句就需要退出来的情况,该属性可以避免出现错误信息。
8.6、attribute 参数: weak
weak 参数是__attribute__ 机制中的若符号声明,如果有两个同名的函数,而其中一个被声明为弱符号,则不会触发重定义错误,编译时自动跳过若符号声明的代码段
例:
void __attribute__ ((section(".after_vectors"),weak))
PendSV_Handler (void)
{
#if defined(DEBUG)
__DEBUG_BKPT();
#endif
while (1)
{
}
}
这是一个STM32的内部可悬挂异常中断函数,在移植OS时,我们需要重定义该函数,而刚开始建立工程时,就用weak声明若定义
好了,复习过__attribute__机制的用法后,再STM32向量表:
很显然,下面代码的意思是将函数指针数组__isr_vector[]放到段.isr_vector中,并且代码中不使用该数组也不发出警告!
再回过头来开sections.ld链接脚本:
KEEP(*(.isr_vector))的意思就是,在GCC编译汇编之后的链接过程中,把所有名为.isr_vector的段的代码,放到.isr_vector节的最前面,也就是把向量表放在生成的二进制文件的最前面。相信玩儿keil MDK软件或学过STM32单片机的人应该了解这一点。
.isr_vector节的最后一部分*(.after_vectors .after_vectors.*) ,分析方法是一样的,其实就是把stm32内部异常的处理函数放在名为.after_vectors的段,然后链接时放在.isr_vector节的最后面,不再过多赘述
搜索一下.after_vectors段的定义的地方,可以看到,该段都包含了那些代码:
另外在节的最后面还有一个 >Flash :
它的意思是这个段放在名为FLASH的内存块,由于我们mem.ld脚本中已经定义好了内存块儿地址,所以该.isr_vector的编码地址就是FLASH,起始为0x8000000
接下来是.inits节:
.inits节首先是两个小段落,如下图所示:
__data_regions_array_start = .;
这一句的语法很简单,就是定义一个符号,等于当前地址
LOADADDR(.data): 获取.data段的加载地址(lma),也就是data段在Flash中存放的起始地址
ADDR(.data): 获取.data段的绝对地址(也叫运行地址vma),也就是加载到RAM的地址
LONG() 则是在此处存入4字节,括号里是要存入的内容!(BYTE():存入1字节,SHORT()存入2字节,QUAD(): 存入8字节)
那么还有一个问题,为什么会有.data段和.bss段?它们是哪儿来的?
请看下面这张图,GCC在编译C语言文件的时候,会分别生成RO、RW、ZI部分,RO是只读段,也就是程序代码段(.text),就是具体函数代码,RW是读写数据段(.data),也就是初始化的全局变量,ZI为未初始化数据段(.bss),也就是那些未赋初值的变量,这个段不占用内存空间,只有在程序运行的时候,在RAM初始化为0;链接脚本的作用也就是将这些编译出来的段整合到一起
好了,知道上面的语法后,这部分代码的意思就很清晰了:
所以说inits节的前边一部分就是放了一些地址信息。
inits后半段解析如下图所示:
紧接着是:flashtext节:
该节没有什么用,可以直接跳过
下面.text节:
如下图所示:
*(.text ): 所有的编译出来得代码段,都放在这里
.rodata、.constdata 是指常量数据编译出来的代码段
*(.glue_7), *(.glue_7t)这些部分是提供给存储的 ARM/Thumb 互通代码。我正在使用仅支持的 Cortex-M3 CPU thumb 指令集,所以这些部分在我的构建中都是空的。
glue_7 用于 ARM 代码调用 Thumb 代码,
.glue_7t 用于 Thumb 代码调用 ARM 代码。
下面这两个节,没有用到,所以不用管
.data 节:
.data节主要是存放那些初始化过的数据,注意其绝对地址(运行地址)是RAM,加载地址是Flash,意思是这些数据在Flash中存放,运行时地址是基于RAM地址的
STM32在启动后,系统初始化函数_start在进入main函数之前,会做一个数据初始化的事情,就是把.data从Flash中搬运到RAM中。
那么单片机怎么知道.data存放在Flash的什么地址呢?看下图,_sidata记录了.data对应的Flash地址
.bss节:
.bss节在单片机启动后,进入main之前,会在RAM中初始化为0
Section.ld链接文件中,除了向量表、.text、.data、.bss这四个节的内容需要重点关注之外,其余内容可不做深究可参照ld中文手册就行学习:[GCC ld中文手册]
结语
** 更多内容见后续文章 ~~~~**。