linux下动态链接过程

总是在工作中会遇到符号表,链接等字眼,之前看过《程序员自我修养》这本书,但是基本上都忘记了,这几天再刷一遍,顺便记录一下,加深记忆。

本文会完整的描述程序运行的动态加载及运行的整个流程,会涉及到elf文件,elf中的section表,以及ld.so的相关知识,且其中会夹杂着一些命令,方便工作时的使用。

本文讲述并不深,比较浅显,很多比较烧脑和难理解就美提及,同时也时不时有例子帮助大家理解,篇幅较长所以分为两篇。

elf文件

elf文件是我们常用到的可执行文件的组织形式,同时还是可重定位文件(也就是.o文件),动态库等组织形式。下图是elf文件的大体结构(有删减):

elf文件是由一些描述信息和存储信息的各个段组成。图中可以看到首先是elf文件头,用来表明该文件的一些基本信息,然后是程序头表,用来运行时使用,接下来就是各种功能的段,后边根据需要一一讲述,然后是节头部表,就是用来描述各个段。

而且各种段我使用两种颜色区分,上边是会载入到内存,下边不会载入到内存,且文件头和程序头都会载入到内存。图中载入到内存的段和我们熟知的进程地址空间一些段有些是一样的(.text, .data, .bss),有一些可能我们并不熟悉。

文件头

我们通过命令来看一个可执行文件的文件头:

