基本概念
.ld文件相关内容整理
/* Entry Point */
ENTRY(Reset_Handler)
//1、设置了用户ram的最高地址
/* Highest address of the user mode stack */
_estack = 0x20020000; /* end of RAM */
//2、设置用户栈空间以及堆空间的大小
/* Generate a link error if heap and stack don't fit into RAM */
_Min_Heap_Size = 0x4000; /* required amount of heap */
_Min_Stack_Size = 0x1000; /* required amount of stack */
//3、指定了RAM与ROM的起始地址,以及大小
/* Specify the memory areas */
MEMORY
{
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K
CCMRAM (rw) : ORIGIN = 0x10000000, LENGTH = 64K
FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 1024K
}
//4、将相关内容放到对应段中
/* Define output sections */
SECTIONS
{
//中断向量表放在.isr_vector段中
/* The startup code goes first into FLASH */
.isr_vector :
{
. = ALIGN(4);
KEEP(*(.isr_vector)) /* Startup code */
. = ALIGN(4);
} >FLASH
//程序代码和其他部分数据存放在.text段中
/* 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
//.rodata 存放常量(只读数据)
/* 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 和 .init_array 保存程序或共享对象加载时的初始化函数指针
.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 保存程序或共享对象退出时的退出函数地址
.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);
//.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
_siccmram = LOADADDR(.ccmram);
/* CCM-RAM section
*
* IMPORTANT NOTE!
* If initialized variables will be placed in this section,
* the startup code needs to be modified to copy the init-values.
*/
//.ccmram 是STM32中只允许内核访问的空间,可以将一些内核放在这里使用
.ccmram :
{
. = ALIGN(4);
_sccmram = .; /* create a global symbol at ccmram start */
*(.ccmram)
*(.ccmram*)
. = ALIGN(4);
_eccmram = .; /* create a global symbol at ccmram end */
} >CCMRAM AT> FLASH
//.bss 保存了那些用到但未被初始化的数据
/* 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 用户的堆栈段
/* User_heap_stack section, used to check that there is enough RAM left */
._user_heap_stack :
{
. = ALIGN(4);
PROVIDE ( end = . );
PROVIDE ( _end = . );
. = . + _Min_Heap_Size;
. = . + _Min_Stack_Size;
. = ALIGN(4);
} >RAM
/* Remove information from the standard libraries */
/DISCARD/ :
{
libc.a ( * )
libm.a ( * )
libgcc.a ( * )
}
.ARM.attributes 0 : { *(.ARM.attributes) }
}
简单的链接器脚本命令
常用关键字
ENTRY(symbol) 用来指定程序执行的入口点
MEMORY 内存分配命令
SECTIONS 段命令 描述输出文件的内存和布局
.text 程序代码段
.rodata 只读数据
.data 可读写且需要初始化的数据
.bss 可读写的清零初始化数据
ASSERT 断言
PROVIDE(symbol=expression) 定义一个符号
AT 后跟MEMORY定义的内存区域或者地址
ALIGN 字节对齐
设置入口点
ENTRY( _start ) /* 入口地址 */
__stack_size = 2048; /* 定义栈大小 */
PROVIDE( _stack_size = __stack_size );/* 定义_stack_size符号,类似于全局变量 */
有几种方式设置入口点,链接器将按照如下顺序设置入口点,一旦某个步骤成功,则停止:
- -e 命令行选项
- 链接器脚本中的ENTRY命令
- 如果定义了start符号,则使用这个符号的值
- 如果存在.text section,则使用其第一个字节地址。
- 地址0
内存布局定义
对MCU的Flash及RAM空间进行分配,其中以ORIGIN定义地址空间的起始地址,LENGTH定义地址空间的长度。这里的attr只能由以下特性组成 ‘R’ - Read-only section ‘W’ - Read/write section ‘X’ - Executable section ‘A’ - Allocatable section ‘I’ - Initialized section ‘L’ - Same as I ‘!’ - Invert the sense of any of the attributes that follow origin是一个数字表达式,代表了内存区域的起始地址。表达式必须等价于一个常数并且不能含有任何符号。关键字ORIGIN缩短为org或者o(但不能写成ORG)。
MEMORY
{
name[(attr)] : ORIGIN = origin, LENGTH = length
...
}
MEMORY
{
rom (rx) : ORIGIN = 0, LENGTH = 256K
ram (!rx) : org = 0x40000000, l = 4M
}
段链接定义
用于定义目标文件(.o)的text、data、bss等段的链接分布。语法如下:
SECTIONS
{
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] [,]
...
}
/* 大多数的段仅使用了上述部分属性,可以简写成如下形式 */
SECTIONS
{
...
secname :
{
output-section-command
}
...
}
链接脚本详解
在脚本中经常看到类似“. = 0×10000;”的语句,该语句表示将当前地址设置为0x10000。在链接开始处没有定位器符号,则表示将输入段定位到MEMORY定义的空间最开始处,即offset为0。
OUTPUT_ARCH(arm)//输出的程序指令集架构为arm
ENTRY(_start) //程序入口
SECTIONS {
. = 0x0;//.代表当前地址,代表程序起始链接的地址
.text :
{
*(.text) //所有文件的代码
}
.data :
{
*(.data)
}
bss_start = . ; // . 表示当前地址
.bss :
{
*(.bss)
}
bss_end = . ;
}
~
SECTIONS 命令
SECTIONS 命令告诉ld 如何把输入文件的sections 映射到输出文件的各个section: 如何将输入section 合为输出section; 如何把输出section 放入程序地址空间
secname:命名这个段
contents:用来确定代码的什么部分放在这个段
start:是这个段的重定位地址,也叫运行地址。如果代码中有位置无关的指令,程序在运行时必须放在这个地址上。
ALIGN(align):虽然指定了运行地址,但仍可以使用ALIGN(align)来指定对齐的要求—-这个对齐的地址才是真正的地址
(NOLOAD):来告诉加载器,在运行时不用加载这个段
AT(ldadr):指定这个段在编译出来的映像文件中的地址——加载地址(存储地址)
简单脚本命令
ENTRY(SYMBOL) :将符号SYMBOL的值设置成入口地址。
入口地址(entry point)是指进程执行的第一条用户空间的指令在进程地址空间的地址
ld有多种方法设置进程入口地址, 按一下顺序: (编号越前, 优先级越高)
1, ld命令行的-e选项
2, 连接脚本的ENTRY(SYMBOL)命令
3, 如果定义了start符号, 使用start符号值
4, 如果存在.text section, 使用.text section的第一字节的位置值
5, 使用值0
INCLUDE filename : 包含其他名为filename的链接脚本
相当于c程序内的的#include指令, 用以包含另一个链接脚本.
脚本搜索路径由-L选项指定. INCLUDE指令可以嵌套使用, 最大深度为10. 即: 文件1内INCLUDE文件2, 文件2内INCLUDE文件3… , 文件10内INCLUDE文件11. 那么文件11内不能再出现 INCLUDE指令了.
INPUT(files): 将括号内的文件做为链接过程的输入文件
ld首先在当前目录下寻找该文件, 如果没找到, 则在由-L指定的搜索路径下搜索. file可以为 -lfile形式,就象命令行的-l选项一样. 如果该命令出现在暗含的脚本内, 则该命令内的file在链接过程中的顺序由该暗含的脚本在命令行内的顺序决定.
GROUP(files) : 指定需要重复搜索符号定义的多个输入文件
file必须是库文件, 且file文件作为一组被ld重复扫描,直到不在有新的未定义的引用出现。
OUTPUT(FILENAME) : 定义输出文件的名字
同ld的-o选项, 不过-o选项的优先级更高. 所以它可以用来定义默认的输出文件名. 如a.out
SEARCH_DIR(PATH) :定义搜索路径,
同ld的-L选项, 不过由-L指定的路径要比它定义的优先被搜索。
STARTUP(filename) : 指定filename为第一个输入文件
在链接过程中, 每个输入文件是有顺序的. 此命令设置文件filename为第一个输入文件。
OUTPUT_FORMAT(BFDNAME) : 设置输出文件使用的BFD格式
同ld选项-o format BFDNAME, 不过ld选项优先级更高.
OUTPUT_FORMAT(DEFAULT,BIG,LITTLE) : 定义三种输出文件的格式(大小端)
若有命令行选项-EB, 则使用第2个BFD格式; 若有命令行选项-EL,则使用第3个BFD格式.否则默认选第一个BFD格式.
TARGET(BFDNAME):设置输入文件的BFD格式
同ld选项-b BFDNAME. 若使用了TARGET命令, 但未使用OUTPUT_FORMAT命令, 则最用一个TARGET命令设置的BFD格式将被作为输出文件的BFD格式.
ASSERT(EXP, MESSAGE):如果EXP不为真,终止连接过程
EXTERN(SYMBOL SYMBOL …):在输出文件中增加未定义的符号,如同连接器选项-u
FORCE_COMMON_ALLOCATION:为common symbol(通用符号)分配空间,即使用了-r连接选项也为其分配
NOCROSSREFS(SECTION SECTION …):检查列出的输出section,如果发现他们之间有相互引用,则报错。对于某些系统,特别是内存较紧张的嵌入式系统,某些section是不能同时存在内存中的,所以他们之间不能相互引用。
OUTPUT_ARCH(BFDARCH):设置输出文件的machine architecture(体系结构),BFDARCH为被BFD库使用的名字之一。可以用命令objdump -f查看。
可通过 man -S 1 ld查看ld的联机帮助, 里面也包括了对这些命令的介绍.
运行地址与存储地址
程序从片内地址0开始,但为什么链接地址又设0x30000000,那不就从0x30000000开始了,反汇编可以看到不是从0开始的?
韦老大回答:
- 裸板程序烧在FLASH上 一上电,肯定从0地址运行
- 但是,0地址要么对应NOR FLASH,要么对应只有4K的片内内存
- 程序要读写数据,或是程序大于4K,怎么办?
- 程序就要复制到SDRAM里去执行
- SDRAM那么大,复制到哪个地址去?能随便选择地址吗
- 不能,要复制到它的链接地址去
- 为什么一定要复制到它的链接地址去?
- 因为这个链接地址是程序运行时“应该位于的地方”,比如要访问某个全局变量时,就是访问这个全局变量的链接地址
- 既然链接地址是SDRAM的地址,那为什么一开始程序可以从0地址运行
- 因为一开始的程序是“位置无关码”
总之:运行地址=-T指定的连接地址+相对偏移地址;存储地址=由oflash指定某存储器件的起始地址+由链接文件中AT指定的加载地址。
运行地址:程序在SRAM、SDRAM中执行时的地址。就是执行这条指令时,PC应该等于这个地址,换句话说,PC等于这个地址时,这条指令应该保存在这个地址内。
加载地址:程序保存在Nand flash中的地址。
位置无关码:B、BL、MOV都是位置位置无关码。
加载时地址就是程序放置的地址,运行地址就是程序定位的绝对地址,也即在编译连接时定位的地址。
SECTIONS {
firtst 0x00000000 : { head.o init.o }
second 0x30000000 : AT(4096) { main.o }
}
以上,head.o放在0x00000000地址开始处,init.o放在head.o后面,他们的运行地址也是0x00000000,即连接和存储地址相同(没有AT指定);
main.o放在4096(0x1000,是AT指定的,存储地址)开始处,但是它的运行地址在0x30000000,运行之前需要从0x1000(加载处)复制到0x30000000(运行处),
此过程也就用到了读取Nand flash。这就是存储地址和连接(运行)地址的不同,称为加载时域和运行时域,可以在.lds连接脚本文件中分别指定。
编写好的.lds文件,在用arm-linux-ld连接命令时带-Tfilename来调用执行,如arm-linux-ld –Tnand.lds x.o y.o –o xy.o。也用-Ttext参数直接指定连接地址,