-
-
[原创]编译与链接学习笔记(1)——静态链接
-
发表于: 2021-7-28 23:54 9477
-
0x00. 前言
最初接触编程时,我遇到过这样一个问题:将一个源代码文件编译成一个可执行文件,编译器只需要将源代码转化成可执行的机器代码(表面上);将多个源代码文件编译成一个可执行文件,编译器又是如何合并多个文件中的代码的呢?
学习了逆向后我知道这个过程叫做“链接”。然而链接这个概念又困扰了我很久:什么是动态链接?什么是静态链接?为什么动态链接的程序到另一台机器上就跑不起来了?等等,包括搭建环境的过程中也没少遇到过与链接相关的错误。
最近发现正好有那么一本书《程序员的自我修养——链接、装载与库》可以解决我的疑惑,在此将我总结的内容与大家分享。
环境:
- Ubuntu 18.04
- GCC version 7.5.0
0x01. GCC编译流程
当我们用GCC编译源代码时,可以分为4个步骤:预处理(Preprocessing)、编译(Compilation)、汇编(Assembly)和链接(Linking)。大致过程如下图所示:
我们通过一个HelloWorld程序进行说明:
1
2
3
4
5
6
7
|
/ / hello.h
#ifndef _HELLO_H_ #define _HELLO_H_ void hello(); #endif |
1
2
3
4
5
6
7
8
9
10
11
|
/ / hello.c
#include #include "hello.h" void hello(){ printf( "Hello World.\n" );
} int main(){
hello();
} |
第一步:预处理
由预处理器cpp(C Pre-Processor)完成,主要工作是合并源文件和头文件,以及对以“#”开头的预编译指令和注释进行处理。
1 |
cpp hello.c > hello.i |
第二步:编译
编译过程就是把预处理完的文件进行一系列的词法分析、语法分析、语义分析及优化后生成相应的汇编代码文件。这个过程相当复杂,但不是本文的重点。
1 |
gcc - S hello.i - o hello.s
|
第三步:汇编
汇编器将汇编代码转化成相应平台上的机器代码,得到.o目标文件。注意,目标文件还需要经过链接后才能执行。
1 |
as hello.s - o hello.o
|
第四步:链接
链接器将多个目标文件链接起来,输出一个可执行文件。链接是最复杂的一个过程,也是本文的重点。
使用以下命令来查看.o文件链接成可执行文件的过程:
1 |
gcc hello.o - o hello - - verbose
|
输出:
1
2
|
/ usr / lib / gcc / x86_64 - linux - gnu / 7 / collect2 - plugin / usr / lib / gcc / x86_64 - linux - gnu / 7 / liblto_plugin.so - plugin - opt = / usr / lib / gcc / x86_64 - linux - gnu / 7 / lto - wrapper - plugin - opt = - fresolution = / tmp / ccpffkdK.res - plugin - opt = - pass - through = - lgcc - plugin - opt = - pass - through = - lgcc_s - plugin - opt = - pass - through = - lc - plugin - opt = - pass - through = - lgcc - plugin - opt = - pass - through = - lgcc_s - - build - id - - eh - frame - hdr - m elf_x86_64 - - hash - style = gnu - - as - needed - dynamic - linker / lib64 / ld - linux - x86 - 64.so . 2 - pie - z now - z relro - o hello / usr / lib / gcc / x86_64 - linux - gnu / 7 / .. / .. / .. / x86_64 - linux - gnu / Scrt1.o / usr / lib / gcc / x86_64 - linux - gnu / 7 / .. / .. / .. / x86_64 - linux - gnu / crti.o / usr / lib / gcc / x86_64 - linux - gnu / 7 / crtbeginS.o - L / usr / lib / gcc / x86_64 - linux - gnu / 7 - L / usr / lib / gcc / x86_64 - linux - gnu / 7 / .. / .. / .. / x86_64 - linux - gnu - L / usr / lib / gcc / x86_64 - linux - gnu / 7 / .. / .. / .. / .. / lib - L / lib / x86_64 - linux - gnu - L / lib / .. / lib - L / usr / lib / x86_64 - linux - gnu - L / usr / lib / .. / lib - L / usr / lib / gcc / x86_64 - linux - gnu / 7 / .. / .. / .. hello.o - lgcc - - push - state - - as - needed - lgcc_s - - pop - state - lc - lgcc - - push - state - - as - needed - lgcc_s - - pop - state / usr / lib / gcc / x86_64 - linux - gnu / 7 / crtendS.o / usr / lib / gcc / x86_64 - linux - gnu / 7 / .. / .. / .. / x86_64 - linux - gnu / crtn.o
COLLECT_GCC_OPTIONS = '-o' 'hello' '-v' '-mtune=generic' '-march=x86-64'
|
collect2是对链接器ld的一个封装,最终还是要调用ld。
可以看到除了hello.o文件,链接器还引用了大量.o后缀的目标文件,最后输出可执行文件。
0x02. 目标文件格式
由汇编器汇编后产生的.o后缀的文件叫做目标文件。目标文件从结构上讲已经是可执行文件(PE/ELF)的格式,只是还没有经过链接的过程,其中有些符号和地址还没有被调整。
用010 Editor打开一个.o文件,并使用ELF模板解析:
PE文件和ELF文件的格式在很多篇文章里已经有讲解了,这里就不再赘述了。接下来主要讲解一下符号在链接中的作用以及ELF的符号表。
链接的接口——符号
在链接中,我们将函数和变量统称为符号(Symbol),函数名或变量名称为符号名(Symbol Name),每一个目标文件都会有一个相应的符号表(Symbol Table),这个表中记录了目标文件中所用到的所有符号。每个定义的符号有一个对应的值,叫做符号值(Symbol Value),对于变量和函数来说,符号值就是它们的地址。
符号主要有以下几种分类:
- 定义在本目标文件的全局符号,可以被其他目标文件引用。
- 在本目标文件中引用的全局符号,却没有定义在本目标文件,叫做外部符号(External Symbol)。
- 段名,通常由编译器产生,符号值就是段的起始地址。如代码段“.text”、数据段“.data”等。
- 局部符号,由函数内部的局部变量产生,这些局部符号对链接过程没有作用,链接器也往往忽视他们。这也是为什么我们在用IDA反编译时看不到局部变量的名字。
- 行号信息,通常用于调试。
ELF符号表结构
ELF文件中的符号表往往是文件中的一个段——“.symtab”。符号表的结构很简单,它是一个Elf64_Sym结构(64位ELF文件)的数组。符号表及Elf64_Sym的结构如下:
1
2
3
4
5
6
7
8
9
|
typedef struct { Elf64_Word st_name; / * Symbol name (string tbl index) * /
unsigned char st_info; / * Symbol type and binding * /
unsigned char st_other; / * Symbol visibility * /
Elf64_Section st_shndx; / * Section index * /
Elf64_Addr st_value; / * Symbol value * /
Elf64_Xword st_size; / * Symbol size * /
} Elf64_Sym; |
几个成员的定义如下表所示:
st_info的低四位表示符号类型,高4位表示符号绑定信息,定义如下表:
在010 Editor中查看main函数的符号类型和绑定信息:
st_shndx定义符号所在段的下标:
main函数对应下标为1的段,即.text段。
0x03. 静态链接
当我们有两个目标文件时,如何将它们链接起来形成一个可执行文件呢?这基本上就是静态链接的核心问题。
静态链接的过程大致分为以下两个步骤:
- 空间与地址分配
- 符号解析与重定位
接下来通过以下两个程序进行讲解:
1
2
3
4
5
6
7
8
9
10
|
/ / a.c
#include int shared = 1 ;
void swap( int * a, int * b){
int t = * a;
* a = * b;
* b = t;
} |
1
2
3
4
5
6
7
8
9
10
|
/ / b.c
#include extern int shared;
extern void swap( int * , int * );
int main(){
int a = 2 ;
swap(&a, &shared);
} |