一、链接器的作用
1.1 编译阶段目标文件的组成:1.2 链接器的作用: 二、代码重定位三、程序的启动过程四、链接器的工作方式
4.1 指定链接起始地址4.2 指定存储区域4.3 指定代码入口
一、链接器的作用源文件被编译成目标文件之后,如何变成最后的可执行文件?
链接器的作用是把各个模块之间相互引用的部分处理好,使各个模块之间能够正确衔接。 1.1 编译阶段目标文件的组成:
各个段没有具体的起始地址,只有段大小各个标识符没有实地址,只有相对地址段和标识符的实地址需要链接器指定 1.2 链接器的作用:
将目标文件和库文件整合成目标可执行程序合并各个目标文件中的段(*.txt *.bss *.data)确定各个段中标识符的目标地址(重定向) 二、代码重定位
下面使用一段代码编译成中间文件,然后使用nm工具查看里面的段信息和符号信息。
nm命令是linux下自带的特定文件分析工具,一般用来检查分析二进制文件、库文件、可执行文件中的符号表,返回二进制文件中各段的信息。
#include#include "module.h" error_t module_timer (system_state_t _state) { return 0; } error_t module_memory (system_state_t _state) { return 0; } void module_registration_entry () { (void) module_register ("Timer", MODULE_TIMER, OS_LEVEL, module_timer); (void) module_register ("Memory", MODULE_HEAP, OS_LEVEL, module_memory); } int main () { module_registration_entry (); printf ("System is going to be upn"); if (0 != system_up ()) { printf ("Error: system cannot be upn"); return -1; } printf ("nSystem is going to be downn"); system_down (); return 0; }
nm中的标识符的含义:
下面是main.o的一些符号信息,module_register 没有在main.c中定义,在编译阶段不确定具体地址,module_timer显示了函数相对于text段的相对地址,如果是全局变量那也只是得到相对于bss或者data段的相对地址。
> nm build/main.o 00000000000000e9 T main 0000000000000058 T module_memory U module_register 00000000000000b0 T module_registration_entry 0000000000000000 T module_timer U puts U system_down U system_up
代码链接完成之后,所有的符号都有了具体地址,重定向代码就是链接器的主要工作之一:
> nm build/target 000000000040180a T main 0000000000401779 T module_memory 0000000000401316 T module_register 00000000004017d1 T module_registration_entry 0000000000401721 T module_timer U puts@@GLIBC_2.2.5 0000000000401658 T system_down 00000000004014a8 T system_up三、程序的启动过程
main函数是第一个被执行的函数吗?
程序加载后,第一个被执行的是_start()函数_start()函数准备好之后调用libc_start_main()libc_start_main准备好运行环境之后再运行main函数_start()函数的入口地址就是代码段起始地址
为了证明上述结论,借助反汇编查找main函数地址:
> objdump -S build/target > target.s
000000000040180a:
然后在target.s中查找 40180a ,下面的代码中告诉我们main函数的地址被放入到寄存器中,除了main()函数之外,还有__libc_csu_fini()和__libc_csu_init(),这个三个函数地址共同作为参数被传递到__libc_start_main()中进行调用。
__libc_start_main()的作用:
调用__libc_csu_init()完成必要的初始化启动主线程,入口为main注册__libc_csu_fini函数,在程序结束时被调用
0000000000401060 <_start>: 401060: f3 0f 1e fa endbr64 401064: 31 ed xor %ebp,%ebp 401066: 49 89 d1 mov %rdx,%r9 401069: 5e pop %rsi 40106a: 48 89 e2 mov %rsp,%rdx 40106d: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp 401071: 50 push %rax 401072: 54 push %rsp 401073: 49 c7 c0 a0 1f 40 00 mov $0x401fa0,%r8 # <__libc_csu_fini> 40107a: 48 c7 c1 30 1f 40 00 mov $0x401f30,%rcx # <__libc_csu_init> 401081: 48 c7 c7 0a 18 40 00 mov $0x40180a,%rdi #401088: ff 15 62 2f 00 00 callq *0x2f62(%rip) # 403ff0 <__libc_start_main@GLIBC_2.2.5> 40108e: f4 hlt 40108f: 90 nop
通过上面还不能证明第一个被调用的函数就是_start,因此还需要借助objdump查看段信息,下面列出了代码段的起始地址,电脑将代码段拷贝到内存中之后会将PC指针指向代码段起始地址然后开始运行,因此_start()才是第一个被执行的函数。
> objdump -h build/target build/target: 文件格式 elf64-x86-64 段名称 段大小 虚拟地址 加载地址 对齐 Idx Name Size VMA LMA File off Algn 12 .text 00000f45 0000000000401060 0000000000401060 00001060 2**4 CONTENTS, ALLOC, LOAD, READONLY, CODE
最后再看一下程序是如何执行的:
四、链接器的工作方式上一节中我们分析了一个程序的启动过程,入口函数是_start,然后创建了一个线程,这样的执行过程是依据什么规则来进行链接的呢?
描述这个规则的文件就是链接脚本,它的功能如下:
合并目标文件中的段重定位段起始地址重定位符号的最终地址
使用 ld --verbose 可以查看默认链接脚本,前面分析的程序启动过程,正是按照这个方式进行链接的。
4.1 指定链接起始地址在链接脚本中可以指定段的起始链接地址,也可以在链接脚本中定义变量并指定变量的地址:
SECTIONS { .text 0x2000000: { *.text } . = 0x8000000; g_flag = . ; .data 0x3000000: { *.data } .bss : { *.bss } }
注意事项:
各个段的链接地址必须符合平台规范,不符合规范链接出的程序不能正常执行链接脚本能直接定义标识符的存储地址链接脚本能指定源代码中标识符的存储位置
通过指定链接脚本之后,我们再来看一下对应的段起始地址是否发生了变化:
> objdump -h build/target Idx Name Size VMA LMA File off Algn 12 .text 00000f45 0000000002000000 0000000002000000 00002000 2**4 CONTENTS, ALLOC, LOAD, READONLY, CODE 14 .rodata 000002a1 0000000002001000 0000000002001000 00003000 2**3 CONTENTS, ALLOC, LOAD, READONLY, DATA 23 .data 00000010 0000000003000000 0000000003000000 00005000 2**3 CONTENTS, ALLOC, LOAD, DATA 24 .bss 00000668 0000000003000020 0000000003000020 00005010 2**5 ALLOC
在测试代码中打印了在链接脚本中的全局变量:
# includeextern int g_flag; int main() { printf("g_flag addr:%lxn", (long unsigned int)&g_flag); }
结果如下:
g_flag addr:80000004.2 指定存储区域
默认情况下应用程序加载在同一个存储空间运行,但是在一些嵌入式系统中,存在多个存储空间(RAM存储数据,Flash存储代码),需要使用MEMORY指定存储区域。
下面是STM32CubeMx生成的Makefile工程,由于STM32的存储器分为了三个不同的部分,并且互相之间不连续,需要使用MEMORY指定存储区域:
MEMORY { RAM(xrw):ORIGIN = 0x20000000, LENGTH = 128K CCMRAM(xrw):ORIGIN = 0x10000000, LENGTH = 64K FLASH(rx):ORIGIN = 0x80000000, LENGTH = 512K }
下面是MEMORY的属性说明:
根据功能划分将不同的段放入到了不同的存储器中:
SECTIONS { .isr_vector : { . = ALIGN(4); KEEP(*(.isr_vector)) . = ALIGN(4); } >FLASH .text : { . = ALIGN(4); *(.text) *(.text*) *(.glue_7) *(.glue_7t) *(.eh_frame) KEEP (*(.init)) KEEP (*(.fini)) . = ALIGN(4); _etext = .; } >FLASH .rodata : { . = ALIGN(4); *(.rodata) *(.rodata*) . = ALIGN(4); } >FLASH .data : { . = ALIGN(4); _sdata = .; *(.data) *(.data*) . = ALIGN(4); _edata = .; } >RAM AT> FLASH
这里的AT指令是用于指定存储的位置,如果没有AT指令,那么存储这段数据的地址就是对应的链接地址,当程序段链接地址和存储地址不一致的时候,可以使用AT指定,在程序运行之后需要将对应的段从存储地址加载到链接地址。
中断向量表,代码段,数据段初始值,只读数据段都被放到了FLASH中,程序运行之后从RAM中访问数据,因此在访问之前需要把这段数据从FLASH(LMA)加载到RAM(VMA)中,做数据段的初始化,然后再执行后面的程序。
像这种情况下VMA和LMA就是不一致的,前面我们看到的Linux上的VMA和LMA是一致的,这是因为程序的加载地址就是运行时的虚拟地址,而不需要像嵌入式设备中一样需要把数据从一个地址搬到另一个地址,然后再开始运行程序。
4.3 指定代码入口前面我们看到了程序的入口为_start,我们可以查看默认的链接脚本中有这么一行:
OUTPUT_FORMAT("elf64-x86-64", "elf64-x86-64", "elf64-x86-64") OUTPUT_ARCH(i386:x86-64) ENTRY(_start)
这一句在 *** 作系统将程序从硬盘加载到内存中之后,将会把PC指针指向_start的地址,然后开始执行。
我们也可以看一下STM32的链接脚本入口为ResetHandler,意味着如果从内部bootrom执行完成之后,将跳转到Reset_Handler开始运行:
ENTRY(Reset_Handler)
这里将名为.isr_vector的段写入了FLASH起始位置0x80000000,
SECTIONS { .isr_vector : { . = ALIGN(4); KEEP(*(.isr_vector)) . = ALIGN(4); } >FLASH }
下面是.isr_vector的部分定义,最前面的4字节并不是Reset_Handler,而是栈顶指针,内部bootrom将0x80000004处存储的Reset_Handler地址取出,然后进行跳转:
.section .isr_vector,"a",%progbits .type g_pfnVectors, %object .size g_pfnVectors, .-g_pfnVectors g_pfnVectors: .word _estack .word Reset_Handler .word NMI_Handler .word HardFault_Handler .word MemManage_Handler .word BusFault_Handler .word UsageFault_Handler
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)