链接库

链接库

为什么需要链接库?

我们在编写程序时,会包含大量的库,如标准C库、标准数学库。这些我们不需要从头编写,因为从头编写可能要和API打交道,甚至像正弦函数这些函数自己写很难优化到最好。这就要求我们使用许多第三方程序代码

第三方程序代码如何通过链接和程序建立关系?
  • 我们可以把常用的函数放到一个.c文件里,然后生成一个可重定位目标文件.o,再链接生成程序。但是这样.o文件非常大,有千万个函数,与自己代码链接后会生成很大程序,且链接过程慢,时间空间效率都很低。
  • 也可以把1000个函数做成1000个模块,生成不同的可重定位目标文件。这样做的好处是生成程序小,时间短。但问题在于链接过于繁琐,需要知道代码用到那几个.o文件,再去一个个链接,编写链接脚本和命令就会复杂。
  • 前两种方法,要么程序本身太大,包含许多无用代码;要么程序体积下来了,但是程序员操心的东西太多,因此我们通常采用第三种方法,即采用库的方式解决问题
静态库
  • 最早我们采用静态库解决问题。静态库在linux系统中以归档文件存在,以.a作为后缀
  • 静态库采用思想:把每个函数作为一个模块生成对应.o文件,再将.o文件打包生成归档文件,这样一个归档文件中会包含多个.o文件,同时有一个索引,就构成了静态库。索引用于对归档文件进行描述,描述现在归档文件中有多少.o文件,每个.o文件在哪个位置。也就是说,静态库就是一些可重定向目标文件通过一些方式打包在一起,但不是简单地打包在一起,而是带有索引,通过索引可以查到静态库的可重定向目标文件的位置。
  • 静态库如何取用?在链接阶段直接连接到静态库,链接器到静态库里边解析扫描各个.o文件的符号表,找到这个程序需要使用到的目标文件,并且从静态库中提取出来进行连接,生成一个大的目标文件。静态库在链接时只用到一个库,而且链接时按需链接,这样就解决了前两个的问题。
  • 链接库_第1张图片

以最常用的libc.a静态库为例,各个库函数都有对应的.c文件,对应的.c文件编译成对应.o文件,然后通过归档程序把各个可重定位目标文件打包在libc.a 中。libc.a最新版本大概4.6mb,包含1496个文件,有字符串管理、随机数、数据时间、I/O等函数。libm.a大概2mb,包含444个可重定位文件,主要是浮点数运算的一些函数

  • 下边看一个静态库的例子,这里有三个文件,其中main函数中调用addvec函数。链接库_第2张图片
  • 值得注意的是,我们发布库时,不仅要包含库,还要有对应的描述库的头文件。可以从生物医学的大数据挖掘一课中理解
  • 链接顺序是非常重要的,要考虑到链接器的效率问题。如main.o链接a库和b库,若a库使用b库符号,一定要这样main.o -a -b,否则找不到符号。这是因为链接器是从左到右依次扫描,而且只进行一次。链接器工作过程实际上就是维护表,链接器先扫描main.o中发现两个地方没有定义,将两个外部符号放到表中;扫描a库时解决掉一个符号,但是又加了一个符号,扫描完a库后扫描b库,把两个都解决掉。若改变位置,a中扫描时有一个无法解决,会报错
  • 若两个库相互依赖,如何解决?从程序员角度讲,尽量不要形成这种交叉依赖。但是若形成了,这样写main.o -a -b -a,让某一库链接两次。
