精华内容
下载资源
问答
  • 动态网站链接 动态链接 万次阅读 多人点赞
    2019-09-28 21:21:41

    1. 概述

    在实际编程中,我们可以把完成某项功能的函数放在一个动态链接库里,然后提供给其他程序调用。

    1.1 静态库和动态库

    • 静态库:这类库的名字一般是libxxx.a,在使用静态库的情况下,在编译链接可执行文件时,链接器从静态库中复制这些函数和数据,并把它们和应用程序的其他模块组合起来创建最终的可执行文件(.exe)。当发布产品时,只需要发布这个可执行文件,并不需要发布被使用的静态库。
    • 动态库:是一种不可执行的二进制程序文件,它允许程序共享执行特殊任务所必需的代码和其他资源。Windows平台上动态链接库的后缀名是”.dll”,Linux平台上的后缀名是“.so”。Linux上动态库一般是libxxx.so;相对于静态函数库,动态函数库在编译的时候并没有被编译进目标代码中,你的程序执行到相关函数时才调用该函数库里的相应函数,因此动态函数库所产生的可执行文件比较小。由于函数库没有被整合进你的程序,而是程序运行时动态的申请并调用,所以程序的运行环境中必须提供相应的库。动态函数库的改变并不影响你的程序,所以动态函数库的升级比较方便。

    1.2 动态链接库的优点

    • 复用性:DLL的编制与具体的编程语言以及编译器无关,不同语言编写的程序只要按照函数调用约定就可以调用同一个DLL函数
    • 可扩展性:DLL文件与EXE文件独立,只要接口不变,升级程序只需更新DLL文件不需要重新编译应用程序
    • 节省内存:如果多个应用程序使用同一个dll,该dll的页面只需要存入内存一次,所有的应用程序都可以共享它的页面,从而节省内存

    2. 生成动态链接库

    2.1 windows版本

    下面以codeblocks编译器为例,其他编译器也就是建立dll工程不一样:
    File->New->Projects->Dynamic Link library->Go
    项目的命名就是最后dll的名字,新建main.c和main.h
    main.c

    #include "main.h"
    #include <stdio.h>
    
    /* 输入年月日计算一年中第几天 */
    int Day_of_year(int year, int month, int day)
    {
       int sum,leap;
       switch(month) // 先计算某月以前月份的总天数
       {
           case 1:sum=0;break;
           case 2:sum=31;break;
           case 3:sum=59;break;
           case 4:sum=90;break;
           case 5:sum=120;break;
           case 6:sum=151;break;
           case 7:sum=181;break;
           case 8:sum=212;break;
           case 9:sum=243;break;
           case 10:sum=273;break;
           case 11:sum=304;break;
           case 12:sum=334;break;
           default:printf("data error");break;
       }
       sum=sum+day; // 再加上某天的天数
       if(year%400==0||(year%4==0&&year%100!=0)) {// 判断是不是闰年
           leap=1;
       } else {
          leap=0;
       }
       if(leap==1&&month>2) { // *如果是闰年且月份大于2,总天数应该加一天
           sum++;
       }
       return sum;
    }
    

    main.h

    #ifndef __MAIN_H__
    #define __MAIN_H__
    
    #include <windows.h>
    
    #ifdef __cplusplus
    #define EXPORT extern "C" __declspec (dllexport)
    #else
    #define EXPORT __declspec (dllexport)
    #endif // __cplusplus
    
    EXPORT int Day_of_year(int year, int month, int day);
    
    #endif // __MAIN_H__
    

    编译成功后在bin\Debug目录下生成3个文件:dll.dll,libdll.a,libdll.def

    2.2 Linux版本

    main.c

    #include "main.h"
    #include <stdio.h>
    
    /* 输入年月日计算一年中第几天 */
    int Day_of_year(int year, int month, int day)
    {
       int sum,leap;
       switch(month) // 先计算某月以前月份的总天数
       {
           case 1:sum=0;break;
           case 2:sum=31;break;
           case 3:sum=59;break;
           case 4:sum=90;break;
           case 5:sum=120;break;
           case 6:sum=151;break;
           case 7:sum=181;break;
           case 8:sum=212;break;
           case 9:sum=243;break;
           case 10:sum=273;break;
           case 11:sum=304;break;
           case 12:sum=334;break;
           default:printf("data error");break;
       }
       sum=sum+day; // 再加上某天的天数
       if(year%400==0||(year%4==0&&year%100!=0)) {// 判断是不是闰年
           leap=1;
       } else {
          leap=0;
       }
       if(leap==1&&month>2) { // *如果是闰年且月份大于2,总天数应该加一天
           sum++;
       }
       return sum;
    }
    

    main.h

    #ifndef __MAIN_H__
    #define __MAIN_H__
    
    int Day_of_year(int year, int month, int day);
    
    #endif
    

    在命令行下输入 gcc -shared -fPIC main.c -o libday.so
    即可生成一个名为libday.so的动态链接库

    3. 调用动态链接库

    3.1 windows版本

    3.1.1 隐式调用

    新建工程,把上面生成的dll.dll和libdll.a(不可缺)拷贝到新工程的bin\Debug目录下
    main.c

    #include <stdio.h>
    #include "main.h"
    int main()
    {
        printf("day = %d\n",  Day_of_year(2015,10,1) );
        system("pause");
        return 0;
    }
    

    main.h 保持一样

    #ifndef __MAIN_H__
    #define __MAIN_H__
    
    #include <windows.h>
    
    #ifdef __cplusplus
    #define EXPORT extern "C" __declspec (dllexport)
    #else
    #define EXPORT __declspec (dllexport)
    #endif // __cplusplus
    
    EXPORT int Day_of_year(int year, int month, int day);
    
    #endif // __MAIN_H__
    

    Project - Build options - Linker settings - Add 选择 bin\Debug\libdll.a - 确定,然后再编译即可
    在这里插入图片描述

    3.1.2 显示调用(推荐)

    新建工程,把dll.dll拷贝到新工程的bin\Debug目录下
    main.c

    #include <stdio.h>
    #include <windows.h>
    
    typedef int(*Getday)(int, int, int); //定义函数类型
    HINSTANCE hDll; //DLL句柄
    Getday getday;
    int main()
    {
        hDll = LoadLibrary("dll.dll"); //加载 dll
        getday = (Getday)GetProcAddress(hDll, "Day_of_year");//通过指针获取函数方法
        printf("day = %d\n",  getday(2015, 10, 1) );//调用函数
        FreeLibrary(hDll);//释放Dll句柄
        system("pause");
        return 0;
    }
    

    编译就可以使用,当dll程序升级时,只需要替换dll,而不用重新编译exe

    3.2 Linux版本

    linux版本动态链接库没有windows版本那么多文件,只有一个so文件

    3.2.1 隐式调用

    test.c

    #include <stdio.h>
    #include "main.h"
    
    int main()
    {
        printf("day = %d\n", Day_of_year(2015, 10, 1));
    }
    

    在命令行输入 gcc -o test test.c -L./ libday.so
    然后执行编译生成的可执行文件 ./test
    注意加上导出函数头文件main.h,-L指定动态链接库的搜索路径

    3.2.2 显式调用(推荐)

    test.c

    #include <stdio.h>
    #include <dlfcn.h> // 显式加载需要用到的头文件
    
    int  main()
    {
        void *pdlHandle = dlopen("./libday.so", RTLD_LAZY); // RTLD_LAZY 延迟加载
        char *pszErr = dlerror();
        if( !pdlHandle || pszErr )
        {
            printf("Load lib.so failed!\n");
            return 1;
        }
    
        int (*Day_num)() = dlsym(pdlHandle, "Day_of_year"); // 定位动态链接库中的函数
        if( !Day_num )
        {
            pszErr = dlerror();
            printf("Find symbol failed!%s\n", pszErr);
            dlclose(pdlHandle);
            return 1;
        }
    
        printf("day = %d\n", Day_num(2015, 10, 1)); // 调用动态链接库中的函数
    
        dlclose(pdlHandle); // 系统动态链接库引用数减1
    
        return 0;
    }
    

    在命令行输入 gcc -o test test.c -ldl
    然后执行编译生成的可执行文件 ./test
    优点:不必在编译时就确定要加载哪个动态链接库,可以在运行时再确定。

    3.2.3 调试案例

    1)错误一

    /tmp/ccMpgzNu.o: In function `main':
    test.c:(.text+0x13): undefined reference to `dlopen'
    test.c:(.text+0x1c): undefined reference to `dlerror'
    test.c:(.text+0x53): undefined reference to `dlsym'
    test.c:(.text+0x63): undefined reference to `dlerror'
    test.c:(.text+0x89): undefined reference to `dlclose'
    test.c:(.text+0xc7): undefined reference to `dlclose'
    collect2: error: ld returned 1 exit status
    

    解决方案:

    • 头文件添加:#include <dlfcn.h>
    • 编译选项加上- ldl,即 gcc -o test test.c -ldl ,网上有gcc -ldl -o test test.c,这种方式也是会报这个错误的

    2)错误二

    error while loading shared libraries: libtiger.so: cannot open shared object file: No such file or direct
    

    我的这段代码里则会打印Load lib.so failed!

    解决方案:

    • 在程序代码里配置路径void *pdlHandle = dlopen(“libday.so”, RTLD_LAZY);
      将动态链接库配上路径,如 ./libday.so表示可执行文件与链接库同一路径
    • 将动态链接库的目录放到程序搜索路径中,可以将库的路径加到环境变量
      export LD_LIBRARY_PATH=pwd:$LD_LIBRARY_PATH(pwd带反撇号的哈)
    • 拷贝libday.so到绝对目录 /lib 下(但是要超级用户才可以,因此要使用sudo哦)
    更多相关内容
  • C++ 动态链接

    万次阅读 多人点赞 2021-06-01 09:01:19
    2>静态加载方式(.h .lib .dll三件套加载) 1>认识DLL(动态链接库) 动态链接库DLL(Dynamic-Link Library)通常包含程序员自定义的变量和函数, 可以在运行时动态链接到可执行文件(我们的exe程序)中。 2 >格式后缀 ...

    原创文章,转载请注明出处。

    如果Windows系统有缺失的DLL文件,可以去这个链接下载。点击去这里寻找

    1>认识DLL(动态链接库)

    动态链接库DLL(Dynamic-Link Library)通常包含程序员自定义的变量和函数, 可以在运行时动态链接到可执行文件(我们的exe程序)中。

    2 >格式后缀

    Windows上:(.dll)
    Linux上:(.so)
    Android上:(.so)
    IOS上:(.dylib)

    3 >DLL优点

    1)模块化,耦合小:大规模软件开发中,开发过程独立,耦合度小,比如UE4里面的模块(每一个.build.cs)都是一个DLL。

    2)扩展性:DLL文件与EXE文件是独立的,只要接口不变,升级程序只需更新DLL文件不需要重新编译应用程序。并且我们的EXE文件较小。

    3)复用性:DLL的编制与具体的编程语言以及编译器无关,不同语言编写的程序只要按照函数调用约定就可以调用同一个DLL函数。

    4)节省内存:当应用程序使用动态链接时,多个应用程序可以共享磁盘上单个DLL副本。

    5)隐私性:可以当做黑盒使用,可以将我们的具体实现代码隐藏起来,比如我们想将算法的具体实现隐藏起来,不让别人看到我是怎么做的。一般SDK接入时候,里面的具体算法实现都是不会公开cpp的。

    4 >DLL缺点

    如果是动态loaddll的话,会牺牲部分性能吧。微乎其微的。

    5 >加载DLL

    1>动态加载(运行时加载)通过LoadLibary加载, GetProcAddress调用。

    如果加载失败,通过GetLastError()获取失败原因。

    下面是LoadLibary的示例代码
    第一步,在.h中声明了static HMODULE hDLL;

    #include<Windows.h> //加载的头文件
    
    class QIDCardReader : public QMainWindow
    {
        Q_OBJECT
    
    public:
        QIDCardReader(QWidget* parent = 0);
    private:
    	static HMODULE hDLL;
    };
    

    第二步,定义要调用的dll中的方法。并通过LoadLibrary给hDLL赋值

    #include "QIDCardReader.h"
    
    //CPP中预声明一下我要调用dll里面的方法
    typedef int(*MyRouton_RepeatRead)(bool);
    typedef int(*MyInitComm)(int);
    typedef int(*MyAuthenticate)();
    typedef int(*MyReadBaseInfosPhoto)(char * , char * , char * ,
    	char *, char * , char * ,
    	char *, char * , char* , char * );
    typedef int(*MyCloseComm)();
    
    HMODULE QIDCardReader::hDLL;
    
    QIDCardReader::QIDCardReader(QWidget* parent) : QMainWindow(parent)
    	,ui_(new Ui::QIDCardReaderClass)
    {
    	hDLL = LoadLibrary(L"D:\\bimvr-vrlauncher\\test\\Sdtapi.dll"); //加载dll文件 
    	//如果加载失败的话, 通过GetLastError()进行获取,看失败原因。
    	auto code5 = GetLastError();
    }
    

    第三步,调用dll中的方法 GetProcAddress(hDLL, “实际的dll端函数导出名称”)

    //调用1
    if (hDLL)
    {
    	MyRouton_RepeatRead MyRouton_RepeatReadFunc = (MyRouton_RepeatRead)GetProcAddress(hDLL, ("Routon_RepeatRead"));//直接使用原工程函数名 
    	if (MyRouton_RepeatReadFunc)
    	{
    		MyRouton_RepeatReadFunc(true);
    	}
    
    }
    
    //调用2
    int iPort = 1001;
    //int ret = InitComm(iPort);
    int ret = -1000;
    if (hDLL)
    {
    	MyInitComm MyInitCommFunc = (MyInitComm)GetProcAddress(hDLL, ("InitComm"));//直接使用原工程函数名 
    	if (MyInitCommFunc)
    	{
    		ret = MyInitCommFunc(iPort);
    	}
    }
    if (ret)
    {
    	//ret = Authenticate();
    	if (hDLL)
    	{
    		MyAuthenticate MyAuthenticateFunc = (MyAuthenticate)GetProcAddress(hDLL, ("Authenticate"));//直接使用原工程函数名 
    		if (MyAuthenticateFunc)
    		{
    			ret = MyAuthenticateFunc();
    		}
    	}
    	if (ret)
    	{
    		char user_name[31] = { 0 };
    		char user_gender[3] = { 0 };
    		char user_folk[11] = { 0 };
    		char user_birthday[9] = { 0 };
    		char user_code[19] = { 0 };
    		char user_address[71] = { 0 };
    		char user_agency[31] = { 0 };
    		char expire_start[9] = { 0 };
    		char expire_end[9] = { 0 };
    
    		QByteArray data_path = QString::fromStdWString(AppUtils::GetDataPath()).toLatin1();
    		ret = ReadBaseInfosPhoto(user_name, user_gender, user_folk, user_birthday, user_code,
    			user_address, user_agency, expire_start, expire_end, str/*data_path.data()*/);
    		if (hDLL)
    		{
    			MyReadBaseInfosPhoto MyReadBaseInfosPhotoFunc = (MyReadBaseInfosPhoto)GetProcAddress(hDLL, ("ReadBaseInfosPhoto"));//直接使用原工程函数名 
    			if (MyReadBaseInfosPhotoFunc)
    			{
    				ret = MyReadBaseInfosPhotoFunc(user_name, user_gender, user_folk, user_birthday, user_code,
    						user_address, user_agency, expire_start, expire_end, str/*data_path.data()*/);
    			}
    		}
    	}
    }
    

    第四步,可以选择性调用FreeLibrary(hDLL)卸载Dll,这个是下面的静态三件套加载方式做不到的。
    也就是说我们可以动态控制Dll的加载与卸载。

    2>静态加载方式(.h .lib .dll三件套加载)

    1>项目->属性->配置属性->VC++ 目录-> 在“包含目录”里添加头文件Sdtapi.h所在的目录
    2>项目->属性->配置属性->VC++ 目录-> 在“库目录”里添加头文件Sdtapi.lib所在的目录
    3>项目->属性->配置属性->链接器->输入-> 在“附加依赖项”里添加“Sdtapi.lib”(若有多个 lib 则以空格隔开)
    也可以使用#pragma comment(lib, “Sdtapi.lib”)代替上面的1、2、3步骤。

    在这里插入图片描述
    QIDCardReader.cpp 包含.h,将.h添加到项目中

    #include "sdtapi.h"
    
    void QIDCardReader::onNotify()
    {
    	Routon_RepeatRead(true);
    
    	int iPort = 1001;
    	int ret = InitComm(iPort);
    	if (ret)
    	{
    		ret = Authenticate();
    		if (ret)
    		{
    			char user_name[31] = {0};
    			char user_gender[3] = {0};
    			char user_folk[11] = {0};
    			char user_birthday[9] = {0};
    			char user_code[19] = {0};
    			char user_address[71] = {0};
    			char user_agency[31] = {0};
    			char expire_start[9] = {0};
    			char expire_end[9] = {0};
    
    			QByteArray data_path = QString::fromStdWString(AppUtils::GetDataPath()).toLatin1();
    			ret = ReadBaseInfosPhoto(user_name, user_gender, user_folk, user_birthday, user_code,
    				user_address, user_agency, expire_start, expire_end, data_path.data());
    		}
    	}
    }
    

    现在是可以通过编译了,但是运行会报错 提示LinkError
    因为程序分为编译和链接两步骤,这就是为什么报错误的原因。
    我们解决这个报错就是将.dll放到和.exe同级目录就好了

    如果最终exe找不到dll的话,会提示下面的错误。解决方法就是将dll放到和exe同级目录就好了。
    在这里插入图片描述
    谢谢,创作不易,大侠请留步… 动起可爱的双手,来个赞再走呗 <( ̄︶ ̄)>

    展开全文
  • 深入浅出静态链接和动态链接

    万次阅读 多人点赞 2018-05-06 09:24:48
    作为一名C/C++程序员,对于编译链接的过程要了然于胸。首先大概介绍一下,编译分为3步,首先对源文件进行预处理,这个过程主要是处理一些#号定义的命令或语句(如宏、#include、预编译指令#ifdef等),生成*.i文件;...

            作为一名C/C++程序员,对于编译链接的过程要了然于胸。首先大概介绍一下,编译分为3步,首先对源文件进行预处理,这个过程主要是处理一些#号定义的命令或语句(如宏、#include、预编译指令#ifdef等),生成*.i文件;然后进行编译,这个过程主要是进行词法分析、语法分析和语义分析等,生成*.s的汇编文件;最后进行汇编,这个过程比较简单,就是将对应的汇编指令翻译成机器指令,生成可重定位的二进制目标文件。以上就是编译的过程,下面主要介绍两种链接方式--静态链接和动态链接。

            静态链接和动态链接两者最大的区别就在于链接的时机不一样,静态链接是在形成可执行程序前,而动态链接的进行则是在程序执行时,下面来详细介绍这两种链接方式。

    一、静态链接

    1.为什么要进行静态链接

            在我们的实际开发中,不可能将所有代码放在一个源文件中,所以会出现多个源文件,而且多个源文件之间不是独立的,而会存在多种依赖关系,如一个源文件可能要调用另一个源文件中定义的函数,但是每个源文件都是独立编译的,即每个*.c文件会形成一个*.o文件,为了满足前面说的依赖关系,则需要将这些源文件产生的目标文件进行链接,从而形成一个可以执行的程序。这个链接的过程就是静态链接

    2.静态链接的原理

             由很多目标文件进行链接形成的是静态库,反之静态库也可以简单地看成是一组目标文件的集合,即很多目标文件经过压缩打包后形成的一个文件,如下图,使用ar命令的-a参数查看静态库的组成:


            这里的*.o目标文件在我前面的博客《从编写源代码到程序在内存中运行的全过程解析》中已经讲的很清楚了,不清楚的可以看一下。

            以下面这个图来简单说明一下从静态链接到可执行文件的过程,根据在源文件中包含的头文件和程序中使用到的库函数,如stdio.h中定义的printf()函数,在libc.a中找到目标文件printf.o(这里暂且不考虑printf()函数的依赖关系),然后将这个目标文件和我们hello.o这个文件进行链接形成我们的可执行文件。


            这里有一个小问题,就是从上面的图中可以看到静态运行库里面的一个目标文件只包含一个函数,如libc.a里面的printf.o只有printf()函数,strlen.o里面只有strlen()函数。

            我们知道,链接器在链接静态链接库的时候是以目标文件为单位的。比如我们引用了静态库中的printf()函数,那么链接器就会把库中包含printf()函数的那个目标文件链接进来,如果很多函数都放在一个目标文件中,很可能很多没用的函数都被一起链接进了输出结果中。由于运行库有成百上千个函数,数量非常庞大,每个函数独立地放在一个目标文件中可以尽量减少空间的浪费,那些没有被用到的目标文件就不要链接到最终的输出文件中。

    3.静态链接的优缺点

            静态链接的缺点很明显,一是浪费空间,因为每个可执行程序中对所有需要的目标文件都要有一份副本,所以如果多个程序对同一个目标文件都有依赖,如多个程序中都调用了printf()函数,则这多个程序中都含有printf.o,所以同一个目标文件都在内存存在多个副本;另一方面就是更新比较困难,因为每当库函数的代码修改了,这个时候就需要重新进行编译链接形成可执行程序。但是静态链接的优点就是,在可执行程序中已经具备了所有执行程序所需要的任何东西,在执行的时候运行速度快。

    问题:

    二、动态链接
    1.为什么会出现动态链接

            动态链接出现的原因就是为了解决静态链接中提到的两个问题,一方面是空间浪费,另外一方面是更新困难。下面介绍一下如何解决这两个问题。

    2.动态链接的原理

            动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。下面简单介绍动态链接的过程:

            假设现在有两个程序program1.o和program2.o,这两者共用同一个库lib.o,假设首先运行程序program1,系统首先加载program1.o,当系统发现program1.o中用到了lib.o,即program1.o依赖于lib.o,那么系统接着加载lib.o,如果program1.o和lib.o还依赖于其他目标文件,则依次全部加载到内存中。当program2运行时,同样的加载program2.o,然后发现program2.o依赖于lib.o,但是此时lib.o已经存在于内存中,这个时候就不再进行重新加载,而是将内存中已经存在的lib.o映射到program2的虚拟地址空间中,从而进行链接(这个链接过程和静态链接类似)形成可执行程序。

    3.动态链接的优缺点

            动态链接的优点显而易见,就是即使需要每个程序都依赖同一个库,但是该库不会像静态链接那样在内存中存在多分,副本,而是这多个程序在执行时共享同一份副本;另一个优点是,更新也比较方便,更新时只需要替换原来的目标文件,而无需将所有的程序再重新链接一遍。当程序下一次运行时,新版本的目标文件会被自动加载到内存并且链接起来,程序就完成了升级的目标。但是动态链接也是有缺点的,因为把链接推迟到了程序运行时,所以每次执行程序都需要进行链接,所以性能会有一定损失。

            据估算,动态链接和静态链接相比,性能损失大约在5%以下。经过实践证明,这点性能损失用来换区程序在空间上的节省和程序构建和升级时的灵活性是值得的。

    4.动态链接地址是如何重定位的呢?

            前面我们讲过静态链接时地址的重定位,那我们现在就在想动态链接的地址又是如何重定位的呢?虽然动态链接把链接过程推迟到了程序运行时,但是在形成可执行文件时(注意形成可执行文件和执行程序是两个概念),还是需要用到动态链接库。比如我们在形成可执行程序时,发现引用了一个外部的函数,此时会检查动态链接库,发现这个函数名是一个动态链接符号,此时可执行程序就不对这个符号进行重定位,而把这个过程留到装载时再进行。

    展开全文
  • 动态链接

    千次阅读 2020-04-08 20:29:46
    动态链接 为什么需要动态链接 静态链接使得不同的程序开发者和部门能够相对独立的开发和测试自己的程序模块,从某种意义上来讲大大促进了程序开发的效率,原先现在程序规模也随之扩大。 但静态链接的缺点也暴露出来...

    动态链接

    为什么需要动态链接

    静态链接使得不同的程序开发者和部门能够相对独立的开发和测试自己的程序模块,从某种意义上来讲大大促进了程序开发的效率,原先现在程序规模也随之扩大。
    但静态链接的缺点也暴露出来:浪费内存、磁盘空间、模块更新困难。

    内存与磁盘空间

    静态链接在计算机早期还是比较流行的,但是到了后面,其缺点也非常明显。比如浪费内存和磁盘空间,更新模块困难等。

    举个例子,每个程序内部除了都保留了printf()、scanf()等这样的公共函数库,还有相当一部分的其他函数库及辅助数据结构都会包含在其中。现在Linux中,一个程序用到C语言静态库至少1MB以上,那么100个程序就会浪费掉100MB空间。

    图示:

    图1.png

    Program1 & Program2分别包含Program1.o和Program2.o两个模块,并且还共用了Lib.o这个模块。静态链接下,P1和P2都用到了Lib.o这模块,所以它们同时在链接输出的可执行文件P1和P2有两个副本,当同时运行两个程序,Lib.o在磁盘和内存中都有两个副本,浪费空间。

    程序开发与发布

    静态链接另一个问题是对程序的更新,部署和发布也会很麻烦,如程序P1使用的Lib.o是由一个第三方A提供的,那么A更新Lib.o时候,P1的厂商需要拿到最新版的Lib.o,然后将P1与其链接后,将新的P1整个发布给用户,这么做有很明显的缺点:

    程序有任何模块更新,整个程序就要重新链接,发布给用户,

    动态链接

    动态链接的出现解决了上面的问题。将程序模块相互独立的分隔开来,形成独立的文件,不再将它们静态地链接到一起。简单而言就是对那些组成程序目标文件的链接,等到程序运行时才进行链接,也就是把链接的过程推迟到运行时才进行,这就是动态链接的基本思想。

    如上面的例子,假如现在保留了Program1.o、Program2.o和Lib.o,当运行Program1这个程序的时候,系统首先加载Program1.o,当系统发现Program1.o依赖Lib.o的时候,那么系统再去加载Lib.o,如果还依赖其他目标文件,则同样以类似于懒加载的方式去加载其他目标文件。

    当所有的目标文件加载完之后,依赖关系也得到了满足,则系统才开始进行链接,这个链接过程和现在链接非常相似。之前介绍过静态链接的过程,包含符号解析,重定向等。完成这些之后,系统再把控制权交过Program1.o的执行入口,开始执行。如果这个时候Program2需要运行,则会发现系统中已经存在了Lib.o的副本所以就不需要重新加载Lib.o,直接将Lib.o链接起来就可以了。

    图示:

    图2.png

    根据前面介绍的,这样的方式不仅仅减少了内存、磁盘空间的浪费,还减少了物理页面的换入换出,也可以增加CPU缓存的命中率,因为不同进程的数据和指令偶读集中在了一个共享模块上。

    至于更新也就更加简单了,只需要简单的将旧的目标文件覆盖掉。无需从先将程序链接一遍,下次程序运行的时候,新的目标文件就会自动装载到内存中。

    扩展性及兼容性

    动态链接还有一个特点就是程序在运行时可以动态地选择加载各种程序模块,这个优点被用来制作插件。

    动态链接还可以加强程序的兼容性。一个程序在不同的平台运行时可以动态地链接到由操作系统提供的动态链接库,这些动态链接库相当于在程序和操作系统之间增加一个中间层,从而消除了程序对不同平台之间依赖的差异性。

    比如操作系统A和B对于printf方法的实现机制不同,那么如果是静态链接,程序需要分别链接成能够运行A和B的两个版本并且分开发布,要是动态链接,需要A和B能够体统一个动态链接库包含printf方法,且这个方法使用一样的接口,那么程序只需要一个版本,就可以在两个OS上跑起来了

    动态链接的基本实现

    动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起,形成一个完整程序,而不是像静态链接一样把所有的程序模块链接成一个单独的可执行文件。

    动态链接涉及运行时的链接及多个文件的转载,必需要有操作系统的支持,因为动态链接的情况下,进程的虚拟地址空间的分布会比静态链接情况下更为复杂,还有一些存储管理、内存共享、进程线程等机制在动态链接下也会有一些微妙变化。

    Linux系统中,ELF动态链接文件被称为动态共享对象,简称共享对象,它们一般是”.so”文件。在windows系统中,动态链接被称为动态链接库,它们通常就是我们常见的”.dll”为扩展名的文件。

    当程序被转载的时候,系统的动态链接器会将程序所需要的所有动态链接库装载到进程的地址空间,并将程序中所有未决议的符号绑定到相应的动态链接库中,并进行重定位工作。

    Linux中,常用C语言库的运行库glibc,它的动态链接形式版本保存在“/lib”目录下,文件名叫做libc.so,整个系统只保留了一份C语言库的动态链接文件libc.so,而所有C编程的,动态链接的程序都可以在运行时使用它,当程序被装载时,系统的动态链接器会将程序所需要的所有动态链接库装载到进程的地址空间,并且将程序中所欲未决议的符号绑定到相应的动态链接库中,并进行重定位工作

    简单动态链接例子

    演示代码文件:

    Program1.c

    
    
    #include "Lib.h"
    
    int main()
    {
        foobar(1);
        return 0;
    }
    

    Program2.c

    
    #include "Lib.h"
    
    int main()
    {
        foobar(2);
        return 0;
    }
    
    

    Lib.c

    
    #include <stdio.h>
    void foobar(int i) {
        printf("Printint from Lib.so %d\n", i);
    }
    
    
    

    Lib.h

    
    #ifndef LIB_H
    #define LIB_H 
    void foobar(int i);
    #endif
    
    
    

    使用GCC将Lib.c编译成一个共享对象文件:

    
    gcc -fPIC -shared -o Lib.so Lib.c
    

    参数:

    • -shared表示产生共享对象

    可以得到一个Lib.so文件。这就是包含了Lib.c的foobar函数的共享对象文件

    接下来再分别编译Program1.c & Program2.c

    
    gcc -o Program1 Program1.c ./Lib.so
    
    
    gcc -o Program2 Program2.c ./Lib.so
    
    

    从Program1的角度看,整个编译和链接过程图示:

    图3.png

    Lib.c被编译成Lib.so共享对象文件,Program1.c被编译成Program1.o之后,链接称为可执行文件Program1,但上图中有一步与静态链接不一样,那就是Program1.o被链接成可执行文件这一步,在静态链接中,这一步链接过程会把Program1.o & Lib.o链接到一起,并且产生可执行文件Program1,但是这里Lib.o没有被链接进来,链接的输入目标文件只有Program1.o,但是命令行中可以发现Lib.so参与了链接过程

    模块

    静态链接中,整个程序最终只有一个可执行文件,它是一个不可以分割的整体,但是在动态链接下,一个程序被分成了若干个文件,有程序的主要部分,即可执行文件(Program1)和
    程序所依赖的共享对象(Lib.so),很多时候把这些部分叫做模块,即动态链接下的可执行文件和共享对象都可以看做是程序的一个模块

    当程序模块Program1.c被编译成Program1.o时,编译器还不不知道foobar函数的地址,当链接器将Program1.o链接成可执行文件时,这时候链接器必须确定Program1.o中所引用的foobar函数的性质。

    • 如果foobar是一个定义与其它静态目标模块中函数,那么链接器将会按照静态链接的规则,将Program1.o中的foobar地址引用重定位

    • 如果foobar是一个定义在某个动态共享对象中的函数,那么链接器就会将这个符号的引用标记为一个动态链接的符号,不对它进行地址重定位,把这个过程留到装载时再进行

    这就引出了一个问题?

    链接器如何知道foobar的引用是一个静态符号还是一个动态符号?实际上这就是用到Lib,so的原因,Lib.so中保存了完整的符号信息,把Lib.so也作为链接的输入文件之一,链接器在解析符号时就知道:foobar是一个定义在Lib.so的动态符号,这样链接器就可以对foobar的引用做特殊的处理,使它成为一个对动态符号的引用

    动态链接程序运行时地址空间分布

    静态链接而言,整个进程只有一个可执行文件被映射,之前介绍过静态的内存分布。动态链接而言除了可执行文件外还有其他共享目标文件。

    在Lib.c中的foobar加入sleep函数防止一运行程序就结束了。

    
    #include<stdio.h>
    void foobar(int i)
    {
        printf("Printing from Lib.so %d\n",i);
        sleep(-1);
    }
    
    

    然后查看进程的虚拟地址空间分布:

    
    $ ./Program1 &
    
    [1] 4471
    
    $ cat /proc/4471/maps
    
    55fc314d9000-55fc314da000 r-xp 00000000 08:01 1179978                    /home/mrlin/桌面/project/p6/Program1
    55fc316d9000-55fc316da000 r--p 00000000 08:01 1179978                    /home/mrlin/桌面/project/p6/Program1
    55fc316da000-55fc316db000 rw-p 00001000 08:01 1179978                    /home/mrlin/桌面/project/p6/Program1
    55fc31df4000-55fc31e15000 rw-p 00000000 00:00 0                          [heap]
    7fb5fba9d000-7fb5fbc84000 r-xp 00000000 08:01 1185496                    /lib/x86_64-linux-gnu/libc-2.27.so
    7fb5fbc84000-7fb5fbe84000 ---p 001e7000 08:01 1185496                    /lib/x86_64-linux-gnu/libc-2.27.so
    7fb5fbe84000-7fb5fbe88000 r--p 001e7000 08:01 1185496                    /lib/x86_64-linux-gnu/libc-2.27.so
    7fb5fbe88000-7fb5fbe8a000 rw-p 001eb000 08:01 1185496                    /lib/x86_64-linux-gnu/libc-2.27.so
    7fb5fbe8a000-7fb5fbe8e000 rw-p 00000000 00:00 0 
    7fb5fbe8e000-7fb5fbe8f000 r-xp 00000000 08:01 1179974                    /home/mrlin/桌面/project/p6/Lib.so
    7fb5fbe8f000-7fb5fc08e000 ---p 00001000 08:01 1179974                    /home/mrlin/桌面/project/p6/Lib.so
    7fb5fc08e000-7fb5fc08f000 r--p 00000000 08:01 1179974                    /home/mrlin/桌面/project/p6/Lib.so
    7fb5fc08f000-7fb5fc090000 rw-p 00001000 08:01 1179974                    /home/mrlin/桌面/project/p6/Lib.so
    7fb5fc090000-7fb5fc0b7000 r-xp 00000000 08:01 1185468                    /lib/x86_64-linux-gnu/ld-2.27.so
    7fb5fc29f000-7fb5fc2a2000 rw-p 00000000 00:00 0 
    7fb5fc2b5000-7fb5fc2b7000 rw-p 00000000 00:00 0 
    7fb5fc2b7000-7fb5fc2b8000 r--p 00027000 08:01 1185468                    /lib/x86_64-linux-gnu/ld-2.27.so
    7fb5fc2b8000-7fb5fc2b9000 rw-p 00028000 08:01 1185468                    /lib/x86_64-linux-gnu/ld-2.27.so
    7fb5fc2b9000-7fb5fc2ba000 rw-p 00000000 00:00 0 
    7ffe34baa000-7ffe34bcb000 rw-p 00000000 00:00 0                          [stack]
    7ffe34bdc000-7ffe34bdf000 r--p 00000000 00:00 0                          [vvar]
    7ffe34bdf000-7ffe34be1000 r-xp 00000000 00:00 0                          [vdso]
    ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]
    
    
    

    整个进程调度虚拟地址空间多出了几个文件的映射。Lib.so与Program1一样,被操作系统以同样的方式映射到虚拟地址空间,只是占据的虚拟地址范围不同。

    Program1除了使用Lib.so移位,其中还用到了动态链接形式的C语言运行库libc-2.27.so,还有一个非常重要的共享对象ld-2.27.so,其实ld-2.27.so就是Linux下的动态链接器。动态链接器和普通的共享对象一样被映射到了进程的地址空间,系统开始运行程序之前,会把控制权给动态链接器,由动态链接器完成链接工作,之后再把控制权给Program1

    通过readelf工具查看Lib.so的装载属性:

    
    
    readelf -l Lib.so
    
    
    
    Elf 文件类型为 DYN (共享目标文件)
    Entry point 0x580
    There are 7 program headers, starting at offset 64
    
    程序头:
      Type           Offset             VirtAddr           PhysAddr
                     FileSiz            MemSiz              Flags  Align
      LOAD           0x0000000000000000 0x0000000000000000 0x0000000000000000
                     0x0000000000000754 0x0000000000000754  R E    0x200000
      LOAD           0x0000000000000e10 0x0000000000200e10 0x0000000000200e10
                     0x0000000000000220 0x0000000000000228  RW     0x200000
      DYNAMIC        0x0000000000000e20 0x0000000000200e20 0x0000000000200e20
                     0x00000000000001c0 0x00000000000001c0  RW     0x8
      NOTE           0x00000000000001c8 0x00000000000001c8 0x00000000000001c8
                     0x0000000000000024 0x0000000000000024  R      0x4
      GNU_EH_FRAME   0x00000000000006b4 0x00000000000006b4 0x00000000000006b4
                     0x0000000000000024 0x0000000000000024  R      0x4
      GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                     0x0000000000000000 0x0000000000000000  RW     0x10
      GNU_RELRO      0x0000000000000e10 0x0000000000200e10 0x0000000000200e10
                     0x00000000000001f0 0x00000000000001f0  R      0x1
    
     Section to Segment mapping:
      段节...
       00     .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame 
       01     .init_array .fini_array .dynamic .got .got.plt .data .bss 
       02     .dynamic 
       03     .note.gnu.build-id 
       04     .eh_frame_hdr 
       05     
       06     .init_array .fini_array .dynamic .got 
    

    可以看到除了文件类型和可执行文件不同与装载地址从0x0000 0000开始之外,其余基本上都一样。很明显这个装载地址是无效地址。共享对象最终的装载地址在编译时是不确定的,而是在装载的时候,装载器根据当前地址空间的空闲情况,动态分配一块足够大小的虚拟地址空间给相应的共享对象。

    地址无关代码

    固定装载地址的困扰

    问题?

    共享对象被装载时,如何确定它在进程虚拟地址空间中的位置?

    为了实现动态链接,第一个问题就是共享对象地址的冲突问题,程序模块的指令和数据中可能会包含一些绝对地址的引用,在链接产生输出文件时候,就要假设模块被装载的目标地址

    动态链接下,不同的模块目标装载地址都一样是不行的,对于单个程序,可以手工指定各个模块的地址,如把0x1000到0x2000分配给模块A,把多少到多少分配给B,但是一旦模块多了或者使用的人多起来之后,就很麻烦。

    早期系统就采用这种方法,叫做静态共享库

    静态共享库和静态库有很明显的区别。静态库是在链接的时候就确定了符号地址,而静态共享库是吧程序各个模块统一交给操作系统来管理,操作系统在某个特定的地址划分出一个地址块,为已知的模块预留足够的空间。

    静态共享库有很多问题,比如地址冲突;还有就是升级之后共享库必须保持共享库中的全局函数和变量地址不变,一旦在链接的时候绑定了这些地址,更改之后就需要重新链接整个程序。

    装载时重定位

    为了让共享对象在任意地址装载,所以对所有绝对地址的引用不做重定位,而是把这步推迟到装载的时候再完成,比如一旦模块的装载地址确定了也就是目标地址确定,那么系统对程序所有的绝对地址引用进行重定位,来实现任意地址装载。

    比如前面的例子foorbar相对于代码段的其实位置是0x100,当模块被装载到0x10000000时,假设代码段在模块最开始的位置,则foobar的地址就是0x10000100。这个时候遍历所有模块中的重定位表,把所有对foorbar的地址引用都重定位为0x10000100

    类似这种方法很早就就存在了,早先没有虚拟存储概念下,程序是直接装载进入物理内存的

    比如一个程序在编译时假设被装载的目标地址为0x1000,但是装载时发现这个地址被别的程序使用了,所从0x4000开始有足够大的空间可以容纳该程序,那么程序就装载到0x4000

    前面在静态链接时提到过重定位,那时的重定位叫做链接时重定位(Link Time Relocation),而现在这种情况经常被称为装载时重定位(Load Time Relocation),在Windows中,这种装载时重定位又被叫做基址重置(Rebasing)。

    但是装载时重定位的方法并不适合用来解决上面的共享对象中所存在的问题。可以想象,动态链接模块被装载映射至虚拟空间后,指令部分是在多个进程之间共享的,由于装载时重定位的方法需要修改指令,所以没有办法做到同一份指令被多个进程共享,因为指令被重定位后对于每个进程来讲是不同的。当然,动态链接库中的可修改数据部分对于不同的进程来说有多个副本,所以它们可以采用装载时重定位的方法来解决。Linux和GCC支持这种装载时重定位的方法,我们前面在产生共享对象时,使用了两个GCC参数“-shared”和“-FPIC”,如果只使用“-shared”,那么输出的共享对象就是使用装载时重定位的方法。

    地址无关代码

    什么是"-fPIC"?这个参数有什么效果?

    **装载时重定位是解决动态模块中有绝对地址引用的办法之一,但是它有一个很大的缺点是指令部分无法在多个进程之间共享,这样就失去了动态链接节省内存的一大优势。**我们还需要有一种更好的方法解决共享对象指令中对绝对地址的重定位问题。其实我们的目的很简单,希望程序模块中共享的指令部分在装载时不需要因为装载地址的改变而改变,所以实现的基本想法就是把指令中那些需要被修改的部分分离出来,跟数据部分放在一起,这样指令部分就可以保持不变,而数据部分可以在每个进程中拥有一个副本。这种方案就是目前被称为地址无关代码(PIC, Position-independent Code)的技术。

    对于现代的机器来说,产生地址无关的代码并不麻烦。我们先来分析模块中各种类型的地址引用方式:这里把共享对象模块中的地址引用按照是否为跨模块分成两类:模块内部引用和模块外部引用;按照不同的引用方式又可以分为指令引用和数据访问,这样就得到了四种情况,如下图所示:

    • 1 模块内部的函数调用 跳转等

    • 2 模块内部的数据访问,比如模块中定义的全局变量,静态变量

    • 3 模块外部的函数调用,跳转等

    • 4 模块外部的数据访问,比如其他模块中定义的全局变量

    
    static int a;
    extern int b;
    
    extern void ext();
    
    void bar()
    {
        a = 1; //对应第2
        b = 2;//对应第4
    }
    
    void foo()
    {
        bar();//对应第1
        ext();//对应第3
    }
    
    

    编译器编译pic.c时,它实际上不能确定变量b和函数ext()是模块外部的还是模块内部的,因为它们有可能被定义在同一个共享对象的其它目标文件中,所以编译器只能把它们都当做模块外部的函数和变量来处理

    类型一 模块内部的函数调用、跳转等

    被调用的函数与调用者都处于同一个模块,它们之间的相对位置是固定的,所以这种情况比较简单。对于现代的系统来讲,模块内部的跳转、函数调用都可以是相对地址调用,或者是基于寄存器的相对调用,所以对于这种指令是不需要重定位的。

    类型二 模块内部的数据访问,比如模块中定义的全局变量、静态变量

    **指令中不能直接包含数据的绝对地址,那么唯一的办法就是相对寻址。**我们知道,一个个模块前面一般是若干个页的代码,后面紧跟着若干个页的数据,这些页之间的相对位置是固定的,也就是说,任何一条指令与它需要访问的模块内部数据之间的相对位置是固定的,那么只需要相对于当前指令加上固定的偏移量就可以访问模块内部数据了。现代的体系结构中,数据的相对寻址往往没有相对于当前指令地址(PC)的寻址方式,所以ELF用了一个很巧妙的办法来得到当前的PC值,然后再加上一个偏移量就可以达到访问相应变量的目的了。得到PC值的方法很多。

    类型三 模块外部的数据访问,比如其它模块中定义的全局变量

    **模块间的数据访问目标地址要等到装载时才决定,比如上面例子中的变量b,它被定义在其它模块中,并且该地址在装载时才能确定。要使得代码地址无关,基本的思想就是把跟地址相关的部分放到数据段里面,**很明显,这些其它模块的全局变量的地址是跟模块装载地址有关的。ELF的做法是在数据段里面建立一个指向这些变量的指针数组,也被称为全局偏移表(Global Offset Table, GOT),当代码需要引用该全局变量时,可以通过GOT中相对应的项间接引用。当指令中需要访问变量b时,程序会先找到GOT,然后根据GOT中变量所对应的项找到变量的目标地址。每个变量都对应一个4个字节的地址,链接器在装载模块的时候会查找每个变量所在的地址,然后填充GOT中的各个项,以确保每个指针所指向的地址正确。由于GOT本身是放在数据段的,所以它可以在模块装载时被修改,并且每个进程都可以有独立的副本,相互不受影响。

    GOT如何做到指令的地址无关性?从第二种类型的数据访问我们了解到,模块在编译时可以确定模块内部变量相对于当前指令的偏移,那么我们也可以在编译时确定GOT相对于当前指令的偏移。确定GOT的位置跟上面的访问变量a的方法基本一样,通过得到PC值然后加上一个偏移量,就可以得到GOT的位置。然后我们根据变量地址在GOT中的偏移就可以得到变量的地址,当然GOT中每个地址对应于哪个变量是由编译器决定的。

    类型四 模块外部的函数调用、跳转等

    也可以采用上面类型三的方法来解决,与上面的类型有所不同的是,GOT中相应的项保存的是目标函数的地址,当模块需要调用目标函数时,可以通过GOT中的项进行间接跳转。

    各种地址引用方式:

    指令跳转 & 调用数据访问
    模块内部1 相对跳转和调用2 相对地址访问
    模块外部3间接跳转&调用(GOT)4 间接访问(GOT)

    -fpic和-fPIC

    使用GCC产生地址无关代码很简单,我们只需要使用“-fPIC”参数接口。实际上GCC还提供了另外一个类似的参数叫做“-fpic”,即”PIC”3个字母小写,这两个参数从功能上来讲完全一样,都是指示GCC产生地址无关代码。唯一的区别是,“-fPIC”产生的代码要大,而“-fpic”产生的代码相对较小,而且较快。那么我们为什么不使用“-fpic”而要使用“-fPIC”呢?原因是,由于地址无关代码都是跟硬件平台相关的,不同的平台有着不同的实现,“-fpic”在某些平台上会有一些限制,比如全局符号的数量或者代码的长度等,而“-fpic”则没有这样的限制。所以为了方便起见,绝大部分情况下,我们都使用“-fPIC”参数来产生地址无关代码。

    $ readelf -d Lib.so | grep TEXTREL
    

    上面的命令可以用来区分一个DSO是否为PIC。如果上面的命令有任何输出,那么Lib.so就不是PIC的,否则就是PIC的。PIC的DSO是不会包含任何代码段重定位表的,TEXTREL表示代码段重定位表地址。

    PIC与PIE

    地址无关代码技术除了可以用在共享对象上面,它也可以用于可执行文件,一个以地址无关方式编译的可执行文件被称作地址无关可执行文件(PIE, Position-Independent Executable)。与GCC的“-fPIC”与”“-fpic”参数类似,产生PIE的参数为“-fPIE”或“-fpie”

    共享模块的全局变量问题

    定义在模块内的全局变量?当一个模块引用了一个定义在全局变量的时候,编译器无法判断这个变量在定义同一模块还是定义在另一个共享对象之中。

    比如:

    一个共享对象定义了一个全局变量global,而模块module.c中这么引用:

    
    extern int global;
    
    int foo()
    {
        golbal = 1;
    }
    
    

    当编译器编译module.c时,它无法根据上下文判断global是定义在同一个模块的其它目标文件还是定义在另外一个共享对象之中,即无法判断是否为跨模块间的调用

    数据段地址无关性

    数据部分是否也有绝对地址引用问题?

    代码:

    
    static int a;
    static int* p = &a;
    

    上面代码,指针p的地址就是一个绝对地址,它指向变量a,a的地址会随着共享对象的装载地址改变而改变

    对于数据段来说,它在每个进程都有一份独立的副本,所以并不担心被进程改变。从这点来看,我们可以选择装载时重定位的方法来解决数据段中绝对地址引用问题。

    对于共享对象来说,如果数据段中有绝对地址引用,那么编译器和链接器就会产生一个重定位表,这个重定位表里面包含了“R_386_RELATIVE”类型的重定位入口,用于解决上述问题。当动态链接器装载共享对象时,如果发现该共享对象有这样的重定位入口,那么动态链接器就会对该共享对象进行重定位。

    实际上,我们甚至可以让代码段也使用这种装载时重定位的方法,而不使用地址无关代码。但是,如果代码不是地址无关的,它就不能被多个进程之间共享,于是也就失去了节省内存的优点。但是装载时重定位的共享对象的运行速度要比使用地址无关代码的共享对象快,因为它省去了地址无关代码中每次访问全局数据和函数时需要做一次计算当前地址以及间接地址寻址的过程。

    对于可执行文件来说,默认情况下,如果可执行文件是动态链接的,那么GCC会使用PIC的方法来产生可执行文件的代码段部分,以便于不同的进程能够共享代码段,节省内存。所以我们可以看到,动态链接的可执行文件中存在”.got”这样的段。

    延迟绑定(PLT)

    动态链接的确有很多优势,比静态链接要灵活得多,但它是以牺牲一部分性能为代价的。据统计ELF程序在静态链接下要比动态库稍微快点,当然这取决于程序本身的特性及运行环境等。

    我们知道动态链接比静态链接慢的主要原因是动态链接下对于全局和静态的数据访问都要进行复杂的GOT定位,然后间接寻址;对于模块间的调用也要先定位GOT,然后再进行间接跳转,如此一来,程序的运行速度必定会减慢。

    另外一个减慢运行速度的原因是动态链接的链接工作在运行时完成,即程序开始执行时,动态链接器都需要进行一次链接工作,动态链接器会寻找并装载所需要的共享对象,然后进行符号查找地址重定位等工作,这些工作势必减慢程序的启动速度。这是影响动态链接性能的两个主要问题。

    延迟绑定实现

    在动态链接下,程序模块之间包含了大量的函数引用(全局变量往往比较少,因为大量的全局变量会导致模块之间耦合度变大),所以在程序开始执行前,动态链接会耗费不少时间用于解决模块之间的函数引用的符号查找以及重定位。不过可以想象,在一个程序运行过程中,可能很多函数在程序执行完时都不会被用到,比如一些错误处理函数或者是一些用户很少用到的功能模块等,如果一开始就把所有函数都链接好实际上是一种浪费。

    所以ELF采用了一种延迟绑定(Lazy Bingding)的做法,基本的思想

    就是当函数第一次被用到时才进行绑定(符号查找、重定位等),如果没有用到则不进行绑定。所以程序开始执行时,模块间的函数调用都没有进行绑定,而是需要用到时才由动态链接器来负责绑定。这样的做法可以大大加快程序的启动速度,特别有利于一些有大量函数引用和大量模块的程序。

    ELF使用PLT(Procedure Linkage Table)的方法来实现延迟绑定,这种方法使用了一些很精巧的指令序列来完成。

    假设liba.so需要调用libc.so中的bar函数,那么当liba.so第一次调用bar时,这时候就需要调用动态链接器中的某个函数来完成地址绑定工作,假设这个函数即lookup(),那么lookup()需要知道哪些必要的信息才能完成这个函数地址绑定工作?

    • lookup()至少需要知道这个地址绑定发生在哪个模块?哪个函数?

    假设lookup的原型为lookup(module,function),这两个参数的值在例子中分别为liba.so & bar(),

    当我们调用某个外部模块的函数时,如果按照通常的做法应该是通过GOT中相应的项进行间接跳转。PLT为了实现延迟绑定,在这个过程中间又增加了一层间接跳转。调用函数并不直接通过GOT跳转,而是通过一个叫做PLT项的结构来进行跳转。每个外部函数在PLT中都有一个相应的项。

    ELF将GOT拆分成了两个表叫做”.got”和”.got.plt”。其中”.got”用来保存全局变量引用的地址,”.got.plt”用来保存函数引用的地址

    也就是说,所有对于外部函数的引用全部被分离出来放到了”.got.plt”中。另外”.got.plt”还有一个特殊的地方是它的前三项是有特殊意义的,分别含义如下:第一项保存的是”.dynamic”段的地址,这个段描述了本模块动态链接相关的信息;第二项保存的是本模块的ID;第三项保存的是_dl_runtime_resolve()的地址。其中第二项和第三项由动态链接器在装载共享模块的时候负责将它们初始化。”.got.plt”的其余项分别对应每个外部函数的引用。PLT在ELF文件中以独立的段存放,

    段名通常叫做”.plt”,因为它本身是一些地址无关的代码,所以可以跟代码段等一起合并成同一个可读可执行的”Segment”被装载入内存。

    动态链接相关结构

    动态链接下,可执行文件的装载与静态链接基本一样

    • 操作系统读取可执行文件头部,检查文件合法性

    • 从头部中“Program Header”读取每个“Segment”的虚拟地址,文件地址,属性,将它们映射到进程虚拟空间的相应位置

    这些操作跟静态链接下的装载基本一致,在静态链接下,操作系统接着就可以吧控制权转交给可执行文件的入口地址,然后程序开始执行

    但在动态链接下,操作系统还不能在装载完可执行文件之后就把控制权交给可执行文件,因为可执行文件依赖于很多共享对象,此时,可执行里面对于很多外部符号的引用还处于无效地址的状态,即还没有跟相应的共享对象中的实际位置链接起来,所以在映射完可执行文件之后,操作系统会启动一个动态链接器

    Linux下,动态链接器ld.so其实是一个共享对象,操作系统同样通过映射的方式将它加载到进程的地址空间中,操作系统在加载完动态链接器之后,就将控制权交给动态链接器的入口地址,当动态链接器得到控制权之后,它开始执行一系列自身的初始化操作,然后根据当前的环境参数,开始对可执行文件进行动态链接工作,当所有动态链接工作完成之后,动态链接器会将控制权转交给可执行文件的入口地址,程序正式执行

    ".interp"段

    系统中哪个才是动态链接器?位置由谁决定?

    实际上,动态链接器的位置既不是由系统配置指定,也不是由环境参数决定,而是由ELF可执行文件决定,在动态链接的ELF可执行文件中,有一个专门的段叫做“.interp”

    可以使用odjdump工具查看:

    
    $ objdump -s a.out
    
    a.out:     文件格式 elf64-x86-64
    
    Contents of section .interp:
     0238 2f6c6962 36342f6c 642d6c69 6e75782d  /lib64/ld-linux-
     0248 7838362d 36342e73 6f2e3200           x86-64.so.2.    
    
    
    
    

    内容就是一个字符串,这个字符串是可执行文件所需要的动态链接器的路径,在Linux下,可执行文件所需要的动态链接器的路径几乎都是"/lib/ld-linux.so.2"

    Linux中,操作系统在对可执行文件的进行加载时候,它会去寻找装载该可执行文件所需要相应的动态链接器,即“.interp”段指定的路径的共享对象

    Linux可以通过命令行查看一个可执行文件所需要的动态链接器的路径

    $ readelf -l a.out | grep interpreter
          [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
    
    

    “.dynamic”段

    动态链接ELF中最重要的结构应该是“.dynamic”段,这个段里面保存了动态链接器所需要的基本信息,比如依赖于哪些共享对象,动态链接符号表的位置,动态链接重定位表的位置,共享对象初始化代码的地址等等,

    使用readelf工具查看:

    
    $ readelf -d Lib.so
    
    Dynamic section at offset 0xe20 contains 24 entries:
      标记        类型                         名称/值
     0x0000000000000001 (NEEDED)             共享库:[libc.so.6]
     0x000000000000000c (INIT)               0x520
     0x000000000000000d (FINI)               0x690
     0x0000000000000019 (INIT_ARRAY)         0x200e10
     0x000000000000001b (INIT_ARRAYSZ)       8 (bytes)
     0x000000000000001a (FINI_ARRAY)         0x200e18
     0x000000000000001c (FINI_ARRAYSZ)       8 (bytes)
     0x000000006ffffef5 (GNU_HASH)           0x1f0
     0x0000000000000005 (STRTAB)             0x368
     0x0000000000000006 (SYMTAB)             0x230
     0x000000000000000a (STRSZ)              163 (bytes)
     0x000000000000000b (SYMENT)             24 (bytes)
     0x0000000000000003 (PLTGOT)             0x201000
     0x0000000000000002 (PLTRELSZ)           48 (bytes)
     0x0000000000000014 (PLTREL)             RELA
     0x0000000000000017 (JMPREL)             0x4f0
     0x0000000000000007 (RELA)               0x448
     0x0000000000000008 (RELASZ)             168 (bytes)
     0x0000000000000009 (RELAENT)            24 (bytes)
     0x000000006ffffffe (VERNEED)            0x428
     0x000000006fffffff (VERNEEDNUM)         1
     0x000000006ffffff0 (VERSYM)             0x40c
     0x000000006ffffff9 (RELACOUNT)          3
     0x0000000000000000 (NULL)               0x0
    
    
    

    此外,Linux还提供了一个命令用来查看一个程序主模块或者一个共享库依赖于哪些共享库

                 
    $ ldd Program1
    	linux-vdso.so.1 (0x00007ffcaabe0000)
    	./Lib.so (0x00007fe1e974c000)
    	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fe1e935b000)
    	/lib64/ld-linux-x86-64.so.2 (0x00007fe1e9b50000)
    

    动态符号表

    完成动态链接,最关键的还是所依赖的符号和相关文件的信息

    静态链接中,有一个专门的段叫做符号表“.symtab”,里面保存了所有关于该目标文件的符号的定义以及引用,动态链接的符号表实际上与静态链接类似

    如前面例子Program1程序依赖于Lib.so,引用到了里面的foobar函数,那么对于Program1来说,往往叫Program1导入了foobar函数,foobar函数是Program1的导入函数

    从Lib.so角度,它实际上定义了foobar函数,并且提供给其他模块使用,叫Lib.so导出了foobar函数,foobar是Lib.so的导出函数

    为了表示动态链接这些模块之间的符号导入导出关系,ELF专门有一个叫做动态符号表的段用来保存这些信息,这个段叫做“.dynsym”

    “.dynsym”只保存了与动态链接有关的符号,对于那些模块内部的符号,比如模块私有变量则不保存

    readelf工具来查看

    
    
    
    $ readelf -sD Lib.so
    
    Symbol table of `.gnu.hash' for image:
      Num Buc:    Value          Size   Type   Bind Vis      Ndx Name
        7   0: 0000000000201030     0 NOTYPE  GLOBAL DEFAULT  22 _edata
        8   0: 0000000000201038     0 NOTYPE  GLOBAL DEFAULT  23 _end
        9   1: 0000000000201030     0 NOTYPE  GLOBAL DEFAULT  23 __bss_start
       10   1: 0000000000000520     0 FUNC    GLOBAL DEFAULT   9 _init
       11   2: 0000000000000690     0 FUNC    GLOBAL DEFAULT  13 _fini
       12   2: 000000000000065a    51 FUNC    GLOBAL DEFAULT  12 foobar
    
    

    动态链接重定位表

    动态链接的可执行文件使用PIC方法,虽然其代码段不需要重定位(因为地址无关),但是数据端还是包含了绝对地址的引用,因为代码段中绝对地址相关部分被分离了出来,编程了GOT(全局偏移表),而GOT实际上是数据端的一部分,除了GOT,数据端还可以能包含绝对地址引用。

    重定位相关数据结构

    和静态链接类似,动态链接重定位表分为.rel.dyn和.rel.plt他们分别相当于.rel.text和.rel.data。.rel.dyn是对数据的修真,位于.got段,.rel.plt是对函数的修正位于.got.plt段。

    readelf查看一个动态链接的文件的重定位表

    
    
    $ readelf -r Lib.so
    
    重定位节 '.rela.dyn' at offset 0x448 contains 7 entries:
      偏移量          信息           类型           符号值        符号名称 + 加数
    000000200e10  000000000008 R_X86_64_RELATIVE                    650
    000000200e18  000000000008 R_X86_64_RELATIVE                    610
    000000201028  000000000008 R_X86_64_RELATIVE                    201028
    000000200fe0  000100000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_deregisterTMClone + 0
    000000200fe8  000300000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0
    000000200ff0  000400000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_registerTMCloneTa + 0
    000000200ff8  000600000006 R_X86_64_GLOB_DAT 0000000000000000 __cxa_finalize@GLIBC_2.2.5 + 0
    
    重定位节 '.rela.plt' at offset 0x4f0 contains 2 entries:
      偏移量          信息           类型           符号值        符号名称 + 加数
    000000201018  000200000007 R_X86_64_JUMP_SLO 0000000000000000 printf@GLIBC_2.2.5 + 0
    000000201020  000500000007 R_X86_64_JUMP_SLO 0000000000000000 sleep@GLIBC_2.2.5 + 0
    
    

    动态链接时进程堆栈信息初始化

    从动态链接器角度看,当操作系统把控制权交给它的时候,它将开始做链接的工作,那么它需要知道关于可执行文件和本进程的一些信息,比如可执行文件有几个段,每个段的属性,程序的入口地址等等。这些信息往往由操作系统传递给动态链接器,保存在进程的堆栈里面

    可以写一个程序把堆栈中的初始化信息全部打印出来:

    
    #include <stdio.h>
    #include <elf.h>
    
    int main(int argc, char* argv[])
    {
        void** p = (void**)argv;
        printf("%p\n", p);
    
        printf("Argument count: %d\n", *((int*)p - 1));
    
        int i;
        for (i = 0; i < argc; ++i)
        {
            printf("Argument %d: %s\n", i, (char*)*p);
            p++;
        }
    
        // skip 0
        p++;
    
        printf("Environment:\n");
        while (*p) {
            printf("%s\n", (char*)*p);
            p++;
        }
    
        // skip 0
        p++;
    
        printf("Auxiliary Vectors:\n");
        Elf64_auxv_t* aux = (Elf64_auxv_t*)p;
        while (aux->a_type != AT_NULL) {
            printf("Type: %02ld Value: %#lx\n", aux->a_type, aux->a_un.a_val);
            aux++;
        }
    
        return 0;
    }
    
    
    

    输出;

    
    0x7ffe605a00c8
    Argument count: 0
    Argument 0: ./pr
    Environment:
    CLUTTER_IM_MODULE=xim
    LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc=01;31:*.arj=01;31:*.taz=01;31:*.lha=01;31:*.lz4=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.tzo=01;31:*.t7z=01;31:*.zip=01;31:*.z=01;31:*.Z=01;31:*.dz=01;31:*.gz=01;31:*.lrz=01;31:*.lz=01;31:*.lzo=01;31:*.xz=01;31:*.zst=01;31:*.tzst=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.alz=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.cab=01;31:*.wim=01;31:*.swm=01;31:*.dwm=01;31:*.esd=01;31:*.jpg=01;35:*.jpeg=01;35:*.mjpg=01;35:*.mjpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=00;36:*.au=00;36:*.flac=00;36:*.m4a=00;36:*.mid=00;36:*.midi=00;36:*.mka=00;36:*.mp3=00;36:*.mpc=00;36:*.ogg=00;36:*.ra=00;36:*.wav=00;36:*.oga=00;36:*.opus=00;36:*.spx=00;36:*.xspf=00;36:
    LESSCLOSE=/usr/bin/lesspipe %s %s
    XDG_MENU_PREFIX=gnome-
    LANG=zh_CN.UTF-8
    MANAGERPID=1882
    DISPLAY=:0
    INVOCATION_ID=acc39dd9b8c647aeb29dd7db54b97631
    GNOME_SHELL_SESSION_MODE=ubuntu
    COLORTERM=truecolor
    USERNAME=mrlin
    XDG_VTNR=2
    SSH_AUTH_SOCK=/run/user/1000/keyring/ssh
    XDG_SESSION_ID=2
    USER=mrlin
    DESKTOP_SESSION=ubuntu
    QT4_IM_MODULE=xim
    TEXTDOMAINDIR=/usr/share/locale/
    GNOME_TERMINAL_SCREEN=/org/gnome/Terminal/screen/d4ed0b15_155b_4b4f_89d0_07c180b08c8c
    PWD=/home/mrlin/桌面/project/p6
    HOME=/home/mrlin
    JOURNAL_STREAM=9:36807
    TEXTDOMAIN=im-config
    SSH_AGENT_PID=2004
    QT_ACCESSIBILITY=1
    XDG_SESSION_TYPE=x11
    XDG_DATA_DIRS=/usr/share/ubuntu:/usr/local/share:/usr/share:/var/lib/snapd/desktop
    XDG_SESSION_DESKTOP=ubuntu
    DBUS_STARTER_ADDRESS=unix:path=/run/user/1000/bus,guid=1acbc5cacc124a89e1f576115e888be9
    GTK_MODULES=gail:atk-bridge
    WINDOWPATH=2
    TERM=xterm-256color
    SHELL=/bin/bash
    VTE_VERSION=5202
    QT_IM_MODULE=xim
    XMODIFIERS=@im=ibus
    IM_CONFIG_PHASE=2
    DBUS_STARTER_BUS_TYPE=session
    XDG_CURRENT_DESKTOP=ubuntu:GNOME
    GPG_AGENT_INFO=/run/user/1000/gnupg/S.gpg-agent:0:1
    GNOME_TERMINAL_SERVICE=:1.86
    XDG_SEAT=seat0
    SHLVL=1
    LANGUAGE=zh_CN:zh
    GDMSESSION=ubuntu
    GNOME_DESKTOP_SESSION_ID=this-is-deprecated
    LOGNAME=mrlin
    DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus,guid=1acbc5cacc124a89e1f576115e888be9
    XDG_RUNTIME_DIR=/run/user/1000
    XAUTHORITY=/run/user/1000/gdm/Xauthority
    XDG_CONFIG_DIRS=/etc/xdg/xdg-ubuntu:/etc/xdg
    PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
    SESSION_MANAGER=local/mrlin-virtual-machine:@/tmp/.ICE-unix/1911,unix/mrlin-virtual-machine:/tmp/.ICE-unix/1911
    LESSOPEN=| /usr/bin/lesspipe %s
    GTK_IM_MODULE=ibus
    _=./pr
    Auxiliary Vectors:
    Type: 33 Value: 0x7ffe605b7000
    Type: 16 Value: 0xf8bfbff
    Type: 06 Value: 0x1000
    Type: 17 Value: 0x64
    Type: 03 Value: 0x55cfc4890040
    Type: 04 Value: 0x38
    Type: 05 Value: 0x9
    Type: 07 Value: 0x7f7c4b6ea000
    Type: 08 Value: 0
    Type: 09 Value: 0x55cfc4890580
    Type: 11 Value: 0x3e8
    Type: 12 Value: 0x3e8
    Type: 13 Value: 0x3e8
    Type: 14 Value: 0x3e8
    Type: 23 Value: 0
    Type: 25 Value: 0x7ffe605a03e9
    Type: 26 Value: 0
    Type: 31 Value: 0x7ffe605a2ff3
    Type: 15 Value: 0x7ffe605a03f9
    
    
    

    动态链接的步骤 & 实现

    动态链接器的自举

    我们知道动态链接器本身也是一个共享对象,但是事实上它有一些特殊性。对于普通共享对象文件来说,它的重定位工作由动态链接器来完成。他也可以依赖其他共享对象,其中的被依赖共享对象由动态链接器负责链接和装载。可是对于动态链接器来说,它的重定位工作由谁来完成?它是否可以依赖于其他共享对象?

    这是一个“鸡生蛋,蛋生鸡”的问题,为了解决这种无休止的循环,动态链接器这个“鸡” 必须有些特殊性。

    • 首先是,动态链接器本身不可以依赖于其他任何共享对象;

    • 其次是动态链接器本身所需要的全局和静态变量和重定位工作由它本身完成。

    对于第一个条件我们可以人为的控制。在编写动态链接器时必须保证不使用任何系统库,运行库;

    对于第二个条件,动态链接器必须在启动时有一段非常精巧的代码可以完成这项艰巨的工作而同时又不能使用全局和静态变量。这种具有一定限制条件的启动代码往往被称为自举(Bootstrap)。

    动态链接器入口地址即是自举代码的入口,当操作系统将进程控制权交给动态链接器时,动态链接器的自举代码即开始运行。自举代码首先会找到它自己的GOT。而GOT的第一个入口保存的是“.dynamic”段的偏移地址,由此找到了动态连机器本身的“.dynamic”段。通过“.dynamic”的信息,自举代码便可以获得动态链接器本身的重定位表和符号表等,从而得到动态链接器本身的重定位入口,先将它们全部重定位。从这一步开始,动态链接器代码中才可以使用自己的全局变量和静态变量。

    实际上在动态链接器的自举代码中,除了不可以使用全局变量和静态变量之外,甚至不能调用函数,即动态链接器本身的函数也不能调用。

    这是为什么呢?

    其实我们在前面分析地址无关代码时已经提到过,实际上使用PIC模式编译的共享对象,对于模块内部的函数调用也是采用跟模块外部函数调用一样的方式,即使用 GOT/PLT的方式,所以在 GOT/PLT没有被重定位之前,自举代码不可以使用任何全局变量,也不可以调用函数。

    装载共享对象

    完成基本自举以后,动态链接器将可执行文件和链接器本身的符号表都合并到一个符号表当中,我们可以称它为全局符号表( Global Symbol Table)。

    然后链接器开始寻找可执文件所依赖的共享对象,我们前面提到过“.dynamic”段中,有一种类型的入口DT_NEEDED,它所指出的是该可执行文件(或共享对象)所依赖的共享对象。由此,链接器可以列出可执行文件所需要的所有共享对象,并将这些共享对象的名字放入到一个装载集合中。然后链接器开始从集合里取个所需要的共享对象的名字,找到相应的文件后打开该文件,读取相应的ELF文件头和“ .dynamic”段,然后将它相应的代码段和数据段映射到进程空间中。

    如果这个ELF共享对象还依赖于其他共享对象,那么将所依赖的共享对象的名字放到装载集合中。如此循环直到所有依赖的共享对象都被装载进来为止,当然链接器可以有不同的装载顺序,如果我们把依赖关系看作一个图的话,那么这个装载过程就是一个图的遍历过程,链接器可能会使用深度优先或者广度优先或者其他的顺序来遍历整个图,这取决于链接器,比较常见的算法一般都是广度优先的。

    符号优先级

    a1.c

    #include <stdio.h>
    void a() {
        printf("a1.c\n");
    }
    
    

    a2.c

    #include <stdio.h>
    void a() {
        printf("a2.c\n");
    }
    

    b1.c

    #include <stdio.h>
    void a();
    void b1() {
        a();
    }
    

    b2.c

    
    #include <stdio.h>
    void a();
    void b2() {
        a();
    }
    

    a1.c & a2.c都定义了名字为a的函数,那么b1.c & b2.c 都使用了外部函数a,但是源代码中没有指定依赖于哪个共享对象中的函数a,所以在编译时指定依赖关系,

    假设b1.so依赖于a1.so,b2.so依赖于a2.so,将b1.so与a1.so进行链接,b2.so与a2.so进行链接

    
    
    $ gcc -fPIC -shared a1.c -o a1.so
    
    $ gcc -fPIC -shared a2.c -o a2.so
    
    
    $ gcc -fPIC -shared b1.c a1.so -o b1.so
    
    $ gcc -fPIC -shared b2.c a2.so -o b2.so
    
    
    
    
    $ ldd b1.so
    	linux-vdso.so.1 (0x00007ffc06ff4000)
    	a1.so => not found
    
    $ ldd b2.so
    	linux-vdso.so.1 (0x00007ffd56bc7000)
    	a2.so => not found
    
    
    

    当有程序同时使用b1.c的函数b1和b2.c中的函数b2是会怎样?

    
    #include<stdio.h>
    
    void b1();
    
    void b2();
    
    int main()
    {
        b1();
        b2();
        return 0;
    }
    

    然后我们将main.c编译成可执行文件并且运行:

    
    $ gcc main.c b1.so b2.so -o main -Xlinker -rpath ./
    
    

    关于全局符号介入这个问题,实际上Linux下的动态链接器是这样处理的:

    它定义了一个规则,那就是当一个符号需要被加入全局符号表时,如果相同的符号名已经存在,则后加入的符号被忽略从动态链接器的装载顺序可以看到,它是按照广度优先的顺序进行装载的,首先是main,然后是b1.so、b2.so、a1.so,最后是a2.so。当a2.so中的函数a要被加入全局符号表时,先前装载a1.so时,al.o中的函数a已经存在于全局符号表,那么a2.so中的函数a只能被忽略。所以整个进程中,所有对于符合“a”的引用都会被解析到a1.so中的函数a,这也是为什么main打印出的结果是两个“a1.c”而不是理想中的“alc”和“a2.c”。

    由于存在这种重名符号被直接忽略的问题,当程序使用大量共享对象时应该非常小心符号的重名问题,如果两个符号重名又执行不同的功能,那么程序运行时可能会将所有该符号名的引用解析到第-个被加入全局符号表的使用该符号名的符号,从而导致程序莫名其妙的错误。

    全局符号介入与地址无关代码

    地址无关代码,对于第一类模块内部调用或跳转的处理时,我们简单地将其当作是相对地址调用/跳转。但实际上这个问题比想象中要复杂,结合全局符号介入,关于调用方式的分类的解释会更加清楚。还是拿前面“pic.c”的例子来看,由于可能存在全局符号介入的问题,foo函数对于bar的调用不能够采用第一类模块内部调用的方法,因为一旦bar函数由于全局符号介入被其他模块中的同名函数覆盖,那么foo如果采用相对地址调用的话,那个相对地址部分就需要重定位,这又与共享对象的地址无关性矛盾。所以对于bar()函数的调用,编译器只能采用第三种,即当作模块外部符号处理,bar()函数被覆盖,动态链接器只需要重定位“.got .plt”,不影响共享对象的代码段

    为了提高模块内部函数调用的效率,有一个办法是把bar()函数变成编译单元私有函数,即使用“ statIc”关键字定义bar()函数,这种情况下,编译器要确定bar()函数不被其他模块覆盖,就可以使用第一类的方法,即模块内部调用指令,可以加快函数的调用速度。

    重定位与初始化

    当上面的步骤完成之后,链接器开始重新遍历可执行的文件和每个共享对象的重定位表,将它们的GOT/PLT的每个需要重定位的位置进行修正。

    因为此时动态链接器已经拥有了进程的全局符号表,所以这个修正过程也显得比较容易,跟我们前面提到的地址重定位的原理基本相同。在前面介绍动态链接的重定位表时,我们已经碰到了几种重定位类型,每种重定位入口地址的计算方法我们在这里就不再重复介绍了。

    重定位完成之后,如果某个共享对象有“.init”段,那么动态链接器会执行“.init”段中的代码,用以实现共享对象特有的初始化过程,比如最常见的,共享对象中的C++ 的全局静态对象的构造就需要通过“init”来初始化。相应地,共享对象中还可能有“ finit”段,当进程退出时会执行“.finit"段中的代码,可以用来实现类似C++全局对象析构之类的操作。

    如果进程的可执行文件也有“init”段,那么动态链接器不会执行它,因为可执行文件中的“init”段和“ finit”段由程序初始化部分代码负责执行,我们将在后面的“库”这部分详细介绍程序初始化部分。

    当完成了重定位和初始化之后,所有的准备工作就宣告完成了,所需要的共享对象都已经装载并且链接完成了,这时候动态链接器就如释重负,将进程的控制权转交给程序的入口并且开始执行。

    Linux动态链接器的实现

    在前面分析 Linux下程序的装载时,己经介绍了一个通过 execve()系统调用被装载到进程的地址空间的程序,以及内核如何处理可执行文件。

    内核在装载完ELF可执行文件以后就返回到用户空间,将控制权交给程序的入口。对于不同链接形式的ELF可执行文件,这个程序的入口是有区别的

    • 对于静态链接的可执行文件来说,程序的入口就是ELF文件头里面的 e_entry指定的入口;

    • 对于动态链接的可执行文件来说,如果这时候把控制权交给e_entry指定的入口地址,那么肯定是不行的,因为可执行文件所依赖的共享库还没有被装载,也没有进行动态链接。所以对于动态链接的可执行文件,内核会分析它的动态链接器地址(在“.interp”段),将动态链接器映射至进程地址空间,然后把控制权交给动态链接器。

    Linux动态链接器是个很有意思的东西,它本身是一个共享对象,它的路径是lib/ld-linux.so.2,这实际上是个软链接,它指向lib/ld-x.y.z.so,这个才是真正的动态连接器文件。共享对象其实也是ELF文件,它也有跟可执行文件一样的EF文件头(包括 e_entry、段表等)。动态链接器是个非常特殊的共享对象,它不仅是个共享对象,还是个可执行的程序,可以直接在命令行下面运行:

    其实 Linux的内核在执行 execve()时不关心目标ELF文件是否可执行(文件头 e_type是 ET_EXEC还是 ET_DYN),它只是简单按照程序头表里面的描述对文件进行装载然后把控制权转交给ELF入口地址(没有“.interp”就是ELF文件的 e_entry;如果有“.interp”的话就是动态链接器的 e_entry)。

    这样我们就很好理解为什么动态链接器本身可以作为可执行程序运行,这也从一个侧面证明了共享库和可执行文件实际上没什么区别,除了文件头的标志位和扩展名有所不同之外,其他都是一样的。 Windows系统中的EXE和DLL也是类似的区别,DLL也可以被当作程序来运行, Windows提供了一个叫做rund32exe的工具可以把一个DLL当作可执行文件运行。

    展开全文
  • 本文摘抄于程序员的自我修养-链接装载与库7.1节,这段写的很好,直接拿过来来收藏 http://www.wq3028.top/technology/compile/20180727124/ 静态链接使得不同的程序开发者和部门能够相对独立地开发和测试自己的...
  • C++ 动态链接库和静态链接库

    千次阅读 多人点赞 2019-09-23 15:59:58
      今天对C++生成动态链接路和静态链接库非常感兴趣,必须搞定,否则都没有心情干其他事了。Let’s go~ 文章目录源程序编译链接生成文件格式预编译编译和优化编译优化生成目标文件链接什么是库?动态静态的区别...
  • 动态链接库与静态链接库的区别

    千次阅读 2021-04-26 18:23:00
    动态链接库与静态链接库的区别 库文件的概念 库文件是计算机上的一类文件,提供给使用者一些开箱即用的变量、函数或类。库文件分为静态库和动态库,静态库和动态库的区别体现在程序的链接阶段:静态库在程序的...
  • 进程虚拟地址空间、装载与动态链接、GOT、全局符号表、共享库的组织、DLL、C++与动态链接
  • 静态链接与动态链接的区别和使用

    万次阅读 多人点赞 2018-11-27 13:34:59
     编程中用到链接的是.c文件和库(库里一般都是些函数,变量),.c文件要想用库里的内容就得做链接。    1、静态链接    静态链接:譬如让书本和白板上的笔记之间做静态链接,就是把白板上的笔记抄在书上书和...
  • 静态链接库和动态链接库的区别

    千次阅读 2018-04-23 20:45:30
    一、静态链接库的使用 静态连接库就是把(lib)文件中用到的函数代码直接链接进目标程序,程序运行的时候不再需要其它的库文件。 在VS2015平台上,创建一个静态库(.lib)项目方案,选择【创建项目/Win32/Win32控制台...
  • Linux动态链接

    千次阅读 2018-05-16 11:03:57
    我开始以为动态链接器ld-linux.so.x是linux内核的一部分,其实这种想法是错误的。分析完ELF可执行文件在内核中加载启动的源码后,可以参考博主的这两篇博客Linux加载启动可执行程序的过程(一)内核空间加载ELF的...
  • C++程序在运行时有两种方式加载动态连接库:隐式链接和显式链接。 加载动态库文件就是将动态库中的代码逻辑映射到用户进程地址空间,就在程序执行的时候进行函数调用。 隐式链接 隐式链接是在程序开始执行时就将动态...
  • Android动态链接库减小体积

    千次阅读 2021-09-07 18:11:21
    用NDK编译动态链接库(so); 编译完成后,如果生成的库体积比较大,可以用NDK自带工具strip.exe来瘦身 64位: ndk\21.4.7075529\toolchains\aarch64-linux-android-4.9\prebuilt\windows-x86_64\aarch64-linux-...
  • 静态链接和动态链接区别

    万次阅读 多人点赞 2019-09-22 22:17:18
    1.静态链接与动链接的区别 在C语言中,我们知道要生成可执行文件,必须经历两个阶段,即编译、链接。 在编译过程中,只有编译,不会涉及到链接。 在链接过程中,静态链接和...而动态链接这个过程却没有把内容链接...
  • ae pr如何联动、建立动态链接

    千次阅读 2021-08-02 05:49:45
    那么,它们是可以联动、建立动态链接的你知道吗?如果你还不知道有这种操作,那么就赶快一起看一看、学一学吧!或许能够对你有所帮助和启发!1、首先,我们打开premiere软件,导入我们需要处理的视频素材,我们把想...
  • Linux-动态链接与静态链接对比(动态库和静态库)

    千次阅读 多人点赞 2017-12-14 17:52:46
    一、库的基础概念: 在windows平台和linux平台下都大量存在着库。本质上来说库是一种可执行代码的二进制形式,可以被操作系统载入内存执行。...按照库的使用方式又可分为动态库和静态库,在不同平台下...
  • 动态链接的整个过程

    万次阅读 多人点赞 2017-08-29 20:57:37
    1.静态链接的缺点 (1)同一个模块被多个模块链接时,那么这个模块在磁盘和内存中都有多个副本,导致很大一部分空间被浪费了...也就是说,把链接这个过程推迟到了运行时再进行,这就是动态链接(Dynamic Linking)的基
  • Media Encoder 动态链接

    千次阅读 2018-11-15 13:17:57
    Media Encoder 动态链接 适用情况 安装好将ME移动至其他位置(注册表信息没有改变) 由于注册表信息没有改变,默认会到注册表的安装位置寻找,因为移动,自然是找不到 免安装版(没有注册表信息) 没有注册表信息,...
  • 动态链接库之动态加载

    千次阅读 2019-01-21 14:22:51
    动态加载比静态加载难一些 这里是针对windows下的API来讨论动态加载   首先windows下的动态加载是靠三个函数来实现的 #include &lt;windows.h&gt; LoadLibrary(); GetProcAddress(); FreeLibrary();...
  • Go语言的静态链接与动态链接

    万次阅读 2021-08-27 22:45:20
    Go语言的静态链接与动态链接@TOC Go语言在默认情况下是静态链接的: 但是,有一些库可能会导致动态链接: 这时候可以增加 -ldflags="-extldflags --static" 参数来进行静态链接:
  • 1. 链接、静态链接和动态链接的概念 程序设计追求的是模块化,一个复杂软件由许多模块组成,通常将每个模块进行单独编译,然后将它们组装起来,组装的过程就是链接。链接处理的是把各个模块之间相互应用的部分都...
  • cmake 链接动态链接

    千次阅读 2019-04-08 10:17:20
    使用相对路径的时候,你要让cmake能够搜索到找到你动态库,就像直接使用gcc/g++来链接的时候一样,要使用-L来指定第三方库所在路径。 cmake可以使用 LINK_DIRECTORIES 命令来指定第三方库所在路径,比如,你的动态库...
  • 栈帧 动态链接

    千次阅读 2019-03-05 17:08:00
    ,叫动态链接。 一个方法调用另一个方法,或者一个类使用另一个类的成员变量时, 需要知道其名字 符号引用就相当于名字, 这些被调用者的名字就存放在Java字节码文件里(.class 文件)。 ...
  • 动态链接库的建立与调用

    千次阅读 2020-05-25 10:33:03
    (1)理解动态链接库的实现原理。 (2)掌握Windows系统动态链接库的建立方法。 (3)掌握Windows环境下动态链接库的调用方法。 二:实验准备知识:动态链接库介绍 ​ 动态链接库(Dynamic Link Library DLL)是一个可...
  • 动态链接和静态链接的区别

    千次阅读 2018-12-16 21:16:44
    动态链接和静态链接的区别 动态链接和静态链接的区别 https://www.cnblogs.com/njczy2010/p/5793477.html 一、分别编译与链接(Linking) 大多数高级语言都支持分别编译,程序员可以显式地把程序划分为独立的...
  • 动态链接库在企业级开发中无处不在,本课程包含Windows动态链接库相关知识点,通过浅显易懂的代码与讲解,让你熟悉掌握动态链接库技术!
  • 动态链接boost静态库

    千次阅读 2018-08-21 20:42:59
    为了避免项目布署麻烦,需要将执行文件尽量静态链接 1. boost库全部静态链接 2. c++库静态链接 1,2点的改变如下,强制链静态库的方法为参数下为-l:libXXXX.a; 对于boost log, 需要将宏-DBOOST_LOG_DYN_LINK去掉 ...
  • vs添加动态链接库的三种方法

    千次阅读 2019-11-07 14:07:26
     3、添加工程引用的dll动态库:把引用的dll放到工程的可执行文件所在的目录下。 这种方法比较繁琐,且不直观,而且还可能要争对debug版本和release版本作不同的配置,因为我们生成的两个版本的库可能放在不同的目录...
  • 动态链接库和静态链接库

    万次阅读 多人点赞 2018-08-28 11:49:04
    1. 库的介绍 库是写好的现有的,成熟的,可以复用的代码。现实中每个程序都要依赖很多基础的底层库,不可能每个人的代码都从零开始,因此库的存在意义非同寻常。...库有两种:静态库(.a、.lib)和动态库(.so...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 908,741
精华内容 363,496
关键字:

动态网站链接