首页
社区
课程
招聘
[原创]编译与链接学习笔记(1)——静态链接
发表于: 2021-7-28 23:54 9477

[原创]编译与链接学习笔记(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);
}

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

收藏
免费 5
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回