动态库
  • 但是静态链接库不是最好的解决方案,存在问题。问题主要在于存储和可执行程序维护的问题。静态链接库和文件进行静态链接,文件中要包含静态链接代码,但像printf这种函数,许多程序会频繁的使用,这样每个应用程序都会包含printf代码片段副本,就会在磁盘中存储多份。进一步讲,每个程序运行都加载到内存中,这样内存中会有多份,而内存是对使用空间更敏感的东西。再加载到缓存里,缓存就甭干别的了,所有缓存缓存了不同程序的printf(哈哈哈哈哈)。所以使用静态库链接的应用程序在静态存储和动态运行时都会浪费空间,存储上非常消耗空间。
  • 然后是软件维护问题,若某个标准库函数中出现了缺陷,我们需要重新发布标准c库,修改这个漏洞。但是程序也得随之更新升级,重新构建发布应用程序,这个过程是非常麻烦的。因为重新构建程序要对程序进行完整的测试,这是必须要求,改一点也要重新测试一遍。去年双11淘宝就是改了界面效果,但是阿里发布时没有做系统测试就发布了,但是更新后就造成了闪退,带来了巨大损失。这样静态库频繁升级就造成应用软件频繁升级。因此静态库是一种过时的技术,现在多使用共享库,是一种更好的解决方案。
  • 动态库不打包在最终可执行文件里,而是随着可执行文件一起发布。在链接时也不是和静态链接一样生成可执行目标文件,把可执行目标文件放进去,而是仍然在外边作为独立文件存在。因此动态库升级不影响可执行文件,且最大优势在于只需要在系统中存一份就可以,所有使用标准c库的程序只要连接到一个共享库上就可,在磁盘和内存中只要存储一份,这样可以极大提高存储效率。
  • 接下来谈一下动态库是如何完成工作的?动态库链接工作和静态库非常类似,分为符号解析过程和重定位过程。但是动态链接的整个过程发生在程序的加载期和运行时,并不是在构建程序时就完成了符号解析和重定位,而是在加载和运行时完成符号解析和重定位。
  • 动态链接的发生时期可以是两个时间节点,一个是加载期。这个时候程序刚刚开始启动,程序先执行systemcode,在里边调用动态链接器。动态链接器以共享库或动态库形式出现,所有应用程序加载动态库时都需要动态链接器完成。动态链接器会分析当前程序要使用哪些动态库,然后从响应动态库路径搜索到动态库,进而把动态库加载到内存中。另一个是运行时链接,不是由systemcode完成动态库加载,而是自己编写程序手动完成。在标准库函数中有libdl.so这样一个库,在这个库里包含一组操作动态库的函数,可以通过代码加载某个动态库,访问某个动态库函数。
  • 运行时链接的主要过程:

链接库_第3张图片
用到libdl.so库,提供了一种用代码控制动态库加载的方法。使用dlopen可以指定打开某个位置的动态库,返回一个无类型的指针,一般叫做句柄,通过这个handle变量控制动态库,相当于开门的门把手。
dlsym函数传入句柄和一个字符串,从动态库中把这个符号对应的函数映射出来,然后赋值给addvec。
addvec是一个函数指针,它与普通指针的最大区别是,函数指针指向的是.text区域中的地址,指向一个指令,即函数入口;而普通指针则指向.data数据区或是栈、堆这些数据区,这就是函数指针和普通指针的最大区别,接下来操作函数指针就是操作这个映射出来的函数。
当我们不使用动态库时,就用dlclose关掉这个句柄,这样后边再使用这个句柄就会报空指针的错误。
运行时链接,即在程序运行过程中链接,为我们提供了非常大的灵活性。这样函数调用就不是非得用代码调用去执行他,而可以通过字符串方式传进来,即调用哪个函数可以通过外部数据输入给进来,给了我们极大的想象空间。

  • 运行时链接在C语言、C++中是唯一实现把数据转换成指令的可靠安全的方式;而真正做到数据和指令间无缝衔接和转换的都是非常高级的技术,如Java、javasc在程序语言中原生支持。
  • 在讨论完动态库加载期链接和运行时链接的两种加载方式后,再讨论在动态链接中如何实现重定位?首先,重定位在动态库中不能做,因为动态库代码都是位置无关代码。由于动态库是可以被多个应用程序所共享的,因此内部地址不能提前决定,否则不同程序使用的内存地址和动态库的内存地址可能发生冲突。所以重定位过程不能在生成动态库过程决定的,而应该在连接到程序之后再去分配地址,故而动态库不能出现绝对地址,都是位置无关地址,即相对地址
  • 链接库_第4张图片

