Linux动态链接(1)惰性链接

一、动态链接
在Linux(unix族谱)下,共享目标文件称为so文件,它和windows下的DLL机制对应,该功能在节省物理内存使用量上有重要意义,但是更重要的它还是一种扩展框架,也就是很多所谓的“插件”的实现基础。
从它的出现频率上来看,它和Linux下的多线程具有同等重要的地位,甚至更高。因为很多可执行文件都没有使用pthread库,但是几乎所有的Linux发行版本的可执行文件都是动态链接生成的可执行文件。
从实现机制上看,两种机制也有相同之处,它们都有共享和隔离,而线程和共享库内的线程私有数据第一个“多对多”的关系,而多对多关系的描述和实现会比较相对复杂,这一点可以参考一些复杂数据库以及之前博客中曾经简单讨论过的cgroup实现机制。
从对程序员的印象上来讲,两个都相对比较高级一些,因为平时很多程序不需要考虑这些问题,也就是说,可能是相对“高级”一些的内容。当然这里说复杂,可能很多人不以为然。这就是一个所谓“深入简出”的问题,简单的hello world程序,可以引申出很多问题,例如用户态printf缓冲区如何管理(考虑一些超长字符串)、多线程下如何保证打印的粒度互斥(一个printf是否中间夹杂其它printf的内容)、当格式化中有些参数异常时打印结果(考虑格式化“%#30.2”这个打印结果)、printf不定长参数个数如何实现(可能以后参数的入栈顺序)、printf如何进入系统调用、不同文件对相同字节流的不同行为(考虑串口对彩色支持)、hello world是如何显示在屏幕上的等问题。所以同样是一个简单的问题,可以引申出很多东西,这就是不应该看一个东西执行一下就说“精通”之类。我看之前的博客,觉得内容也是比较简单,这都是一个渐进的过程:吾生也有涯,而知也无涯。何况技术这东西,你汪深处挖挖总是有的。
二、延迟绑定
这个就是所谓“懒惰思想”(lazy)在程序设计中的有一个体现,另一个体现就是经典的内存管理。其实这里的“lazy”并不是我们通常所说的懒惰,而是一种坦然和自信。因为别人交给你的任务,比可以随时快速的答应下来,而当别人真正需要的时候,可以快速的交互,而懒惰则是消极的拒绝,不答应做,也做不下来。做技术到最后,也不是说把所有的东西全部学完,而是可以很多东西不学,但是当真正用的时候可以快速理解和运用。
这里的延迟绑定就是对于一个模块使用的另一个so文件中的符号,它可以在这个符号第一次被使用的时候完成真正的重定位而不是在程序运行的开始就匆匆忙忙把所有的符号动态链接完成(当然也可以通过LD_BIND_NOW来要求它在加载so是就一次性完成动态重定位)。作为一个直接推论,如果这个重定位位置上的代码从来没有被真正执行过,那么这个动态重定位永远也不会发生。这样的另一个好处就是可以将重定位的时间分摊到运行的各个阶段,从而提高程序刚启动起来之后的交互感受。
三、测试代码
//测试代码使用到的文件
[tsecer@Harry SoDemo]$ ls
main.c  Makefile  miniso.c
//使用动态库内容的主函数
[tsecer@Harry SoDemo]$ cat main.c 
extern int bay,baz;
extern int foo(int);
extern int bar(int);
int main()
{
    return foo(bay) + bar(baz);
}
//简单Makefile
[tsecer@Harry SoDemo]$ cat Makefile
main.exe:miniso.so
    gcc main.c -fPIC -L. -lminiso -o main.exe -g 
miniso.so:
    gcc -fPIC miniso.c -shared -o libminiso.so
clean:
    rm -f *.so *.exe *.o
//该文件用来生成so文件。
[tsecer@Harry SoDemo]$ cat miniso.c 
/*
 * miniso source
 */
int bay = 0x44444444;
int baz = 0x22222222;

/* multipy 2*/
int foo(int arg)
{
    return arg<<1; 
}

/*divide 2*/
int bar(int arg )
{
    return arg>>1;
}
[tsecer@Harry SoDemo]$ 
四、main函数代码分析
(gdb) disas main
Dump of assembler code for function main:
0x080484f4 <main+0>:    push   %ebp
0x080484f5 <main+1>:    mov    %esp,%ebp
0x080484f7 <main+3>:    and    $0xfffffff0,%esp
0x080484fa <main+6>:    push   %esi
0x080484fb <main+7>:    push   %ebx
0x080484fc <main+8>:    sub    $0x18,%esp
0x080484ff <main+11>:    call   0x8048538 <__i686.get_pc_thunk.bx> 该函数执行完成之后,ebx寄存器中存放本进程的GOT表位置,这一点可以通过之后的readelf输出确认。
0x08048504 <main+16>:    add    $0x1274,%ebx
0x0804850a <main+22>:    mov    -0x8(%ebx),%eax bay的地址为 ebx -8,
0x08048510 <main+28>:    mov    (%eax),%eax
0x08048512 <main+30>:    mov    %eax,(%esp)
0x08048515 <main+33>:    call   0x8048428 <foo@plt>
0x0804851a <main+38>:    mov    %eax,%esi
0x0804851c <main+40>:    mov    -0x4(%ebx),%eax baz的地址为ebx -4
0x08048522 <main+46>:    mov    (%eax),%eax
0x08048524 <main+48>:    mov    %eax,(%esp)
0x08048527 <main+51>:    call   0x80483f8 <bar@plt>
0x0804852c <main+56>:    lea    (%esi,%eax,1),%eax
0x0804852f <main+59>:    add    $0x18,%esp
0x08048532 <main+62>:    pop    %ebx
0x08048533 <main+63>:    pop    %esi
0x08048534 <main+64>:    mov    %ebp,%esp
0x08048536 <main+66>:    pop    %ebp
0x08048537 <main+67>:    ret    
End of assembler dump.
(gdb) disas 0x8048428
Dump of assembler code for function foo@plt:
0x08048428 <foo@plt+0>:    jmp    *0x8049790 跳转地址为
0x0804842e <foo@plt+6>:    push   $0x18
0x08048433 <foo@plt+11>:    jmp    0x80483e8
End of assembler dump.
(gdb) disas 0x80483f8
Dump of assembler code for function bar@plt:
0x080483f8 <bar@plt+0>:    jmp    *0x8049784
0x080483fe <bar@plt+6>:    push   $0x0
0x08048403 <bar@plt+11>:    jmp    0x80483e8 两个plt都跳转到了同一个地址0x80483e8,但是这个地址并没有位于代码段。
End of assembler dump.
(gdb) 
1、0x80483e8 地址意义
使用objdump -D main.exe命令,可以看到其中该地址处的指令为:
080483e8 <bar@plt-0x10>:
 80483e8:    ff 35 7c 97 04 08        pushl  0x804977c 
 80483ee:    ff 25 80 97 04 08        jmp    *0x8049780 这里两条语句使用了两个编译时确定地址,并且它们的地址是连续的。
 80483f4:    00 00                    add    %al,(%eax)
2、0x804977c 地址的意义
调试器中
(gdb) x  0x8049780 
0x8049780 <_GLOBAL_OFFSET_TABLE_+8>:    0x001fc850
(gdb) disas 0x001fc850 
Dump of assembler code for function _dl_runtime_resolve:
……
这说明0x8049780处存放了_dl_runtime_resolve函数的实现,注意,这个函数位于ld-linux.so,也就是它的位置是程序运行起来之后才能确定的。
3、这个_dl_runtime_resolve地址由谁何时填充
看可执行文件的动态节中PLTGOT节中内容
[tsecer@Harry SoDemo]$ readelf -a main.exe 
……
Dynamic section at offset 0x69c contains 21 entries:
  Tag        Type                         Name/Value
 ……
 0x00000003 (PLTGOT)                     0x8049778 这个PLTGOT从0x8049778 开始和上面的连续地址最为接近。
 ……
 0x00000000 (NULL)                       0x0
动态链接器相关代码glibc-2.7\sysdeps\i386\dl-machine.h
static inline int __attribute__ ((unused, always_inline))
elf_machine_runtime_setup (struct link_map *l, int lazy, int profile)
{
  Elf32_Addr *got;
  extern void _dl_runtime_resolve (Elf32_Word) attribute_hidden;
  extern void _dl_runtime_profile (Elf32_Word) attribute_hidden;

if (l->l_info[DT_JMPREL] && lazy)
    {
      /* The GOT entries for functions in the PLT have not yet been filled
     in.  Their initial contents will arrange when called to push an
     offset into the .rel.plt section, push _GLOBAL_OFFSET_TABLE_[1],
     and then jump to _GLOBAL_OFFSET_TABLE[2].  */
      got = (Elf32_Addr *) D_PTR (l, l_info[DT_PLTGOT]);
      /* If a library is prelinked but we have to relocate anyway,
     we have to be able to undo the prelinking of .got.plt.
     The prelinker saved us here address of .plt + 0x16.  */
      if (got[1])
    {
      l->l_mach.plt = got[1] + l->l_addr;
      l->l_mach.gotplt = (Elf32_Addr) &got[3];
    }
      got[1] = (Elf32_Addr) l;    /* Identify this shared object.  */

/* The got[2] entry contains the address of a function which gets
     called to get the address of a so far unresolved function and
     jump to it.  The profiling extension of the dynamic linker allows
     to intercept the calls to collect information.  In this case we
     don‘t store the address in the GOT so that all future calls also
     end in this function.  */
      if (__builtin_expect (profile, 0))
    {
      got[2] = (Elf32_Addr) &_dl_runtime_profile;

if (GLRO(dl_profile) != NULL
          && _dl_name_match_p (GLRO(dl_profile), l))
        /* This is the object we are looking for.  Say that we really
           want profiling and the timers are started.  */
        GL(dl_profile_map) = l;
    }
      else
    /* This function will get called to fix up the GOT entry indicated by
       the offset on the stack, and then jump to the resolved address.  */
    got[2] = (Elf32_Addr) &_dl_runtime_resolve;
    }

return lazy;
}
这意味着每个PIC文件中,它的PLTGOT标签说明了一个特殊的got地址,该地址开始第一个地址意义未知,第二项为标识该文件的link_map对象地址,第三个为动态链接时确定的_dl_runtime_resolve函数地址,而这个函数是完成惰性链接的真正执行者。
结合上面的例子
 0x00000003 (PLTGOT)                     0x8049778 
该地址为代码中got的的值,然后got[2] = 0x8049778 + 2*4=0x8049780,而这个地址也就是 
080483e8 <bar@plt-0x10>:
 80483e8:    ff 35 7c 97 04 08        pushl  0x804977c 
 80483ee:    ff 25 80 97 04 08        jmp    *0x8049780 
中使用的跳转地址。
这个调用关系为:_dl_relocate_object--->>>ELF_DYNAMIC_RELOCATE--->>>elf_machine_runtime_setup
五、外部数据动态链接实现
这里的外部数据访问和外部函数调用是不同的,外部数据访问必须在so加载之后马上完成重定位,它不能使用惰性链接,所谓惰性链接只能对函数调用实现。因为数据可以通过指令直接访问,动态连接器没有位置拦截对于这个数据的使用,而对于外部函数的调用链接器可以方便的定义自己的适配函数。
1、glibc实现代码
ELF_DYNAMIC_RELOCATE---->>>ELF_DYNAMIC_DO_REL
#  define _ELF_DYNAMIC_DO_RELOC(RELOC, reloc, map, do_lazy, test_rel) \
  do {                                          \
    struct { ElfW(Addr) start, size; int lazy; } ranges[2];              \
    ranges[0].lazy = 0;                                  \对于数据访问,始终不会使用lazy方式
    ranges[0].size = ranges[1].size = 0;                      \
    ranges[0].start = 0;                              \
                                          \
    if ((map)->l_info[DT_##RELOC])                          \
      {                                          \
    ranges[0].start = D_PTR ((map), l_info[DT_##RELOC]);              \
    ranges[0].size = (map)->l_info[DT_##RELOC##SZ]->d_un.d_val;          \
      }                                          \
    if ((map)->l_info[DT_PLTREL]                          \
    && (!test_rel || (map)->l_info[DT_PLTREL]->d_un.d_val == DT_##RELOC)) \
      {                                          \
    ElfW(Addr) start = D_PTR ((map), l_info[DT_JMPREL]);              \
                                          \
    if (! ELF_DURING_STARTUP                          \
        && ((do_lazy)                              \
        /* This test does not only detect whether the relocation      \
           sections are in the right order, it also checks whether    \
           there is a DT_REL/DT_RELA section.  */              \
        || ranges[0].start + ranges[0].size != start))              \
      {                                      \
        ranges[1].start = start;                          \
        ranges[1].size = (map)->l_info[DT_PLTRELSZ]->d_un.d_val;          \
        ranges[1].lazy = (do_lazy);                          \对于PLT类型动态重定位项,根据参数确定是否进行惰性链接
      }                                      \
    else                                      \
      {                                      \
        /* Combine processing the sections.  */                  \
        assert (ranges[0].start + ranges[0].size == start);              \
        ranges[0].size += (map)->l_info[DT_PLTRELSZ]->d_un.d_val;          \
      }                                      \
      }                                          \
                                          \
    if (ELF_DURING_STARTUP)                              \
      elf_dynamic_do_##reloc ((map), ranges[0].start, ranges[0].size, 0);     \
    else                                      \
      {                                          \
    int ranges_index;                              \
    for (ranges_index = 0; ranges_index < 2; ++ranges_index)          \
      elf_dynamic_do_##reloc ((map),                      \
                  ranges[ranges_index].start,              \
                  ranges[ranges_index].size,              \
                  ranges[ranges_index].lazy);              \
      }                                          \
  } while (0)
2、以测试程序说明相关内容
对于我们测试的main.exe,其相关动态节内容为
 0x00000002 (PLTRELSZ)                   32 (bytes)
 0x00000014 (PLTREL)                     REL
 0x00000017 (JMPREL)                     0x8048398
 0x00000011 (REL)                        0x8048380
 0x00000012 (RELSZ)                      24 (bytes)
 0x00000013 (RELENT)                     8 (bytes)
这里看到,其中的DT_REL从0x8048380开始,总共占用DT_RELSZ=24 字节,所以结束位置为0x8048398,而这个位置也就是DT_JMPREL的起始位置,而plt的大小为DT_PLTRELSZ=32字节,共32/DT_RELENT=24/8=4项。
其它对应输出
Relocation section ‘.rel.dyn‘ at offset 0x380 contains 3 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
0804976c  00000206 R_386_GLOB_DAT    00000000   __gmon_start__
08049770  00000606 R_386_GLOB_DAT    00000000   bay
08049774  00000706 R_386_GLOB_DAT    00000000   baz  数据重定位共三项。

Relocation section ‘.rel.plt‘ at offset 0x398 contains 4 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
08049784  00000107 R_386_JUMP_SLOT   00000000   bar
08049788  00000207 R_386_JUMP_SLOT   00000000   __gmon_start__
0804978c  00000407 R_386_JUMP_SLOT   00000000   __libc_start_main
08049790  00000507 R_386_JUMP_SLOT   00000000   foo  代码重定位共四项。
六、todo
ldd的如何实现,
动态库的ELF header中 entry意义
so文件中初始化函数(init)何时调用,如何定义和配置
动态链接器直接加载文件 ld-linux.so prog args
动态链接器bootstrap启动
共享库文件单独运行时地址如何确定,内核如何处理

原文地址:https://www.cnblogs.com/tsecer/p/10486370.html

时间: 2024-10-16 21:58:41

Linux动态链接(1)惰性链接的相关文章

再探Linux动态链接 -- 关于动态库的基础知识

  在近一段时间里,由于多次参与相关专业软件Linux运行环境建设,深感有必要将这些知识理一理,供往后参考. 编译时和运行时 纵观程序编译整个过程,细分可分为编译(Compiling,指的是语言到平台相关目标文件这一层次)和链接(Linking,指目标文件到最终形成可执行文件这一层次),这个总的过程可称为编译时:就动态链接而言,还存在一个运行时,即程序在被操作系统加载的过程中,系统将该程序需要的动态库加载至内存到程序开始运行的这一段过程.明确这两个过程在一般linux开发中的地位,以及了解每个"

Linux的软连接与硬链接

Linux的软连接相当于window系统的快捷方式,如我们桌面的QQ等. 硬连接相当于复制一个文件,但不同的是两个文件内容同步.如创建一个文件A的硬连接B, 如果我修改A里面的内容,同时B中的内容也会更新. 创建软连接的命令:ln -s  源文件  目标文件 创建硬连接的命令:ln   源文件  目标文件 硬连接不能跨区创建,比如/home 目录与/ect目录不在一个分区,a这个文件现在在/home目录中,这时你想对a文件创建硬连接到/ect目录下是不行的. 软连接可以跨区创建 Linux的软连

Linux下软链接与硬链接的区别

Linux下软链接与硬链接的区别 Linux中的文件都文件名和数据,在linux上面被分为两个部分:元数据与数据.用户数据,即文件数据块(data block),数据块是记录文件真实内容的地方,而元数据是文件的附加属性,如大小,创建时间,所有者等信息.在Linux中,元数据中的inode号(inode是文件的元数据的一部分,但其不包含文件名,inode号即索引节点号)才是文件的唯一标识而不是文件名.文件名仅是为了方便人们的记忆和使用,系统或程序通过inode号寻找正确的文件数据快.下图为程序通过

动态链接和静态链接的区别

动态链接和静态链接的区别 一.分别编译与链接(Linking) 大多数高级语言都支持分别编译,程序员可以显式地把程序划分为独立的模块或文件,然后每个独立部分分别编译.在编译之后,由链接器把这些独立的片段(称为编译单元)“粘接到一起”.(想想这样做有什么好处?) 在C/C++中,这些独立的编译单元包括obj文件(一般的源程序编译而成).lib文件(静态链接的函数库).dll文件(动态链接的函数库)等. 静态链接方式:在程序执行之前完成所有的组装工作,生成一个可执行的目标文件(EXE文件). 动态链

Linux 文件软链接、硬链接

linux的硬链接和软链接 链接方式有两种:硬链接和软链接 在Linux的文件系统中,保存在磁盘分区中的文件不管是什么类型都给它分配一个编号,称为索引节点号(Inode Index). (1)硬链接:多个文件名指向同一索引节点 作用:允许一个文件拥有多个有效路径名,这样用户就可以建立硬连接到重要文件,以防止"误删"的功能. 其实一个硬链接就是一个文件的一个别名,只删除一个连接并不影响索引节点本身和其它的连接,只有当最后一个连接被删除后,文件的数据块及目录的连接才会被释放.也就是说,文件

linux下添加链接与删除链接(ln命令的用法)

添加链接使用ln命令用法:#ln --help用法:ln [选项]... 目标 [链接名]或:ln [选项]... 目标... 目录或:ln [选项]... --target-directory=目录 目标...创建连至指定<目标>的链接,并可选择性指定<链接名>.如果没有指定<链接名>,会在目前的目录中创建一个和<目标>名称一样的链接.当使用第二种格式而<目标>多於一个时,最后的参数必须是目录:这样会在指定的<目录>中分别创建连至每

安装LINUX X86-64的10201出现链接ins_ctx.mk错误-转自yingtingkun

详细错误信息为: Error in invoking target 'install' of makefile '/opt/oracle/product/10.2/ctx/lib/ins_ctx.mk'. See '/opt/oracle/oraInvertory/logs/installActions2010-09-28_10-27-06AM.log'for details. 从日志中获取的详细信息为: INFO: gcc -m32 -o ctxhx -L/opt/oracle/product

linux中软链接和硬链接的区别与小结

ln命令 该命令在文件之间创建链接.这种操作实际上是给系统中已有的某个文件指定另外一个可用于访问它的名称.对于这个新的文件名,我们可以为之指定不同的访问权限,以控制对信息的共享和安全性的问题. 如果链接指向目录,用户就可以利用该链接直接进入被链接的目录而不用打一大堆的路径名.而且,即使我们删除这个链接,也不会破坏原来的目录. 语法:ln [选项] 目标 [链接名] ln [选项] 目标 目录 链接有两种,一种被称为硬链接(Hard Link),另一种被称为符号链接(Symbolic Link).

Linux中软、硬链接那些事儿

软连接与硬链接 上一篇文章已经向大家介绍了Linux系统中文件目录结构,见http://vinsent.blog.51cto.com/13116656/1959522 .基于次,这篇博客向大家介绍Linux文件系统中的软连接(也称:符号链接)和硬链接:主要包括软.硬连接的概念,软连接与硬链接的区别以及如何创建软.硬链接.该文章中引用了https://www.ibm.com/developerworks/cn/linux/l-cn-hardandsymb-links/  中的一些概念:如有侵权,敬