# readelf -h /bin/bash
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              DYN (Shared object file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x2f630
  Start of program headers:          64 (bytes into file)
  Start of section headers:          1166920 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         11
  Size of section headers:           64 (bytes)
  Number of section headers:         29
  Section header string table index: 28

头信息包含了一些elf文件的基本信息,我们根据源码中的注释来简单介绍几个我们可能需要关注的:

typedef struct
{
  unsigned char	e_ident[EI_NIDENT];	/* Magic number and other info */
  Elf64_Half	e_type;			/* Object file type */
  Elf64_Half	e_machine;		/* Architecture */
  Elf64_Word	e_version;		/* Object file version */
  Elf64_Addr	e_entry;		/* Entry point virtual address */
  Elf64_Off	e_phoff;		/* Program header table file offset */
  Elf64_Off	e_shoff;		/* Section header table file offset */
  Elf64_Word	e_flags;		/* Processor-specific flags */
  Elf64_Half	e_ehsize;		/* ELF header size in bytes */
  Elf64_Half	e_phentsize;		/* Program header table entry size */
  Elf64_Half	e_phnum;		/* Program header table entry count */
  Elf64_Half	e_shentsize;		/* Section header table entry size */
  Elf64_Half	e_shnum;		/* Section header table entry count */
  Elf64_Half	e_shstrndx;		/* Section header string table index */
} Elf64_Ehdr;

Magic即为魔数,用来标识这是一个elf文件,平台属性(32位/64位)等等。

Type则为elf文件的类型,可以看到bash这个被定义为是Shared object file,elf支持的文件格式有: 可重定位文件,可执行文件,动态库,core文件。

#define ET_NONE		0		/* No file type */
#define ET_REL		1		/* Relocatable file */
#define ET_EXEC		2		/* Executable file */
#define ET_DYN		3		/* Shared object file */
#define ET_CORE		4		/* Core file */

Entry point address表示文件执行的入口虚拟地址,可重定位文件这个字段为0。

然后还有程序头表(program headers)在elf文件的位置及大小,节头部表(section headers)在elf文件的位置及大小。
其他暂且就不展开了

节头部表

我们通过指令来查看elf的节头部表:

# readelf -S /bin/bash
There are 29 section headers, starting at offset 0x11ce48:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .interp           PROGBITS         00000000000002a8  000002a8
       000000000000001c  0000000000000000   A       0     0     1
  [ 5] .dynsym           DYNSYM           0000000000004db0  00004db0
       000000000000e400  0000000000000018   A       6     1     8
  [ 9] .rela.dyn         RELA             000000000001dca8  0001dca8
       000000000000dc80  0000000000000018   A       5     0     8
  [10] .rela.plt         RELA             000000000002b928  0002b928
       0000000000001470  0000000000000018  AI       5    24     8
  [11] .init             PROGBITS         000000000002d000  0002d000
       0000000000000017  0000000000000000  AX       0     0     4
  [12] .plt              PROGBITS         000000000002d020  0002d020
       0000000000000db0  0000000000000010  AX       0     0     16
  [13] .plt.got          PROGBITS         000000000002ddd0  0002ddd0
       0000000000000018  0000000000000008  AX       0     0     8
  [14] .text             PROGBITS         000000000002ddf0  0002ddf0
       00000000000ac991  0000000000000000  AX       0     0     16
  [15] .fini             PROGBITS         00000000000da784  000da784
       0000000000000009  0000000000000000  AX       0     0     4
  [16] .rodata           PROGBITS         00000000000db000  000db000
       0000000000019930  0000000000000000   A       0     0     32
  [22] .dynamic          DYNAMIC          0000000000114cf0  00113cf0
       0000000000000200  0000000000000010  WA       6     0     8
  [23] .got              PROGBITS         0000000000114ef0  00113ef0
       0000000000000100  0000000000000008  WA       0     0     8
  [24] .got.plt          PROGBITS         0000000000115000  00114000
       00000000000006e8  0000000000000008  WA       0     0     8
  [25] .data             PROGBITS         0000000000115700  00114700
       0000000000008604  0000000000000000  WA       0     0     32
  [26] .bss              NOBITS           000000000011dd20  0011cd04
       0000000000009c78  0000000000000000  WA       0     0     32
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  l (large), p (processor specific)

这个命令会展示出该elf文件中所有的段(section),我这里为了方便展示省略掉一些不需要关注的段。

接下来看下表示段的各个字段,还是使用源码中来看下,注释相对来说比较清晰,

typedef struct
{
  Elf64_Word	sh_name;		/* Section name (string tbl index) */
  Elf64_Word	sh_type;		/* Section type */
  Elf64_Xword	sh_flags;		/* Section flags */
  Elf64_Addr	sh_addr;		/* Section virtual addr at execution */
  Elf64_Off	sh_offset;		/* Section file offset */
  Elf64_Xword	sh_size;		/* Section size in bytes */
  Elf64_Word	sh_link;		/* Link to another section */
  Elf64_Word	sh_info;		/* Additional section information */
  Elf64_Xword	sh_addralign;		/* Section alignment */
  Elf64_Xword	sh_entsize;		/* Entry size if section holds table */
} Elf64_Shdr;
  • Address是这个段最终的虚拟地址,不被载入到内存的段这个地址就是0.
  • Flags即为下边表述的Key to Flags:W就是这个段可写,A就是会在内存中分配空间,X就是这个段是可执行的。
    比如说.text段中存放的是代码,flags是AX,可执行且需要被载入到内存中的。
  • Offset即为该段在elf文件的偏移。

然后我们再简单描述下几个相对来说比较简单且常用的段:

  • .text 存放的是代码,我们也会成为它是代码段,我们的可执行代码都放到这个段。
  • .data 存放的是初始化的全局变量和静态变量
  • .bss 存放的是未初始化的全局变量和静态变量

其他和我们比较相关的我们下边详细讲述下

程序头表

涉及到程序的装载,我们就需要用到程序头表。操作系统将程序装载进进程的地址空间中时,往往只需要关注段的权限(可读,可写,可执行等)。为了装载的方便,elf的做法是将相同权限的段合并为1个segment(程序头),那么描述这写segment的信息就是在程序头表中。

# readelf -l /bin/bash

Elf file type is DYN (Shared object file)
Entry point 0x2f630
There are 11 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000000040 0x0000000000000040
                 0x0000000000000268 0x0000000000000268  R      0x8
  INTERP         0x00000000000002a8 0x00000000000002a8 0x00000000000002a8
                 0x000000000000001c 0x000000000000001c  R      0x1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x000000000002cd98 0x000000000002cd98  R      0x1000
  LOAD           0x000000000002d000 0x000000000002d000 0x000000000002d000
                 0x00000000000ad78d 0x00000000000ad78d  R E    0x1000
  LOAD           0x00000000000db000 0x00000000000db000 0x00000000000db000
                 0x0000000000035730 0x0000000000035730  R      0x1000
  LOAD           0x00000000001113f0 0x00000000001123f0 0x00000000001123f0
                 0x000000000000b914 0x00000000000155a8  RW     0x1000
  DYNAMIC        0x0000000000113cf0 0x0000000000114cf0 0x0000000000114cf0
                 0x0000000000000200 0x0000000000000200  RW     0x8
...

 Section to Segment mapping:
  Segment Sections...
   00
   01     .interp
   02     .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt
   03     .init .plt .plt.got .text .fini
   04     .rodata .eh_frame_hdr .eh_frame
   05     .init_array .fini_array .data.rel.ro .dynamic .got .got.plt .data .bss
   06     .dynamic
...

也是省略了一些子项,同样也是各个字段看下:

typedef struct
{
  Elf64_Word	p_type;			/* Segment type */
  Elf64_Word	p_flags;		/* Segment flags */
  Elf64_Off	p_offset;		/* Segment file offset */
  Elf64_Addr	p_vaddr;		/* Segment virtual address */
  Elf64_Addr	p_paddr;		/* Segment physical address */
  Elf64_Xword	p_filesz;		/* Segment size in file */
  Elf64_Xword	p_memsz;		/* Segment size in memory */
  Elf64_Xword	p_align;		/* Segment alignment */
} Elf64_Phdr;

可以看到每个字段的含义,type其实我们只需要关注lOAD类型,表示这个segment会加载进内存。flags同样也表示权限相关的,R是可读,E是可执行,W是可写的。我们根据上边的输出可以看到每个segment包含哪些段以及每个segment的权限。

.dynamic

我们在动态加载和重定位的阶段会用到dynamic段,它主要也是包含一些动态加载和动态重定位的一些关键的段信息,很类似节头部表,是的你没看错:

# readelf -d /bin/bash

Dynamic section at offset 0x113cf0 contains 28 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libtinfo.so.6]
 0x0000000000000001 (NEEDED)             Shared library: [libdl.so.2]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
 0x000000000000000c (INIT)               0x2d000
 0x000000000000000d (FINI)               0xda784
 0x000000006ffffef5 (GNU_HASH)           0x308
 0x0000000000000005 (STRTAB)             0x131b0
 0x0000000000000006 (SYMTAB)             0x4db0
 0x000000000000000a (STRSZ)              38696 (bytes)
 0x000000000000000b (SYMENT)             24 (bytes)
 0x0000000000000015 (DEBUG)              0x0
 0x0000000000000003 (PLTGOT)             0x115000
 0x0000000000000014 (PLTREL)             RELA
 0x0000000000000017 (JMPREL)             0x2b928
 0x0000000000000007 (RELA)               0x1dca8
 0x0000000000000008 (RELASZ)             56448 (bytes)
 0x0000000000000009 (RELAENT)            24 (bytes)
 0x000000006ffffffb (FLAGS_1)            Flags: PIE
 0x000000006ffffff9 (RELACOUNT)          2338
 0x0000000000000000 (NULL)               0x0