链接库_第5张图片
怎么去使用相对地址,从全局变量和函数调用两部分去看:在动态库中不分配绝对地址,而是依赖于全局偏移量表(GOT),全局偏移量表里存放着当前这个模块使用的所有符号地址。全局偏移量表的地址是在动态加载动态库时,为addcnt动态分配一个地址。动态链接分配完了,更新到全局偏移量表中,好处:代码操作的是全局偏移量表,而不是直接操作全局偏移量地址。

  • 全局变量如何操作的?rip的值是指向下一条即将执行的指令的地址,0x2008b9是下一条指令和got【3】的相对偏移量。为什么got【3】的地址可以出现在里边?动态链接有规定,全局偏移量表放到data区里,动态库代码放到text区里,这两个区的相对位置是固定的,这条指令和got【3】不管加载到哪个程序里边,都会保持固定偏移量。使用全局偏移量表可以把真正使用符号和动态生成库代码隔离开了,只要保证有相对位置即可。
  • 对于函数怎么做的?借助全局偏移量表,还要借助另外机制,延迟地址计算机制:理论上全局偏移量表里应该包含整个库函数的所有地址,但是要注意,若动态库在加载时把所有地址一个个算好,动态库大计算地址花费时间就会太多。动态库加载时完成地址计算意味着加载非常慢,所以对于函数地址是在调用时再计算,相当于把地址计算推后了。实现延迟加载机制要使用另外一个结构:过程连接表,使用PLT和GOT配合完成,下边是一个例子:调用addvec,call指令不进入addvec实际位置,由于有了延迟计算技术,got中一开始不存放addvec地址,所以call跳转的是过程连接表的一项地址。执行call指令首先把控制流跳转到这个地方,plt负责对addvec函数做解析,先做跳转指令,跳转到got【4】指向位置,跳到了紧挨着的指令位置,相当于什么都没做。push把1这个立即数压倒了栈里。1是表示访问的addvec函数,4005a0是固定位置,plt【0】负责做调用动态连接器,向栈中pushgot【1】,got【1】是动态链接库的可冲定位信息的地址。

got【2】动态链接器的入口地址,做addvec的计算,基于对哪个符号做重定位和重定位信息,把1和重定位信息压倒栈里,动态链接器里把这两个值出栈,然后计算对应函数入口,把got【4】更新,更新成真正的addvec函数入口。在执行addvec函数前,做了这么多工作来找到addvec地址。后边再调用就不需要重复计算了

库打桩

库打桩技术能做啥?

提供一种方法,使我们能够拦截正常应用程序的库函数调用,并且对拦截信息做一定处理,然后转发到真正库函数中

实现方法:编译期、连接期、运行期的库打桩

值得注意的是,拦截malloc库函数请求,跟踪调用了多少空间,检查对于堆的使用是否存在内存泄漏,加密...库打桩技术都是在不修改函数调用中的代码的前提下进行做得

编译期库打桩

链接库_第6张图片

  • 编写一个程序,然后定义了头文件,里边的宏定义是编译器拦截的精髓。宏定义定义了宏mymalloc,宏是在预编译期处理,所以会首先发挥作用。调用宏就相当于调用了malloc函数,file、line是编译器固定支持的变量,file当前函数动用所在的文件名,line表示出现在哪一行,预编译器自动转换成文件名和行号。include这个头文件后,调用malloc时首先调用mymalloc函数,在预编译阶段欺骗了编译器,通过宏定义编译时malloc替换成了mymalloc
  • 存在问题:需要把头文件引入到hello.c,需要修改源码;必须有源码
链接期库打桩

链接库_第7张图片

  • 连接期库打桩不需要源码,只需要可重定位目标文件,拦截到这个相当于打桩的程序,拦截的程序都到这个里边,在这里边调用真正的malloc,真正的malloc会被重新命名为realmalloc,这两个必须这样写,这时候编译器会提供一种机制进行伪装,realmalloc、realfear才是要调用的malloc,我们只是做了封装
  • 连接器加了这个东西链接库_第8张图片通过这个选项加入,连接器就提供了这样一个后门,在解析malloc符号时,不是指向标准库,而是找到这个wrap定义。链接时所有调用限定为wrapmalloc,在调用malloc,欺骗了符号表,完成链接
只有可执行程拦截库函数调用:运行期的库打桩技术

编写一个程序,封装成动态库,在同名函数里边调用真正库函数,在运行时,给一个环境变量,让加载动态库时优先加载的放到这,lib用到,再加载mymalloc,再加载别的。动态链接时两个malloc函数先执行我们malloc里边。对动态链接器实现了欺骗
链接库_第9张图片

你可能感兴趣的:(计算机基础)