我们已经简单学习完IAR和MDK链接文件的基本用法,接下来我们再简单的对ld链接文件做一下介绍。
在这里之前我们需要了解输入段和输出段,输入段就是我需要给链接器的信息,包括各种section,加载地址,链接地址,输出段就是链接器根据输入段的各种信息条件之后输出的东西。(section就是段,段就是section,有时候也叫它节,都可以。)
(注:不管是IAR的icf,还是MDK的scf,还有GNU的ld,大家的功能都是一样的,给链接器提供地址信息,让链接器按照这些地址信息将编译器编出来的section和地址一一对应生成最后MCU需要的可执行文件。所以很多东西都是想通的,唯一不同的是三者提供地址信息的方式不同而已。)
ld链接文件的语法就相对比较复杂了,各种花里胡哨的符号,各种修饰符,反正就是很烦的玩意,建议大家不要深究,不值得,除非你想成为这一行的大佬级别的人物,关于ld链接文件的相关语法介绍大家可以参考文章《LD说明文档–3.LD链接脚本》,这写的很全了。
下面我就以土话介绍几个最常见的语句。
① ENTRY(Reset_Handler):这是一个设置入口函数的命令,也就是说启动后,PC最先指向Reset_Handler,然后运行它。
② MEMORY:定义地址空间,我们可在在里面定义一段段的空间,相当于IAR中的region,也就是待链接地址空间,可以将section链接到这些空间。
MEMORY
{
my_mem (RX) : ORIGIN = 0x60000000, LENGTH = 0x00001000
}
上面语句的意思就是定义了一个region, 起始地址是0x60000000,大小为0x00001000。
③ SECTIONS:定义输出段,相当于是一个加载空间,我们会往这个加载空间里面慢慢的放section,同时给这些section链接地址空间,同时里面会放各种输出段描述和输入段描述。
SECTIONS
{
.text :
{
*(.text)
}
}
上面语句的意思就是定义了一个加载空间,然后把.text段放进去了。其中外面的.text是输出段描述,意思就是这一部分输出的数据为text类型,大括号里面的.text为输入段描述,意思就是输入的数据类型为text, 其中*则是通配符。所以这句话的意思就是我要把所有文件中的.text类型的数据放在这里,链接完成后输出的数据类型也是text类型。
④ . :这个 . 代表的是位置计数器的值,我们可以把这个值取出来,也可以改变这个值,换句话说,这个值大有用处,我们可以通过它获取记录很多信息,后面碰到后在慢慢体会。
除了上面三个,还有很多零碎的,下面我们结合一个完整的实例给大家做分解。
接下来就以一个RT1050芯片的工程链接文件做分析解释。大家自行看着解释对照着语法介绍,你就会有一个非常新的了解。
ENTRY(Reset_Handler) //设置入口地址
HEAP_SIZE = DEFINED(__heap_size__) ? __heap_size__ : 0x0400; //设置堆大小
STACK_SIZE = DEFINED(__stack_size__) ? __stack_size__ : 0x0400; //设置栈大小
MEMORY //定义链接地址空间
{
/*定义只读空间m_interrupts ,起始地址0x00000000, 大小0x00000400 */
m_interrupts (RX) : ORIGIN = 0x00000000, LENGTH = 0x00000400
/*定义只读空间m_text,起始地址0x00000400, 大小0x0001FC00*/
m_text (RX) : ORIGIN = 0x00000400, LENGTH = 0x0001FC00
/*定义读写空间m_data,起始地址0x20000000, 大小0x00020000*/
m_data (RW) : ORIGIN = 0x20000000, LENGTH = 0x00020000
/*定义读写空间m_data2s ,起始地址0x20200000, 大小0x00000400*/
m_data2 (RW) : ORIGIN = 0x20200000, LENGTH = 0x00040000
}
SECTIONS //定义输出段
{
__NCACHE_REGION_START = ORIGIN(m_data2); //将m_data2的起始地址赋值给__NCACHE_REGION_START
__NCACHE_REGION_SIZE = 0;//__NCACHE_REGION_SIZE赋值0
.interrupts : //输出段描述,表示这一段是中断向量表
{
__VECTOR_TABLE = .; //把当前位置计数器的值赋值给__VECTOR_TABLE ,位置计数器的值一开始默认为0
__Vectors = .;//把当前位置计数器的值赋值给__VECTOR_TABLE ,位置计数器的值一开始默认为0
. = ALIGN(4); //这句话不是再给位置计数器赋值,而是给它增加限制条件,表示其增加一次增加4,在内存中的表现即为4字节对齐
KEEP(*(.isr_vector))//放置所有文件中的.isr_vector section,*是通配符,表示所有,就跟我们搜索文件使用*时一样的。使用KEEP的意思就是告诉编译器这段数据非常重要,不要把它当成垃圾优化掉了
. = ALIGN(4);
} > m_interrupts //将这一输出段链接至m_interrupts区域处,所以中断向量表的链接地址就是m_interrupts的起始地址
.text : //定义输出段,就是一个名字,大家可以自己取
{
. = ALIGN(4); //4字节对齐
*(.text) /*放置所有文件的.text段(code) */
*(.text*) /*放置所有文件的.text*段(code) */
*(.rodata) /*放置所有文件的.rodata段(constants, strings, etc.) */
*(.rodata*) /*放置所有文件的.rodata*段(constants, strings, etc.) */
*(.glue_7) /*同上*/
*(.glue_7t) /*同上*/
*(.eh_frame) /*同上*/
KEEP (*(.init))
KEEP (*(.fini))
. = ALIGN(4);
} > m_text //将这一输出段链接至m_text区域处
.ARM.extab : //定义输出段,就是一个名字,大家可以自己取
{
*(.ARM.extab* .gnu.linkonce.armextab.*) //
} > m_text //将这一输出段链接至m_text区域处
.ARM : //定义输出段,就是一个名字,大家可以自己取
{
__exidx_start = .;
*(.ARM.exidx*)
__exidx_end = .;
} > m_text //将这一输出段链接至m_text区域处
.ctors : //定义输出段,就是一个名字,大家可以自己取
{
__CTOR_LIST__ = .;
KEEP (*crtbegin.o(.ctors)) //放置*crtbegin.o中的.ctors段,并保证不被优化
KEEP (*crtbegin?.o(.ctors)) //同上
/*下面的语句中出现了EXCLUDE_FILE函数,这个函数的意思就是把括号里面的除外,
意思就是说放置所有文件除了*crtend?.o *crtend.o文件的 .ctors段,
因为在上面已经放置过了*/
KEEP (*(EXCLUDE_FILE(*crtend?.o *crtend.o) .ctors))
/*下面的语句中出现了SPORT函数,SOPT是SORT_BY_NAME的别名,
意思是放置.ctors.*段的时候,按照名字的排列顺序来放置*/
KEEP (*(SORT(.ctors.*)))
KEEP (*(.ctors))
__CTOR_END__ = .;
} > m_text //将这一输出段链接至m_text区域处
.dtors : //定义输出段,就是一个名字,大家可以自己取
{
__DTOR_LIST__ = .;
KEEP (*crtbegin.o(.dtors))
KEEP (*crtbegin?.o(.dtors))
KEEP (*(EXCLUDE_FILE(*crtend?.o *crtend.o) .dtors))
KEEP (*(SORT(.dtors.*)))
KEEP (*(.dtors))
__DTOR_END__ = .;
} > m_text
.preinit_array : //定义输出段,就是一个名字,大家可以自己取
{
/*下面的出现了PROVIDE_HIDDEN, 意思就是后面的这个符号__preinit_array_start 只能在
链接器中被使用,外部文件是不能调用的,与它相反的还有PROVIDE, PROVIDE表示这个符号可以
被外部调用,而且如果外部文件也定义了同样的符号也不会发生冲突,优先使用外部定义值,
后面会出现很多PROVIDE*/
PROVIDE_HIDDEN (__preinit_array_start = .);
KEEP (*(.preinit_array*))
PROVIDE_HIDDEN (__preinit_array_end = .);
} > m_text
.init_array : //定义输出段,就是一个名字,大家可以自己取
{
PROVIDE_HIDDEN (__init_array_start = .);
KEEP (*(SORT(.init_array.*)))
KEEP (*(.init_array*))
PROVIDE_HIDDEN (__init_array_end = .);
} > m_text
.fini_array : //定义输出段,就是一个名字,大家可以自己取
{
PROVIDE_HIDDEN (__fini_array_start = .);
KEEP (*(SORT(.fini_array.*)))
KEEP (*(.fini_array*))
PROVIDE_HIDDEN (__fini_array_end = .);
} > m_text
__etext = .;
__DATA_ROM = .;
__VECTOR_RAM = ORIGIN(m_interrupts);
__RAM_VECTOR_TABLE_SIZE_BYTES = 0x0;
.data : AT(__DATA_ROM) //AT的作用就是给当前输出段指定加载地址
{
. = ALIGN(4);
__DATA_RAM = .;
__data_start__ = .;
*(m_usb_dma_init_data)
*(.data)
*(.data*)
KEEP(*(.jcr*))
. = ALIGN(4);
__data_end__ = .;
} > m_data
__NDATA_ROM = __DATA_ROM + (__data_end__ - __data_start__);
.ncache.init : AT(__NDATA_ROM)
{
__noncachedata_start__ = .;
*(NonCacheable.init)
. = ALIGN(4);
__noncachedata_init_end__ = .;
} > m_data
. = __noncachedata_init_end__;
.ncache :
{
*(NonCacheable)
. = ALIGN(4);
__noncachedata_end__ = .;
} > m_data
__DATA_END = __NDATA_ROM + (__noncachedata_init_end__ - __noncachedata_start__);
text_end = ORIGIN(m_text) + LENGTH(m_text);
ASSERT(__DATA_END <= text_end, "region m_text overflowed with text and data")
/* Uninitialized data section */
.bss :
{
/* This is used by the startup in order to initialize the .bss section */
. = ALIGN(4);
__START_BSS = .;
__bss_start__ = .;
*(m_usb_dma_noninit_data)
*(.bss)
*(.bss*)
/*放置COMMON块,关于COMMON块是链接器为弱符号所制定的编译解决方案,
本质上其实就是bass段,感兴趣的小伙伴可以自行去搜一搜*/
*(COMMON)
. = ALIGN(4);
__bss_end__ = .;
__END_BSS = .;
} > m_data
.heap :
{
. = ALIGN(8);
__end__ = .;
PROVIDE(end = .);
__HeapBase = .;
. += HEAP_SIZE;
__HeapLimit = .;
__heap_limit = .;
} > m_data
.stack :
{
. = ALIGN(8);
. += STACK_SIZE;
} > m_data
__StackTop = ORIGIN(m_data) + LENGTH(m_data);
__StackLimit = __StackTop - STACK_SIZE;
PROVIDE(__stack = __StackTop);
.ARM.attributes 0 : { *(.ARM.attributes) }
/*ASSERT表示断言,跟C中的assert功能是一摸一样的*/
ASSERT(__StackLimit >= __HeapLimit, "region m_data overflowed with stack and heap")
}
好了,到这里,一些最常见的命令大家都搞清楚了。
接下我们以RT1050来实战一下学习成果。IDE为NXP的MCUXpresso,我们可以通过Properties->C/C++ Build->Setting中指定我们自己的ld链接文件:
使用下面语句自定义section。
__attribute__((section(".my_code_section")))
void my_code_fun(void)
{
PRINTF("hello world.\r\n");
}
如果我们不指定链接地址,查看map文件,发现它的链接地址在FLASH中,如下:
接下来在ld链接文件中给这个section指定链接地址至RAM:
MEMORY
{
......
......
/*定义我们想链接的region,起始地址为0x2001F000,也就是将其链接到RT的OCRAM(RAM),定义自己的地址空间时,避免两个空间有重合的区域*/
MY_RAM (rx) : ORIGIN = 0x20210000, LENGTH = 0x10000
}
SECTIONS
{
......
......
//自定义输出段,并4字节对齐,注意此处的名字不要和其他输出段名字重复
.my_text : ALIGN(4)
{
*(.my_code_section)
. = ALIGN(4);
} > MY_RAM
}
链接后查看map文件,发现它的链接地址已经在我们指定的RAM中,如下:
运行代码查看跳转地址:
在ld链接文件中使用以下代码将该文件编译后的fsl_gpio.o文件链接至我们指定的RAM空间。
MEMORY
{
......
......
/*定义我们想链接的region,起始地址为0x2001F000,也就是将其链接到RT的DTCM(RAM)*/
MY_RAM (rx) : ORIGIN = 0x20210000, LENGTH = 0x10000
}
SECTIONS
{
......
......
//自定义输出段,并4字节对齐,注意此处的名字不要和其他输出段名字重复
.text : ALIGN(4)
{
*(EXCLUDE_FILE(*fsl_gpio.o) .text*) //放置所有文件除了*my_code.c的text*段
*( .rodata .rodata.* .constdata .constdata.*)
. = ALIGN(4);
} > BOARD_FLASH /*将所有文件除了*my_code.c的text*段链接至FLASH*/
.my_text : ALIGN(4)
{
*fsl_gpio.o(.text*) //放置*my_code.c的text*段
. = ALIGN(4);
} > MY_RAM //将*my_code.c的text*段链接至MY_RAM
}
链接后查看map文件,发现fsl_gpio.c中的函数的链接地址已经在我们指定的RAM中,如下:
运行代码查看跳转地址:
(注意:ld链接文件的语法非常的严格,所以每一个文件的section只能被链接一次,如果链接多次则会出现链接错误,但是编译器却不报错。所以一定要巧妙地应用EXCLUDE_FILE函数来隔离相关的section。)
使用以下代码将所有代码链接至我们指定的RAM中:
MEMORY
{
......
......
/*定义我们想链接的region,起始地址为0x2001F000,也就是将其链接到RT的DTCM(RAM)*/
MY_RAM (rx) : ORIGIN = 0x20210000, LENGTH = 0x10000
}
SECTIONS
{
......
......
//自定义输出段,并4字节对齐,注意此处的名字不要和其他输出段名字重复
.text : ALIGN(4)
{
*(.text*) //将所有代码链接至RAM
*(.rodata .rodata.* .constdata .constdata.*)//将所有只读性质的数据链接至RAM
. = ALIGN(4);
} > MY_RAM //将所有代码段链接至RAM
}
在这里我们可以使用 (.text)链接所有代码的原因是NXP的SDK已经在MCUXpresso中将启动相关的代码自定义了一个叫做.after_vectors*( * 表示通配符)的段,然后将其链接至了FLASH中来保证MCU的正常启动和代码搬运,如下图为.after_vectors*段的一部分:
链接后查看map文件,发现除了除了有关启动代码之外代码的链接地址已经在我们指定的RAM中,如下:
运行代码查看跳转地址:
(注意1:对于MCU来说,我们不能将所有的代码的执行域都放入至RAM中,换句话说,其实entry入口地址必须得是FLASH,因为必须运行完MCUXpresso的初始化函数,该初始化函数将完成相关代码和数据的拷贝之后,RAM中才会有代码,之后再跳转进入RAM中运行, 所以我们至少需要将与MCUXpresso的初始化相关的section链接至FLASH,不然是无法正常启动的。如果想完全将代码放入RAM中运行,则是可以直接将代码下载至RAM中,或者像RT1050这种支持non-XIP启动的MCU,则需要修改头信息,让BootROM实现所有代码的拷贝,类似于non-XIP启动。)
(注意2:如果想在正常启动过程中将中断向量表链接至RAM,则需要注意两个点,第一点就是将中断向量链接至RAM的同时还要保证芯片的正常启动,我们可以通过将与启动相关的部分中断向量表拷贝副本链接至FLASH保证正常启动,再将完整的中断向量链接至RAM。第二点则是需要通过修改SCB->VTOR寄存器改变中断向量映射地址。)
END