该段以NULL子项结束,我们先来看下这个dynamic在代码中的结构:

typedef struct
{
  Elf64_Sxword	d_tag;			/* Dynamic entry type */
  union
    {
      Elf64_Xword d_val;		/* Integer value */
      Elf64_Addr d_ptr;			/* Address value */
    } d_un;
} Elf64_Dyn;

其实它只有两个字段,tag表示子项(entry)的类型,d_un要么是一个int值,要么是一个地址。

简单解读几个:

  • NEEDED:是该动态库依赖的其他动态库

大家可能会比较意外,不是存放的是数字吗,怎么能知道是哪个库,其实存放的是一个索引,这个索引是.dynstr段的索引,可以从这个段中提取出来字符串,.dynstr存放的全是字符串,.dynstr是动态链接下的字符串表,涉及到字符串的动态链接下的查找都可以在这个字符串中找到。除此之外还有静态字符串表.strtab,在静态链接中使用。

  • STRTAB:动态字符串表的位置
  • SYMTAB:动态链接符号表的位置
  • SYMENT:动态链接符号表每个子项的大小
  • RELA:动态重定位表的地址
  • RELASZ:动态重定位表的大小
    先简单讲述这么多,因为动态链接是发生在装载期间,所以为了方便维护及查找,会使用.dynamic来记录动态链接期间所使用到的一些段。我们也看到我描述每个段的时候前边都加了动态两个字,说明是发生在动态链接期间,同时也说明了其实静态链接期间有相同的一系列的段来描述这些信息,这里关于静态链接先不展开。

符号表

无论是静态链接还是动态链接符号都作为接口的存在,充当这粘合剂的角色。函数和变量统称为符号,函数名和变量名就是符号名。符号表则是记录符号信息的,我们还是先来看下符号表:

# readelf -s /bin/dd

Symbol table '.dynsym' contains 88 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __ctype_toupper_loc@GLIBC_2.3 (2)
     2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND getenv@GLIBC_2.2.5 (3)
     3: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND sigprocmask@GLIBC_2.2.5 (3)
     4: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __snprintf_chk@GLIBC_2.3.4 (4)
     ...
    77: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __ctype_b_loc@GLIBC_2.3 (2)
    78: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __sprintf_chk@GLIBC_2.3.4 (4)
    79: 0000000000013348     8 OBJECT  GLOBAL DEFAULT   26 stdout@GLIBC_2.2.5 (3)
    80: 0000000000000000     0 FUNC    WEAK   DEFAULT  UND __cxa_finalize@GLIBC_2.2.5 (3)
    81: 0000000000013340     8 OBJECT  GLOBAL DEFAULT   26 __progname@GLIBC_2.2.5 (3)
    82: 0000000000013358     8 OBJECT  WEAK   DEFAULT   26 program_invocation_name@GLIBC_2.2.5 (3)
    83: 0000000000013358     8 OBJECT  GLOBAL DEFAULT   26 __progname_full@GLIBC_2.2.5 (3)

readelf -s是展示符号表,readelf --dyn-syms是展示动态符号表,符号表是包含了该文件的所有符号,动态符号表仅仅包含动态链接中会使用的符号。符号表包含动态符号表。不一定所有的文件都有两个段的。
那么这个段到底记录了什么信息呢,看下各个字段的描述

