Skip to content
发布日期:2021/03/28
阅读量:加载中...
标签:memory

链接

链接就是将不同代码和数据收集和组合成为一个单一文件的过程,这个文件可被加载(或被拷贝)到存储器并执行。

链接发生的时机有以下几种:

  • 链接可以发生于编译时,也就是在源代码被翻译成机器代码时;
  • 也可以发生与加载时,也就是在程序被加载器加载到存储器并执行时;
  • 甚至执行与运行时,由应用程序来执行;

链接通常是由链接器来安静的处理,对于那些在编程入门课堂上构造小程序的学生来说,链接不是一个重要的议题。那为什么还有这么麻烦的学习关于链接的知识呢?

​ 链接器的两个主要的任务:

  • 符号解析(symbol resolution):目标文件定义和引用符号。符号解析的作用是将每个符号引用和一个符号定义联系起来;
  • 重定位(relocation):编译器和汇编器生成从零开始的代码和数据节。链接器通过把每个符号定义与一个存储器位置联系起来,然后修改所有这些对符号的引用,使得他们指向这个存储器的位置,从而重定位这些节。

在你阅读的时候,要记住关于链接器的一些基本的事实:目标文件存粹是字节快的集合。这些快中,有些包含程序代码,有效包含程序数据,而其他的则包含指导链接器和加载器的数据结构。链接器将这些库链接起来,确定被链接块的运行时位置,并且修改代码和数据块中的各个位置。链接器对目标机器了解甚少,产生目标文件的编译器和汇编器已经完成了大部分工作。

了解链接的好处

  • 理解链接器将帮助你构造大型程序。构造大型程序的程序员经常会遇到由于缺少模块,缺少库或者不兼容的库版本引起的链接器错误。除非你理解链接器是如何的解析引用,什么是库以及链接器是如何的使用库来解析引用的,否则这些错误将会使用迷惑和挫败;
  • 理解链接器将帮助你避免一些危险的编程错误。Unix链接器解析符号引用时所做的决定可以不动声色的影响你程序的正确性。在默认情况下,错误的定义多个全局变量的程序将通过链接器,而不产生任何警告信息。由此得到的程序会产生迷惑的运行时行为,而且非常难以调试。
  • 理解链接将帮助你理解语言的作用域规则是如何实现的。例如全局和局部变量的区别是什么?当你定义一个具有静态属性的变量或者函数时,到底实际意味着什么?
  • 理解链接器将帮助你理解其他重要的系统概念。
  • 理解链接器将使你能够开发共享库。随着共享库和动态链接在现代操作系统中日益加强的重要性,链接成为了一个复杂的过程,它为知识丰富的程序员提供了强大的能力。比如,许多软件产品使用共享库在运行时来升级压缩包装的二进制程序。

目标文件

​ 目标文件有三种形式:

  • 可重定位目标文件:包含二进制代码和数据,其形式可以在编译时与其他可重定位目标文件合并起来,创建一个可执行目标文件;
  • 可执行目标文件:包含二进制代码和数据,其形式可以直接被拷贝到存储器并执行;
  • 共享目标文件:一种特殊类型的可重定位目标文件,可以在加载或者运行时,被动态的加载到存储器并链接。

编译器和汇编器生成可重定位目标文件(包含共享目标文件),链接器生成可执行目标文件。

加载可执行目标文件

​ 每个Unix程序都有一个运行时存储器映像。如图linuxmemory.png

从应用程序中加载和链接共享库

​ 应用程序还可能在它运行时要求动态链接器加载和链接任意共享库,而无需在编译时链接那些库到应用中。动态链接是一项强大有用的技术。向linux这样的Unix系统,为动态链接器提供了一个简单的接口,允许应用程序在运行时加载和链接共享库。

cpp
#include <dlfcn.h>                                                                                                                          

void* dlopen(const char* filename, int flag);
//返回:若成功则为指向句柄的指针,若出错则为NULL

dlopen函数加载和链接共享库filename。用以前带RELD_GLOBAL选项打开的库解析filename中的外部符号。如果当前可执行文件是带-rdynamic选项编译的,那么对符号解析而言,它的全局符号也是可用的。flag参数必须要么包括RELD_NOW,该标志告诉链接器立即解析外部符号的引用,要么包含RELD_LAZY标志,该标志指示链接器推迟符号解析直到执行来自库中的代码时。这两个中的任何一个都可以和RELD_GLOBAL标志取或。

