ILD

Linkers and Loaders 初学笔记
作者:Herbert Yuan 邮箱:yuanjp89@163.com
发布时间:2017-5-20 站点:Inside Linux Development

1 编译过程

对于源文件a.c,编译为目标文件a.o要经过预处理、编译和汇编3个步骤。多个目标文件生成可执行文件要经过链接处理。

1.1 预处理 Preprocess

源文件a.c的源码如下:

1
2
3
4
5
#define NUM_2  2
int double_num(int num)
{
    return num * NUM_2;
}

预处理一个重要的作用是宏替换,预处理命令如下。

cpp a.c a.i

gcc -E a.c -o a.i

输出结果为:

1
2
3
4
5
6
7
8
9
10
11
12
# 1 "a.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 1 "<command-line>" 2
# 1 "a.c"
  
int double_num(int num)
{
 return num * 2;
}

1.2 编译 Compile

编译就是将c语言代码编译为汇编代码,编译命令如下:

gcc -S a.i -o a.S

输出结果为汇编文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
        .file   "a.c"
        .text
        .globl  double_num
        .type   double_num, @function
double_num:
.LFB0:
        .cfi_startproc
        pushl   %ebp
        .cfi_def_cfa_offset 8
        .cfi_offset 5, -8
        movl    %esp, %ebp
        .cfi_def_cfa_register 5
        movl    8(%ebp), %eax
        addl    %eax, %eax
        popl    %ebp
        .cfi_restore 5
        .cfi_def_cfa 4, 4
        ret
        .cfi_endproc
.LFE0:
        .size   double_num, .-double_num
        .ident  "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.4) 5.4.0 20160609"
        .section        .note.GNU-stack,"",@progbits

1.3 汇编 Assembly

汇编就是将汇编代码翻译为机器二进制码。汇编命令如下:

as a.S -o a.o

gcc -c a.S -o a.o

汇编的结果为目标文件(object file),其为二进制。

1.4 链接 Link

如果还有一个源文件b.c,其包含main入口函数,调用double_num(),使用上述相同的方法编译出目标文件b.o,然后和a.o可链接为可执行文件。

1
2
3
4
5
6
7
8
#include <stdio.h>
 
extern int double_num(int);
 
void main()
{
    printf("%d\n", double_num(2));
}

链接命令如下:

gcc a.o b.o -o a.out

也可以用ld命令,gcc做了很多工作,可以使用-v查看gcc的链接过程。

2 链接器和加载器的任务

加载器执行程序加载,链接器执行符号解析,它们都能执行重定向。

3 Object Files

目标文件有3种形式:

编译器和汇编器产生可重定向目标文件和共享目标文件,链接器产生可执行目标文件。目标文件的格式随系统不同而变化,早期的System V使用COFF格式。Windows使用COFF的变种PE (portable excutable) 格式。IBM使用IBM 360格式,现代Unix系统,包括Linux和Solaris使用Unix ELF (executable and linking format)格式。

4 ELF格式

ELF Header包括

.textCPU指令
.rodata只读数据段
.data已初始化的全局变量段
.bssblock storage start, 未初始化的全局变量。在目标文件中不实际占用空间。
.symtab函数和全局变量的符号表。定义在目标文件中或者被目标文件引用。
.rel.text.text section的地址列表,当链接器和其它目标文件结合时,这些地址需要重定向。
.rel.data引用的全局数据的重定向信息。
.debug使用-g选项时才有,局部和全局变量的调试符号表。
.line  .text中机器码和c源码之间对应的行号。
.strtab .symtab和.debug等中的字符串表。

等。ELF文件以4字节的magic string: 0x7F ELF 开始。

5 Symbols and Symbol Resolution

每一个可重定向目标文件都包含符号表和相关的符号。在链接器上下文中,包含下述类型符号。

链接器通过关联每个引用到唯一的定义来解析符号。