typedef struct
{
  Elf64_Word	st_name;		/* Symbol name (string tbl index) */
  unsigned char	st_info;		/* Symbol type and binding */
  unsigned char st_other;		/* Symbol visibility */
  Elf64_Section	st_shndx;		/* Section index */
  Elf64_Addr	st_value;		/* Symbol value */
  Elf64_Xword	st_size;		/* Symbol size */
} Elf64_Sym;

vis段暂且忽略

  • name就是这个符号的名字,在结构中存储的是字符串表的索引,我们上边将dynamic段的时候说到过
  • bind信息是通过info计算得出,包含LOCAL,GLOBAL,WEAK三种,LOCAL就是局部符号,对外部文件不可见。GLOBAL就是全局符号,WEAK弱引用。
  • type信息也是通过info计算得出,分为NOTYPE ,OBJECT, FUNC, SECTION, FILE。OBJECT就是指变量,FUNC就是函数,SECTION就是段,FILE就是文件(所以此来看符号表不仅仅包含变量和函数)。
  • value就是符号的值,如果该符号是变量或者函数,value就是符号的地址。
  • Ndx即为shndex表示这个符号所在的段的索引。另外如果是UND就表示这个符号未找到,也就是不在本文件,应该是定义在其他文件。

总结下,符号表表示的各个符号的类型信息以及这个符号所在的位置

重定位表

链接器链接的过程,需要对文件进行重定位,静态链接针对目标文件,动态链接针对加载到内存的文件。主要是针对代码段或者数据段中引用了一些符号的地址进行重新定位,举例来说,比如目标文件a引用了b文件的符号,但是在链接之前还是不知道地址的,链接时用真实的地址来对之前的地址进行修正。那么重定位表中记录这些信息。

# readelf -r /bin/dd

Relocation section '.rela.dyn' at offset 0x10a8 contains 32 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000012d10  000000000008 R_X86_64_RELATIVE                    4410
000000012d18  000000000008 R_X86_64_RELATIVE                    43d0
...
000000012ff0  004800000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_registerTMCloneTa + 0
000000012ff8  005000000006 R_X86_64_GLOB_DAT 0000000000000000 __cxa_finalize@GLIBC_2.2.5 + 0
000000013340  005100000005 R_X86_64_COPY     0000000000013340 __progname@GLIBC_2.2.5 + 0
000000013348  004f00000005 R_X86_64_COPY     0000000000013348 stdout@GLIBC_2.2.5 + 0

Relocation section '.rela.plt' at offset 0x13a8 contains 74 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000013018  000100000007 R_X86_64_JUMP_SLO 0000000000000000 __ctype_toupper_loc@GLIBC_2.3 + 0
000000013020  000200000007 R_X86_64_JUMP_SLO 0000000000000000 getenv@GLIBC_2.2.5
000000013028  000300000007 R_X86_64_JUMP_SLO 0000000000000000 sigprocmask@GLIBC_2.2.5
000000013030  000400000007 R_X86_64_JUMP_SLO 0000000000000000 __snprintf_chk@GLIBC_2.3.4

我们使用readelf -r命令来查询重定位表,因为我们查询的是可执行文件,所以展示的是动态可重定位表,也看出来了有.rela.dyn和.rela.plt段。.rela.dyn 是对数据引用修正,修正的段是代码段和.got段。.rela.plt是对函数引用的修正,修正的段是got.plt。关于got和plt的相关概念下一章节讲述。
然后来看下各个字段:

typedef struct
{
  Elf64_Addr	r_offset;		/* Address */
  Elf64_Xword	r_info;			/* Relocation type and symbol index */
  Elf64_Sxword	r_addend;		/* Addend */
} Elf64_Rela;

这个结构体也太过简单了,我们来看下各个字段的含义吧。

  • Offset在动态链接中表示的是引用该数据的虚拟地址,静态链接表示引用所在段的偏移。
  • info可以计算出重定位类型和该符号引用在符号表的索引。
  • type是比较关键的,表示的是重定位类型,该类型可以就可以表明如何来计算符号引用所在的最终地址。
  • r_addend是一个显式的附加值,这里我们也先不展开
    因为可以通过info来计算出来符号表的位置,这里自然也展示了符号的值和符号的名字

通过这里我们就可以知道比较关键的信息是重定位表记录的是各个符号在哪里引用的。符号表表示的是符号具体的位置,这一点要做一下区分。

动态加载和链接

本文主要是讲述动态链接的过程,所以关于静态链接就简单提到。动态链接是发生在可执行文件和库的加载过程中,由动态链接器完成。(接上一篇)

地址无关和延迟绑定

我们讲述的动态链接是比较常见的编译模式-采用地址无关的形式,也即使用编译命令-fPIC(Position-independent-code<地址无关代码>)。由于加载进来的库可以给其他进程共享(节省内存),且这个库在不同的进程地址空间的虚拟地址是不同的,那么这个库中引用别处的符号地址就需要是相同的,不然各个进程地址不同,库就不可以共享。由于此原因,elf采用了地址无关的方案供使用。

