静态链接,动态链接和strip

论坛 期权论坛 脚本     
匿名网站用户   2020-12-20 03:32   21   0

缘起

如果你自己折腾linux的话,肯定碰到过当你编译安装一些软件时,要求所依赖的某个共享库必须以PIC方式来编译的问题,我遇到过,还遇到过很多次。不是很明白这个PIC是什么。

自己一直也没有很系统的去研究这个问题,知道几年前碰到了《程序员的自我修养》这本书。这本书讲的实在不错。不过看完之后,感觉自己懂了,但是又好像没懂。让自己说,又说不出来什么。所以看了一遍之后,就把这本书放到了一边,直到最近,又拿起这本书翻了翻,然后在网上又找了两篇好文Load-time relocation of shared libraries, Position Independent Code (PIC) in shared libraries,决定就把这个知识梳理一下。

说明

这里我使用了section,而并没有翻译成段,是因为在ELF文件中,section跟segment还是有区别的。section是ELF文件自己的概念,它会根据不同的需求把不同的数据放到不同的section中,但是一般一些section是比较小的,而且section中又可以分为只读section跟可写,可执行section;但是现代操作系统的装载单位一般是4K,如果将一个不满1K大小的section就以4K大小装载,岂不是很浪费内存。所以在装载ELF文件的时候,还提供另外一种view,叫做segment,它会把readonly的section放在一起,一起装载。writable的section放在一起,这叫做一个segment,实际上操作系统在装载时,是按照segment进行的。

有兴趣的可以进一步的去看一下ELF的官方文档。

静态链接

静态连接相对比较简单。一句话

静态连接就是在编译时把不能确定的符号留空,在链接的时候由链接器确定具体的符号位置。

过程简介

我们知道每个c语言源文件就是一个编译单元,在一个项目中,我们为了得到最后的可执行文件,一般都会执行下面的步骤

    gcc -c first.c -o first.o
    gcc -c second.c -o second.o
    gcc first.o second.o -o hello.exe

我们先不说first.csecond.c文件中都包含什么内容,但是一般含有多个源文件的项目都会这么做的。

那么前面两步就是把单个的源文件编译成目标文件的过程,注意这里只是编译,而且这个编译过程是一个串行过程,在编译first.c的时候,是不知道有second.c的,那么这里面的符号引用就只好留空了。反应到first.o这个文件里面就是,这个文件里的以.rela开头的一些section。这些符号的装载的地址在编译期间是不能确定,编译器就会留出空来,并在.rela的section中指出这些符号的装载位置要在链接时处理。

当我们执行第三歩的时候,实际上是在做链接的操作,把两个源文件连接成我们的目标文件hello.exehello.exe将包含first.osecond.o两个文件中的全部内容,只不过它们在文件中位置是重新排列过的。这个时候,任何符号的装载地址都会在这里被确定下来,根据.rela段的指引将这些地址填写好。然后这个程序就能够被装载运行了。

静态链接的优劣

好处

符号都是解析好的,执行速度快

坏处

由于每个静态库都会编译进最终的可执行文件中,那么如果系统中有多个该可执行文件的执行实例的话,那么这些静态库就在系统有多份拷贝,内存浪费严重

动态链接

动态链接就是为了解决静态链接的缺点而生的。

有两种不同动态链接方案

  1. Load-time relocation
  2. Position independent code (PIC)

Load-time relocation

这种动态链接的实现方式现在已经不怎么用了,主要还是它虽然实现了共享库的动态加载,但是它并没有解决共享库在多个进程中共享的问题。当有多个实例运行时,一个共享库还是会被加载多份。

Position independent code (PIC)

这种便是现在多种类型的操作系统实现共享库的方式。它不光解决了共享库动态加载的问题,还让共享库全局只有一份,多个运行实例对同一个共享库的使用,都只会在系统内存中加载一份。

PIC中的把重定位分成两种

  1. 全局数据的重定位
  2. 函数的重定位

全局数据的重定位

一句话

通过GOT进行符号的定位

如果说.rela section是为了静态链接用的,那么got这个section就是为了动态链接用的,在编译链接时,链接器会标明那些符号是需要dynamic loader进行解析地址的,并把它们放到got section中,什么是got, Global Offset Table。

这么看,这个动态加载就很简单,就是根据got,让操作系统的dynamic loader解析该符号目前的地址。
GOT保证了动态库中的数据对于每个进程都是独立的,每个进程所访问的共享库中的数据都是独立的,跟其他进程是不相干的。 这时如何实现的呢?主要是dynamic loader会将共享库中的数据拷贝一份到进程的GOT中entry所指向的地址。

函数调用的重定位

一句话

通过PLT进行函数的地址解析

The Procedure Linkage Table (PLT)

这里写图片描述

如上图所示,对于动态库里面的函数调用,会被编译器翻译成call func@PLT。也就是说函数调用是通过PLT实现的。PLT中包含有很多个entry,除了第一个entry外,其他的entry都包含了相应的resolver,用来对相应符号进行解析。

这里每个entry中的内容值得看一下

  • jump到GOT中的第n个位置,
  • 但是看GOT中的内容,它是一个指向那个prepare resolver的指针,为resolver准备一些参数,在这个值被resolver解释出来之前,它只是一个指针,这也是为什么它是另外一个颜色的原因
  • 之后resolver被调用
  • resolver计算出func的真正地址,并把这个地址放到GOT[n]中,然后调用func

