linker.ld在链接阶段的行为
linker.ld
抽出编译AM程序中的“打包用户程序am-test到ELF”步骤,看看链接脚本abstract-machine/scripts/linker.ld
如何将库函数和用户程序链接起来的。
链接命令
首先看下链接命令:
echo + LD "->" build/amtest-riscv32-nemu.elf
($CROSS_COMPILE)ld -z noexecstack -melf64lriscv
-T /abstract-machine/scripts/linker.ld
--defsym=_pmem_start=0x80000000
--defsym=_entry_offset=0x0
--gc-sections
-e _start
-melf32lriscv
-o $AM_TEST/build/amtest-riscv32-nemu.elf
--start-group
$AM_TEST/build/riscv32-nemu/src/main.o
$AM_TEST/build/riscv32-nemu/src/tests/video.o
$AM_TEST/build/riscv32-nemu/src/tests/mp.o
$AM_TEST/build/riscv32-nemu/src/tests/hello.o
$AM_TEST/build/riscv32-nemu/src/tests/devscan.o
$AM_TEST/build/riscv32-nemu/src/tests/audio/audio-data.o
$AM_TEST/build/riscv32-nemu/src/tests/audio.o
$AM_TEST/build/riscv32-nemu/src/tests/keyboard.o
$AM_TEST/build/riscv32-nemu/src/tests/intr.o
$AM_TEST/build/riscv32-nemu/src/tests/rtc.o
$AM_TEST/build/riscv32-nemu/src/tests/vm.o
/abstract-machine/am/build/am-riscv32-nemu.a
/abstract-machine/klib/build/klib-riscv32-nemu.a
--end-group
这里蕴含几个关键信息:
linker.ld
作为链接的自定义脚本- 设置符号(symbol)
_pmem_start
的值为0x80000000,_entry_offset
为0。 - 设置程序的入口地址为
_start
链接脚本linker.ld
接下来我们转移到链接脚本abstract-machine/scripts/linker.ld
的具体实现:
ENTRY(_start)
PHDRS { text PT_LOAD; data PT_LOAD; }
SECTIONS {
/* _pmem_start and _entry_offset are defined in LDFLAGS */
. = _pmem_start + _entry_offset;
.text : {
*(entry) /* 引用与符号 entry 相关的所有内容*/
*(.text*) /* 将所有文件的 .text 段的内容合并到当前段*/
} : text /* 指定 .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);
}
linker.ld
的链接内容:
-
设置
_start
作为程序的入口点:ENTRY(_start)
-
加载
.text
和.data
段到内存中:PHDRS { text PT_LOAD; data PT_LOAD; }
。PT_LOAD
是Program Header的类型 -
设置当前地址
_pmem_start + _entry_offset
:.
表示当前地址,_pmem_start + _entry_offset
就是定义当前地址的位置。这里的地址就是0x80000000
-
设置
.text
、.rodata
、.data
、.bss
段。其中拿出.text
段的处理.text : { *(entry) /* 引用与符号 entry 相关的所有内容*/ *(.text*) /* 将所有文件的 .text 段的内容合并到当前段*/ } : text /* 指定 .text 段被加载到名为 text 的内存区域中*/
-
设置栈和堆的布局
_stack_top = ALIGN(0x1000);
确保栈从 4KB 对齐的位置开始。. = _stack_top + 0x8000;
设置栈的初始位置,分配了 32KB 的栈空间。stack_pointer = .;
设置_stack_pointer
符号,指示栈指针的位置
-
设置结束符号:
end
和_end
-
设置堆的起始地址,并确保堆从 4KB 对齐的位置开始
_heap_start = ALIGN(0x1000)
\
入口地址
而其中符号entry
是在abstract-machine/am/src/riscv/nemu/start.S
中定义的
.section entry, "ax"
.globl _start
.type _start, @function
_start:
mv s0, zero # 清零s0寄存器
la sp, _stack_pointer # 将标签 _stack_pointer 的地址加载到栈指针 sp 中,符号在linker.ld中定义。
jal _trm_init # 这条指令会跳转到 _trm_init 标签,并将当前指令地址(即 jal 的下一条指令的地址)压入栈中
# _trm_init就是trm初始化函数
此程序设置了程序的入口点_start
,并且将符号 _start
声明为一个函数类型。
结合trm.c
的TRM初始化函数_trm_init()
分析
void _trm_init() {
int ret = main(mainargs);
halt(ret);
}
总结
综上所有关于链接的符号和语法,我们可以得知链接后的ELF程序的一些信息是:
- 程序入口地址
_start
设置为0x80000000
(ElfN_Ehdr.e_entry = 0x80000000
) - 程序段有两个:
data
和text
(Elf32_Phdr.p_type == PT_LOAD
) .text
代码段的开始就是start.S
中的.section entry
指示的地址(text.Elf32_Phdr.p_vaddr == 0x80000000
)etext
和_etext
符号所代表的地址为.text
段的结尾处(addr(_start)
+len(.text)
)eadta
和_data
符号位于.data
的末尾
查看ELF文件
riscv64-linux-gnu-readelf -a build/amtest-riscv32-nemu.elf
-
在
ELF Header
栏中,程序入口地址被设置为0x80000000
Entry point address: 0x80000000
-
在段头表
Section Headers
中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 001f70 00 AX 0 0 8 [ 2] .rodata PROGBITS 80001f70 002f70 000e0f 00 A 0 0 4 [ 3] .srodata.mainargs PROGBITS 80002d80 003d80 000002 00 A 0 0 4 [ 4] .data PROGBITS 80002d88 003d88 00de0c 00 WA 0 0 8 [ 5] .data.code PROGBITS 80010b94 011b94 00000f 00 WA 0 0 4 [ 6] .sdata.first_trap PROGBITS 80010ba4 011ba4 000004 00 WA 0 0 4 [ 7] .sdata.heap PROGBITS 80010ba8 011ba8 000008 00 WA 0 0 4 [ 8] .data.lut PROGBITS 80010bb0 011bb0 000200 00 WA 0 0 4 [ 9] .sdata.next PROGBITS 80010db0 011db0 000004 00 WA 0 0 4 [10] .bss NOBITS 80010db4 011db4 084240 00 WA 0 0 4 [11] .comment PROGBITS 00000000 011db4 00002b 01 MS 0 0 1 [12] .riscv.attributes RISCV_ATTRIBUTE 00000000 011ddf 000023 00 0 0 1 [13] .symtab SYMTAB 00000000 011e04 000dc0 10 14 138 4 [14] .strtab STRTAB 00000000 012bc4 000500 00 0 0 1 [15] .shstrtab STRTAB 00000000 0130c4 0000a0 00 0 0 1
其中
.text
段在进程中的地址为0x80000000
,大小为001f70
,所以结束地址为0x80001f70
.标志Flg
为AX
A
:该节是可分配的,意味着它将被映射到进程的虚拟地址空间中X
:该节是可执行的,表示它包含程序的机器指令,可以被 CPU 执行
.data
地址为80002d88
,大小为00de0c
字节,结束地址为80010b94
-
程序头表
Program Headers
Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align RISCV_ATTRIBUT 0x011ddf 0x00000000 0x00000000 0x00023 0x00000 R 0x1 LOAD 0x001000 0x80000000 0x80000000 0x02d82 0x02d82 R E 0x1000 LOAD 0x003d88 0x80002d88 0x80002d88 0x0e02c 0x9226c RW 0x1000 Section to Segment mapping: Segment Sections... 00 .riscv.attributes 01 .text .rodata .srodata.mainargs 02 .data .data.code .sdata.first_trap .sdata.heap .data.lut .sdata.next .bss
- 第一个
LOAD
的程序段从.text
段开始,以及后面的.rodata
和.srodata.mainargs
- 第一个
-
符号表
.symtab
中_start
和.text
地址为80000000
.rodata
、_etext
和etext
地址为80001f70
,恰好是.text
的结束地址.data
地址为80002d88
,edata
和_data
地址为80010b94
,也是.data
的结束地址_bss_start
地址为80010db4
,是.bss
段的开始地址end
和_end
的地址为8009d000
至此,linker.d
的行为就分析完了。