地址无关代码主要是针对于模块间的数据访问和函数调用,模块内的数据或者函数可以使用相对地址来访问到,这样也是地址无关的。那么模块间的数据或者函数调用,使用GOT这个结构来实现。GOT即为全局偏移表,GOT存放了使用的每个数据或者函数的最终地址,GOT可以理解成是一个以4字节(32位下)为一个子项的数组。GOT不会被多个进程共享,每个进程都有一份GOT。这样当代码中想要使用变量或者函数调用,会先找到GOT中,进而再从GOT中找到相应的函数或者变量进行访问。代码找GOT这个操作是相对寻址,所以可以被共享。GOT是在程序的装载时被修改

当开启了PIC时,默认就有延迟绑定的功能。也就是说在函数第一次调用的时候才进行绑定,所谓的绑定就是符号查找,重定位GOT或者数据段等。而不是加载的时候就一股脑的将所有的符号全部重定位完成。elf是通过PLT(Procedure Linkage Table)的方法来实现的。简单的原理就是会有一个plt的段,当进行模块间的函数调用时,代码段中的调用都是先到plt段中,plt中会继续调用dl_runtime_resolve函数进行符号的解析和重定位进一步到got中的地址,当函数第二次调用到plt段中就能直接找到相应got中的地址实现跳转。

来看下和PLT及GOT相关的段有:

PLT相关:plt,plt.got, plt.sec (存放代码)

GOT相关:got,got.plt (存放地址)

  • got.段存放的是全局变量的地址,也可以存放不需要延迟绑定函数地址。每个子项4字节(32位)或者8字节(64位)
  • got.plt段前三个子项分别是.dynamic段的地址,本模块的ID,_dl_runtime_resolve的地址,然后就是存放的是用于延迟绑定的函数的地址
  • plt存放的第一个子项存放的指令的大意是:push 本模块ID然后跳转到_dl_runtime_resolve去调用。且规定每个子项的大小是16字节。之后的子项就是各个延迟绑定函数实现。
  • plt.got中存放的是__cxa_finalize 函数对应的 PLT 条目
  • plt.sec这个段有的elf有,有的是没有的。查了下资料,是因为引入了endbr64指令,该指令占用4字节,在原来的plt中规定16字节一个子项就放不下了,那么plt.sec段中的指令其实仅仅是跳转到相应的plt地址。这么来说就又多了一步:call addr -> plt.sec -> plt -> got.plt 这个流程。如果没有plt.sec那么就直接跳转到plt这个段中。

我们通过一个例子来完整的描述下地址无关下延迟绑定的函数调用流程:

# cat hello.c
#include <stdio.h>
int main() {
  printf("hello: %d\n", 111);
  return 0;
}

使用gcc -fPic hello.c -o hello编译后,来看下hello的各个段:

# readelf -S hello
There are 30 section headers, starting at offset 0x3960:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [12] .plt              PROGBITS         0000000000001020  00001020
       0000000000000020  0000000000000010  AX       0     0     16
  [13] .plt.got          PROGBITS         0000000000001040  00001040
       0000000000000008  0000000000000008  AX       0     0     8
  [14] .text             PROGBITS         0000000000001050  00001050
       0000000000000171  0000000000000000  AX       0     0     16
  [21] .dynamic          DYNAMIC          0000000000003df8  00002df8
       00000000000001e0  0000000000000010  WA       6     0     8
  [22] .got              PROGBITS         0000000000003fd8  00002fd8
       0000000000000028  0000000000000008  WA       0     0     8
  [23] .got.plt          PROGBITS         0000000000004000  00003000
       0000000000000020  0000000000000008  WA       0     0     8

使用objdump来看反汇编代码,-d就是查看反汇编,–section来指定看哪一个段
# objdump -d --section=text hello

0000000000001135 <main>:
    1135:	55                   	push   %rbp
    1136:	48 89 e5             	mov    %rsp,%rbp
    1139:	be 6f 00 00 00       	mov    $0x6f,%esi
    113e:	48 8d 3d bf 0e 00 00 	lea    0xebf(%rip),%rdi        # 2004 <_IO_stdin_used+0x4>
    1145:	b8 00 00 00 00       	mov    $0x0,%eax
    114a:	e8 e1 fe ff ff       	callq  1030 <printf@plt>
    114f:	b8 00 00 00 00       	mov    $0x0,%eax
    1154:	5d                   	pop    %rbp
    1155:	c3                   	retq
    1156:	66 2e 0f 1f 84 00 00 	nopw   %cs:0x0(%rax,%rax,1)
    115d:	00 00 00

我们看main函数的callq 1030 <printf@plt>这里,那么我们就去plt段中去找:
# objdump -d --section=.plt hello

