含大量图文解析及例程 | Linux下的ELF文件、链接、加载与库(下)

2022-08-26 15:53:58 浏览数 (1)

入口函数和运行库

入口函数

初学者可能一直以来都认为C程序的第一条指令就是从我们的main函数开始的,实际上并不是这样,在main开始前和结束后,系统其实帮我们做了很多准备工作和扫尾工作,下面这个例子可以证明:

我们有两个C代码:

代码语言:javascript复制
// entry.c
#include <stdio.h>

__attribute((constructor)) void before_main()
{ printf("%sn",__FUNCTION__); }

int main() {
    printf("%sn",__FUNCTION__);
}


// atexit.c
#include <stdio.h>

void post(void)
{
    printf("goodbye!n");
}

int main()
{
    atexit(&post);
    printf("exiting from mainn");
}

分别编译运行这两个程序,输出结果分别为:

代码语言:javascript复制
# entry.c
before_main
main
# atexit.c
exiting from main
goodbye!

可见,在main开始前和结束后,其实还有一部分程序在运行。

事实上操作系统装载程序之后首先运行的代码并不是我们编写的main函数的第一行,而是某些运行库的代码,它们负责初始化main函数正常执行所需要的环境,并负责调用main函数,并且在main返回之后,记录main函数的返回值,调用atexit注册的函数,最后结束进程。以Linux的运行库glibc为例,所谓的入口函数,其实 就是指ld 默认的链接脚本所指定的程序入口_start (默认情况下)。

运行库

glibc = GNU C library

Linux环境下的C语言运行库glibc包括:

  • 启动和退出相关的函数
  • C标准库函数的实现 (标准输入输出,字符处理,数学函数等等)

事实上运行库是和平台相关的,和操作系统联系的非常紧密,我们可以把运行库理解成我们的C语言(包括c )程序和操作系统之间的抽象层,使得大部分时候我们写的程序不用直接和操作系统的API和系统调用直接打交道,运行库把不同的操作系统API抽象成相同的库函数,方便应用程序的使用和移植。

Glibc有几个重要的辅助程序运行的库 /usr/lib64/crt1.o, /usr/lib64/crti.o, /usr/lib64/crtn.o。

其中crt1包含了基本的启动退出代码, ctri和crtn包含了关于.init段及.finit段相关处理的代码(实际上是_init()和_finit()的开始和结尾部分)

Glibc是运行库,它对语言的实现并不太了解,真正实现C 语言特性的是gcc编译器,所以gcc提供了两个目标文件crtbeginT.o和crtend.o来实现C 的全局构造和析构 – 实际上以上两个高亮出来的函数就是gcc提供的,有兴趣的读者可以自己翻阅gcc源代码进一步深入学习。

几组概念的辨析

动态链接的可执行文件和共享库文件的区别

问题: 可执行文件和动态库之间的区别?我们在第一节中提到过动态链接的可执行文件和动态库文件file命令的查看结果是类似的,都是shared object,一个不同之处在于可执行文件指明了解释器intepreter:

可执行文件和动态库之间的区别,简单来说:可执行文件中有main函数,动态库中没有main函数,可执行文件可以被程序执行,动态库需要依赖程序调用者。

在可执行文件的所有符号中,main函数是一个很特别的函数,对C/C 程序开发人员来说,main函数是整个程序的起点;但是,main函数却不是程序启动后真正首先执行的代码。

除了由程序员编写的源代码编译成目标文件进而链接到程序内存映射,还有一部分机器指令代码是在链接过程中添加到程序内存映射中。比如,程序的启动代码,放在内存映射的起始处,在执行main函数之前执行以及在程序终止后完成一些任务编译动态库时,链接器没有添加这部分代码。这是可执行文件和动态库之间的区别。

静态链接 / 动态链接的可执行文件的第一条指令地址

  • 静态链接可执行文件的第一条指令地址

我们之前提到过,静态链接的可执行文件的其实地址就是本文件的_strat,即readelf -h所得到的的起始地址。对于一个hello程序:

代码语言:javascript复制
// hello.c
#include <stdio.h>

int main(){
    printf("Hellow World.n");
    return 0;
}

我们先用选项-static来静态链接它,得到hello-st:

gcc -static hello.c -o hello-st

我们先用file命令看一下:

它是静态链接的可执行文件。

我们用readelf -h查看其入口地址,并在gdb中starti查看它实际的第一条指令的地址:

可以看到,与我们的预期是一致的,确是是从文件本身真正的入口地址entry point0x400a50开始执行第一条指令。而在动态链接的可执行文件中,我们将看到不同。

  • 动态链接的可执行文件的第一条指令地址

我们现在动态链接(默认)编译hello程序得到hello-dy:

gcc hello.c -o hello-dy

还是先来file一下:

我们看到hello-dy是一个动态链接的共享目标文件,当然它也是可执行的,共享库文件和可执行的共享目标文件的区别我们上面已经介绍过了。大家注意,这里还多了一个奇怪的家伙:解释器,interpreter /lib64/ld-linux-x86-64.so.2。

实际上,它就是动态链接文件的链接加载器。我们之前已经介绍过,在动态链接的可执行文件中,外部符号的地址在程序加载、运行的过程中才被确定下来。这个链接加载器 ld 就是负责完成这个工作的。当 ld 将外部符号的地址都确定好之后,才将指令指针执行程序本身的_start。也就是说,在动态链接的可执行文件中,第一条指令应该在链接加载器 ld 中。我们接下来还是通过readelf -h和gdb来验证一下。

可以看到,我们的动态链接的可执行程序的第一条指令的地址并不是本文件的entry point 0x530,而是链接加载器 ld 的第一条指令_start的地址 0x7ffff7dd4090。

这就验证了我们上面的说法:动态链接的可执行文件的第一条指令是链接加载器的程序入口,它会完成外部符号地址的绑定,然后将控制权交还给程序本身,开始执行。

静态库和共享库

库:有时候需要把一组代码编译成一个库,这个库在很多项目中都要用到,例如libc就是这样一个库,我们在不同的程序中都会用到libc中的库函数(例如printf)。

共享库和静态库的区别:在链接libc共享库时只是指定了动态链接器和该程序所需要的库文件,并没有真的做链接,可执行文件调用的libc库函数仍然是未定义符号,要在运行时做动态链接。而在链接静态库时,链接器会把静态库中的目标文件取出来和可执行文件真正链接在一起。

  • 静态库链接后,指令由相对地址变为绝对地址,各段的加载地址定死了。
  • 共享库链接后,指令仍是相对地址,共享库各段的加载地址并没有定死,可以加载到任意位置。

静态库好处:静态库中存在很多部分,链接器可以从静态库中只取出需要的部分来做链接 (比如main.c需要stach.c其中的一个函数,而stach.c中有4个函数,则打包库后,只会链接用到那个函数)。另一个好处就是使用静态库只需写一个库文件名,而不需要写一长串目标文件名。

5T技术资源大放送!包括但不限于:C/C ,Arm, Linux,Android,人工智能,单片机,树莓派,等等。在上面的【人人都是极客】公众号内回复「peter」,即可免费获取!!

记得点击分享、赞和在看,给我充点儿电吧

0 人点赞