静态链接和动态链接

1.基础知识


  程序由源代码变成可执行文件,一般可以分解为四个步骤,分别是:

    [1]预处理(Prepressing):预处理过程主要处理源代码中以“#”开始的预编译指令;

    [2]编译(Compilation) :编译过程把预处理完成的文件进行词法、语法、语义等分析并产生相应的汇编代码文件;

    [3]汇编(Assembly)    :汇编过程将汇编代码文件翻译成机器可以执行的目标文件;

    [4]链接(Linking)     :链接过程将汇编生成的目标文件集合相连接并生成最终的可执行文件。

  链接器在链接静态链接库的时候是以目标文件(.o)为单位的,如果该静态库里的某些方法没有任何地方调用,则这些没有被调用到的方法或变量将会被丢弃掉,不会被链接到目标程序中,这样做可以大大减小生成二进制文件的体积。

  动态链接在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。此时在程序的链接阶段时,链接器只是拷贝了一些重定位和符号信息。在程序加载(execve)时才会解析so文件中代码和数据的引用。某个程序在运行中要调用某个动态链接库函数的时候,操作系统首先会查看所有正在运行的程序,看在内存里是否已有此库函数的拷贝了。如果有,则让其共享那一个拷贝;只有没有才链接载入。在程序运行的时候,被调用的动态链接库函数被安置在内存的某个地方,所有调用它的程序将指向这个代码段。因此,这些代码必须使用相对地址,而不是绝对地址。在编译的时候,我们需要告诉编译器,这些对象文件是用来做动态链接库的,所以要用地址无关代码(Position Independent Code (PIC))。

  动态链接库的加载方式有两种:隐式加载和显示加载。linux下进行连接的缺省操作是首先连接动态库,也就是说,如果同时存在静态和动态库,不特别指定的话,将与动态库相连接。

2.库的搜索路径


  库的搜索路径,包括编译时的搜索(.a和.so)和运行时的搜索(.so)。所以虽然使用-L可以链接so编译过程序,但是运行时并不会从-L指定的目录搜索从而导致可能运行时找不到文件。

  【1】编译链接库时的搜索路径顺序:

    [1]-L选项指定的目录,例如:g++ -o test_fun test_fun.c -L. -lfun

    [2]-Wl,rpath指定的目录。例如:gcc -o foo foo.c -L. -lfoo -Wl,-rpath=./

    [3]环境变量LD_LIBRARY_PATH中设置的目录。

    [4]/etc/ld.so.cache文件中缓存的文件位置。

    [5]默认的/usr/lib或者/usr/lib64。

  【2】程序运行时搜索动态库文件的顺序:

    [1]-Wl,rpath指定的目录。例如:gcc -o foo foo.c -L. -lfoo -Wl,-rpath=./

    [2]环境变量LD_LIBRARY_PATH中设置的目录。

    [3]/etc/ld.so.cache文件中缓存的文件位置。

    [4]默认的/usr/lib或者/usr/lib64。

  【3】ldconfig

    ldconfig命令是在默认搜寻目录(/lib和/usr/lib)以及动态库配置文件/etc/ld.so.conf内所列的目录下,搜索出可共享的动态链接库(lib*.so*),进而创建出动态装入程序(ld.so)所需的连接和缓存文件,缓存文件默认为 /etc/ld.so.cache,此文件保存已排好序的动态链接库名字列表。程序连接的时候首先从这个缓存文件里边查找,然后再到ld.so.conf的路径里边去详细找。

    添加动态链接库的方法:

      [1]往/lib和/usr/lib里面加东西。此时不用修改/etc/ld.so.conf文件的,但是添加完后必须调用ldconfig,不然添加的library会找不到。另外如果是加到/lib64或者/usr/lib64,则不执行ldconfig也可以找到。

      [2]往普通目录添加library。此时一定要修改/etc/ld.so.conf文件,往该文件追加library所在的路径,然后调用ldconfig命令。

      [3]设置环境变量LD_LIBRARY_PATH。因为ldconfig的作用和这个环境变量无关,所以不用执行ldconfig。例如export LD_LIBRARY_PATH=/usr/local/test

