(一)前提概念
LMA,VMA和”与位置无关的代码“,与位置有关的代码“的关联
1. VMA(Virtual Memory Address):表示程序在虚拟地址中的位置,即程序运行时所使用的地址,对于需要在内存中运行的代码,VMA是程序在内存(RAM)中的位置。
2. LMA(Load Memory Address):加载到物理内存中的地址,通常用于确定程序启动时的实际物理地址。
Tips:
编译器和链接器会在生成可执行文件时记录并处理 VMA 和 LMA 之间的关系,以确保他们正确对应。
1 对于有OS情况下考虑了操作系统对进程的虚拟地址空间进行的映射或重定位,这是VMA和LMA的值是不一样的,对于裸机状态下VMA和LMA通常是一样的。
2 对于一些变量(动态分配内存的变量和常量除外)的LMA和VMA一般都不一样,因为他们都是在 启动阶段从可执行文件的data,bss段加载到RAM, 未初始化的变量是在启动后根据bss段大小分配对应的RAM空间。
3 对于需要在RAM中运行的代码,他们的VMA和LMA也是不一样的。
总结: 加载地址就是在物理内存中的地址(ROM,Flash),虚拟地址就是运行时候的地址,可能与加载地址一样在(ROM,Flash)也可能不一样,比如需要运行在RAM的代码,VMA在RAM, 或者使用了AT关键字 ( >FLASH AT > FLASH_offset)。
3. 与位置无关的代码(Position-Independent Code, PIC):
这种代码不依赖于特定的加载地址,可以在内存的任何位置正确执行。PIC 通常用于共享库等需要动态加载和链接的场景。在链接时,通常会为 PIC 代码生成适当的重定位信息,以确保它们能够在加载到任何地址后正确运行。
4. 与位置有关的代码(Position-Dependent Code, PDC):
这种代码依赖于特定的加载地址,只能在特定的内存地址正确执行。PDC 通常用于固定在特定地址执行的程序,如嵌入式系统中的启动代码和中断向量表等。在链接时,PDC 的加载地址会被硬编码到生成的可执行文件中。
**5. 他们之间的关联 ** :
对于” 与位置有关的代码 “ 他们的VMA和LMA是一样的,因为他们需要加载到固定的地址上才能执行
对于” 与位置无关的代码 “ 他们的VMA和LMA可能不一样,因为这些代码可以在内存的任何地执行,因此加载时需要重定位。
这两种代码的概念与代码的加载地址相关, 对于前者其加载地址时固定的,对于后者其加载地址是不确定的,因而不管时代码编写过程中还是编译时都必须生产的是适用于任意位置的代码,并在链接时适当的重定位信息。
(二)链接文件解析
常用链接脚本关键字
1. ENTRY( )
用于指定程序的入口点,也称为起始地址或入口地址, 即第一个执函数的入口 (symbol 称为符号,可以表示一些变量或函数入口地址,在链接文件中这些都统称为符号
// example.c
void example(){......}
// example.ld
ENTRY(example); // 这样硬件重启后进入的第一个函数就example()
需要注意的是: example这个符号或函数需要在启动文件的向量表中定义
2. PROVIDE( )
给符号(symbol)指定一个值。在链接过程中,如果某个符号没有被其他地方定义,而链接脚本中使用了 PROVIDE 指令来为这个符号提供一个值,那么链接器就会使用 PROVIDE 指定的值来定义这个符号。
保证被引用但未定义的符号都有一个合适的值,仅在符号被引用且未由链接中包含的任何对象定义的情况下给符号分配一个值。
.ram.data : ALIGN(4)
{
. =0x00, //只是一个示例
/* 获取.ram.data在可执行文件中的加载地址 */
PROVIDE(_data_load = LOADADDR(.ram.data));
/* 获取.ram.data在RAM中运行时的首地址 */
PROVIDE(_data_run = ADDR(.ram.data));
/* 计算.ram.data在RAM中运行时结束地址 */
PROVIDE(_data_run_end = ADDR(.ram.data)+ SIZEOF(.ram.data));
}> RAM
tisp:
1。加载地址(也就是物理地址 LMA) 可以用LOADADDR(section)读取
2。运行地址(虚拟地址 VMA)可以用ADDR(section) 读取
3。链接地址 (链接定位器地址,相对于可执行文件起始地址的偏移量),决定各个段在可执行文件的位置,上述代码中’’ . ''就代表当前链接地址。
3. KEEP(*(.vector_table))
保留符号或段, 告诉链接器这个符号或段不能被优化或丢弃。
通常在链接时,未被引用的段或符号在来链接时会被优化,最终会从可执行文件删除,但这些段可能会被外部工具调用或在运行时有特定作用,所以不能让连接器优化它。
KEEP(*(.vector_table)) // 指定所有属于.vector_table段或所有.vector_table类型的段的内容不能被优化
4. SECTIONS
用于定义程序各个段的布局和分布方式,
SECTIONS 可以包含一下几个部分:
段名称和内容匹配规则:在 SECTIONS 块中,你可以通过使用通配符和规则来匹配特定类型的段内容。例如,*(.text) 匹配所有 .text 类型的段内容。
段的位置和分配规则:使用大于符号 > 和内存区域名称来指定段在内存中的位置和分配方式。例如,> FLASH 将段分配到 FLASH 内存区域中。
SECTIONS
{
/* 定义代码段 */
.text :
{
/* 指定代码段的内容匹配规则 */
*(.text) // 将所有名为.text的段的内容分配到FLASH
} > FLASH
/* 定义数据段 */
.data :
{
/* 指定数据段的内容匹配规则 */
*(.data) // 将所有名为.data的段的内容分配到RAM
} > RAM
}
5. MEMORY
链接脚本中用来定义内存区域的关键字, 可以指定程序运行时可用内存区域的起始地址和大小
MEMORY
{
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 32K
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 256K
// rwx 表示可读可写
// rx 表示只读
}
6. AT
指定某个段在存储中的地址或偏移量,就是一个偏移量,通过这个偏移量可以某个区域的具体位置。
" > add" : 分配段的运行地址(VMA)为add
" AT > loadadd" :分配段的加载地(LMA)为loadadd
" > RAM AT > FLASH1" 将段的VMA分配为RAM,LMA分配为FLASH1 可以理解为启动后该段从FLASH1加载到RAM中运行
一般给段分配时没用AT指定加载地址,这个段的VMA和LMA地址一样。(链接时链接器会逐步处理输入文件中各个段)
MEMORY
{
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 32K
FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 256K
FLASH1 (rx) : ORIGIN = 0x00040000, LENGTH = 256K
}
SECTIONS
{
.data :
{
*flash_drv.o(.text*)
} > RAM AT > FLASH1
// 启动后flash_drv的代码将在RAM运行, 即调用flash_drv里的函数是,PC将到RAM里取指令
7. LOADDADDR和ADDR
LOADADDR(section) : 返回section段的加载地址(LMA)
ADDR(section): 返回section段的运行地址(VMA)
**8. TARGET,OUTPUT_FORMAT,INPUT **
TARGET:用于指定输出文件的格式,例如 binary、elf32-littlearm 等
OUTPUT_FORMAT:用于设置输出文件的格式选项,例如默认的输出格式选项 default,或者其他特定的格式选项。它可以用来设置输出文件的特定格式属性,如输出文件是否压缩等。
TARGET 确定输出文件的格式类型,而 OUTPUT_FORMAT 则用于设置输出文件的格式选项, 通常链接脚本中使用其中一个命令来指定输出文件的格式即可。
INPUT :在链接脚本中用于指定输入文件,即要链接的目标文件或库文件的名称。
INPUT(file1.o file2.o lib1.a)
//这个命令告诉链接器在链接过程中使用了三个输入文件:file1.o、file2.o 和 lib1.a。
//链接器会将这些文件中的符号和段组合在一起,生成最终的输出文件
代码分析
// 若__heap_size__没定义则使用默认值0x00000200
HEAP_SIZE = DEFINED(__heap_size__) ? __heap_size__ : 0x00000200;
// 若__stack_size__没定义则使用默认值0x00000400
CSTACK_SIZE = DEFINED(__stack_size__) ? __stack_size__ : 0x00000400;
MEMORY
{
/* Pflash */
BOOT_FLASH (xr) : ORIGIN = 0x00000000, LENGTH = 0x0010000
APP_VECT (xr) : ORIGIN = 0x00010000, LENGTH = 0x00000100
APP_VERSION (RX) : ORIGIN = 0x00010100, LENGTH = 0x00000200
APP_PROGRAM (xr) : ORIGIN = 0x00010A00, LENGTH = 0x0000F600
APP_PROGRAM1 (xr) : ORIGIN = 0x00020000, LENGTH = 0x0001A000
/* Dflash */
CAR_PARM_CONFIG (xr) : ORIGIN = 0x01004000, LENGTH = 0x00000100
CAR_PARM_CONFIG1 (xr) : ORIGIN = 0x01004100, LENGTH = 0x00000100
/* RAM */
RAM0 (xrw) : ORIGIN = 0x1fffc000, LENGTH = 0x00004000
RAM1 (xrw) : ORIGIN = 0x20000000, LENGTH = 0x00004000
}
// 程序入口
ENTRY(Reset_Handler)
//这个命令用于指定输出文件的格式, binary 表示输出文件的格式为二进制文件。
TARGET(binary)
//这个命令用于指定输入文件的名称, ..\\Booter\\Booter.bin 为输入文件的路径和文件名
INPUT(..\\Booter\\Booter.bin)
//这个命令用于指定输出文件的格式选项, default 表示使用默认的输出文件格式选项
OUTPUT_FORMAT(default)
SECTIONS
{
// .my_booter 是一个段名称, 链接时.my_booter会用来标识这个段
.my_booter :
{
// 这里的(.data)不是链接里的数据段,而是bin文件的所有数据
..\\Booter\\Booter.bin (.data)
} > BOOT_FLASH
.isr_vector :
{
. = ALIGN(4);
KEEP(*(.intvec))
. = ALIGN(4);
} > APP_VECT /*APP_VECT BOOT_FLASH*/
.prog_version :
{
. = ALIGN(4);
// 存放一些APP程序的版本号等, 程序内没用到,外部工具会使用到,所以加KEEP关键字
KEEP(*(.prog_version))
. = ALIGN(4);
} > APP_VERSION
.ram.data : ALIGN(4)
{
/* 定义_data_load 来表示.ram.data的加载地址(告诉链接器,在链接时将.ram.data加载地址赋值到_data_load ) */
PROVIDE(_data_load = LOADADDR(.ram.data));
/* 定义_data_load 来表示.ram.data的运行地址(告诉链接器,在链接时将.ram.data运行地址赋值到_data_load )) */
PROVIDE(_data_run = ADDR(.ram.data));
/* 计算.ram.data在RAM中运行时结束地址 */
PROVIDE(_data_run_end = ADDR(.ram.data)+ SIZEOF(.ram.data));
/* 将所有以.data开头的段放置在.ram.data区域内,.data后不加 * 表示所有属于.data段的内容 ,这些段一般为已初始化的变量数据*/
*(.data*)
. = ALIGN(8);
*(.code_ram) /*将所有属于.cade_ram自定义段的内容放置在.ram.data中,这个段可能包含需要在RAM运行的代码 */
. = ALIGN(8);
*flash_drv.o(.text*) /* 从flash_drv.o目标文件中提取名称以.text开头的段,放置到.ram.data中*/
} > RAM1 AT > APP_PROGRAM /* > RAM1指定段的运行地址为RAM1, AT > APP_PROGRAM指定段加载地址为PROGRAM */
.rom : ALIGN(4)
{
*(.text*) // 将输入文件中所有.text段合并到.rom段
*(.glue_7) // 将包含RAM指令集调跳转到Thumb指令集的转换代码合并到.rom段
*(.glue_7t) // 将包含Thumb指令集调跳转到RAM指令集的转换代码合并到.rom段
*(.eh_frame) // 将输入文件中的异常处理框架数据合并到.rom段
KEEP (*(.init))
KEEP (*(.fini))
// 将只读数据段的内容合并到.rom段
*(.rodata)
*(.rodata*)
} > APP_PROGRAM
// NOLOAD 告诉链接器不要将这个段加载到输出文件, 因为bss段存储的未初始化的变量不需要在可执行文件中分配空间
.ram.bss (NOLOAD) : ALIGN(4)
{
_start_bss = .; // 将当前链接地址赋值给_start_bss, 用于记录bss段起始地址
*(.bss*) // 将输入文件中所有bss段的内容合并到.ram.bss段
*(COMMON) // 将所有未初始化的全局变量合并到.ram.bss段
_end_bss = .; // 记录bss结束地址
} > RAM1 // 告诉链接器将这个段分配到RAM1
.heap :
{
. = ALIGN(8);
__end__ = .;
__heap_start__ = .;
PROVIDE(end = .);
PROVIDE(_end = .);
PROVIDE(__end = .);
__HeapBase = .;
. += HEAP_SIZE;
__HeapLimit = .;
__heap_limit = .;
__heap_end__ = .;
} > RAM1
__StackTop = ORIGIN(RAM1) + LENGTH(RAM1); // 栈顶地址指示
__StackLimit = __StackTop - CSTACK_SIZE; // 栈低地址指示
PROVIDE(CSTACK = __StackTop);
.stack __StackLimit : //将.stack段从 __StackLimit(栈底)开始存储 (栈顶地址>栈底地址)
{
. = ALIGN(8);
__stack_start__ = .; //栈区域起始地址 (栈底)
. += CSTACK_SIZE;
__stack_end__ = .; //栈区域结束地址(栈顶)
} > RAM1
RamStart = ORIGIN(RAM0); // RAM起始地址
RamEnd = ORIGIN(RAM1) + LENGTH(RAM1); // RAM结束地址
PROVIDE(__RAM_START = RamStart); // 定义符号供外部使用
PROVIDE(__RAM_END = RamEnd);
}
Tips:
- 因为bin文件已经是链接过后的一个连续的字节序列,已经不存在数据段,代码段,bss段的概念,所以需要把bin文件放入某个段时可以直接写它 INPUT时的路径文件名加后缀。
比如: …\Booter\Booter.bin (.data)