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设置为0x80000000ElfN_Ehdr.e_entry = 0x80000000
  • 程序段有两个:datatextElf32_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.标志FlgAX

    • 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_etextetext地址为80001f70,恰好是.text的结束地址
    • .data地址为80002d88edata_data地址为80010b94,也是.data的结束地址
    • _bss_start地址为80010db4,是.bss段的开始地址
    • end_end的地址为8009d000

至此,linker.d的行为就分析完了。

posted @ 2024-11-15 18:06  上山砍大树  阅读(5)  评论(0编辑  收藏  举报