ELF

Table of Contents

1. ELF

1.1. ELF Format

ELF 格式的主要结构为:

  1. ELF Header (ehdr)
  2. Program Header (phdr)
  3. segments / sections (content)
  4. Section Header (shdr)

elf 文件在磁盘上的布局为:


+-------------------------------+
| ELF File Header               |
+-------------------------------+
| Program Header for segment #1 |
+-------------------------------+
| Program Header for segment #2 |
+-------------------------------+
| ...                           |
+-------------------------------+
| Contents (Byte Stream)        |
| ...                           |
+-------------------------------+
| Section Header for section #1 |
+-------------------------------+
| Section Header for section #2 |
+-------------------------------+
| ...                           |
+-------------------------------+
| ".shstrtab" section           |
+-------------------------------+
| ".symtab"   section           |
+-------------------------------+
| ".strtab"   section           |
+-------------------------------+

elf 同时需要为链接 (static linker) 和加载 (kernel 以及 runtime/dynamic linker) 服务, 而两者关注的内容并不相同:

  1. 链接过程关注 section header 和 section
  2. 加载过程关注 segment (prgram) header 和 segment, 这些内容需要加载到内存

section 和 segment 实际上对应的都是同一块数据:

             +-----------------+
        +----| elf file header |----+
        |    +-----------------+    |
        v                           v
+-----------------+      +-----------------+
| program headers |      | section headers |
+-----------------+      +-----------------+
     ||                               ||
     ||                               ||
     ||                               ||
     ||   +------------------------+  ||
     +--> | data (segction/segment)|<--+
          +------------------------+

1.2. ELF Section

1.2.1. overview

section 的格式:

typedef struct elf32_shdr {
  Elf32_Word sh_name;           /* 相对于 .shstrtab 的 offset */
  Elf32_Word sh_type;
  Elf32_Word sh_flags;
  Elf32_Addr sh_addr;           /* memory address */
  Elf32_Off sh_offset;          /* file offset */
  Elf32_Word sh_size;
  Elf32_Word sh_link;
  Elf32_Word sh_info;
  Elf32_Word sh_addralign;
  Elf32_Word sh_entsize;
} Elf32_Shdr;

1.2.2. init/finit

1.2.2.1. init

init section 保存的是 init 时要执行的函数 (而不是函数指针), 但现在看来 init section 应该慎重使用, 各种平台和编译器 (linker?) 对它的支持都不太相同. 例如:

  1. glibc 定义了自己的 .init section, 代码再修改 .init 的话会有 segmentation fault.
  2. arm 没有定义 .init section, 代码可以自由修改, 但 dynamic 里并没有 DT_INIT 项, 所以 .init 不会被调用
  3. arm 通过 -Wl,-init 可以把 DT_INIT 指向 init 函数, 但它并没有把 init 函数放在 .init section 中
1.2.2.1.1. sample code
#include <stdio.h>

__attribute__((section (".init"))) void test() {
    printf("hello world!\n");
}

int main(int argc, char *argv[]) {
    printf("main: %p\n", &main);
    return 0;
}

1.2.2.1.2. arm_linux_androideabi-gcc
$> ./a.out
main: 0xb662e3b4

$> arm-linux-androideabi-readelf -a ./a.out

Dynamic section at offset 0xec8 contains 28 entries:
  Tag        Type                         Name/Value
 0x00000003 (PLTGOT)                     0x1fe4
 0x00000002 (PLTRELSZ)                   32 (bytes)
 0x00000017 (JMPREL)                     0x340
 0x00000014 (PLTREL)                     REL
 0x00000011 (REL)                        0x318
 0x00000012 (RELSZ)                      40 (bytes)
 0x00000013 (RELENT)                     8 (bytes)
 0x6ffffffa (RELCOUNT)                   5
 0x00000015 (DEBUG)                      0x0
 0x00000006 (SYMTAB)                     0x1e0
 0x0000000b (SYMENT)                     16 (bytes)
 0x00000005 (STRTAB)                     0x260
 0x0000000a (STRSZ)                      84 (bytes)
 0x00000004 (HASH)                       0x2b4
 0x00000001 (NEEDED)                     Shared library: [libc.so]
 0x00000001 (NEEDED)                     Shared library: [libdl.so]
 0x0000001a (FINI_ARRAY)                 0x1ea8
 0x0000001c (FINI_ARRAYSZ)               8 (bytes)
 0x00000019 (INIT_ARRAY)                 0x1eb0
 0x0000001b (INIT_ARRAYSZ)               16 (bytes)
 0x00000020 (PREINIT_ARRAY)              0x1ec0
 0x00000021 (PREINIT_ARRAYSZ)            0x8
 0x0000001e (FLAGS)                      BIND_NOW
 0x6ffffffb (FLAGS_1)                    Flags: NOW
 0x6ffffff0 (VERSYM)                     0x2e8
 0x6ffffffe (VERNEED)                    0x2f8
 0x6fffffff (VERNEEDNUM)                 1
 0x00000000 (NULL)                       0x0

// 并不包含 DT_INIT, 所以 .init section 中的函数不会被 loader 调用
1.2.2.1.3. gcc on x86_64

gcc on x86_64 本身已经使用了 .init section 来做某种初始化动作, 应用自己再设置 .init 会有问题

$> ./a.out
hello world!
Segmentation fault (core dumped)

$> objdump -D ./a.out
Disassembly of section .init:

0000000000400400 <_init>:
  400400:	48 83 ec 08          	sub    $0x8,%rsp
  400404:	48 8b 05 ed 0b 20 00 	mov    0x200bed(%rip),%rax        # 600ff8 <__gmon_start__>
  40040b:	48 85 c0             	test   %rax,%rax
  40040e:	74 02                	je     400412 <test>
  400410:	ff d0                	callq  *%rax

0000000000400412 <test>:
  400412:	55                   	push   %rbp
  400413:	48 89 e5             	mov    %rsp,%rbp
  400416:	bf f4 05 40 00       	mov    $0x4005f4,%edi
  40041b:	e8 20 00 00 00       	callq  400440 <puts@plt>
  400420:	90                   	nop
  400421:	5d                   	pop    %rbp
  400422:	c3                   	retq
  400423:	48 83 c4 08          	add    $0x8,%rsp
  400427:	c3                   	retq


1.2.2.2. finit
1.2.2.3. init_array

init_array section 中保存的是函数指针.

1.2.2.3.1. 使用 init_array
#include <stdio.h>

void test() {
    printf("hello world!\n");
}

void (*x) () __attribute__((section (".init_array"))) = test;
void (*y) () __attribute__((section (".init_array"))) = test;

int main(int argc, char *argv[]) {
    printf("main: %p, %p\n", &main, &test);
    return 0;
}
$> arm-linux-androideabi-gcc test.c -fPIE -pie -O3 -g3
$> ./a.out
hello world!
hello world!
main: 0xb42933b4, 0xb4293490
$> arm-linux-androideabi-objdump -D a.out

Disassembly of section .init_array:

00001ea8 <__INIT_ARRAY__>:
    1ea8:	ffffffff 			; <UNDEFINED> instruction: 0xffffffff

00001eac <y>:
    1eac:	00000490 	muleq	r0, r0, r4

00001eb0 <x>:
    1eb0:	00000490 	muleq	r0, r0, r4
    1eb4:	00000000 	andeq	r0, r0, r0


...

