新年的第一篇博文,先祝我工作顺利,万事如意!祝福大家的话就不赘述了。
在开始操作系统的内存管理相关内容前,首先来关注链接脚本,因为动态内存即堆区的地址是在链接脚本中分配的,知道了堆的起始地址和长度才能进行内存的分配和管理。
1. 链接脚本的作用是什么?
链接的作用就是把编译生成的多个目标文件(.o)合并起来,生成最后的可执行文件(.elf)。如上图中间的就是.o目标文件,最右的则是链接生成的.elf文件。除此之外,链接脚本还关注一个问题,就是生成的各个段被加载在内存的什么位置。
举个例子很容易就明白,下面是一个RISC-V代码的链接脚本:
OUTPUT_ARCH( "riscv" ) /* 代码采用的是RISC-V架构*/
ENTRY( _start ) /*代码入口符号是_start,就是汇编启动函数的符号*/
MEMORY
{
/* 定义了一段起始地址为0x80000000,长度为128MB的内存区域,取名叫ram*/
ram (wxa!ri) : ORIGIN = 0x80000000, LENGTH = 128M
}
SECTIONS
{
/* 所有输入文件中的.text段、.text.*段都合在一起,组成输出elf文件中的.text段;
* 此外,定义了两个符号_text_start和_text_end ,注意符号'.'代表的是当前地址;
* 生成的.text段被放在了ram这个内存区域中。
*/
.text : {
PROVIDE(_text_start = .);
*(.text .text.*)
PROVIDE(_text_end = .);
} >ram
.rodata : {
PROVIDE(_rodata_start = .);
*(.rodata .rodata.*)
PROVIDE(_rodata_end = .);
} >ram
.data : {
. = ALIGN(4096);
PROVIDE(_data_start = .);
*(.sdata .sdata.*)
*(.data .data.*)
PROVIDE(_data_end = .);
} >ram
.bss :{
PROVIDE(_bss_start = .);
*(.sbss .sbss.*)
*(.bss .bss.*)
*(COMMON)
PROVIDE(_bss_end = .);
} >ram
PROVIDE(_memory_start = ORIGIN(ram));
PROVIDE(_memory_end = ORIGIN(ram) + LENGTH(ram));
PROVIDE(_heap_start = _bss_end);
PROVIDE(_heap_size = _memory_end - _heap_start);
}
从上面这个简单的链接脚本中可以看出,MEMORY语法和SECTIONS语法是链接脚本里最关键的两个。该链接脚本依次将.text、.rodata、.data等段按顺序放在了ram内存区域内。并通过定义_heap_start和_heap_size把分配以后剩下的全部空间都分给了堆区(该例子中没有给栈分配空间,因为栈也可以start.S中分配在代码空间里,就是上面的.text空间里包含了)。
表面上看,我们的问题解决了,堆的起始地址和长度都知道了,但是问题出现了,在链接脚本中定义的符号可以在C代码中直接使用吗?请看第二小节。
2. 在C语言中使用链接脚本中定义的符号
符号就是名字,变量名和函数名都是符号。在目标文件中有一个专门的"符号表"段用来放符号。比如我们定义一个全局变量:
int g_foo = 100;
显然在data段会有四字节的空间存放100这个值,假设是0x00001000~0x00001003这四个字节。而g_foo这个符号或者说变量名字,则跟0x00001000这个地址一起存放在符号表里。
typedef struct
{
Elf32_Word st_name; /* Symbol name (string tbl index) */字符串表中的字节偏移,指向符号的以null结尾的字符串名称。
Elf32_Addr st_value; /* Symbol value */符号的地址,是距定义目标的节的起始位置的偏移,对于可执行目标文件来说,该值是一个绝对运行时地址。
Elf32_Word st_size; /* Symbol size */对象的长度,例如一个指针的长度或struct中包含的字节数,如果长度未知,其值可以设置为0。
unsigned char st_info; /* Symbol type and binding */ 一个符号的确切用途由st_info定义,它分为两部分。
unsigned char st_other; /* Symbol visibility */未使用
Elf32_Section st_shndx; /* Section index */保存一个节的索引(在节头表中),符号将绑定到该节,该符号通常定义在此节的代码中。
} Elf32_Sym;
符号的结构体定义如上所示。对于上述g_foo这个变量来说,其符号结构体中st_name间接表示"g_foo"这个字符串,st_value值就是0x00001000,st_size代表的则是4。在C语言中,当我们使用&g_foo
就可以得到g_foo变量的地址,即st_value。
但是,在链接脚本中定义的符号与变量不同,链接脚本中的符号我们可以认为就是一个地址的别名,目标文件中不会为这个符号开辟存储空间。举个例子,一个符号_heap_start = 0x00002000
,其符号结构体中,st_value值就是0x00002000。当我们在C语言中想使用_heap_start,可以通过下面程序实现:
extern int _heap_start ;
int val = &_heap_start ;
val的值就是0x00002000。
顺着思路继续,我们可以在汇编代码中定义一个真正的变量,其值等于符号值,这样在C语言中使用的时候就不用取地址来用了。
在汇编中写下面的代码,.word即为HEAP_START开辟一个word大小的内存空间,值为_heap_start:
.global HEAP_START
HEAP_START: .word _heap_start
然后,就可以在C语言中做extern声明后直接使用HEAP_START,其值就是0x00002000。
参考:https://blog.youkuaiyun.com/dropping_1979/article/details/42555349
3. 利用链接脚本把变量指定存放的物理位置
MEMORY
{undefined
ps7_ddr_0_S_AXI_BASEADDR : ORIGIN = 0x00100000, LENGTH = 0x3FF00000
ps7_ram_0_S_AXI_BASEADDR : ORIGIN = 0x00000000, LENGTH = 0x00030000
ps7_ram_1_S_AXI_BASEADDR : ORIGIN = 0xFFFF0000, LENGTH = 0x0000FE00
ps7_ddr_0_A_AXI_MATRIX : ORIGIN = 0x20000000, LENGTH = 0x100000
}
.matirx : {undefined
__matrix_start = .;
*(.matrix)
*(.matrix.*)
__matrix_end = .;
} > ps7_ddr_0_A_AXI_MATRIX
_end = .;
}
首先在链接脚本中定义一段单独的内存区域和单独的section。
然后在C语言编程时,采用下面语句指定全局变量的存储位置就好了。在Xilinx SDK编程中亲测可用。
int matrix[16][16384] __attribute__((section(".matrix")));