起因
有一个string.c
程序,其中有几种不同的全局变量:
初始化的全局变量str1,其值是一串字符,占
6
个字节初始化的全局变量s,其是一组(指向常量字符串的)指针,占
6 * 4 = 24 = 0x18
个字节未初始化的全局变量str,大小为
0
char *s[] = {
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab",
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
", World!\\n",
"Hello, World!\\n",
"#####"
};
char str1[] = "Hello";
char str[20];
当不需要将代码段和数据段分开存储的时候,下面的链接脚本是能用的:
ENTRY(_start)
PHDRS { text PT_LOAD; data PT_LOAD; }
SECTIONS {
. = 0x80000000;
.text : {
*(entry)
*(.text*)
} : text
etext = .;
_etext = .;
.rodata : {
*(.rodata*)
}
.data : {
*(.data)
} : data
edata = .;
_data = .;
.bss : {
_bss_start = .;
*(.bss*)
*(.sbss*)
*(.scommon)
}
_stack_top = ALIGN(0x1000);
. = _stack_top + 0x8000;
_stack_pointer = .;
end = .;
_end = .;
_heap_start = ALIGN(0x1000);
}
解释起来也很简单,就是把所有SECTION一股脑全放到以0x80000000开始的地址处
最终会生成如下的ELF:
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS 80000000 001000 0002f0 00 AX 0 0 4
[ 2] .rodata PROGBITS 800002f0 0012f0 0000b8 00 A 0 0 4
[ 3] .sdata.str1 PROGBITS 800003a8 0013a8 000006 00 WA 0 0 4
[ 4] .data.s PROGBITS 800003b0 0013b0 000018 00 WA 0 0 4
[ 5] .bss NOBITS 800003c8 0013c8 000014 00 WA 0 0 4
[ 6] .comment PROGBITS 00000000 0013c8 00002b 01 MS 0 0 1
[ 7] .riscv.attributes RISCV_ATTRIBUTE 00000000 0013f3 000025 00 0 0 1
[ 8] .symtab SYMTAB 00000000 001418 000300 10 9 23 4
[ 9] .strtab STRTAB 00000000 001718 0000de 00 0 0 1
[10] .shstrtab STRTAB 00000000 0017f6 00005d 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
D (mbind), p (processor specific)
There are no section groups in this file.
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
RISCV_ATTRIBUT 0x0013f3 0x00000000 0x00000000 0x00025 0x00000 R 0x1
LOAD 0x001000 0x80000000 0x80000000 0x003a8 0x003a8 R E 0x1000
LOAD 0x0013a8 0x800003a8 0x800003a8 0x00020 0x00034 RW 0x1000
Section to Segment mapping:
Segment Sections...
00 .riscv.attributes
01 .text .rodata
02 .sdata.str1 .data.s .bss
节选重要的部分:
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
...
LOAD 0x0013a8 0x800003a8 0x800003a8 0x00020 0x00034 RW 0x1000
...
Section to Segment mapping:
Segment Sections...
...
02 .sdata.str1 .data.s .bss
...
可以看到,全局变量str1
和s
以及节.bss
被包括到同一个segment 中,且大小为0x20
。而不考虑内存对齐的情况下,这三个的总大小为0x6 + 0x18 + 0x0 = 0x1e
,而不是上面的0x20
,这说明在segment内,对str1
和s
做了内存对齐,这样str1
占0x8
,s
占0x18
并且,眼尖的你可能发现了,为何str1
和s
被输出为section:.sdata.str1
和.data.s
,但str
就直接是.bss
呢?(这直接为我后面的错误实现埋下了伏笔)
错误的实现
现在,考虑到真实的存储器环境,代码和全局变量初始上电加载时需要存放在非易失存储器中(比如FLASH、MROM),但是非易失存储器一般是不可写的,那么其中的全局变量就只能读而不能写。
为解决这个问题,需要在程序运行之前将非易失存储器中的全局变量搬到可写的易失存储器(比如SRAM)中,同时通知程序从新的存储器中访问这些全局变量。
上述工作就是通过链接脚本完成的,经过修改,有了一个新的链接脚本:
ENTRY(_start)
MEMORY {
MROM (rx) : ORIGIN = 0x20000000, LENGTH = 4K /* 只读,存放代码,全局变量初始位置 */
SRAM (rw) : ORIGIN = 0x0f000000, LENGTH = 8K /* 可读写,存放数据,全局变量运行时位置 */
}
SECTIONS {
. = ORIGIN(MROM);
.text : {
*(entry)
*(.text*)
} > MROM AT> MROM
etext = .;
_etext = .;
.rodata : {
*(.rodata*)
} > MROM AT> MROM
.data :{
*(.data)
} > SRAM AT> MROM
_data_vma = ADDR(.data);
_data_lma = LOADADDR(.data);
_data_size = SIZEOF(.data);
edata = .;
_data = .;
.bss : {
_bss_start = .;
*(.bss*)
*(.sbss*)
*(.scommon)
} > SRAM
_stack_top = ALIGN(0x100);
. = _stack_top + 0x1000; /* 栈大小改为4KB */
_stack_pointer = .;
end = .;
_end = .;
_heap_start = ALIGN(0x100);
}
上述脚本中,通过MEMORY
定义了两种内存区域:MROM
可读可执行但是不能写,用来存放代码和初始全局变量;SRAM
可读可写不能执行,用来搬运后的全局变量以及栈、堆等数据。
然后在定义sections时,通过> region
声明该section在运行时处于内存的哪个区域,通过AT> region
声明该section在加载时处于内存的哪个区域。
所以下面的定义说明,.data
section从MROM
加载,但是运行时处于SRAM
。同时,我们称呼前者为lma
(加载内存地址),称呼后者为vma
(虚拟内存地址)
.data :{
*(.data)
} > SRAM AT> MROM
_data_vma = ADDR(.data);
_data_lma = LOADADDR(.data);
_data_size = SIZEOF(.data);
目前位置,我们只是通过链接脚本通知程序全局变量被搬运到了vma
,但实际上这些数据还在lma
中,所以需要一段另外的程序负责搬运这些数据,并把这段程序放到入口程序(比如上文中的_start)中:
extern char _data_lma[];
extern char _data_vma[];
extern char _data_size[];
void _boot_loader() {
memcpy((void *)_data_vma, (void *)_data_lma, (int)_data_size);
}
没错,这就是bootloader
中loader
的功能啦
编译后通过ELF
看看实现是否正确:
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS 20000000 001000 000330 00 AX 0 0 4
[ 2] .rodata PROGBITS 20000330 001330 0000b8 00 A 0 0 4
[ 3] .sdata.str1 PROGBITS 0f000000 002000 000006 00 WA 0 0 4
[ 4] .data.s PROGBITS 0f000008 002008 000018 00 WA 0 0 4
[ 5] .bss NOBITS 0f000020 002020 000014 00 WA 0 0 4
[ 6] .comment PROGBITS 00000000 002020 00002b 01 MS 0 0 1
[ 7] .riscv.attributes RISCV_ATTRIBUTE 00000000 00204b 000025 00 0 0 1
[ 8] .symtab SYMTAB 00000000 002070 000380 10 9 26 4
[ 9] .strtab STRTAB 00000000 0023f0 00011e 00 0 0 1
[10] .shstrtab STRTAB 00000000 00250e 00005d 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
D (mbind), p (processor specific)
There are no section groups in this file.
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
RISCV_ATTRIBUT 0x00204b 0x00000000 0x00000000 0x00025 0x00000 R 0x1
LOAD 0x001000 0x20000000 0x20000000 0x003e8 0x003e8 R E 0x1000
LOAD 0x002000 0x0f000000 0x200003e8 0x00006 0x00006 RW 0x1000
LOAD 0x002008 0x0f000008 0x200003ee 0x00018 0x0002c RW 0x1000
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x10
Section to Segment mapping:
Segment Sections...
00 .riscv.attributes
01 .text .rodata
02 .sdata.str1
03 .data.s .bss
04
可以看到,还是生成了三个section,.sdata.str1
、.data.s
和 .bss
,它们的lma
位于MROM
里,vma
位于SRAM
里,似乎没什么问题。但是现在这三个section并不处于一个segment中了。
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
...
LOAD 0x002000 0x0f000000 0x200003e8 0x00006 0x00006 RW 0x1000
LOAD 0x002008 0x0f000008 0x200003ee 0x00018 0x0002c RW 0x1000
...
Section to Segment mapping:
Segment Sections...
...
02 .sdata.str1
03 .data.s .bss
...
仔细观察,.sdata.str1
的物理地址PhysAddr
即 lma
是0x200003e8
,而.data.s
的PhysAddr
是0x200003ee
,两者正好相差0x06
也就是.sdata.str1
的大小。但是两者的虚拟地址VirtAddr
分别为0x002000
和0x002008
,相差为0x08
。这意味着在物理地址中,两者没有按照4字节对齐(str1
占6个字节,而s
紧跟在其后),但是在虚拟地址中两者是按照4字节对齐的(str1
实际占6个字节,但空出2个字节,再接着才是s
)。
这下问题可大了,如果继续按照上面的实现搬运这些全局变量,那么由于物理地址中str1
后没有空出这两个字节, 程序会从0x0f000008
处访问s
,但是搬运时s
被放到了0x0f000006
处,同时不要忘记了s
存放的可是指针,这样程序一旦想解引用这些指针就会访问到其他地方。
分析1:错误的通配(copy-paste error)
在经过痛苦的阅读文档和调试后,我才惊奇的发现问题出在.data
的定义上。
还记得上面我们提到,为何str1
和s
被输出为section:.sdata.str1
和.data.s
,而str
就直接是.bss
?
让我们来看看string.o
的sections:
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS 00000000 000034 000000 00 AX 0 0 4
[ 2] .data PROGBITS 00000000 000034 000000 00 WA 0 0 1
[ 3] .bss NOBITS 00000000 000034 000000 00 WA 0 0 1
[ 4] .text.check PROGBITS 00000000 000034 00001c 00 AX 0 0 4
[ 5] .rela.text.check RELA 00000000 000624 000024 0c I 18 4 4
[ 6] .text.startu[...] PROGBITS 00000000 000050 00013c 00 AX 0 0 4
[ 7] .rela.text.s[...] RELA 00000000 000648 000258 0c I 18 6 4
[ 8] .bss.str NOBITS 00000000 00018c 000014 00 WA 0 0 4
[ 9] .sdata.str1 PROGBITS 00000000 00018c 000006 00 WA 0 0 4
[10] .rodata.str1.4 PROGBITS 00000000 000194 000072 01 AMS 0 0 4
[11] .data.s PROGBITS 00000000 000208 000018 00 WA 0 0 4
[12] .rela.data.s RELA 00000000 0008a0 000048 0c I 18 11 4
[13] .comment PROGBITS 00000000 000220 00002c 01 MS 0 0 1
[14] .note.GNU-stack PROGBITS 00000000 00024c 000000 00 0 0 1
[15] .eh_frame PROGBITS 00000000 00024c 000050 00 A 0 0 4
[16] .rela.eh_frame RELA 00000000 0008e8 000090 0c I 18 15 4
[17] .riscv.attributes RISCV_ATTRIBUTE 00000000 00029c 000025 00 0 0 1
[18] .symtab SYMTAB 00000000 0002c4 0002e0 10 19 35 4
[19] .strtab STRTAB 00000000 0005a4 00007e 00 0 0 1
[20] .shstrtab STRTAB 00000000 000978 0000c0 00 0 0 1
可以看到,跟data
相关的不止有.data
还有.data.s
和 .sdata.str1
;同理跟bss
相关的不止有.bss
还有.bss.str
。
它们分别对应了全局变量s
,str1
,str
。而下面链接脚本中.data
的写法只是将所有输入目标文件的.data
输出到同一个.data
中,至于其他的.data.s
和 .sdata.str1
会隐式的创建对应的section;相反.bss
不止通配了*(.bss)
,所以string.o中的.bss.str
被输出到.bss
中
.data :{
*(.data)
} > SRAM AT> MROM
.bss : {
_bss_start = .;
*(.bss*)
*(.sbss*)
*(.scommon)
} > SRAM
分析2:为什么VMA对齐了,但是LMA没有
按照分析1的结果,.data
的通配项写错了导致隐式的输出了对应的section,而根据输出的segment可以看到,物理地址是连续的但是虚拟地址是对齐的,同时物理地址和虚拟地址都正确的处于MROM
和SRAM
中。
那么有理由怀疑隐式输出的section.sdata
使用了.data
的 > SRAM AT> MROM
,于是我修改上述通配如下来验证:
.sdata :{
*(.sdata*)
} > SRAM AT> MROM
.data :{
*(.data*)
} > SRAM AT> MROM
.bss : {
_bss_start = .;
*(.bss*)
*(.sbss*)
*(.scommon)
} > SRAM
输出的ELF果然有.data
,.sdata
和 .bss
,.data
和.bss
同样是被分配到同一个segment中。观察它们的地址,发现在物理地址上.data
还是没有对齐,而虚拟地址却对齐了。
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
...
LOAD 0x002000 0x0f000000 0x200003e8 0x00006 0x00006 RW 0x1000
LOAD 0x002008 0x0f000008 0x200003ee 0x00018 0x0002c RW 0x1000
...
Section to Segment mapping:
Segment Sections...
...
02 .sdata
03 .data .bss
...
这么说,只要在定义时让 .data
的起始地址对齐不就好了,但是随后就会发现无法简单通过操作.
来改变LMA:
.sdata :{
*(.sdata*)
} > SRAM AT> MROM
. = ALIGN(4);
.data :{
*(.data*)
} > SRAM AT> MROM
但是可以曲线实现,只是这样的话_data_size
就要更复杂了(这里懒得弄直接加2了):
.sdata : {
*(.sdata*)
} > SRAM AT> MROM
. = LOADADDR(.sdata) + SIZEOF(.sdata);
. = ALIGN(0x4);
_align_addr = .;
.data : AT(_align_addr) { // 不能直接使用'.'
*(.data*)
} > SRAM
_data_vma = ADDR(.sdata);
_data_lma = LOADADDR(.sdata);
_data_size = SIZEOF(.data) + SIZEOF(.sdata) + 0x2;
或者可以通过在section内部使用ALIGN来对齐:
.sdata :{
*(.sdata*)
. = ALIGN(4);
} > SRAM AT> MROM
.data :{
*(.data*)
} > SRAM AT> MROM
当然,最好的解决方法是仿照.bss
的通配,重新定义.data
:
.data :{
*(.data*)
*(.sdata*)
*(.scommon)
} > SRAM AT> MROM
这样,.data
中物理地址和虚拟地址都没有对齐:
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
...
LOAD 0x002000 0x0f000000 0x200003e8 0x0001e 0x0001e RW 0x1000
LOAD 0x000020 0x0f000020 0x20000406 0x00000 0x00014 RW 0x1000
...
Section to Segment mapping:
Segment Sections...
...
02 .data
03 .bss
...
而后续的.bss
虽然与.data
在物理地址上没对齐,在虚拟地址上对齐了,但是这部分数据不需要搬运,不会对程序造成影响。不过我们也可以仿照上面在section内ALIGN让它们对齐:
.data :{
*(.data*)
*(.sdata*)
*(.scommon)
. = ALIGN(4);
} > SRAM AT> MROM
分析3:为什么之前这么写没问题
TODO