00000490 <test>:
 490:	e59f0004 	ldr	r0, [pc, #4]	; 49c <test+0xc>
 494:	e08f0000 	add	r0, pc, r0
 498:	eaffffbf 	b	39c <puts@plt>
 49c:	00000004 	andeq	r0, r0, r4

另外, 使用 PIE 后 init_array 中的数据 (函数指针) 需要被 linker 重定位, init_array 所处的 PT_LOAD segment 与 got, data, bss 等相同, 是可写的.

1.2.2.3.2. constructor

gcc 的 constructor attribute 是通过 init_array 实现的

#include <stdio.h>

__attribute__((constructor)) void test() {
    printf("hello world!\n");
}

__attribute__((constructor)) void test2 () {
    printf("hello world2!\n");
}

int main(int argc, char *argv[]) {
    printf("main: %p, %p\n", &main, &test);
    return 0;
}
$> ./a.out
hello world!
hello world2!
main: 0xba82c3c4, 0xba82c3b4

$> arm-linux-androideabi-objdump -D ./a.out
Disassembly of section .init_array:

00001ea8 <__INIT_ARRAY__>:
    1ea8:	ffffffff 			; <UNDEFINED> instruction: 0xffffffff
    1eac:	000003b4 			; <UNDEFINED> instruction: 0x000003b4
    1eb0:	000003f8 	strdeq	r0, [r0], -r8
    1eb4:	00000000 	andeq	r0, r0, r0

000003f8 <test2>:
 3f8:	e59f0004 	ldr	r0, [pc, #4]	; 404 <test2+0xc>
 3fc:	e08f0000 	add	r0, pc, r0
 400:	eaffffe5 	b	39c <puts@plt>
 404:	000000cc 	andeq	r0, r0, ip, asr #1
1.2.2.3.3. init_array 如何被调用
  1. so 中的 init_array 需要依赖 runtime linker linker_so.call_constructors
  2. 主程序的 init_array 由 startfiles (crt0.o) 提供的 _start 去调用, 所以使用了 nostartfiles 编译的程序无法通过 init_array 完成 constructor 的动作 crt0.o

    #0  0x000055555555520c in A::A() ()
    #1  0x00005555555551d8 in __static_initialization_and_destruction_0(int, int) ()
    #2  0x000055555555520a in _GLOBAL__sub_I_a ()
    #3  0x000055555555527d in __libc_csu_init ()
    #4  0x00007ffff7dcf040 in __libc_start_main (main=0x555555555180 <main>, argc=1, argv=0x7fffffffc868, init=0x555555555230 <__libc_csu_init>, fini=<optimized out>, rtld_fini=<optimized out>,
        stack_end=0x7fffffffc858) at ../csu/libc-start.c:264
    #5  0x000055555555508e in _start ()
    (gdb)
    

    __libc_csu_init 的实现与下面的代码基本相同的:

    void call_init_array() {
        typedef void (*FP)();
        extern FP __init_array_start;
        extern FP __init_array_end;
    
        for (FP *p = &__init_array_start; p < &__init_array_end; p++) {
            (*p)();
        }
    }
    

    即它会依赖 __init_array_start 和 __init_array_end 符号, 如果在 linker script 中修改了这两个符号, 就可以影响 init_array 的执行

Backlinks

crt0.o (Bare Metal > crt > crt0.o): 1. 初始化 GP 2. 清空 BSS 3. 通过 atexit 注册 _libc_fini_array, 以便在程序结束时调用 finit_array 4. 通过 _libc_init_array 以调用 init_array 5. 调用 main 6. exit

1.2.2.4. finit_array
Backlinks

crt0.o (Bare Metal > crt > crt0.o): 1. 初始化 GP 2. 清空 BSS 3. 通过 atexit 注册 _libc_fini_array, 以便在程序结束时调用 finit_array 4. 通过 _libc_init_array 以调用 init_array 5. 调用 main 6. exit

1.2.3. text

1.2.4. data

1.2.5. rodata

1.2.6. bss

1.2.7. static linker 相关

1.2.7.1. symtab

symtab 与 dynsym 都是 symbol table, 格式相同:

struct Elf32_Sym {
  Elf32_Word    st_name;  // Symbol name (index into string table)
  Elf32_Addr    st_value; // Value or address associated with the symbol
  Elf32_Word    st_size;  // Size of the symbol
  unsigned char st_info;  // Symbol's type and binding attributes
  unsigned char st_other; // Must be zero; reserved
  Elf32_Half    st_shndx; // Which section (header table index) it's defined in
};

elf 中和 symbol 相关的有四个 section:

  1. dynsym
  2. dynstr
  3. symtab
  4. strtab

其中 dynsym 和 dynstr 在运行阶段给 loader 使用, symtab 和 strtab 是链接阶段给 linker 使用的, 并且包含一些 debug 信息给 debugger 使用.

symtab 中引用的名字位于 strtab 中, 而 dynsym 中引用的名字位于 dynstr 中.

strip 命令默认会删除掉 symtab 和 strtab 以及 dwarf 相关的 section. 但不会影响 dynsym 和 dynstr, 所以:

  1. 被 strip 了的 so 不影响它的 dynamic linking, 也不影响 gdb 看到它定义的 global symbol, 因为它们都定义在 dynsym 中.
  2. 被 strip 了的 relocatable file 无法被 linker 使用, 例如:

    $gcc -c test.c
    $strip test.o
    $gcc main.c test.o
    main.c:(.text+0x15): undefined reference to `foo'
    
  3. 被 strip 了的 executable file 通过 gdb 无法看到那些非 so 中定义的 symbol, 例如:

    $gcc -shared -fPIC -o libtest.so
    $gcc main.c -L. -ltest
    $strip /a.out
    $gdb /a.out
    (gdb) b foo
    Breakpoint 1 at 0x400500
    (gdb) b main
    Function "main" not defined.
    

dynsym 中既包含自己定义的符号, 也包括自己引用的其他 so 中的符号, 例如:

Symbol table '.dynsym' contains 13 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 00000000000004c8     0 SECTION LOCAL  DEFAULT    9
     2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND puts@GLIBC_2.2.5 (2)
     3: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
     4: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _Jv_RegisterClasses
     5: 0000000000000000     0 FUNC    WEAK   DEFAULT  UND __cxa_finalize@GLIBC_2.2.5 (2)
     6: 000000000020101c     0 NOTYPE  GLOBAL DEFAULT  ABS _edata
     7: 0000000000201018     4 OBJECT  GLOBAL DEFAULT   22 x
     8: 00000000000005dc    31 FUNC    GLOBAL DEFAULT   11 foo
     9: 0000000000201030     0 NOTYPE  GLOBAL DEFAULT  ABS _end
    10: 000000000020101c     0 NOTYPE  GLOBAL DEFAULT  ABS __bss_start
    11: 00000000000004c8     0 FUNC    GLOBAL DEFAULT    9 _init
    12: 0000000000000638     0 FUNC    GLOBAL DEFAULT   12 _fini

其中 foo,x 等是 libfoo.so 中自己定义的符号, 其它 so 要调用 foo 时, rtld 可以通过 libfoo.so 的 dynsym 中的 foo 对应的表项获得 foo 函数真正的地址.

而 puts 的 Ndx 为 UND, 表示 libfoo.so 中引用了 puts, 但本身没有它的定义, libfoo.so 调用 puts 时, rtld 通过 rela.plt 会引用这个 dynsym 中的 puts 对应的表项, 以便知道 rtld 需要动态加载哪个符号.

即, 符号表会包含所有定义或引用的符号.

Backlinks

Android Linker (Android Linker > summary): 1. 根据 dynamic 获得 rel.dynrel.plt 以及 dynsym (以及 dynstr)

1.2.7.2. strtab

strtab 与 dynstr 都是 string table, 格式相同. 它们包含是多个以 \0 结尾的字符串. 例如:

$> readelf -a ./a.out
  ...
  [ 4] .dynstr           STRTAB          00000270 000270 00005d 00   A  0   0  1
  ...
$> od -c ./a.out +0x270
// with space stripped
0001160  \0__libc_init\0LIB
0001200  C\0libc.so\0__cxa_
0001220  atexit\0printf\0x\0
0001240  y\0_edata\0_end\0__
0001260  bss_start\0libfoo
0001300  .so\0libdl.so\0

$> readelf -p .dynstr ./a.out

String dump of section '.dynstr':
  [     1]  __libc_init
  [     d]  LIBC
  [    12]  libc.so
  [    1a]  __cxa_atexit
  [    27]  printf
  [    2e]  x
  [    30]  y
  [    32]  _edata
  [    39]  _end
  [    3e]  __bss_start
  [    4a]  libfoo.so
  [    54]  libdl.so
Backlinks

Android Linker (Android Linker > summary): 1. 根据 dynamic 获得 rel.dynrel.plt 以及 dynsym (以及 dynstr)

1.2.7.3. rel.text

rel.text 存在于 object 文件中, 用来保存 static linker 需要的重定位信息, 例如

Relocation section '.rela.text' at offset 0xd8 contains 1 entry:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
00000000000a  000400000002 R_X86_64_PC32     0000000000000000 a - 4

BFD 操作 reloc 相关的功能就是在操作 rel.text

`objdump -r` 也是通过 rel.text 来显示 reloc 信息

1.2.8. dynamic linker 相关

1.2.8.1. .dynamic section

dynamic linker 需要的所有信息都需要在运行时通过 dynamic section 得到, 它主要是通过 DT_XXX 指出动态链接需要的其它 section 的位置信息.

struct Elf32_Dyn
{
  Elf32_Sword d_tag;            // Type of dynamic table entry.
  union
  {
    Elf32_Word d_val;         // Integer value of entry.
    Elf32_Addr d_ptr;         // Pointer value of entry.
  } d_un;
};

d_tag tag_value d_un for exectuable for so
DT_NULL 0 Ignored Mandatory Mandatory
DT_NEEDED 1 d_val Optional Optional
DT_PLTRELSZ 2 d_val Optional Optional
DT_PLTGOT 3 d_ptr Optional Optional
DT_HASH 4 d_ptr Mandatory Mandatory
DT_STRTAB 5 d_ptr Mandatory Mandatory
DT_SYMTAB 6 d_ptr Mandatory Mandatory
DT_RELA 7 d_ptr Mandatory Optional
DT_RELASZ 8 d_val Mandatory Optional
DT_RELAENT 9 d_val Mandatory Optional
DT_STRSZ 10 d_val Mandatory Mandatory
DT_SYMENT 11 d_val Mandatory Mandatory
DT_INIT 12 d_ptr Optional Optional
DT_FINI 13 d_ptr Optional Optional
DT_SONAME 14 d_val Ignored Optional
DT_RPATH 15 d_val Optional Optional
DT_REL 17 d_ptr Mandatory Optional
DT_RELSZ 18 d_val Mandatory Optional
DT_RELENT 19 d_val Mandatory Optional
DT_JMPREL 23 d_ptr Optional Optional
DT_BIND_NOW 24 Ignored Optional Optional
DT_INIT_ARRAY 25 d_ptr Optional Optional
DT_FINI_ARRAY 26 d_ptr Optional Optional
DT_INIT_ARRAYSZ 27 d_val Optional Optional
DT_FINI_ARRAYSZ 28 d_val Optional Optional
DT_RUNPATH 29 d_val Optional Optional
DT_FLAGS 30 d_val Optional Optional
DT_PREINIT_ARRAY 32 d_ptr Optional Ignored
DT_PREINIT_ARRAYSZ 33 d_val Optional Ignored
       
$> readelf -a ./a.out
  [16] .dynamic          DYNAMIC         00001ec4 000ec4 000110 08  WA  4   0  4

Dynamic section at offset 0xec4 contains 29 entries:
  Tag        Type                         Name/Value
 0x00000003 (PLTGOT)                     0x1fe8
 0x00000002 (PLTRELSZ)                   24 (bytes)
 0x00000017 (JMPREL)                     0x328
 0x00000014 (PLTREL)                     REL
 0x00000011 (REL)                        0x300
 0x00000012 (RELSZ)                      40 (bytes)
 0x00000013 (RELENT)                     8 (bytes)
 0x6ffffffa (RELCOUNT)                   5
 0x00000015 (DEBUG)                      0x0

$> od -x ./a.out +0xec4
0007304 0003 0000 1fe8 0000 0002 0000 0018 0000
0007324 0017 0000 0328 0000 0014 0000 0011 0000
0007344 0011 0000 0300 0000 0012 0000 0028 0000
0007364 0013 0000 0008 0000 fffa 6fff 0005 0000
0007404 0015 0000 0000 0000 0006 0000 01e0 0000
0007424 000b 0000 0010 0000 0005 0000 0250 0000
0007444 000a 0000 004d 0000 0004 0000 02a0 0000
0007464 0001 0000 0012 0000 0001 0000 0044 0000
0007504 000c 0000 0420 0000 001a 0000 1ea4 0000

其中需要注意的是:

  1. DT_SYMTAB 和 DT_STRTAB 指向的是实际是 .dynsym 和 .dynstr (而不是 .symtab 和 .strtab), 因为 .dynamic 是给 linker 用的, 它只需要关注 .dynsym/.dynstr
  2. DT_NEEDED, DT_RUNPATH, DT_SONAME 的 d_val 是对应 .dynstr 的 offset, 而不是对应 .dynsym 的 index.
  3. DT_REL{A} 对应的实际上是 .rel{a}.dyn, DT_JMPREL{A} 对应的是 .rel{a}.plt
1.2.8.1.1. DT_SYMBOLIC

https://stackoverflow.com/questions/1588915/whats-the-difference-between-the-symbolic-and-shared-gcc-flags

symbolic 会在一定程序上阻塞 so 上全局符号的 interposition

$> cat foo.c

void bar() {
    printf("%s\n", "bar");
}
void foo() {
    printf("%s\n", "foo");
    bar();
}

$> cat main.c

#include <stdio.h>

extern void foo();
void bar() {
    printf("%s\n", "bar from main");
}
int main(int argc, char *argv[]) {
    foo();
    return 0;
}

$> gcc -shared -fPIC -O0 -g3 foo.c -o libfoo.so -Wl,-Bsymbolic
# -Bsymbolc 导致链接 libfoo.so 时优先从 libfoo.so 本身查找 bar, 生成 `call bar`,
# 而不是生成 `call bar@plt`, 然后让 rtld 在运行时在所有 so(及 elf) 中查找
$> gcc main.c libfoo.so
$> LD_LIBRARY_PATH=. ./a.out
foo
bar

$> gcc -shared -fPIC -O0 -g3 foo.c -o libfoo.so
$> LD_LIBRARY_PATH=. ./a.out
foo
bar from main

比较两者的代码:

$> gcc -shared -O0 -g3 -fPIC foo.c -o libfoo.so -Wl,-Bsymbolic
$> objdump -D libfoo.so

000000000000063d <foo>:
 63d:	55                   	push   %rbp
 63e:	48 89 e5             	mov    %rsp,%rbp
 641:	48 8d 3d 21 00 00 00 	lea    0x21(%rip),%rdi        # 669 <_fini+0xd>
 648:	e8 e3 fe ff ff       	callq  530 <puts@plt>
 64d:	b8 00 00 00 00       	mov    $0x0,%eax
 652:	e8 d3 ff ff ff       	callq  62a <bar>             <------
 657:	90                   	nop
 658:	5d                   	pop    %rbp
 659:	c3                   	retq


$> gcc -shared -O0 -g3 -fPIC foo.c -o libfoo.so
$> objdump -D libfoo.so

000000000000065d <foo>:
 65d:	55                   	push   %rbp
 65e:	48 89 e5             	mov    %rsp,%rbp
 661:	48 8d 3d 21 00 00 00 	lea    0x21(%rip),%rdi        # 689 <_fini+0xd>
 668:	e8 d3 fe ff ff       	callq  540 <puts@plt>
 66d:	b8 00 00 00 00       	mov    $0x0,%eax
 672:	e8 d9 fe ff ff       	callq  550 <bar@plt>          <---- 通过 plt 查找,
                                                               loader 会负责将 main 中的 bar 写到 got.plt 中
 677:	90                   	nop
 678:	5d                   	pop    %rbp
 679:	c3                   	retq

实际上即使没有针对 symbolic 生成 `call bar` 形式的代码, rtld 在处理 DT_SYMBOLIC 时, 也会优先从当前 so 中查找符号, 保证在有 DT_SYMBOLIC 时将 libfoo.so 中的 bar 写到 got.plt 中以满足 symbolic 的要求.

Bsymbolic 的一个使用场景是 rtld 本身: 做为一个 DSO, 它会使用 Bsymbolic 保证在完成自身的动态链接之前也可以正确的进行函数调用, 因为 Bsymbolic 可以避免对函数进行重定位.

通过控制函数的 visibility 也可以达到相同的效果 (例如使用 `protected` visibility), 但由于 rtld 需要调用 libc.a 中的代码, 这部分公共代码是不能修改 visibility 的

1.2.8.2. interp
const char interp_section[] __attribute__((section(".interp"))) = "/path/to/dynamic/linker";
1.2.8.3. got
1.2.8.3.1. got 的格式

got 的格式类似于 int[], 数组中保存的符号没有经过 patch 的地址, 例如:

$> readelf -a ./a.out
  ...                                    vaddr    offset
  [17] .got              PROGBITS        00001fd4 000fd4 00002c 00  WA  0   0  4

$> arm-linux-androideabi-objdump -D ./a.out

Disassembly of section .got:

00001fd4 <_GLOBAL_OFFSET_TABLE_-0x14>:
    1fd4:	00001ebc 			; <UNDEFINED> instruction: 0x00001ebc
    1fd8:	00001eac 	andeq	r1, r0, ip, lsr #29
    1fdc:	00001ea4 	andeq	r1, r0, r4, lsr #29
    1fe0:	00001eb4 			; <UNDEFINED> instruction: 0x00001eb4
    1fe4:	00000454 	andeq	r0, r0, r4, asr r4

00001fe8 <_GLOBAL_OFFSET_TABLE_>:
        ...
    1ff4:	00000374 	andeq	r0, r0, r4, ror r3
    1ff8:	00000374 	andeq	r0, r0, r4, ror r3
    1ffc:	00000374 	andeq	r0, r0, r4, ror r3

$> od -x ./a.out +0xfd4

0007724 1ebc 0000 1eac 0000 1ea4 0000 1eb4 0000
0007744 0454 0000 0000 0000 0000 0000 0000 0000
0007764 0374 0000 0374 0000 0374 0000 000c 0000
0010004 0000 0000 4700 4343 203a 4728 554e 2029
1.2.8.3.2. 全局变量与 got (arm)
~/source/sharklj1@tj02433pcu> d debug exec out/target/product/sp9850j_1h10/symbols/system/bin/linker /system/bin/linker
Remote debugging from host 127.0.0.1
__dl__start () at bionic/linker/arch/arm/begin.S:32
32        mov r0, sp
(gdb) b __dl___linker_init
Breakpoint 1 at 0xaaab6e38: __dl___linker_init. (2 locations)
(gdb) c
Continuing.

Breakpoint 1, __linker_init (raw_args=0xfffefa80) at bionic/linker/linker.cpp:4400
4400      KernelArgumentBlock args(raw_args);
(gdb) n
4402      ElfW(Addr) linker_addr = args.getauxval(AT_BASE);
(gdb)
4403      ElfW(Addr) entry_point = args.getauxval(AT_ENTRY);
(gdb)
4404      ElfW(Ehdr)* elf_hdr = reinterpret_cast<ElfW(Ehdr)*>(linker_addr);
(gdb)
4405      ElfW(Phdr)* phdr = reinterpret_cast<ElfW(Phdr)*>(linker_addr + elf_hdr->e_phoff);
(gdb)
4407      soinfo linker_so(nullptr, nullptr, nullptr, 0, 0);
(gdb)
4416      if (reinterpret_cast<ElfW(Addr)>(&_start) == entry_point) {
(gdb) ni
0xf7787e80      4416      if (reinterpret_cast<ElfW(Addr)>(&_start) == entry_point) {
(gdb) disass
Dump of assembler code for function __linker_init(void*):
   ...
   0xf7787e7e <+82>:    ldr     r1, [sp, #544]  ; 0x220
=> 0xf7787e80 <+84>:    ldr     r2, [pc, #316]  ; (0xf7787fc0 <__linker_init(void*)+404>)
   0xf7787e82 <+86>:    add     r2, pc
   0xf7787e84 <+88>:    ldr     r2, [r2, #0]
   0xf7787e86 <+90>:    cmp     r2, r1

(gdb) p /x $r2
$1 = 0x62f6a
// 0x62f6a 存储在 [pc,#316] 处, 这里是 __dl___linker_init 函数的 literal pool 区域, 表示 pc 与 GOT[__dl__start] 的偏移量
// 所以后面通过 pc+0x62f6a (0xf77eadf0) 可以得到 GOT[__dl__start]
(gdb) ni
0xf7787e84      4416      if (reinterpret_cast<ElfW(Addr)>(&_start) == entry_point) {
(gdb) p /x $r2
$3 = 0xf77eadf0
(gdb) x 0xf77eadf0
0xf77eadf0:     0x000028fc
// 由于 linker 做为 interp map 在 0xf777b000, 所以 GOT 在原 elf 中的位置是 0xf77eadf0 - 0xf777b000 = 0x6fdf0
// Disassembly of section .got:
// 0006fddc <__dl__GLOBAL_OFFSET_TABLE_-0x218>:
//    6fddc:	000715b8 			; <UNDEFINED> instruction: 0x000715b8
//    6fde0:	000711d0 	ldrdeq	r1, [r7], -r0
//    6fde4:	000716bc 			; <UNDEFINED> instruction: 0x000716bc
//    6fde8:	0006eaf4 	strdeq	lr, [r6], -r4
//    6fdec:	00070174 	andeq	r0, r7, r4, ror r1
//    6fdf0:	000028fc 	strdeq	r2, [r0], -ip     <--- 28fc
1.2.8.3.3. 全局变量与 got (x86)
/* main.c */
extern void foo();
extern void bar();

int main(int argc, char *argv[])
{
    foo();
    bar();

    return 0;
}

/* foo.c */
int x = 0xab;

int foo() {
    printf("foo\n");
    x = 0xcd;
}

/* bar.c */
extern int x;
int bar() {
    printf("bar\n");
    x = 0x12;
}

$ gdb ./a.out
(gdb) b main
Breakpoint 1 at 0x400648
(gdb) r
Starting program: /home/sunway/a.out

Breakpoint 1, 0x0000000000400648 in main ()

(gdb) disass foo
Dump of assembler code for function foo:
   0x00007ffff7bd85dc <+0>:     push   %rbp
   0x00007ffff7bd85dd <+1>:     mov    %rsp,%rbp
   0x00007ffff7bd85e0 <+4>:     lea    0x5f(%rip),%rdi        # 0x7ffff7bd8646
   0x00007ffff7bd85e7 <+11>:    callq  0x7ffff7bd84f0 <puts@plt>
   0x00007ffff7bd85ec <+16>:    mov    0x2009d5(%rip),%rax        # 0x7ffff7dd8fc8
   0x00007ffff7bd85f3 <+23>:    movl   $0xcd,(%rax)
   0x00007ffff7bd85f9 <+29>:    pop    %rbp
   0x00007ffff7bd85fa <+30>:    retq
End of assembler dump.

0x7ffff7dd8fc8 这个地址实际上位于 foo.so 的 .got section. 因为:

pmap 显示 libfoo.so 被映射到 0x00007ffff7bd8000:
00007ffff7bd8000      4K r-x--  /home/sunway/libfoo.so
00007ffff7bd9000   2044K -----  /home/sunway/libfoo.so
00007ffff7dd8000      4K r----  /home/sunway/libfoo.so
00007ffff7dd9000      4K rw---  /home/sunway/libfoo.so

0x7ffff7dd8fc8 - 0x00007ffff7bd8000 = 0x200fc8

然后通过 readelf -S libfoo.so | grep " .got " 结果为:

[20] .got              PROGBITS         0000000000200fc8  00000fc8

所以 0x7ffff7dd8fc8 实际就是 libfoo.so 被加载后的 GOT[0]

(gdb) x /xg 0x7ffff7dd8fc8
0x7ffff7dd8fc8: 0x00007ffff7dd9018

0x00007ffff7dd9018 这个地址是 x 变量的实际地址, 去掉 libfoo.so 映射的首地址 00007ffff7bd8000 后为 0x201018,
通过 readelf -S libfoo.so:
 [22] .data             PROGBITS         0000000000201010  00001010
       000000000000000c  0000000000000000  WA       0     0     8
 [23] .bss              NOBITS           0000000000201020  0000101c
       0000000000000010  0000000000000000  WA       0     0     8

可以确定这个地址位于 .data section.

(gdb) x 0x00007ffff7dd9018
0x7ffff7dd9018 <x>:     0x00000000000000ab

所以真正的 x 变量位于 libfoo.so 的 .data 中, 且其地址被写在 libfoo.so 的 GOT[0] 中, libfoo.so 中对 x 的引用都通过 GOT[0] 进行.

同理, 观察 libbar.so 对 x 的引用:

(gdb) disass bar
Dump of assembler code for function bar:
   0x00007ffff79d65dc <+0>:     push   %rbp
   0x00007ffff79d65dd <+1>:     mov    %rsp,%rbp
   0x00007ffff79d65e0 <+4>:     lea    0x5f(%rip),%rdi        # 0x7ffff79d6646
   0x00007ffff79d65e7 <+11>:    callq  0x7ffff79d64f0 <puts@plt>
   0x00007ffff79d65ec <+16>:    mov    0x2009d5(%rip),%rax        # 0x7ffff7bd6fc8
   0x00007ffff79d65f3 <+23>:    movl   $0x12,(%rax)
   0x00007ffff79d65f9 <+29>:    pop    %rbp
   0x00007ffff79d65fa <+30>:    retq
End of assembler dump.

0x7ffff7bd6fc8 这个地址位于 libbar.so 的 GOT[0]

(gdb) x /gx 0x7ffff7bd6fc8
0x7ffff7bd6fc8: 0x00007ffff7dd9018

可见 libbar.so 的 GOT[0] 与 libfoo.so 的 GOT[0] 均指向 libfoo.so 的 .data section 中 x 的实际地址.

在使用 GOT 表时, so 的代码并不需要 `GOT 基址` 这样的数据, 因为 GOT 与代码的距离的在静态链接时就是可以确定的:

以 objdump -d libfoo.so 为例:

00000000000005dc <foo>:
 5dc:   55                      push   %rbp
 5dd:   48 89 e5                mov    %rsp,%rbp
 5e0:   48 8d 3d 5f 00 00 00    lea    0x5f(%rip),%rdi        # 646 <_fini+0xe>
 5e7:   e8 04 ff ff ff          callq  4f0 <puts@plt>
 5ec:   48 8b 05 d5 09 20 00    mov    0x2009d5(%rip),%rax        # 200fc8 <_DYNAMIC+0x180>
 5f3:   c7 00 cd 00 00 00       movl   $0xcd,(%rax)
 5f9:   5d                      pop    %rbp
 5fa:   c3                      retq

这里的 0x2009d5 就是代码与 GOT 的偏移量, 当 so 被静态链接时, so 基址为 0, rip 为 5f3, 所以 GOT 位于 0x5f3 + 0x2009d5 = 0x200fc8, 当 so 被加载后,假设加载到 0x7ffff7bd8000, 则运行时 rip 会变为 0x7ffff7bd8000 + 0x5f3, 所以运行时 GOT 计算的结果为: 0x200fc8 + 0x7ffff7bd8000 = 0x7ffff7dd8fc8

  1. 总结

    与 PLT 类似, 每个 so 均有其 GOT 表, so 中对非 static 的全局变量的引用都通过 GOT 来实现.

1.2.8.4. plt
1.2.8.4.1. 函数与 plt
/* gcc -shared -fPIC -o libfoo.so foo.c */
/* gcc -shared -fPIC -o libbar.so bar.c */
/* gcc main.c -L. -lfoo -lbar */

/* main.c */
extern void foo();
extern void bar();

int main(int argc, char *argv[])
{
    foo();
    bar();
    return 0;
}

/* foo.c */
int foo() {
    printf("foo\n");
}

/* bar.c */
int bar() {
    printf("bar\n");
}
$ LD_LIBRARY_PATH=. gdb a.out
(gdb) b main
(gdb) r
(gdb) disass
Dump of assembler code for function main:
0x0000000000400644 <+0>:     push   %rbp
   0x0000000000400645 <+1>:     mov    %rsp,%rbp
   0x0000000000400648 <+4>:     sub    $0x10,%rsp
   0x000000000040064c <+8>:     mov    %edi,-0x4(%rbp)
   0x000000000040064f <+11>:    mov    %rsi,-0x10(%rbp)
=> 0x0000000000400653 <+15>:    mov    $0x0,%eax
   0x0000000000400658 <+20>:    callq  0x400550 <foo@plt>
   0x000000000040065d <+25>:    mov    $0x0,%eax
   0x0000000000400662 <+30>:    callq  0x400530 <bar@plt>
   0x0000000000400667 <+35>:    mov    $0x0,%eax
   0x000000000040066c <+40>:    leaveq
   0x000000000040066d <+41>:    retq
End of assembler dump.

foo 函数显示的地址为 0x400550, 这个并不是 foo.c 中定义的那个真正的
foo 函数的地址, 而只是 .plt 表中的地址: 这个 plt 表项位于 a.out 的
第一个 LOAD segment 中 (类型为 RE)

实际上, a.out 的 .plt 中一共有四项, 分别为 plt0 (__ld_runtime_resolve 的代码), bar@plt, __libc_start_main@plt 以及 foo@plt
比如:

0x400520 是 .plt 的起始地址:

~@sunway-work> readelf -S ./a.out |grep " .plt"
  [12] .plt              PROGBITS         0000000000400520  00000520

(gdb) x /10g 0x400520
0x400520:       0x25ff00200aca35ff      0x00401f0f00200acc
0x400530 <bar@plt>:     0x006800200aca25ff      0xffffffe0e9000000
0x400540 <__libc_start_main@plt>:       0x016800200ac225ff      0xffffffd0e9000000
0x400550 <foo@plt>:     0x026800200aba25ff      0xffffffc0e9000000
0x400560 <_start>:      0x89485ed18949ed31      0x495450f0e48348e2


看一下 foo@plt (PLT[2]) 的具体代码:
(gdb) disass 0x400550
Dump of assembler code for function foo@plt:
   0x0000000000400550 <+0>:     jmpq   *0x200aba(%rip)        # 0x601010 <[email protected]>
   0x0000000000400556 <+6>:     pushq  $0x2
   0x000000000040055b <+11>:    jmpq   0x400520
End of assembler dump.

0x601010 对应于 .got.plt section 中的一项:

(gdb) x  0x601010
0x601010 <[email protected]>: 0x0000000000400556

当 .got.plt 表项第一次被使用时 (未经过 rtld 解析过), 其值均为相应的 plt 指令的下一条指令地址, 在这
里即 pushq $0x2 指令的地址, rtld 解析过后会修改 .got.plt 表项的值为真正的地址.

pushq $0x2 指令将 0x2 入栈, 这里的 0x2 指的实际上是 foo 函数在 a.out 的 .rela.plt section 中的索引,

.rela.plt 的内容为:

Relocation section '.rela.plt' at offset 0x4b8 contains 3 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000601000  000100000007 R_X86_64_JUMP_SLO 0000000000000000 bar + 0
000000601008  000200000007 R_X86_64_JUMP_SLO 0000000000000000 __libc_start_main + 0
000000601010  000400000007 R_X86_64_JUMP_SLO 0000000000000000 foo + 0

rtld 通过 .rela.plt[2] 就可以解析到 dynsym 中对应的符号信息 (foo) 以及要写回的 .got.plt 表的位置 (0x601010)

jmpq 0x400520 是跳转到 rtld (__ld_runtime_resolve) 函数去动态加载 foo 函数. 0x400520 实际上就是 PLT[0]

(gdb) disass 0x400520,0x400530
Dump of assembler code from 0x400520 to 0x400530:
   0x0000000000400520:  pushq  0x200aca(%rip)        # 0x600ff0 <_GLOBAL_OFFSET_TABLE_+8>
   0x0000000000400526:  jmpq   *0x200acc(%rip)        # 0x600ff8 <_GLOBAL_OFFSET_TABLE_+16>
   0x000000000040052c:  nopl   0x0(%rax)
End of assembler dump.


0x600ff8 即 .got.plt[16] 保存着 __ld_runtime_resolve 函数的地址:

(gdb) x 0x600ff8
0x600ff8 <_GLOBAL_OFFSET_TABLE_+16>:    0x00007ffff7def200

(gdb) disass 0x00007ffff7def200
Dump of assembler code for function _dl_runtime_resolve:
   0x00007ffff7def200 <+0>:     sub    $0x38,%rsp
   0x00007ffff7def204 <+4>:     mov    %rax,(%rsp)
   0x00007ffff7def208 <+8>:     mov    %rcx,0x8(%rsp)
   0x00007ffff7def20d <+13>:    mov    %rdx,0x10(%rsp)
   0x00007ffff7def212 <+18>:    mov    %rsi,0x18(%rsp)
   0x00007ffff7def217 <+23>:    mov    %rdi,0x20(%rsp)
   0x00007ffff7def21c <+28>:    mov    %r8,0x28(%rsp)
   0x00007ffff7def221 <+33>:    mov    %r9,0x30(%rsp)
   0x00007ffff7def226 <+38>:    mov    0x40(%rsp),%rsi
   0x00007ffff7def22b <+43>:    mov    0x38(%rsp),%rdi
   0x00007ffff7def230 <+48>:    callq  0x7ffff7de8680 <_dl_fixup>
   0x00007ffff7def235 <+53>:    mov    %rax,%r11
   0x00007ffff7def238 <+56>:    mov    0x30(%rsp),%r9
   0x00007ffff7def23d <+61>:    mov    0x28(%rsp),%r8
   0x00007ffff7def242 <+66>:    mov    0x20(%rsp),%rdi
   0x00007ffff7def247 <+71>:    mov    0x18(%rsp),%rsi
   0x00007ffff7def24c <+76>:    mov    0x10(%rsp),%rdx
   0x00007ffff7def251 <+81>:    mov    0x8(%rsp),%rcx
   0x00007ffff7def256 <+86>:    mov    (%rsp),%rax
   0x00007ffff7def25a <+90>:    add    $0x48,%rsp
   0x00007ffff7def25e <+94>:    jmpq   *%r11
End of assembler dump.

(gdb) n
foo
7           bar();

foo 执行完毕, 此时再看一次 foo@plt 相关的信息:
(gdb) disass 0x400550
Dump of assembler code for function foo@plt:
   0x0000000000400550 <+0>:     jmpq   *0x200aba(%rip)        # 0x601010 <[email protected]>
   0x0000000000400556 <+6>:     pushq  $0x2
   0x000000000040055b <+11>:    jmpq   0x400520
End of assembler dump.

.plt[2] 的内容没有变化, 再看相应的 .got.plt 表项:

(gdb) x 0x601010
0x601010 <[email protected]>: 0x00007ffff7bd85ac

而执行前的结果为:

(gdb) x  0x601010
0x601010 <[email protected]>: 0x0000000000400556

可见, __ld_runtime_resolve 已经将 [email protected] 的值修改了, 这个 0x00007ffff7bd85ac 实际上就是 libfoo.so 中 foo 函数的真正地址:

(gdb) disass 0x00007ffff7bd85ac
Dump of assembler code for function foo:
   0x00007ffff7bd85ac <+0>:     push   %rbp
   0x00007ffff7bd85ad <+1>:     mov    %rsp,%rbp
   0x00007ffff7bd85b0 <+4>:     lea    0x4f(%rip),%rdi        # 0x7ffff7bd8606
   0x00007ffff7bd85b7 <+11>:    callq  0x7ffff7bd84c0 <puts@plt>
   0x00007ffff7bd85bc <+16>:    pop    %rbp
   0x00007ffff7bd85bd <+17>:    retq
End of assembler dump.

1.2.8.4.2. 总结

每个 so 均有其 PLT 表, 当使用 PIE 时所有非 static 的函数均通过 PLT 表引用.

一个动态加载的函数符号的解析需要涉及到以下的 elf section:

  1. .plt
  2. .got.plt
  3. .rela.plt
  4. 其他 so 的 .dynsym 和 .dynstr

plt[0] 保存着 __ld_runtime_resolve 的地址, 进行符号的动态加载.

.got.plt 与 .got 类似, 都是需要被 rtld 修改的, 不同的是 .got 主要是为了定位全局变量, 而 .got.plt 是为了定位函数.

这个 section 被加载到类型为 RW 的第二个 LOAD segment, 而 .plt 和 .rela.plt 是不变的, 所以它们被加载到类型 RE 的第一个 LOAD segment, 即:

Section to Segment mapping:
 Segment Sections...
  00
  01     .interp
  02     .interp ... .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .text .fini .rodata ...
                                                                           ~~~~~~~~~~      ~~~~~
  03     .ctors .dtors .jcr .dynamic .got .got.plt .data .bss
                                          ~~~~~~~~
  04     .dynamic
  ...

__ld_runtime_resolve 通过 .rela.plt 获得要加载的符号的名字和要改写的 .got.plt 项的地址.

最终 __ld_runtime_resolve 需要查找其它 so 的 .dynsym 和 .dynstr 来确定函数真正的地址.

1.2.8.4.3. no-plt

arm 有一个编译选项是 `-mno-plt`, 相当于对 plt 进行 inline, 可以避免一次跳转

foo:
    stp x29, x30, [sp, -32]!
    mov x29, sp
    str w0, [sp, 28]
    # 下面三行相当于 bar@plt, 直接被 inline 在这里了
    adrp        x0, :got:bar
    ldr x0, [x0, #:got_lo12:bar]
    blr x0
    nop
    ldp x29, x30, [sp], 32
    ret
1.2.8.5. dynsym
1.2.8.6. dynstr
1.2.8.7. got.plt

got.plt 与 got 类似, 但它保存的是函数的地址 (而不是全局变量的地址). plt 需要用到 got.plt

1.2.8.8. rel.dyn

rel.dyn 是用来指示哪些需要重定位的, 重定位一般是修改 GOT 表 (但也有例外, 比如后面的 rela.dyn, 重定位修改的是.data 部分的值), 通过 rel.dyn, loader 才知道 GOT 表中的各个项需要如何修改

rel.dyn 的格式为:

struct Elf32_Rel {
  Elf32_Addr r_offset;
  Elf32_Word r_info;
}

其中:

  1. r_offset 是需要 patch 的 vaddr, 一般位于 got 表中
  2. r_info 是一个 int, 包含了 rel 的类型和 dynsym 的索引
1.2.8.8.1. 例子
/* test.c */
#include <stdio.h>
extern int x;
extern int y;
int main(int argc, char *argv[]) {
    printf("main: %x\n", &x, &y);
    return 0;
}

/* foo.c */
int x = 10;
int y = 11;
$> arm-linux-androideabi-gcc foo.c -fPIC -shared -o libfoo.so -g3 -O0
$> arm-linux-androideabi-gcc test.c -fPIE -pie -O0 -g3 -L. -lfoo
$> arm-linux-androideabi-readelf -a ./a.out

Relocation section '.rel.dyn' at offset 0x33c contains 7 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
00001fcc  00000017 R_ARM_RELATIVE
00001fd0  00000017 R_ARM_RELATIVE
00001fd4  00000017 R_ARM_RELATIVE
00001fd8  00000017 R_ARM_RELATIVE
00001fdc  00000017 R_ARM_RELATIVE
00001fe0  00000415 R_ARM_GLOB_DAT    00000000   x
00001fe4  00000515 R_ARM_GLOB_DAT    00000000   y

其中:
0x1fe0 位于 GOT 表中, 它存储的值是 x 的地址;
0x415 << 8 = 4, 代表 x 在 dynsym 中的索引.
0x415 & 0xff = 0x15, 代表 rel 的类型: R_ARM_GLOB_DAT

Symbol table '.dynsym' contains 9 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
     0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 00000000     0 FUNC    GLOBAL DEFAULT  UND __libc_init@LIBC (2)
     2: 00000000     0 FUNC    GLOBAL DEFAULT  UND __cxa_atexit@LIBC (2)
     3: 00000000     0 FUNC    GLOBAL DEFAULT  UND printf@LIBC (2)
     4: 00000000     0 OBJECT  GLOBAL DEFAULT  UND x
     5: 00000000     0 OBJECT  GLOBAL DEFAULT  UND y
     6: 00002000     0 NOTYPE  GLOBAL DEFAULT  ABS _edata
     7: 00002004     0 NOTYPE  GLOBAL DEFAULT  ABS _end
     8: 00002000     0 NOTYPE  GLOBAL DEFAULT  ABS __bss_start

这样 loader 在工作时, 就可以从 rel.dyn 和 dynsym (以及 dynstr) 中获得需要重定位的项的信息:

  1. 符号的名字
  2. 重定位类型
  3. 定位完成后需要 patch 的地址 (GOT 表项)
Backlinks

Android Linker (Android Linker > summary): 1. 根据 dynamic 获得 rel.dynrel.plt 以及 dynsym (以及 dynstr)

1.2.8.9. rel.plt

rel.plt 与 got.plt 的关系, 类似于 rel.dyn 与 got 的关系:

rel.dyn 用来索引 got, 以便对数据进行重定位. rel.plt 用来索引 got.plt, 以便对函数进行重定位.

rel.plt 的格式与 rel.dyn 是一样的, 都是 Elf32_Rel, 它的 r_offset 指示的 vaddr 位于 got.plt 表中

通过 r_info 中的 relocation type, 可以区分出 rel.plt 与 rel.dyn

$> cat main.c

extern void foo();

int main(int argc, char *argv[])
{
    foo();
    return 0;
}

$> arm-linux-androideabi-gcc main.c -fPIE -pie -L. -lfoo
$> arm-linux-androideabi-readelf -a ./a.out

Relocation section '.rel.plt' at offset 0x330 contains 3 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
00001ff4  00000116 R_ARM_JUMP_SLOT   00000000   __libc_init@LIBC
00001ff8  00000216 R_ARM_JUMP_SLOT   00000000   __cxa_atexit@LIBC
00001ffc  00000316 R_ARM_JUMP_SLOT   00000000   foo
~~~~~~~~

$> arm-linux-androideabi-objdum -D ./a.out

00001fe8 <_GLOBAL_OFFSET_TABLE_>:
        ...
    1ff4:	00000348 	andeq	r0, r0, r8, asr #6
    1ff8:	00000348 	andeq	r0, r0, r8, asr #6
    1ffc:	00000348 	andeq	r0, r0, r8, asr #6

;; 所以 linker 将来需要修改 1ffc 处的值为重定位后的 plt[0] 的地址 (0x348 是没有经过重定位的 plt[0])

00000374 <foo@plt>:
 374:	e28fc600 	add	ip, pc, #0, 12                          <--- ip = pc + 0<<12 = 37c
 378:	e28cca01 	add	ip, ip, #4096	; 0x1000                <--- ip = 37c + 0x10000 = 0x137c
 37c:	e5bcfc80 	ldr	pc, [ip, #3200]!	; 0xc80         <--- pc= [0x137c+0xc80] = [0x1ffc] = 0x348

;; 0x348 实际就是 plt[0], 即 linker 的 resolver.

00000348 <__libc_init@plt-0x14>:
 348:	e52de004 	push	{lr}		; (str lr, [sp, #-4]!)
 34c:	e59fe004 	ldr	lr, [pc, #4]	; 358 <note_end+0x178>
 350:	e08fe00e 	add	lr, pc, lr
 354:	e5bef008 	ldr	pc, [lr, #8]!
 358:	00001c90 	muleq	r0, r0, ip
Backlinks

Android Linker (Android Linker > summary): 1. 根据 dynamic 获得 rel.dynrel.plt 以及 dynsym (以及 dynstr)

1.2.8.10. rela.dyn

rela.dyn 与 rel.dyn 基本是一样的, 唯一的不同的 rela 比 rel 多了一个 addend 成员.

struct Elf32_Rela {
  Elf32_Addr  r_offset; // Location (file byte offset, or program virtual addr)
  Elf32_Word  r_info;   // Symbol table index and type of relocation to apply
  Elf32_Sword r_addend; // Compute value for relocatable field by adding this
}

新的平台如 arm64, x86_64 都使用 rela 代替了 rel. 因为使用 rel 需要比 rela 多访问一次内存.

1.2.8.10.1. rela on arm64
$> cat foo.c
int xxx[4];

$> aarch64-linux-gnu-gcc foo.c -shared -fPIC -o libfoo.so -O0 -g3
$> cat main.c
#include <stdio.h>

extern int xxx[4];
int *j = xxx + 3;
int *k = xxx;

int main(int argc, char *argv[]) {
    printf("%p %p\n", j, k);
    return 0;
}

$> aarch64-linux-gnu-gcc main.c -fPIE -pie -O0 -g3 libfoo.so
$> readelf -a ./a.out
Relocation section '.rela.dyn' at offset 0x638 contains 13 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
...
000000010ce8  001400000401 R_AARCH64_GLOB_DA 0000000000000000 _ITM_registerTMCloneTa + 0
000000010d40  000a00000101 R_AARCH64_ABS64   0000000000000000 xxx + c
000000010d48  000a00000101 R_AARCH64_ABS64   0000000000000000 xxx + 0

;; 其中 10d40 位于 .data, 它存储的值是 j 的地址 (即 xxx + 3*4 = xxx +
;; c), 在 rela 中, c 会作为 addend 保存在 rela entry 中
1.2.8.10.2. rel on arm32
$> arm-linux-androideabi-gcc foo.c -fPIC -shared -o libfoo.so -g3 -O0
$> arm-linux-androideabi-gcc main.c -fPIE -pie -L. -lfoo -O0 -g3
$> readelf -a ./a.out
Relocation section '.rel.dyn' at offset 0x324 contains 7 entries:
 Offset     Info    Type            Sym.Value  Sym. Name
...
00002000  00000402 R_ARM_ABS32       00000000   xxx
00002004  00000402 R_ARM_ABS32       00000000   xxx

;; 在 rel 中, i,j 对应的 entry 都和 xxx 符号有关, 由于没有 addend 值, 如
;; 何确定 i, j 相对 xxx 的偏移量? 实际上, 这个偏移量是做为初值保存在 .data 中的

$> arm-linux-androideabi-objdump -D ./a.out

00001fe8 <_GLOBAL_OFFSET_TABLE_>:
        ...
    1ff4:       00000374        andeq   r0, r0, r4, ror r3
    1ff8:       00000374        andeq   r0, r0, r4, ror r3
    1ffc:       00000374        andeq   r0, r0, r4, ror r3

Disassembly of section .data:

00002000 <j>:
    2000:       0000000c        andeq   r0, r0, ip
                ~~~~~~~~
00002004 <k>:
    2004:       00000000        andeq   r0, r0, r0
                ~~~~~~~~

;; GOT 中对于 i,j 的初值是无用的, 刚好可以用来保存 addend, 但 linker
;; 显然需要多一次内存访问.
1.2.8.10.3. note

从前面 rel 及 rela 的实现方式可以看到, loader 可以通过重定位的方式给全局变量赋初值, 但仅限于 symbol_addr + addend 的方式, 所以考虑下面的代码能否编译通过:

/* 1. false */
extern int x;
int y = x;

/* 2. true */
extern int x;
int *y = &x + 1;

/* 3. true */

extern int x;
int y = (int)&x;

/* 4. false */
extern int x;
int y = (int)(&x) + int(&x);
1.2.8.11. rela.plt
1.2.8.12. data.rel.ro

1.2.9. 其它

1.2.9.1. eh_frame
1.2.9.2. hash
1.2.9.2.1. .hash section 的格式

.hash 使用链表来解决 hash 冲突

nbucket
nchain
bucket[0]
. . .
bucket[nbucket-1]
chain[0]
. . .
chain[nchain-1]
1.2.9.2.2. hash 函数
unsigned long
elf_Hash(const unsigned char *name)
{
    unsigned long h = 0, g;

    while (*name)
    {
        h = (h << 4) + *name++;
        if (g = h & 0xf0000000)
            h ^= g >> 24;
        h &= ~g;
    }
    return h;
}

int main(int argc, char *argv[]) {
    printf("%x\n", elf_Hash(argv[1]));
    return 0;
}

1.2.9.2.3. example

libfoo.so:

int xxx = 0;
void zzz() {

}
$> arm-linux-androideabi-gcc -O0 -g3 foo.c -shared -fPIC -o libfoo.so
$> readelf -a ./libfoo.so

  [ 4] .hash             HASH            00000208 000208 000034 04   A  2   0  4
  ...
Symbol table '.dynsym' contains 8 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
     0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 00000000     0 FUNC    GLOBAL DEFAULT  UND __cxa_finalize@LIBC (2)
     2: 00000000     0 FUNC    GLOBAL DEFAULT  UND __cxa_atexit@LIBC (2)
     3: 00002004     4 OBJECT  GLOBAL DEFAULT   17 xxx
     4: 00000308    20 FUNC    GLOBAL DEFAULT   11 zzz
     5: 00002004     0 NOTYPE  GLOBAL DEFAULT  ABS _edata
     6: 00002008     0 NOTYPE  GLOBAL DEFAULT  ABS _end
     7: 00002004     0 NOTYPE  GLOBAL DEFAULT  ABS __bss_start

$>  od -x ./libfoo.so +0x208|more
0001010 0003 0000 0008 0000 0005 0000 0006 0000
0001030 0007 0000 0000 0000 0000 0000 0000 0000
0001050 0000 0000 0003 0000 0004 0000 0002 0000
0001070 0001 0000 0000 0002 0002 0001 0001 0001
0001110 0001 0001 0001 0001 0001 0001 776f 08d6
0001130 0014 0000 0000 0000 001d 0000 0000 0000
0001150 0001 0001 0015 0000 0010 0000 0000 0000
0001170 0d63 0005 0000 0002 0010 0000 0000 0000
0001210 1ed8 0000 0017 0000 1ff8 0000 0216 0000

按 .hash 的格式解析后为:

  • nbucket: 3
  • nchain: 8
  • bucket[0] = 5
  • bucket[1] = 6
  • bucket[2] = 7
  • chain[0] = 0
  • chain[1] = 0
  • chain[2] = 0
  • chain[3] = 0
  • chain[4] = 3
  • chain[5] = 4
  • chain[6] = 2
  • chain[7] = 1

以查找 __bss_start 为例:

  1. elfhash __bss_start 为 0x90ff134
  2. bucket 索引为 0x90ff134 % nbucket(3) = 2
  3. 检查 bucket[2] = dynsym[7] = __bss_start, 找到

以查找一个不存在的 hello 为例:

  1. elfhash hello = 0x6ec32f % 3 = 1
  2. bucket[1] = 6, dynsym[6] = _end, 不相等
  3. chain[6] = 2, dynsym[2] = __cxa_atexit@LIBC (2), 不相等
  4. chain[2] = 0, 查找结束, 因为 0 是一个占位符, dynsym[0] 不包含有效值
1.2.9.2.4. 关于 bucket 和 chain:

bucket[x] 相当于所有 hash 为 x 的符号组成的链表的头, 而 chain[bucket[x]], chain[chain[bucket[x]]]…构成链表.

所以, dynsym 中的符号构成以下几条链表:

[5,4,3], [6,2] [7,1]

1.2.9.3. gnu.hash
1.2.9.4. gnu.version

gnu.version 是对 dynsym 的补充, 表示每个 dynsym 的符号的 version

typedef Elf32_Half Elf32_Versym;
$> cat test.c

void foo() {
    printf("foo from LIBTEST_V1.0\n");
}

$> cat v.map

LIBTEST_V1.0 {
             global:
                foo;
             local:
                *;
};

$> gcc -shared test.c -o libtest.so -Wl,--version-script,v.map
$> readelf -a ./libtest.so

Symbol table '.dynsym' contains 8 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_deregisterTMCloneTab
     2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND puts@GLIBC_2.2.5 (3)
     3: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
     4: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_registerTMCloneTable
     5: 0000000000000000     0 FUNC    WEAK   DEFAULT  UND __cxa_finalize@GLIBC_2.2.5 (3)
     6: 0000000000000000     0 OBJECT  GLOBAL DEFAULT  ABS LIBTEST_V1.0
     7: 00000000000005ba    19 FUNC    GLOBAL DEFAULT   13 foo@@LIBTEST_V1.0

Version symbols section '.gnu.version' contains 8 entries:
 Addr: 0000000000000364  Offset: 0x000364  Link: 3 (.dynsym)
  000:   0 (*local*)       0 (*local*)       3 (GLIBC_2.2.5)   0 (*local*)
  004:   0 (*local*)       3 (GLIBC_2.2.5)   2 (LIBTEST_V1.0)   2 (LIBTEST_V1.0)

$> od -x libtest.so +0x364

0001544 0000 0000 0003 0000 0000 0003 0002 0002
...
1.2.9.5. gnu.version_d

version definition

typedef struct {
  Elf32_Half vd_version;
  Elf32_Half vd_flags;
  Elf32_Half vd_ndx;
  Elf32_Half vd_cnt;
  Elf32_Word vd_hash;
  Elf32_Word vd_aux;
  Elf32_Word vd_next;
} Elf32_Verdef;

Version definition section '.gnu.version_d' contains 2 entries:
  Addr: 0x0000000000000378  Offset: 0x000378  Link: 4 (.dynstr)  000000: Rev: 1  Flags: BASE   Index: 1  Cnt: 1  Name: libtest.so
  0x001c: Rev: 1  Flags: none  Index: 2  Cnt: 1  Name: LIBTEST_V1.0
  Version definition past end of section
1.2.9.6. gnu.version_r

version needs

typedef struct {
  Elf32_Half vn_version;
  Elf32_Half vn_cnt;
  Elf32_Word vn_file;
  Elf32_Word vn_aux;
  Elf32_Word vn_next;
} Elf32_Verneed;
$> readelf -a libtest.so

// libtest.so 需要 GLIBC_2.2.5 版本的 libc.so.6

Version needs section '.gnu.version_r' contains 1 entries:
 Addr: 0x00000000000003b0  Offset: 0x0003b0  Link: 4 (.dynstr)
  000000: Version: 1  File: libc.so.6  Cnt: 1
  0x0010:   Name: GLIBC_2.2.5  Flags: none  Version: 3

$> gcc main.c ./libtest.so
$> readelf -a ./a.out

// a.out 需要 LIBTEST_V1.0 的 ./libtest.so
Version needs section '.gnu.version_r' contains 2 entries:
 Addr: 0x0000000000000438  Offset: 0x000438  Link: 6 (.dynstr)
  000000: Version: 1  File: ./libtest.so  Cnt: 1
  0x0010:   Name: LIBTEST_V1.0  Flags: none  Version: 3
  0x0020: Version: 1  File: libc.so.6  Cnt: 1
  0x0030:   Name: GLIBC_2.2.5  Flags: none  Version: 2

1.3. ELF Segment

segment 的格式为:

typedef struct {
    Elf32_Word p_type;
    Elf32_Off p_offset;
    Elf32_Addr p_vaddr;
    Elf32_Addr p_paddr;
    Elf32_Word p_filesz;
    Elf32_Word p_memsz;
    Elf32_Word p_flags;
    Elf32_Word p_align;
} Elf32_Phdr;

与 readelf 的输出相对应:

Program Headers:
  Type           Offset   VirtAddr           PhysAddr           FileSiz  MemSiz   Flg Align
  RISCV_ATTRIBUT 0x001063 0x0000000000000000 0x0000000000000000 0x000033 0x000000 R   0x1
  LOAD           0x000000 0x0000000000000000 0x0000000000000000 0x000424 0x000424 R E 0x1000
  LOAD           0x000ed0 0x0000000000001ed0 0x0000000000001ed0 0x000168 0x000170 RW  0x1000
  DYNAMIC        0x000ee0 0x0000000000001ee0 0x0000000000001ee0 0x000120 0x000120 RW  0x8
  NOTE           0x0001c8 0x00000000000001c8 0x00000000000001c8 0x000024 0x000024 R   0x4
  GNU_STACK      0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW  0x10
  GNU_RELRO      0x000ed0 0x0000000000001ed0 0x0000000000001ed0 0x000130 0x000130 R   0x1

1.3.1. PT_INTERP

PT_INTERP 指向 interp section, 包括 interpreter 的路径, 例如 /lib/ld-linux-x86-64.so.2.

只有 executable file 才有这一项.

1.3.2. PT_NOTE

PT_NOTE 和 coredump 有关

1.3.3. PT_LOAD

一个 executable file 或 shared object file 一般会包含两个 LOAD 类型的 segment: 例如:

,-—

LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x00000000000006ac 0x00000000000006ac R E 200000
LOAD 0x0000000000000e28 0x0000000000200e28 0x0000000000200e28
0x00000000000001f4 0x0000000000000208 RW 200000

`-—

LOAD 类型的 segment 表示这个 segment 是需要从 elf 文件中映射进来的, 具体的, 第一个 LOAD segment 是 RE (只读可执行的), 主要包括 elf 中的一些代码段 (例如 text, init)和一些只读数据段 (例如 rodata, dynsym) 等.第二个 LOAD segment 是 RW, 包括一些可写的数据段(例如 got, data, bss, dynamic 等).

PT_LOAD 是 elf 加载的第一步: kernel 或 rtld 负责将所有 PT_LOAD 类型的数据 map 进来

1.3.4. PT_DYNAMIC

PT_DYNAMIC 是给 loader 准备的, 是 loader 工作的起点. PT_DYNAMIC 指向 dynamic section (例如前面的 0x000ee0 就是 .dynamic section 的 offset), loader 需要 dynamic section 中的数据来找到 rela, dynsym 等数据.

需要注意的是 dynamic section 以及它包含的各个 DT_XXX 在前面已经通过 PT_LOAD 加载到内存中了, 所以 loader 可以放心的访问所有的 dynamic 信息, dynamic 信息是特意给 loader 准备的.

Backlinks

Android Linker (Android Linker > summary): 1. 根据 dynamic 获得 rel.dynrel.plt 以及 dynsym (以及 dynstr)

1.3.5. PT_GNU_RELRO

GNU_RELRO 要在 link relocations 完成后把一些 section 用 mprotect 修改为只读. 这些 section 主要包括: .init_array, .fini_array 以及 .got.

若使用了 RTLD_LAZY, 则不包括 .got.plt, 因为 LAZY 要求能在运行时修改 .got.plt

$> cat test.c

int main(int argc, char *argv[]) {
    sleep(1000);
    return 0;
}

$> gcc test.c -O0 -g3 -Wl,-z,relro -no-pie
$> readelf -a ./a.out

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000400040 0x0000000000400040
                 0x00000000000001f8 0x00000000000001f8  R E    0x8
  INTERP         0x0000000000000238 0x0000000000400238 0x0000000000400238
                 0x000000000000001c 0x000000000000001c  R      0x1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x00000000000006ec 0x00000000000006ec  R E    0x200000
  LOAD           0x0000000000000e08 0x0000000000600e08 0x0000000000600e08
                 0x0000000000000228 0x0000000000000230  RW     0x200000
  DYNAMIC        0x0000000000000e18 0x0000000000600e18 0x0000000000600e18
                 0x00000000000001d0 0x00000000000001d0  RW     0x8
  ...
  GNU_RELRO      0x0000000000000e08 0x0000000000600e08 0x0000000000600e08
                 0x00000000000001f8 0x00000000000001f8  R      0x1
  ~~~~~~~~~

 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 .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame
   03     .init_array .fini_array .dynamic .got .got.plt .data .bss
   ...
   08     .init_array .fini_array .dynamic .got
   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Section Headers:
  ...
  [21] .got              PROGBITS         0000000000600fe8  00000fe8
       0000000000000018  0000000000000008  WA       0     0     8
  [22] .got.plt          PROGBITS         0000000000601000  00001000
                                          ~~~~~~~~~~~~~~~~
       0000000000000020  0000000000000008  WA       0     0     8
  [23] .data             PROGBITS         0000000000601020  00001020

$> strace ./a.out

arch_prctl(ARCH_SET_FS, 0x7ff1d5243440) = 0
mprotect(0x7ff1d5056000, 16384, PROT_READ) = 0
mprotect(0x600000, 4096, PROT_READ)     = 0
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
mprotect(0x7ff1d5283000, 4096, PROT_READ) = 0
munmap(0x7ff1d5244000, 255800)          = 0

// 0x600000 是 GNU_RELRO 的地址 (0x600e08) 按 PAGE_SIZE round 后的结果,
所以实际 protect 的区域为 0x600000 ~ 0x601000, 而 0x601000 刚好是
.got.plt 的地址, 所以编译器已经考虑到 mprotect 需要 PAGE_SIZE 对齐的要
求.


$> cat test.c

int main(int argc, char *argv[]) {
    sleep(1000);
    return 0;
}

$> gcc test.c -O0 -g3 -Wl,-z,relro -z now

$> readelf -a a.out
  GNU_RELRO      0x0000000000000de0 0x0000000000200de0 0x0000000000200de0
                 0x0000000000000220 0x0000000000000220  R      0x1

03     .init_array .fini_array .dynamic .got .data .bss
...
08     .init_array .fini_array .dynamic .got

// 0x200de0 + 0x220 = 0x201000, 这个地址刚好是 .data 的首地址, 所以使
// 用 -z now 后 plt 使用的 got 在包含在 relro 之内了

Backlinks

BFD Tutorial (BFD Tutorial > Overview): bfd 支持不同平台上的不同的 object 文件格式, 包括 elf, a.out, coff 等.

objcopy (binutils > usage > objcopy > 生成 bin): 第一个 PT_LOAD 从 0 开始 (而不是 0x318, .interp), 因为它包含了 elf 文件开头的信 息, 例如 `ELF` magic number 和 elf 的 program header, 参考 ELF

Author: [email protected]
Date: 2017-03-31 Fri 00:00
Last updated: 2024-09-10 Tue 22:08

知识共享许可协议