3.库相关命令


  readelf -s libfun.so

    该命令可以打印出静态库或动态库的符号。

  lsof libfun.so

    该命令可以打印出正在使用libfun.so的进程。

4.动态链接库


  【1】隐式方式使用动态库

    创建库源文件,编译.so文件:g++ -fPIC -shared -o libfun.so fun.c

    引用库:g++ -o test_fun test_fun.c -L. -lfun

    此时可以生成可执行文件test_fun,但是执行的时候会报错:libfun.so: cannot open shared object file: No such file or directory。这是因为程序在运行时,会在/usr/lib和/lib等目录中查找需要的动态库文件,如果找不到就会报错,而上面用-L. -lfun指定so路径和文件只是可以编译通过。

  【2】显示方式使用动态库

    LINUX下使用动态链接库,源程序需要包含dlfcn.h头文件,此文件定义了调用动态链接库的函数的原型。

    相关函数:

      void *dlopen (const char *filename, int flag);

        该函数打开指定名字(filename)的动态链接库,并返回操作句柄。如果filename不是以'/'开头,则按照先后顺序从目录中查找文件:LD_LIBRARY,/etc/ld.so.cache,/lib,/usr/lib。

        flag参数指定在什么时候解决未定义的符号,RTLD_LAZY表示在动态链接库的函数代码执行时解决,RTLD_NOW表示在dlopen返回前就解决所有未定义的符号,一旦未解决,dlopen将返回错误。

        例如dlopen("abc/libfun.so",  RTLD_NOW ),则首先查看/usr/local/test/abc/libfun.so是否存在,最后查看/lib/abc/libfun.so是否存在。

      void *dlsym(void *handle, char *symbol);

        该函数用来获取符号的地址。符号包括函数、全局变量等。通过此符号可以调用动态库的函数。

      int dlclose (void *handle);

         关闭动态链接库,其实就是减计数,只有当此动态链接库的使用计数为0时,才会真正被系统卸载。

    例子:这里要注意的是如果使用g++编译成so时,此时的符号就不是fun,而是c++规则的符号(例如:_Z3funv),所以要查找符号"fun"时,需要在共享库的源文件中加上extern “C”,这样编译出的符号就是c语言格式的。

      

   【3】so的更新替换

    针对未被加载的so,利用复制命令(cp new.so old.so)即可直接完成静态替换,新so在下次加载时生效,也就是需要重启程序才可以让新的so生效。

    如果当前的so已经被程序加载,此时直接用cp会导致程序崩溃,正确方法是先把旧的so删除,然后再把新的so复制过去,当前这种方法也需要重启程序,新的so才会被重新加载。

    直接覆盖会导致崩溃的原因:linux中使用了内存映像和需求分页机制,需要确保正在运行程序的镜像不被破坏,因此内核在启动程序后会锁定镜像的inode。跟踪cp命令的执行流程为:

      

    可以看到cp命令的替换操作会破坏系统访问原so的索引节点的inode,inode包含了文件的元信息,如文件字节数、拥有者ID、读写执行权限等。系统以inode标识程序加载的so,不再关心文件名,因此修改so的名称并未改变对应inode,因此程序可以继续正常运行;删除SO只是无法查看到文件,系统直到程序释放so后才真正删除so和inode,因此程序也可以继续正常运行;但是在直接复制替换时,新so将会继承原so的inode,导致inode被破坏,从而导致程序崩溃。

    如果替换正在运行的bin文件,此时cp会报错"text file busy",说明系统对bin文件有特殊保护,不会出现inode被破坏的情况。但是却没有对so文件做特殊保护,所以直接cp会崩溃。

 

 

  

    

 

posted on 2019-03-01 10:51  能量星星  阅读(3571)  评论(0编辑  收藏  举报

导航