cpp
#include <dlfcn.h>                                                                                                                          

void* dlsym(void* handle, char* symbol);
//返回:若成功则为指向符号的指针,若出错则为NULL

dlsym函数的输入是一个指向前面已经打开共享库的句柄和一个符号的名字,如果该符号存在,就返回符号的地址,否则则为NULL。

cpp
#include <dlfcn.h>                                                                                                                          

int* dlclose(void* handle, char* symbol);
//返回:若成功则为指向符号的指针,若出错则为NULL

如果没有其他共享库还在使用这个共享库,dlclose函数就卸载该共享库。

cpp
#include <dlfcn.h>                                                                                                                          

const char* dlerror(void);
//返回:若前面对dlopen,dlsym,dlclose的调用失败,则为错误消息,若前面的调用成功,则为NULL。

dlerror函数返回一个字符串,它描述的是调用dlopen,dlsym,dlclose函数时发生的最近的错误,如果没有错误发生则发回空。

示例

在addvec.c文件中,创建下面的代码,构建一个动态库。

cpp
#include<stdio.h>                                                                                                                           

void addvec(int* x, int* y, int* z, int n)
{
    int i;

    for (i = 0; i < n; i++)
    {   
        z[i] = x[i] + y[i];
    }   

}
shell
gcc -shared -fPIC -o libaddvec.so addvec.c

在test.c文件中,动态的加载共享库,如下

c
#include <dlfcn.h>
#include <stdio.h>

int x[2] = {1, 2};
int y[2] = {3, 4};
int z[2];
int main()
{
    void* handle;
    void (*addvec)(int*, int*, int*, int);
    char* error;
    //dynamically load the shared library that contains addvec()
    handle = dlopen("./libaddvec.so", RTLD_LAZY);
    if (handle == NULL)
    {
        fprintf(stderr, "%s\n", dlerror());
        exit(1);
    }
    //get a pointer to the addvec() function we just loaded
    addvec = dlsym(handle, "addvec");
    if ((error = dlerror()) != NULL)
    {
        fprintf(stderr, "%s\n", dlerror());
        exit(1);
    }
    //Now we can call addvec() just like any other function
    addvec(x, y, z, 2);
    printf("z = [%d %d]\n", z[0], z[1]);
    //unload the shared library
    if (dlclose(handle) < 0)
    {
        fprintf(stderr, "%s\n", dlerror());
        exit(1);
    }
                                                                                                                                                               
    return 0;
}

s
shell
gcc -rdynamic -O2 -o test test.c  -ldl

处理目标文件的工具

​ 在Unix系统中有大量可用的工具可以帮助你理解和处理目标文件。特别地,GNU binutils包尤其有帮助,而且可以运行在每个Unxi平台上。

  • ar:创建静态库,插入,删除,列出和提取成员;
  • strings:列出一个目标文件中,所有可打印的字符串;
  • strip:从目标文件中删除符号表信息;
  • nm:列出一个目标文件的符号表中定义的符号;
  • size:列出目标文件中节的名字和大小;
  • readelf:显示一个目标文件中的完整结构,包括ELF头中编码的所有信息。包含size和nm的功能;
  • objdump:所有二进制工具之母,能够显示一个目标文件中所有的信息。它最有用的功能是反汇编.text节中的二进制指令。
  • ldd:列出一个可执行文件在运行时所需要的共享库;(Unix系统为共享库提供的ldd程序)

操作进程的工具

​ unix系统提供了大量的监控和操作进程的有用工具:

  • strace:打印一个程序和它的子程序调用的每个系统调用的轨迹。对于好奇的学生而言,这是一个令人着迷的工具。用-static编译你的程序,能得到一个清晰的轨迹,而不带有大量与共享库相关的输出;
  • ps:列出系统中当前的进程(包括僵尸进程);
  • top:打印出关于当前进程资源的使用的信息;
  • kill:发送一个信号给进程。对于调试带信号处理程序以及清除难以捉摸的进程是非常有用的。
  • /proc:一个虚拟文件系统,以ASCII文本格式输出大量内核数据结构的内容,用户程序可以读取这些内容。

参考

​ 全文皆是摘录自《深入理解计算机系统》一书,用于自己的学习和理解,若存在侵权请联系作者。

评论