符号解析之后,上面的图变成了下面这样

这里写图片描述

之后再调用func这个函数,就会通过PLT调到GOT直接找到这个函数的地址,直接调用了。

说明

1. 为什么不能直接解析好函数地址,像全局数据那样,放到GOT里面?

共享库中的函数一般多于全局数据,如果在动态加载的时候把全部的函数的地址都重定位出来,一个浪费时间,占用很多空间,这样按需对函数地址进行解析,也算是一种lazy resolvement了吧。

2. 强制把所有函数地址解析好的开关

环境变量LD_BIND_NOW, 告诉dynamic loader在load动态库的时候,总是解析所有符号。
环境变量LD_BIND_NOT, 告诉dynamic loader不要再GOT中缓存函数地址解析结果,每一次函数调用都重新解析。

动态链接程序的运行

如果一个可执行文件在链接的时候是需要动态链接库的,也就是说这个可执行文件的运行时需要共享库的,操作系统在运行它的时候就有些跟静态链接不同。

比如你的可执行文件叫hello.exe,它依赖的共享库是lib.c.so。这种依赖很正常吧,printf函数还是经常用的吧。

那么当操作系统加载hello.exe之后,并不会马上把控制权交给hello.exe,因为这个可执行文件中有一些动态符号还没有解析。操作系统会在这个时候启动动态链接器(Dynamic Linker),并将控制权交给动态链接器;它在完成一系列自身的初始化操作后,根据当前的环境参数,开始对可执行文件进行动态连接工作。完成了这些工作之后,才把控制权交给我们的hello.exe,开始从它的入口开始运行。

动态链接库的搜索过程

* something magic
* 由环境变量LD_LIBRARY_PATH指定的目录
* 缓存路径 /etc/ld.so.cache
* 系统默认路径 /usr/lib, /lib

上面提到的something magic指的是LD_PRELOAD。它所指定的是个文件,这个文件中我们可以指定预先装置的一些共享库或是目标文件,

  1. LD_PRELOAD是第一个被加载的
  2. 在LD_PRELOAD中指定的文件,不管可执行文件是否依赖于它们,肯定会被加载。
  3. LD_PRELOAD中指定的共享库或目标文件中的全局符号会覆盖后面加载同名全局符号。

这个变量的意义就在于,你可以写出你自己的共享库,让它在之前加载,来达到一些调试的目的。

注意,这里说的只是全局符号,如果在你依赖的共享库,有个不是全局符号,而是私有符号,那么这种机制也就无能为力。什么是私有符号呢,以C语音距离,就是那些被声明为static的函数。我们在学习C语言的时候,知道被static声明的变量或函数只是本编译单元可见的,那么在编译链接的时候,对这些符号的引用是直接翻译成他们对应的地址的,而不会使用动态符号解析。

参见Load-time relocation of shared libraries,Extra credit: Why was the call relocation needed?

stripped or not stripped

为什么需要strip

当你使用file命令对一些二进制文件进行查看的时候,你会发现,有些输出后面会跟着striped这个单词,这时什么意思呢?

/usr/bin/ls: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=xxxxxxx, stripped

我们上面一直在说的一个关键点就是“符号”。我们的程序在编译链接的时候需要对符号进行解析。那么在运行的时候呢?这些符号是否还需要呢?

对于静态链接的程序来说,这些符号其实可以不要,因为这些符号都被链接器识别成地址啦,所以带着符号除了给调试带来方便之外,并没有什么好的作用。如果你暴露了你的符号出来,说不定一些黑客会根据你的符号进行一些hack。这种安全手段在二进制分发时是很有必要的。

对于动态链接的程序来说,自己内部的符号其实也是可以不要的,道理同上,只是对第三方库的函数调用还是需要保留符号的,dynamic loader需要根据符号进行地址重定位。

所以,对你的二进制进行strip有以下好处

  1. 安全
  2. 二进制文件变小了,不光磁盘占用减小,内存占用也会减小

debug info file

那么问题来了,符号被strip了之后,一旦有问题,我们怎么debug呢。像gdb这种当我们在一个函数上设断点时b func,gdb需要根据符号表中func的地址来设置断点的。如果没有了符号表,我们的gdb怎么工作呢?

解决方案就是debug info file

gdb提供了两种使用debug info file的方式

  1. 可执行文件使用debug link,来指定debug info file的名字。这个名字的一般格式为exectuable.debug,举例来说, ls.debug is for /usr/bin/ls
  2. 可执行文件使用build ID,这个ID同时也在debug info file中,更详细的介绍可以看gdb的--build-id命令

对于我来说,我只是用过上面第一种方法。

如何产生debug info file

很简单,可以使用objcopy命令

objcopy --only-keep-debug foo foo.debug 
strip -g foo

添加debug link到可执行文件中

objcopy --add-gnu-debuglink=foo.debug foo

这里只是一个简单的实例,debug link最好还是包含你的版本号的,这样在不同版本之间就有对应了。

分享到 :
0 人收藏
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

积分:1136255
帖子:227251
精华:0
期权论坛 期权论坛
发布
内容

下载期权论坛手机APP