hello:     file format elf64-x86-64
Disassembly of section .plt:
0000000000001020 <.plt>:
    1020:	ff 35 e2 2f 00 00    	pushq  0x2fe2(%rip)        # 4008 <_GLOBAL_OFFSET_TABLE_+0x8>
    1026:	ff 25 e4 2f 00 00    	jmpq   *0x2fe4(%rip)        # 4010 <_GLOBAL_OFFSET_TABLE_+0x10>
    102c:	0f 1f 40 00          	nopl   0x0(%rax)

0000000000001030 <printf@plt>:
    1030:	ff 25 e2 2f 00 00    	jmpq   *0x2fe2(%rip)        # 4018 <printf@GLIBC_2.2.5>
    1036:	68 00 00 00 00       	pushq  $0x0
    103b:	e9 e0 ff ff ff       	jmpq   1020 <.plt>

这里也能看到plt中的第一个子项是不是和我们前边说的一致。然后我们看printf@plt的实现:

首先跳转到4018这个位置中存放的地址处,继续看下4018中是啥, 根据上边的段表4018是在.got.plt 中,我们使用 readelf -x 23 hello 查看段表索引是23(.got.plt)的内容:

Hex dump of section '.got.plt':
 NOTE: This section has relocations against it, but these have NOT been applied to this dump.
  0x00004000 f83d0000 00000000 00000000 00000000 .=..............
  0x00004010 00000000 00000000 36100000 00000000 ........6.......

4018处的是1036,那么我们看到1036正好位于printf@plt的第二条指令,继续在plt中执行,pushq $0x0就是printf 的重定位表中索引,然后调转到PLT0,继续push模块ID,然后就是去调用dl_runtime_resolve函数,该函数就会根据重定位表针对.got.plt中进行修复,然后到实际的printf函数执行。

关于dl_runtime_resolve函数调用和重定位我们下一节加载和链接会讲到。

加载流程

当运行可执行文件时,操作系统会将可执行文件加载到内存中,首先判断是否有.interp段,如果有.interp段,这个段中存放的是动态链接器路径,操作系统然后就会加载动态链接器,并跳转到动态链接器的start处(在最开始中讲到readelf -h文件头中可以知道代码的开始是在那个位置)。在这之前,操作系统会准备调用栈的环境,除了参数和环境变量外,操作系统还准备了辅助信息数组(Auxiliary Vector),辅助数组中主要存放的是可执行文件的一些信息,包含程序头表,起始地址等等信息。

数据结构是:

typedef struct
{
  uint64_t a_type;		/* Entry type */
  union
    {
      uint64_t a_val;		/* Integer value */
      /* We use to have pointer elements added here.  We cannot do that,
	 though, since it does not work when using 32-bit definitions
	 on 64-bit platforms and vice versa.  */
    } a_un;
} Elf64_auxv_t;

type类型我们截取部分来看:

#define AT_NULL		0		/* End of vector */
#define AT_IGNORE	1		/* Entry should be ignored */
#define AT_EXECFD	2		/* File descriptor of program */
#define AT_PHDR		3		/* Program headers for program */
#define AT_PHENT	4		/* Size of program header entry */
#define AT_PHNUM	5		/* Number of program headers */
#define AT_PAGESZ	6		/* System page size */
#define AT_BASE		7		/* Base address of interpreter */
#define AT_FLAGS	8		/* Flags */
#define AT_ENTRY	9		/* Entry point of program */
...

然后就是到动态链接器中开始执行了
根据动态链接器的elf文件中的程序入口,我们找到首先是一段汇编代码,这段汇编代码调用到_dl_start函数

static ElfW(Addr) __attribute_used__
_dl_start (void *arg)
{
	//...
  	/* Figure out the run-time load address of the dynamic linker itself.  */
  	bootstrap_map.l_addr = elf_machine_load_address ();

  	/* Read our own dynamic section and fill in the info array.  */
  	bootstrap_map.l_ld = (void *) bootstrap_map.l_addr + elf_machine_dynamic ();
    elf_get_dynamic_info (&bootstrap_map, NULL);
	if (bootstrap_map.l_addr || ! bootstrap_map.l_info[VALIDX(DT_GNU_PRELINKED)])
    {
      /* Relocate ourselves so we can do normal function calls and
	 data access using the global offset table.  */

      ELF_DYNAMIC_RELOCATE (&bootstrap_map, 0, 0, 0);
    }
  	bootstrap_map.l_relocated = 1;

#ifdef DONT_USE_BOOTSTRAP_MAP
    ElfW(Addr) entry = _dl_start_final (arg);
#else
    ElfW(Addr) entry = _dl_start_final (arg, &info);
#endif
}

大家也不需要全部看懂,代码在glibc/elf/rtld.c中,简单看下流程就可以了。