符号定义分为强定义和弱定义,函数和初始化全局变量是强定义,未初始化全局变量是弱定义。全局符号解析有如下规则:

不允许多个强定义。

单个强定义和多个弱定义,选择强定义符号。

多个弱定义符号,选择任何一个。

如下一个强定义,多个弱定义是允许的:

1
2
3
// a.c
int a = 1;
int a;
1
2
// b.c
int a;

静态库是目标文件的集合,以archive的形式存储在磁盘上,ELF archive以8字节的!<arch>\n开头。静态库传给链接器和目标文件合并成可执行文件,最终的可执行文件仅包含静态库中被引用到的变量和函数。

在处理包含静态库的符号解析时,链接器按照命令行输入参数的顺序从左自右扫描object files和archives。扫描过程中,链接器维护3个集合。O,可重定向目标文件;U,未解析符号,D,定义在之前输入模块的符号。开始时这3个集合全部为空。

对于命令行中的每一个参数,链接器决定是object file还是archive,如果是object file,添加到O,并更新U和D。

如果输入是archive,链接器扫描archive的成员模块,如果一些archive成员定义了U中未解析的成员,将他们添加到O中。然后根据archive member中的每个符号更新U和D。

所有的输入参数处理后,如果U为非空,打印错误。否则merge and relocates O中的所有目标文件,构件输出可执行文件。 

这也解释了为什么静态库通常放在链接器参数的最后。静态库中有未定义符号的需要特别注意。

6 Relocation

一旦链接器解析了所有的符号,每个符号引用已经有唯一的定义了。此时,链接器就开始处理重定向,这包括两个步骤。

  1. 重定向段和符号定义,链接器合并所有的相同类型的section为同一个section。链接器分配运行时地址给新合并的section、模块输入的section和每一个符号,从而每条指令和全局变量都有唯一的loadtime address。

  2. 重定向段中的符号引用。链接器修改代码段和数据段中的符号引用,使他们指向正确的加载地址。

当汇编器统计到未解析引用,他产生那个对象的重定向入口,并放到.rel.text/.rel.data段。一个重定向入口(relocation entry)包含如何解决引用的信息。典型的ELF重定向入口包含下述成员。

  1. 段中偏移

  2. 指向符号的符号表索引

  3. 类型,对于X86架构,R_386_PC32,表示PC相对地址,R_386_32表示绝对地址。

链接器重定向所有的重定向目标,对于R_386_PC32,重定向地址为S+A-P,对于R_386_32,地址位S+A。

7 共享库

共享库可被加载到任何内存地址,编译共享库可以使用下述命令

gcc -shared -fPIC -o libfoo a.o b.o

-fPIC高速编译器生成位置无关的代码。

当执行

gcc bar.o ./libfoo.so

生成可执行文件a.out,但是a.out并不包含目标模块a.o和b.o。a.out仅包含重定向和符号信息。这个信息用于在运行时引用libfoo.so中的代码和数据。

可执行文件也包含.interp段,包含动态链接器(dynamic linker)的名字。动态链接器本身是共享目标文件,在Linux系统中,它是ld-linux.so。

当加载器把可执行文件载入内存后,把控制权交给动态链接器,动态连机器包含一些启动代码,它映射动态链接库到程序的地址空间,然后:

  1. 重定向libfoo.so的代码和数据到内存片。

  2. 重定向a.out中所有到libfoo.so的引用。

共享库也可以在程序运行时加载,程序可以要求动态加载器加载和链接共享库,深圳不需要链接这些共享库到可执行文件,Linux提供系统调用,如dlopen, dlsym和dlclose来支持动态加载。

8 维护目标文件的工具

ar创建静态库
objdump显示目标文件的所有信息
strings列出所有可打印字符串
nm列出所有定义的符号
ldd列出目标文件依赖的共享库
strip删除符号表信息

参考资料

http://www.linuxjournal.com/article/6463

Copyright © linuxdev.cc 2017-2024. Some Rights Reserved.