如何将指定变量或函数编译至固定的内存区域?
- 1. 内存类型
- 1.1 bss段(Block Started by Symbol)
- 1.2 data段(data segment)
- 1.3 text段(code segment/text segment)
- 1.4 dec
- 1.5 堆(heap):
- 1.6 栈(stack):
- 2 链接脚本
- 2.1 ld链接脚本
- 2.1.1 链接配置
- 2.1.2 MEMORY
- 2.1.3 定位符'.'
- 2.1.4 SECTION
- 2.1.5 其他常用命令
- 2.1.6 链接脚本完整示例
- 2.2 ARM核内如何把指定函数编译到固定内存(使用ld链接脚本)
- 2.3 lsl链接脚本
1. 内存类型
在我们讨论这个问题之前,需要先了解一下C语言中内存中有哪些类型:
1.1 bss段(Block Started by Symbol)
bss段属于静态内存分配,通常是指用来存放程序中未初始化的(或初始化为0的)全局变量和静态变量的一块内存区域。在程序执行之前bss段会自动清0,所以,未初始的全局变量在程序执行之前已经成0了。
bss段在执行文件中时候不占磁盘空间,要运行的时候才分配空间并清0。
特点是:可读写的,
1.2 data段(data segment)
数据段通常是指用来存放程序中已初始化的全局或静态变量的一块内存区域。
数据段属于静态内存分配。
1.3 text段(code segment/text segment)
text段通常是指用来存放程序执行代码的一块内存区域。
这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读(某些架构也允许代码段为可写,即允许修改程序)。
在代码段中,也有可能=包含一些只读的常数变量,例如字符串常量等。
1.4 dec
dec 是text,data和bss的算术和。
1.5 堆(heap):
堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。
当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);
堆是先进先出(FIFO)数据结构,当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)。
注意:堆内存需要程序员手动管理内存,通常适用于较大的内存分配,如频繁的分配较小的内存,容易导致内存碎片化。
1.6 栈(stack):
栈又称堆栈,是用户存放程序临时创建的局部变量,也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味着在数据段中存放变量)。
除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的先进先出(FIFO)特点,所以栈特别方便用来保存/恢复调用现场。
从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。
注意:由于栈的空间通常比较小,一般 linux 程序只有几 M,故局部变量,函数入参应该避免出现超大栈内存使用,比如超大结构体,数组等,避免出现 stack overflow。
2 链接脚本
什么是链接脚本?链接脚本有什么作用?
链接脚本:用来描述程序是如何在内存空间中分布的(指定.text .data .bss在内存中的地址)。编译器会根据链接脚本输出可执行文件。
ARM编译器用的scat格式的链接脚本,gcc编译器用的是ld格式的脚本,Tricore使用的是lsl格式的链接脚本,不同脚本语法是不一样的。
链接脚本的作用:将多个目标文件(.o)和库文件(.a)链接成一个可执行文件(输出文件),并控制输出文件的内存布局(地址分配)。
注: GCC在编译C语言文件的时候,会分别生成RO、RW、ZI部分。RO是只读段,也就是程序代码段(.text),就是具体函数代码;RW是读写数据段(.data),也就是初始化的全局变量;ZI为未初始化数据段(.bss),也就是那些未赋初值的变量,这个段不占用ROM空间,只有在程序运行的时候在RAM初始化为0。
链接脚本的作用也就是将这些编译出来的段整合到一起。
2.1 ld链接脚本
ld链接脚本由MEMORY和SECTIONS,还有一些链接配置组成:
- 链接配置(有的配置项目是可选的);
- MEMORY可选的,如果没有,链接器则认为所有输入文件都位于同一个内存区域,并且从0x0开始;
- SECTIONS是必须的;
2.1.1 链接配置
如一些符号变量的定义,入口地址,输出格式等;
STACK_SIZE = 0x200
ENTRY(_START)
OUTPUT_ARCH(arm)
OUTPUT_FORMAT(elf32_littlearm)
ENTRY命令
ENTRY(begincode) 代表我们使用 begincode 作为程序入口地址,链接器会默认使用第一个可执行section作为程序入口点,即start。
ENTRY(Reset_Handler)将Reset_Handler函数设为程序的入口点,链接器需要知道程序的入口点,才能正确地组织可执行文件的时序,才能正确地执行编译、链接、优化代码。
ld有多种指定程序入口方式(优先级逐渐降低):
a、ld命令 -e选项;
b、连接脚本中ENTRY(symbol)的命令;
c、如果定义了_start符号,使用_start符号的值;
d、如果存在.text段,则使用.text段的第一字节的地址;
e、使用地址0x00000000;
2.1.2 MEMORY
MEMORY为链接器提供系统内存的布局信息,并确定内存区域的访问权限。链接器根据MEMORY的信息,将编译生成的.o目标文件中的代码、数据、符号等分配到不同的内存区域。
脚本格式如下:
MEMORY
{/*标准格式如下*/mem_name [(attr)] : ORIGIN = origin, LENGTH = lenRAM0 (xrw) : ORIGION = (0x00000000), LENGTH = 2MRAM1 (xrw) : ORIGION = (0x30000000), LENGTH = 128M
}
mem_name 是一块内存的名称,自定义,但不得重复,仅在ld文件中生效。
attr字符串是可选的属性列表:
R | Read-only section | 只读段 |
---|---|---|
W | Read/write section | 读写段 |
X | Executable section | 可执行段 |
A | Allocatable section | 可分配段 |
I | Initialized section | 初始化段 |
L | Initialized section | 同上 |
! | Invert the sense of any of the preceding attributes | 反转任何前述属性的含义 |
关键字ORIGION : 区域的开始地址
关键字LENGTH : 区域的大小
而后,链接脚本可声明将指定的代码放到对应的memory区域——以下链接脚本代码将.text段存放到标号为RAM的内存区域,将.data段存放到标号为FLASH的内存区域:
SECTIONS
{.text >RAM /*.text也可以后面再细分*/.data >FLASH
}
2.1.3 定位符’.’
‘.’ 表示当前地址,它可以被赋值,也可以赋值给某个变量。
如下就是将当前地址赋值给某个变量(链接器是按照SECTIONS里段顺序排列的,前面排完之后才能计算出当前地址)
RAM_START = . ;
如下就是将段放在特定的地址中:
SECTIONS
{. = 0x10000; /*将所有目标文件的text段从0x10000地址开始存放 */.text :{*(.text)}. = 0x80000000;.data : {*(.data)}
}
2.1.4 SECTION
SECTION的基本命令语法如下:
SECTIONS
{ /*标准格式如下*/section-name [address] [(type)] :[AT(lma)][ALIGN(section_align)][SUBALIGN(subsection_align)][constraint]{contentscontents...} [>region] [AT>lma_region] [:phdr :phdr ...] [=fillexp]
}
这么多参数中,只有section-name和contents是必须的,链接脚本的本质是描述输入和输出的关系。secname表示输出文件的段,即输出文件中有那些段,而contents就是描述输出文件的这个段从那些文件里抽取出来的,即输入文件。
region指的是mem_name即内存名(VMA地址)。
输出段名
section-name输出段名必须符合输出格式的约束,例如a.out 输出格式包含’.text’ ‘.data’ ‘.bss’ 三个输出段名。
输出段属性(节点属性)
NOLOAD 该部分应标记为不可加载,以便在程序运行时不会将其加载到内存中。
DSECT
COPY
INFO
OVERLAY 支持这些类型名称以实现向后兼容,并且很少使用。 它们都具有相同的效果:该段应标记为不可分配,以便在程序运行时不为该段分配内存。
输出段地址
每个段都有LMA(加载内存地址)和VMA(虚拟内存地址)
LMA = load memory address
VMA = vitual memory address
LMA就是程序放置的地址,VMA就是运行时的地址。
如果程序是在ram里运行,但程序是存储在flash里,则运行地址指向ram,而加载地址指向flash。
下述例子中VMA为RAM,LMA为FLASH;
[>region] 指定输出段分布在内存上的地址。
[AT(lma)] 指定该段的加载地址。另外,您也可以使用[AT>lma_region]表达式指定
如果未为输出段指定AT或AT>,则链接器将设置当前段的VMA和LMA与同一区域中的先前输出段的设置相同。 如果没有前面的输段或该段不可分配,则链接器会将LMA与VMA设置为相同的值。
输入段 描述
输入段描述由一个文件名(可选)和一个圆括号内的段名称列表组成。
例如:
*(EXCLUDE_FILE (*crtend.o) .ctors) EXCLUDE_FILE(文件列表)表示剔除指定的输入文件,即不包含这些文件的指定段。
*(.text .rdata) 这种方法两个段顺序是不定的。
*(.text) *(.rdata) 这种方法两个段顺序是固定的。
data.o(.data)指定某个文件的某个段。
注: " * "符号在前一般是取址符,在后则是指通配符。
2.1.5 其他常用命令
强制对齐 ALIGN
强制输出对齐ALIGN(n),n一般是2的幂次方;
强制输入对齐SUBALIGN, 指定的值会覆盖输入段给出的任何对齐方式。
PROVIDE
PROVIDE关键字可以被用来定义一个符号(相当于定义了一个全局变量),比如’etext’, 这个定义只在它被引用到的时候有效,而在它被定义的时候无效。PROVIDE定义的符号,允许C语言中重定义(但是不能带前导下划线),重定义后优先使用C中的定义。
SECTIONS
{.text :{*(.text)PROVIDE(etext = .);}
}
KEEP
KEEP(*(.Vectors))
作用是防止垃圾收集机制把重要的节排除在外(防止被优化),也保证了KEEP对象在段中的位置处于最顶端。
ABSOLUTE
PROVIDE(_bss_over = ABSOLUTE(.));
将当前地址的绝对值赋值给_bss_over
2.1.6 链接脚本完整示例
ENTRY(Reset_Handler)_Min_Heap_Size = 0x200; /* required amount of heap */
_Min_Stack_Size = 0x1500; /* required amount of stack */MEMORY
{FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 128KRAM (xrw) : ORIGIN = 0x20000000, LENGTH = 32K
}SECTIONS
{.isr_vector :{. = ALIGN(4);KEEP(*(.isr_vector)) /* Startup code */. = ALIGN(4);} >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);__fsymtab_start = .;KEEP(*(FSymTab))__fsymtab_end = .;. = ALIGN(4);__vsymtab_start = .;KEEP(*(VSymTab))__vsymtab_end = .;. = ALIGN(4);__rt_init_start = .;KEEP(*(SORT(.rti_fn*)))__rt_init_end = .;. = ALIGN(4);_etext = .; /* define a global symbols at end of code */_exit = .;} >FLASH.rodata :{. = ALIGN(4);*(.rodata) /* .rodata sections (constants, strings, etc.) */*(.rodata*) /* .rodata* sections (constants, strings, etc.) */. = ALIGN(4);_shell_command_start = .;KEEP (*(shellCommand))_shell_command_end = .;} >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_sidata = LOADADDR(.data);.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. = 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(4);PROVIDE ( end = . );PROVIDE ( _end = . );. = . + _Min_Heap_Size;. = . + _Min_Stack_Size;_estack = .;. = ALIGN(4);} >RAM/* Remove information from the standard libraries *//DISCARD/ :{libc.a ( * )libm.a ( * )libgcc.a ( * )}.ARM.attributes 0 : { *(.ARM.attributes) }
}
2.2 ARM核内如何把指定函数编译到固定内存(使用ld链接脚本)
问: 基于ARM核 ,把指定函数 MyFunction 编译到固定的内存区域中(起始地址为0x7D000000,大小为0x2000)?
我的答案如下:
/* ld file start */
MEMORY
{EXT_RAM : ORIGIN = 0xa0000000, LENGTH = 0x8000EXT_ROM : ORIGIN = 0x7E680000, LENGTH = 0x16180EXT_BSS : ORIGIN = 0x7FC98000, LENGTH = 0x3C00MY_SECTION : ORIGIN = 0x7D000000, LENGTH = 0x2000;
}SECTIONS
{. = 0x7D000000
.text :
{*(.text.*)} > MY_SECTION
}/* ld file end */ /* C file start */
Void MyFunction(void) __attribute__((section(".text.myfunci")));
Void MyFunction(void){//implement
};
/* C file end */
2.3 lsl链接脚本
参见TC3xx完整工程搭建之链接文件还有基于Tricore的Tasking链接文件解读, 和上述的ld链接脚本有异曲同工之处,但有的地方也更加复杂一点。