首先动态链接器计算自己运行时的内存位置,根据内存位置来计算得到.dynamic段的地址,并且读取.dynamic段并将数据填充到上边的info数组。然后就是重定位自己了。我们知道动态链接器首先也是一个共享对象,那么他的重定位是谁来做呢,就是自己对自己做重定位,也被称为自举,除此之外动态链接器并不依赖其他的系统库,自然在自举的过程就无需涉及到其他库的加载和引用。

最后会调用_dl_start_final函数:

static ElfW(Addr) __attribute__ ((noinline))
_dl_start_final()
{
	// ...
	start_addr = _dl_sysdep_start (arg, &dl_main);
	return start_addr;
}

_dl_start_final函数会调用_dl_sysdep_start函数,_dl_sysdep_start这个函数就会去解析上边说到辅助数组,然后传递给dl_main这个函数并调用,dl_main函数比较大,我们分段来解释:

static void
dl_main (const ElfW(Phdr) *phdr,
	 ElfW(Word) phnum,
	 ElfW(Addr) *user_entry,
	 ElfW(auxv_t) *auxv)
{
	// ...
}

先来看参数,phdr就是可执行文件的程序头表的地址,phnum就是可执行文件文件的程序头表中子项个数,user_entry是可执行文件入口地址,以上参数都是从辅助数组中解析出来,最后一个参数是辅助数组的指针。

static void dl_main(...) {
	// ...
	if (*user_entry == (ElfW(Addr)) ENTRY_POINT) {
		// ...
	} else {
		// ...
	}
}

这里是动态连接器可以独自当作可执行文件直接运行,这里是判断是运行自己还是去动态链接别人。我们下边也直接看else中的代码:

static void dl_main(...) {
	// ...
	if (*user_entry == (ElfW(Addr)) ENTRY_POINT) {} 
	else {
		
	}
}

然后动态链接器就会去解析传进来的可执行文件的程序头表:

