动态链接和静态链接

编译和链接

我们都知道编译和链接是在静态语言开发中,必须要存在的过程。然而这中间所做的具体的事情以及为什么需要这么做,我想很多人是不清楚的。本文正是为大家解释,编译和链接的具体内容以及相关的原因(即解决了什么问题)。

从源码到可执行文件

众所周知,源码要经过编译和链接两大步骤才会生成最终的可执行文件,可是对于其中的细节部分的概念却很多人都不清晰,这也导致了很多时候在程序出现问题时,难以定位并处理。编译和链接过程细分的话,有预处理、编译、汇编和链接,对于编译则包括词法分析、语法分析、语义分析、[中间语言生成]和优化步骤。

  1. 预处理,主要处理文件中的预编译指令,删除源码中的注释,添加调试需要的行号等信息。
  2. 编译,简单点说就是将代码生成为汇编指令并做性能上的优化。
  3. 汇编,将汇编指令生成会特定架构下的CPU指令字节。
  4. 链接,解析多个目标文件中的符号组装成最终的可执行文件。

在上述过程中,较为复杂的为编译和链接两部分。本文主要介绍链接中所做的事情,以及相关的技术。至于其它三个过程,由于本人认知有限,这里不做过多的介绍,感兴趣的朋友可以自己阅读相关书籍学习。

符号是重点

在整体链接的过程中,大家需要记住一个重要的概念——符号,符号本质上就是对内存地址的标识。CPU指令执行和内存范文,都是基于地址,倘若没有地址,CPU也就无法知道自己该执行哪条指令,该访问哪里的内存数据。可是,作为人类我们却很难直接使用二进制数,于是我们通过给地址打上标记的形式来简化我们的使用,这里给地址打上的标记也就是前面所说的符号。在高级语言中,例如C和C++,变量和函数就是符号的另一种表达。在进一步介绍具体的链接之前,先通过一段代码加深大家对于符号的理解。


#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int gSum;
int gCount = 4;

extern void sum(int, int*);

int main(int argc, char** argv) {
  sum(gCount, &gSum);
  printf("gCount=>%d, gSum=>%d\n", gCount, gSum);

  return 0;
}

void
sum(const int count, int * restrict sum){
  srand((unsigned int)time(NULL));
  for(int i=0; i<count; ++i){
    int value = rand();
    printf("%d=>%d\n", i, value);
    *sum += value;
  }
}

上面这段代码,主要就是简单的gCount次随机数的累加,程序很简单。这里主要是为了让大家大致对符号有个感性的认识。

动态链接符号表

静态链接符号表

上面两张图显示的就是随机数累和程序的可执行文件中的符号,我们发现自定义的变量和函数gCount、gSum、sum都在其中,并且调用的API接口 printf、srand、rand、time也在。两者之前主要的区别就是调用的API接口的符号是存储在ddynsym段中,而自定义的符号则在symtab段中。

ELF文件——目标文件结构

在linux操作系统中,ELF文件描述的就是可执行文件的结构。ELF文件整体上,可以认为是以段的形式将可执行文件中信息组织在一起的文件。对于ELF文件的整体布局可以参考下图,细节可以从维基百科了解。

ELF文件结构

需要注意的是,ELF文件中常见的段有**.text、.data、.bss、.rodata**。具体含义见下表:

段名称含义
.text程序执行的代码
.data初始化的全局数据或局部静态数据
.bss未初始化的全局数据或局部静态数据
.rodata只读数据

在目标文件链接的中,相关符号的地址解析,主要使用基地址+偏移的方式实现。在整个链接的过程中,链接器主要做的事情就是匹配符号,决定符号地址。后面所介绍的相关技术,也主要围绕符号匹配地址解析两个方面进行。

链接

链接从技术上可以分为,静态链接和动态链接,二者相辅相成,在可执行文件的生成过程中发挥者至关重要的作用。静态链接在链接中直接将程序引用的符号添加到可执行文件中。而动态链接则将这一过程推迟到了程序运行时,即在操作系统载入可执行文件时,解析出相关符号并载入符号所在库。动态链接主要是为了解决,在静态链接中相同的库会在多个可执行文件中重复,导致存储空间浪费的问题。概括的说,静态链接和动态链接做的就是简单的组装的工作。链接工作的核心内容,个人认为主要是符号匹配和地址解析。

静态链接

静态链接从输入的各个目标文件中,解析出相关的符号,最终生成可执行文件。该过程需要处理两个问题,第一、可执行文件中哪些符号需要解析,即符号在自身文件中找不到,第二、符号地址计算,即将可执行文件中的所有相关符号替换为最终地址。下面以一段代码解释这一过程:


// func.c
extern int foo;

int
bar() {
  return foo;
}

从上面的代码中,我们可以看到foo是外部符号,文件自身并不包含该符号。elf文件中,需要重定向的符号一般保存在以rela开头的段中,例如**.rela.text、.rela.data**,分别指代码段和数据段中需要重定向的符号。上面代码的elf文件中包含的段,如下图所示:

func段表

从上图中,可以看出.rela.text段中,存储了text段需要重定向的符号,其内容如下图所示:

func重定向符号

func.o的所有符号如下图所示:

func符号

结合上述3图可以知道,静态链接重定向中最为核心的段就是rela段,链接的主要步骤就是从中解析出需要重定位的符号,以及重定位的符号是哪个段的,并且符号在该段的偏移地址是多少。

动态链接

动态链接本质上就是将符号解析的过程移后至代码运行时,一般会在操作系统载入程序时,通过系统的动态链接程序,将程序所依赖的库及其相关符号解析至程序中。动态链接基本过程和静态链接一样,区别主要在于部分过程放在代码执行时完成。静态链接库中的符号重定位主要通过.rela段来标记,而动态链接中则主要使用.got段。程序中对外部符号的访问都会导向.got段,.got段中的符号位置会在程序载入时确定。

具体的例子后面补上

转载于:https://my.oschina.net/taodf/blog/1934328

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值