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
的行为就分析完了。
本文作者:上山砍大树
本文链接:https://www.cnblogs.com/shangshankandashu/p/18548419
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步