static void dl_main(...) {
	// ...
	if (*user_entry == (ElfW(Addr)) ENTRY_POINT) {} 
	else {
		/* Create a link_map for the executable itself.
	 	This will be what dlopen on "" returns.  */
      main_map = _dl_new_object ((char *) "", "", lt_executable, NULL,
				 __RTLD_OPENEXEC, LM_ID_BASE);
      assert (main_map != NULL);
      main_map->l_phdr = phdr;
      main_map->l_phnum = phnum;
      main_map->l_entry = *user_entry;
	}

	//...
	for (ph = phdr; ph < &phdr[phnum]; ++ph)
    switch (ph->p_type)
      {
      case PT_PHDR:
      	main_map->l_addr = (ElfW(Addr)) phdr - ph->p_vaddr;
      	break;
      case PT_DYNAMIC:
		/* This tells us where to find the dynamic section,
		   which tells us everything we need to do.  */
		main_map->l_ld = (void *) main_map->l_addr + ph->p_vaddr;
		break;
      case PT_INTERP:
		// ...
	  case PT_LOAD:
	{
	  mapstart = (main_map->l_addr
		      + (ph->p_vaddr & ~(GLRO(dl_pagesize) - 1)));
	  if (main_map->l_map_start > mapstart)
	    main_map->l_map_start = mapstart;

	  /* Also where it ends.  */
	  allocend = main_map->l_addr + ph->p_vaddr + ph->p_memsz;
	  if (main_map->l_map_end < allocend)
	    main_map->l_map_end = allocend;
	  if ((ph->p_flags & PF_X) && allocend > main_map->l_text_end)
	    main_map->l_text_end = allocend;
	}
	break;

	// ...
}

简单顺一下思路,动态连接器读取可执行文件的程序头表将相应数据赋予main_map这个数据结构。

static void dl_main(...) {
	// ...
	if (! rtld_is_main)
    {
      /* Extract the contents of the dynamic section for easy access.  */
      elf_get_dynamic_info (main_map, NULL);
      /* Set up our cache of pointers into the hash table.  */
      _dl_setup_hash (main_map);
    }

	// ...
}

然后解析可执行文件的.dynamic段,相应的数据也还是填充在main_map中。

static void dl_main(...) {
	// ...
	/* Initialize the data structures for the search paths for shared
    objects.  */
 	_dl_init_paths (library_path);

	//... 
	/* We have two ways to specify objects to preload: via environment
     variable and via the file /etc/ld.so.preload.  The latter can also
     be used when security is enabled.  */
	npreloads += handle_preload_list (preloadlist, main_map, "LD_PRELOAD");
	npreloads += handle_preload_list (preloadarg, main_map, "--preload");
	// ...

	/* Load all the libraries specified by DT_NEEDED entries.  If LD_PRELOAD
     specified some libraries to load, these are inserted before the actual
     dependencies in the executable's searchlist for symbol resolution.  */
  	{
    RTLD_TIMING_VAR (start);
    rtld_timer_start (&start);
    _dl_map_object_deps (main_map, preloads, npreloads, mode == trace, 0);
    rtld_timer_accum (&load_time, start);
  	}

}

再接下来是设定库的搜索路径,加载preload的库,然后去加载所需要的全部的动态库。

static void dl_main(...) {
	// ...

	if (l != &GL(dl_rtld_map))
		_dl_relocate_object (l, l->l_scope, GLRO(dl_lazy) ? RTLD_LAZY : 0,
				 consider_profiling);

	 /* Add object to slot information data if necessasy.  */
	 if (l->l_tls_blocksize != 0 && tls_init_tp_called)
	    _dl_add_to_slotinfo (l, true);
}

然后就是比较关键的重定位操作了。

动态重定位

根据加载进来的所有的库的符号表可以定位到每一个符号的位置,再根据重定位表找到哪里使用到某个符号,重定位的方式。以此来修改.got(数据段)和.got.plt(函数引用)。

需要关注的是,在动态链接器进行重定位时,只是重定位那些不是延迟加载的符号,延迟加载的符号的重定位要到_dl_runtime_resolve函数的调用。

总结

本文到这里结束了,主要讲述了elf文件的文件结构,已经其中各个段的用处。然后就继续讲述关于动态链接的一些情况,希望大家有所收获,篇幅较长,感谢看完。

ref

  • https://refspecs.linuxfoundation.org/ELF/zSeries/lzsabi0_zSeries/x2251.html#PROCEDURELINKAGETABLE
  • 《程序员的自我修养-链接、装载与库》
  • https://zhuanlan.zhihu.com/p/544058988
  • https://www.zhihu.com/question/21249496
  • https://blog.csdn.net/welljrj/article/details/90346108
  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Linux链接动态库是指在程序运行时动态加载库文件,而不是在编译时将库文件静态链接到可执行文件中。动态库可以被多个程序共享,减少了内存的占用,提高了程序的运行效率。在Linux系统中,动态库的文件名通常以.so结尾。程序在运行时需要动态库时,会在系统中查找相应的库文件并加载。动态库的使用可以提高程序的可维护性和可扩展性,是Linux系统中常用的一种技术。 ### 回答2: 在Linux系统中,链接动态库是将可执行文件与动态库文件(.so文件)关联起来的过程。动态库是包含一组已编译函数的文件,可以被多个程序共享使用,以减少程序的体积和提高运行效率。 在Linux下,链接动态库有两种方式:隐式链接和显式链接。 隐式链接是在编译源代码时,在编译命令中添加-l选项,指定要链接的动态库名称,编译器会自动在系统库目录中查找并链接对应的动态库。例如,编译命令为: gcc -o test test.c -lm 其中,-lm表示要链接数学库libm.so。隐式链接的优点是方便,无需手动指定动态库的路径,编译器会自动搜索。 显式链接是在程序运行时,通过调用特定的函数来链接动态库。首先,使用dlopen函数打开动态库文件,获取一个句柄;然后,使用dlsym函数根据函数名获取动态库中的符号;最后,使用dlclose函数关闭句柄。这种方式更加灵活,可以在程序运行时动态地加载和卸载动态库,并使用其中的函数。 对于动态库的路径问题,可以使用-L选项指定库文件的搜索路径。例如,编译命令为: gcc -o test test.c -L/path/to/library -lmylib 其中,-L/path/to/library表示动态库所在的路径,-lmylib表示要链接的动态库名称。如果动态库在默认的系统库目录中,可以省略-L选项。 总结一下,Linux链接动态库的方式有隐式链接和显式链接两种,根据需求选择不同的方式。隐式链接通过在编译命令中添加-l选项,自动链接系统库目录中的动态库;显式链接使用dlopen、dlsym和dlclose函数,在程序运行时动态加载和卸载动态库。 ### 回答3: 在Linux中,链接动态库是将程序运行所需的动态库文件与程序本身进行关联的过程。在Linux中,动态库文件通常以.so为后缀名。 要链接一个动态库,首先需要使用编译器的选项来指定动态库的路径和名称。一般来说,可以使用-l选项来指定要链接的库文件,例如使用-lm来链接数学库。同时,还要使用-L选项来指定库文件的搜索路径,以确保编译器能够找到库文件。 例如,假设有一个包含以下代码的程序文件main.c: ```c #include <stdio.h> int main() { printf("Hello, World!\n"); return 0; } ``` 如果想要链接数学库libm.so,可以在命令行中执行以下命令进行编译和链接: ``` gcc main.c -o main -lm ``` 这条命令中,gcc是编译器,main.c是源代码文件,-o main指定生成可执行文件的名称为main,-lm指定链接数学库libm.so。 在链接完成后,可以使用ldd命令来查看程序所依赖的动态库,例如执行以下命令: ``` ldd main ``` ldd命令的输出将显示出程序所依赖的动态库文件的路径。 总结来说,链接动态库的过程就是在编译和链接时指定库文件的路径和名称,确保编译器能够找到所需的动态库文件。然后,在运行时,程序会在指定的库文件路径中查找并加载所需的动态库。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值