微软在二零一八年揭橥了Bash On Windows, 那项技能允许在Windows上运营Linux程序,
作者信任已经有不可胜举作品解释过Bash On Windows的法则,
而前天的那篇小说将会讲课怎么样团结已毕多个简约的原生Linux程序运营器,
这么些运维器在用户层完毕, 原理和Bash On
Windows不完全等同,比较相近Linux上的Wine.

微软在2018年公告了Bash On Windows, 那项技艺允许在Windows上运维Linux程序,
作者信任已经有诸多篇章解释过Bash On Windows的法则,
而明天的那篇小说将会讲课怎么着协调落成3个大致的原生Linux程序运维器,
那些运转器在用户层达成, 原理和Bash On
Windows不完全平等,相比相近Linux上的Wine.

概要

SO文件是Linux下共享库文件,它的文件格式被称为ELF文件格式。由于Android操作系统的尾部基于Linux系统,所以SO文件可以运作在Android平台上。Android系统也同等开放了C/C++接口供开发者开发Native程序。由于基于虚拟机的编程语言JAVA更易于被人反编译,因而更进一步多的运用将中间的着力代码以C/C++为编程语言,并且以SO文件的款式供

上层JAVA代码调用,以确保安全性。本文以SO文件格式为脉络,梳理SO文件格式中每一部分的成效与其背后所蕴藏的技艺(注:本文对SO文件的格式分析基于Android平台A本田CR-VM-V7架构)。

① 、温故而知新

以身作则程序完整的代码在github上, 地址是

以身作则程序完整的代码在github上, 地址是

SO文件格式综述

SO文件格式即ELF文件格式,它是Linux下可执行文件,共享库文件和对象文件的集合格式。根据看待ELF文件的两样措施,ELF文件可以分为链接视图和装载视图。链接视图是链接器从链接的角度看待静态的ELF文件。从链接视图看ELF文件,ELF文件由七个section组成,差距的section拥有不一样的名号,权限。而装载视图是操作系统从加载ELF文书到内存的角度看待动态的ELF文件。从装载视图看ELF文件,ELF文件由多个segment,每一个segment都拥有不相同的权柄,名称。实际上如上图所示,二个segment是对五个有着同等权限的section的汇集。

ELF头表

ELF头表记录了ELF文件的主导消息,包含魔数,目标文件类型(可执行文件,共享库文件或然目的文件),文件的对象体系布局,程序入口地址(共享库文件为此值为0),然后是section表大小和数据,程序头表的轻重和数量,分别对应的是链接视图和装载视图。

Section表

Section表记录了每3个Section的骨干消息,名称,类型,字节数,虚拟地址偏移和文书偏移。文件偏移指的是在ELF文件中,Section距离ELF文件初叶地方的字节数,而虚拟地址偏移指的是当此section被加载到内存中后,该Section距离ELF起先地方的字节数。由于有些section只设有于文件中,而不会被系统加载到内存中,因而虚拟地址偏移恐怕为0.

程序头表

程序头表是装载视图下,系统举行segment解析的输入,它交给了每1个segment的类型,文件偏移,虚拟地址偏移和对齐等。通过对先后头表的遍历,大家得以收获ELF文件全体的segment。

字符串表 .strtab

字符串表记录了ELF文件中的每三个常量字符串值,以”\0″标识字符串结尾。

1. 内存不够如何是好

  • 内存不难分配政策的题材
    • 地址空间不隔离
    • 内存使用频率低
    • 程序运营的地方不明确
  • 至于隔离 : 分为 虚拟地址空间 和 物理地址空间
  • 分段 : 把一段程序所急需的内存空间大小映射到有些地方空间
  • 分页 :
    把地点空间人为地等分成固定大小的页,每一页大小由硬件决定,或硬件协理三种分寸的页,由操作系统决定页的轻重缓急,如今大概全体的
    PC 上的操作系统都采纳 4KB 大小的页。

    • 虚拟页 VP
    • 物理页 PP
    • 磁盘页 DP

千帆竞发精通ELF格式

率先让大家先驾驭什么是原生Linux程序,
以下表达摘自维基百科

In computing, the Executable and Linkable Format (ELF, formerly named Extensible Linking Format), is a common standard file format for executable files, object code, shared libraries, and core dumps. First published in the specification for the application binary interface (ABI) of the Unix operating system version named System V Release 4 (SVR4),[2] and later in the Tool Interface Standard,[1] it was quickly accepted among different vendors of Unix systems. In 1999, it was chosen as the standard binary file format for Unix and Unix-like systems on x86 processors by the 86open project.

By design, ELF is flexible, extensible, and cross-platform, not bound to any given central processing unit (CPU) or instruction set architecture. This has allowed it to be adopted by many different operating systems on many different hardware platforms.

Linux的可执行文件格式拔取了ELF格式,
而Windows采用了PE格式,
相当于大家平常使用的exe文件的格式.

ELF格式的社团如下

澳门金沙国际 1

约莫上可以分成这么些部分

  • ELF头,在文书的最初叶,储存了系列和版本等音信
  • 先后头, 供程序运维时解释器(interpreter)使用
  • 节头, 供程序编译时链接器(linker)使用, 运转时不须要读节头
  • 节内容, 差其余节成效都不平等
    • .text 代码节,保存了首要的程序代码
    • .rodata 保存了只读的数码,例如字符串(const char*)
    • .data 保存了可读写的多寡,例如全局变量
    • 还有任何种种各类的节

让大家来其实看一下Linux可执行程序的金科玉律
以下的编译环境是Ubuntu 16.04 x64 + gcc 5.4.0,
编译环境不平等恐怕会汲取差其余结果

率先创制hello.c,写入以下的代码

#include <stdio.h>

int max(int x, int y) {
    return x > y ? x : y;
}

int main() {
    printf("max is %d\n", max(123, 321));
    printf("test many arguments %d %d %d %s %s %s %s %s %s\n", 1, 2, 3, "a", "b", "c", "d", "e", "f");
    return 100;
}

下一场使用gcc编译那份代码

gcc hello.c

编译完结后您可以看出hello.c一旁多了3个a.out,
这就是linux的可执行文件了, 以往可以在linux上运维它

./a.out

你可以看到以下输出

max is 321
test many arguments 1 2 3 a b c d e f

大家来看望a.out含有了什么样,解析ELF文件可以运用readelf命令

readelf -a ./a.out

可以看出输出了以下的音讯

ELF 头:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  类别:                              ELF64
  数据:                              2 补码,小端序 (little endian)
  版本:                              1 (current)
  OS/ABI:                            UNIX - System V
  ABI 版本:                          0
  类型:                              EXEC (可执行文件)
  系统架构:                          Advanced Micro Devices X86-64
  版本:                              0x1
  入口点地址:               0x400430
  程序头起点:          64 (bytes into file)
  Start of section headers:          6648 (bytes into file)
  标志:             0x0
  本头的大小:       64 (字节)
  程序头大小:       56 (字节)
  Number of program headers:         9
  节头大小:         64 (字节)
  节头数量:         31
  字符串表索引节头: 28

节头:
  [号] 名称              类型             地址              偏移量
       大小              全体大小          旗标   链接   信息   对齐
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .interp           PROGBITS         0000000000400238  00000238
       000000000000001c  0000000000000000   A       0     0     1
  [ 2] .note.ABI-tag     NOTE             0000000000400254  00000254
       0000000000000020  0000000000000000   A       0     0     4
  [ 3] .note.gnu.build-i NOTE             0000000000400274  00000274
       0000000000000024  0000000000000000   A       0     0     4
  [ 4] .gnu.hash         GNU_HASH         0000000000400298  00000298
       000000000000001c  0000000000000000   A       5     0     8
  [ 5] .dynsym           DYNSYM           00000000004002b8  000002b8
       0000000000000060  0000000000000018   A       6     1     8
  [ 6] .dynstr           STRTAB           0000000000400318  00000318
       000000000000003f  0000000000000000   A       0     0     1
  [ 7] .gnu.version      VERSYM           0000000000400358  00000358
       0000000000000008  0000000000000002   A       5     0     2
  [ 8] .gnu.version_r    VERNEED          0000000000400360  00000360
       0000000000000020  0000000000000000   A       6     1     8
  [ 9] .rela.dyn         RELA             0000000000400380  00000380
       0000000000000018  0000000000000018   A       5     0     8
  [10] .rela.plt         RELA             0000000000400398  00000398
       0000000000000030  0000000000000018  AI       5    24     8
  [11] .init             PROGBITS         00000000004003c8  000003c8
       000000000000001a  0000000000000000  AX       0     0     4
  [12] .plt              PROGBITS         00000000004003f0  000003f0
       0000000000000030  0000000000000010  AX       0     0     16
  [13] .plt.got          PROGBITS         0000000000400420  00000420
       0000000000000008  0000000000000000  AX       0     0     8
  [14] .text             PROGBITS         0000000000400430  00000430
       00000000000001f2  0000000000000000  AX       0     0     16
  [15] .fini             PROGBITS         0000000000400624  00000624
       0000000000000009  0000000000000000  AX       0     0     4
  [16] .rodata           PROGBITS         0000000000400630  00000630
       0000000000000050  0000000000000000   A       0     0     8
  [17] .eh_frame_hdr     PROGBITS         0000000000400680  00000680
       000000000000003c  0000000000000000   A       0     0     4
  [18] .eh_frame         PROGBITS         00000000004006c0  000006c0
       0000000000000114  0000000000000000   A       0     0     8
  [19] .init_array       INIT_ARRAY       0000000000600e10  00000e10
       0000000000000008  0000000000000000  WA       0     0     8
  [20] .fini_array       FINI_ARRAY       0000000000600e18  00000e18
       0000000000000008  0000000000000000  WA       0     0     8
  [21] .jcr              PROGBITS         0000000000600e20  00000e20
       0000000000000008  0000000000000000  WA       0     0     8
  [22] .dynamic          DYNAMIC          0000000000600e28  00000e28
       00000000000001d0  0000000000000010  WA       6     0     8
  [23] .got              PROGBITS         0000000000600ff8  00000ff8
       0000000000000008  0000000000000008  WA       0     0     8
  [24] .got.plt          PROGBITS         0000000000601000  00001000
       0000000000000028  0000000000000008  WA       0     0     8
  [25] .data             PROGBITS         0000000000601028  00001028
       0000000000000010  0000000000000000  WA       0     0     8
  [26] .bss              NOBITS           0000000000601038  00001038
       0000000000000008  0000000000000000  WA       0     0     1
  [27] .comment          PROGBITS         0000000000000000  00001038
       0000000000000034  0000000000000001  MS       0     0     1
  [28] .shstrtab         STRTAB           0000000000000000  000018ea
       000000000000010c  0000000000000000           0     0     1
  [29] .symtab           SYMTAB           0000000000000000  00001070
       0000000000000660  0000000000000018          30    47     8
  [30] .strtab           STRTAB           0000000000000000  000016d0
       000000000000021a  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

There are no section groups in this file.

程序头:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000400040 0x0000000000400040
                 0x00000000000001f8 0x00000000000001f8  R E    8
  INTERP         0x0000000000000238 0x0000000000400238 0x0000000000400238
                 0x000000000000001c 0x000000000000001c  R      1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x00000000000007d4 0x00000000000007d4  R E    200000
  LOAD           0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
                 0x0000000000000228 0x0000000000000230  RW     200000
  DYNAMIC        0x0000000000000e28 0x0000000000600e28 0x0000000000600e28
                 0x00000000000001d0 0x00000000000001d0  RW     8
  NOTE           0x0000000000000254 0x0000000000400254 0x0000000000400254
                 0x0000000000000044 0x0000000000000044  R      4
  GNU_EH_FRAME   0x0000000000000680 0x0000000000400680 0x0000000000400680
                 0x000000000000003c 0x000000000000003c  R      4
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     10
  GNU_RELRO      0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
                 0x00000000000001f0 0x00000000000001f0  R      1

 Section to Segment mapping:
  段节...
   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 .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame 
   03     .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss 
   04     .dynamic 
   05     .note.ABI-tag .note.gnu.build-id 
   06     .eh_frame_hdr 
   07     
   08     .init_array .fini_array .jcr .dynamic .got 

Dynamic section at offset 0xe28 contains 24 entries:
  标记        类型                         名称/值
 0x0000000000000001 (NEEDED)             共享库:[libc.so.6]
 0x000000000000000c (INIT)               0x4003c8
 0x000000000000000d (FINI)               0x400624
 0x0000000000000019 (INIT_ARRAY)         0x600e10
 0x000000000000001b (INIT_ARRAYSZ)       8 (bytes)
 0x000000000000001a (FINI_ARRAY)         0x600e18
 0x000000000000001c (FINI_ARRAYSZ)       8 (bytes)
 0x000000006ffffef5 (GNU_HASH)           0x400298
 0x0000000000000005 (STRTAB)             0x400318
 0x0000000000000006 (SYMTAB)             0x4002b8
 0x000000000000000a (STRSZ)              63 (bytes)
 0x000000000000000b (SYMENT)             24 (bytes)
 0x0000000000000015 (DEBUG)              0x0
 0x0000000000000003 (PLTGOT)             0x601000
 0x0000000000000002 (PLTRELSZ)           48 (bytes)
 0x0000000000000014 (PLTREL)             RELA
 0x0000000000000017 (JMPREL)             0x400398
 0x0000000000000007 (RELA)               0x400380
 0x0000000000000008 (RELASZ)             24 (bytes)
 0x0000000000000009 (RELAENT)            24 (bytes)
 0x000000006ffffffe (VERNEED)            0x400360
 0x000000006fffffff (VERNEEDNUM)         1
 0x000000006ffffff0 (VERSYM)             0x400358
 0x0000000000000000 (NULL)               0x0

重定位节 '.rela.dyn' 位于偏移量 0x380 含有 1 个条目:
  偏移量          信息           类型           符号值        符号名称 + 加数
000000600ff8  000300000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0

重定位节 '.rela.plt' 位于偏移量 0x398 含有 2 个条目:
  偏移量          信息           类型           符号值        符号名称 + 加数
000000601018  000100000007 R_X86_64_JUMP_SLO 0000000000000000 printf@GLIBC_2.2.5 + 0
000000601020  000200000007 R_X86_64_JUMP_SLO 0000000000000000 __libc_start_main@GLIBC_2.2.5 + 0

The decoding of unwind sections for machine type Advanced Micro Devices X86-64 is not currently supported.

Symbol table '.dynsym' contains 4 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND printf@GLIBC_2.2.5 (2)
     2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@GLIBC_2.2.5 (2)
     3: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__

Symbol table '.symtab' contains 68 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000400238     0 SECTION LOCAL  DEFAULT    1 
     2: 0000000000400254     0 SECTION LOCAL  DEFAULT    2 
     3: 0000000000400274     0 SECTION LOCAL  DEFAULT    3 
     4: 0000000000400298     0 SECTION LOCAL  DEFAULT    4 
     5: 00000000004002b8     0 SECTION LOCAL  DEFAULT    5 
     6: 0000000000400318     0 SECTION LOCAL  DEFAULT    6 
     7: 0000000000400358     0 SECTION LOCAL  DEFAULT    7 
     8: 0000000000400360     0 SECTION LOCAL  DEFAULT    8 
     9: 0000000000400380     0 SECTION LOCAL  DEFAULT    9 
    10: 0000000000400398     0 SECTION LOCAL  DEFAULT   10 
    11: 00000000004003c8     0 SECTION LOCAL  DEFAULT   11 
    12: 00000000004003f0     0 SECTION LOCAL  DEFAULT   12 
    13: 0000000000400420     0 SECTION LOCAL  DEFAULT   13 
    14: 0000000000400430     0 SECTION LOCAL  DEFAULT   14 
    15: 0000000000400624     0 SECTION LOCAL  DEFAULT   15 
    16: 0000000000400630     0 SECTION LOCAL  DEFAULT   16 
    17: 0000000000400680     0 SECTION LOCAL  DEFAULT   17 
    18: 00000000004006c0     0 SECTION LOCAL  DEFAULT   18 
    19: 0000000000600e10     0 SECTION LOCAL  DEFAULT   19 
    20: 0000000000600e18     0 SECTION LOCAL  DEFAULT   20 
    21: 0000000000600e20     0 SECTION LOCAL  DEFAULT   21 
    22: 0000000000600e28     0 SECTION LOCAL  DEFAULT   22 
    23: 0000000000600ff8     0 SECTION LOCAL  DEFAULT   23 
    24: 0000000000601000     0 SECTION LOCAL  DEFAULT   24 
    25: 0000000000601028     0 SECTION LOCAL  DEFAULT   25 
    26: 0000000000601038     0 SECTION LOCAL  DEFAULT   26 
    27: 0000000000000000     0 SECTION LOCAL  DEFAULT   27 
    28: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS crtstuff.c
    29: 0000000000600e20     0 OBJECT  LOCAL  DEFAULT   21 __JCR_LIST__
    30: 0000000000400460     0 FUNC    LOCAL  DEFAULT   14 deregister_tm_clones
    31: 00000000004004a0     0 FUNC    LOCAL  DEFAULT   14 register_tm_clones
    32: 00000000004004e0     0 FUNC    LOCAL  DEFAULT   14 __do_global_dtors_aux
    33: 0000000000601038     1 OBJECT  LOCAL  DEFAULT   26 completed.7585
    34: 0000000000600e18     0 OBJECT  LOCAL  DEFAULT   20 __do_global_dtors_aux_fin
    35: 0000000000400500     0 FUNC    LOCAL  DEFAULT   14 frame_dummy
    36: 0000000000600e10     0 OBJECT  LOCAL  DEFAULT   19 __frame_dummy_init_array_
    37: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS hello.c
    38: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS crtstuff.c
    39: 00000000004007d0     0 OBJECT  LOCAL  DEFAULT   18 __FRAME_END__
    40: 0000000000600e20     0 OBJECT  LOCAL  DEFAULT   21 __JCR_END__
    41: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS 
    42: 0000000000600e18     0 NOTYPE  LOCAL  DEFAULT   19 __init_array_end
    43: 0000000000600e28     0 OBJECT  LOCAL  DEFAULT   22 _DYNAMIC
    44: 0000000000600e10     0 NOTYPE  LOCAL  DEFAULT   19 __init_array_start
    45: 0000000000400680     0 NOTYPE  LOCAL  DEFAULT   17 __GNU_EH_FRAME_HDR
    46: 0000000000601000     0 OBJECT  LOCAL  DEFAULT   24 _GLOBAL_OFFSET_TABLE_
    47: 0000000000400620     2 FUNC    GLOBAL DEFAULT   14 __libc_csu_fini
    48: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_deregisterTMCloneTab
    49: 0000000000601028     0 NOTYPE  WEAK   DEFAULT   25 data_start
    50: 0000000000601038     0 NOTYPE  GLOBAL DEFAULT   25 _edata
    51: 0000000000400624     0 FUNC    GLOBAL DEFAULT   15 _fini
    52: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND printf@@GLIBC_2.2.5
    53: 0000000000400526    22 FUNC    GLOBAL DEFAULT   14 max
    54: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@@GLIBC_
    55: 0000000000601028     0 NOTYPE  GLOBAL DEFAULT   25 __data_start
    56: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
    57: 0000000000601030     0 OBJECT  GLOBAL HIDDEN    25 __dso_handle
    58: 0000000000400630     4 OBJECT  GLOBAL DEFAULT   16 _IO_stdin_used
    59: 00000000004005b0   101 FUNC    GLOBAL DEFAULT   14 __libc_csu_init
    60: 0000000000601040     0 NOTYPE  GLOBAL DEFAULT   26 _end
    61: 0000000000400430    42 FUNC    GLOBAL DEFAULT   14 _start
    62: 0000000000601038     0 NOTYPE  GLOBAL DEFAULT   26 __bss_start
    63: 000000000040053c   109 FUNC    GLOBAL DEFAULT   14 main
    64: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _Jv_RegisterClasses
    65: 0000000000601038     0 OBJECT  GLOBAL HIDDEN    25 __TMC_END__
    66: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_registerTMCloneTable
    67: 00000000004003c8     0 FUNC    GLOBAL DEFAULT   11 _init

Version symbols section '.gnu.version' contains 4 entries:
 地址: 0000000000400358  Offset: 0x000358  Link: 5 (.dynsym)
  000:   0 (*本地*)       2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   0 (*本地*)    

Version needs section '.gnu.version_r' contains 1 entries:
 地址:0x0000000000400360  Offset: 0x000360  Link: 6 (.dynstr)
  000000: 版本: 1  文件:libc.so.6  计数:1
  0x0010:名称:GLIBC_2.2.5  标志:无  版本:2

Displaying notes found at file offset 0x00000254 with length 0x00000020:
  Owner                 Data size   Description
  GNU                  0x00000010   NT_GNU_ABI_TAG (ABI version tag)
    OS: Linux, ABI: 2.6.32

Displaying notes found at file offset 0x00000274 with length 0x00000024:
  Owner                 Data size   Description
  GNU                  0x00000014   NT_GNU_BUILD_ID (unique build ID bitstring)
    Build ID: debd3d7912be860a432b5c685a6cff7fd9418528

从上面的新闻中咱们得以知晓那些文件的品种是ELF64,
相当于6三人的可执行程序, 并且有7个程序头和三十多个节头,
各种节的机能大家可以在网上找到资料, 那篇文章中只涉及到以下的节

  • .init 程序开头化的代码
  • .rela.dyn 要求重一直的变量列表
  • .rela.plt 需求重一贯的函数列表
  • .plt 调用动态链接函数的代码
  • .text 保存了第三的程序代码
  • .init 保存了程序的初步化代码, 用于初步化全局变量等
  • .fini 保存了程序的停下代码, 用于析构全局变量等
  • .rodata 保存了只读的多少,例如字符串(const char*)
  • .data 保存了可读写的数量,例如全局变量
  • .dynsym 动态链接的符号表
  • .dynstr 动态链接的号子名称字符串
  • .dynamic 动态链接所需求的音信,供程序运维时使用(不要求拜访节头)

开班了然ELF格式

率先让大家先明白怎么样是原生Linux程序,
以下表达摘自维基百科

In computing, the Executable and Linkable Format (ELF, formerly named Extensible Linking Format), is a common standard file format for executable files, object code, shared libraries, and core dumps. First published in the specification for the application binary interface (ABI) of the Unix operating system version named System V Release 4 (SVR4),[2] and later in the Tool Interface Standard,[1] it was quickly accepted among different vendors of Unix systems. In 1999, it was chosen as the standard binary file format for Unix and Unix-like systems on x86 processors by the 86open project.

By design, ELF is flexible, extensible, and cross-platform, not bound to any given central processing unit (CPU) or instruction set architecture. This has allowed it to be adopted by many different operating systems on many different hardware platforms.

Linux的可执行文件格式拔取了ELF格式,
而Windows采用了PE格式,
约等于大家寻常应用的exe文件的格式.

ELF格式的布局如下

澳门金沙国际 2

约莫上得以分为那些部分

  • ELF头,在文书的最伊始,储存了体系和版本等新闻
  • 先后头, 供程序运营时解释器(interpreter)使用
  • 节头, 供程序编译时链接器(linker)使用, 运维时不需要读节头
  • 节内容, 差距的节成效都不雷同
    • .text 代码节,保存了重在的程序代码
    • .rodata 保存了只读的多寡,例如字符串(const char*)
    • .data 保存了可读写的数量,例如全局变量
    • 还有其它各类各种的节

让大家来实在看一下Linux可执行程序的楷模
以下的编译环境是Ubuntu 16.04 x64 + gcc 5.4.0,
编译环境不雷同恐怕会得出差距的结果

首先创制hello.c,写入以下的代码

#include <stdio.h>

int max(int x, int y) {
    return x > y ? x : y;
}

int main() {
    printf("max is %d\n", max(123, 321));
    printf("test many arguments %d %d %d %s %s %s %s %s %s\n", 1, 2, 3, "a", "b", "c", "d", "e", "f");
    return 100;
}

然后采用gcc编译这份代码

gcc hello.c

编译落成后你可以看看hello.c旁边多了三个a.out,
那就是linux的可执行文件了, 以往可以在linux上运转它

./a.out

您可以看来以下输出

max is 321
test many arguments 1 2 3 a b c d e f

咱俩来探望a.out饱含了怎么,解析ELF文件可以接纳readelf命令

readelf -a ./a.out

可以看来输出了以下的信息

ELF 头:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  类别:                              ELF64
  数据:                              2 补码,小端序 (little endian)
  版本:                              1 (current)
  OS/ABI:                            UNIX - System V
  ABI 版本:                          0
  类型:                              EXEC (可执行文件)
  系统架构:                          Advanced Micro Devices X86-64
  版本:                              0x1
  入口点地址:               0x400430
  程序头起点:          64 (bytes into file)
  Start of section headers:          6648 (bytes into file)
  标志:             0x0
  本头的大小:       64 (字节)
  程序头大小:       56 (字节)
  Number of program headers:         9
  节头大小:         64 (字节)
  节头数量:         31
  字符串表索引节头: 28

节头:
  [号] 名称              类型             地址              偏移量
       大小              全体大小          旗标   链接   信息   对齐
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .interp           PROGBITS         0000000000400238  00000238
       000000000000001c  0000000000000000   A       0     0     1
  [ 2] .note.ABI-tag     NOTE             0000000000400254  00000254
       0000000000000020  0000000000000000   A       0     0     4
  [ 3] .note.gnu.build-i NOTE             0000000000400274  00000274
       0000000000000024  0000000000000000   A       0     0     4
  [ 4] .gnu.hash         GNU_HASH         0000000000400298  00000298
       000000000000001c  0000000000000000   A       5     0     8
  [ 5] .dynsym           DYNSYM           00000000004002b8  000002b8
       0000000000000060  0000000000000018   A       6     1     8
  [ 6] .dynstr           STRTAB           0000000000400318  00000318
       000000000000003f  0000000000000000   A       0     0     1
  [ 7] .gnu.version      VERSYM           0000000000400358  00000358
       0000000000000008  0000000000000002   A       5     0     2
  [ 8] .gnu.version_r    VERNEED          0000000000400360  00000360
       0000000000000020  0000000000000000   A       6     1     8
  [ 9] .rela.dyn         RELA             0000000000400380  00000380
       0000000000000018  0000000000000018   A       5     0     8
  [10] .rela.plt         RELA             0000000000400398  00000398
       0000000000000030  0000000000000018  AI       5    24     8
  [11] .init             PROGBITS         00000000004003c8  000003c8
       000000000000001a  0000000000000000  AX       0     0     4
  [12] .plt              PROGBITS         00000000004003f0  000003f0
       0000000000000030  0000000000000010  AX       0     0     16
  [13] .plt.got          PROGBITS         0000000000400420  00000420
       0000000000000008  0000000000000000  AX       0     0     8
  [14] .text             PROGBITS         0000000000400430  00000430
       00000000000001f2  0000000000000000  AX       0     0     16
  [15] .fini             PROGBITS         0000000000400624  00000624
       0000000000000009  0000000000000000  AX       0     0     4
  [16] .rodata           PROGBITS         0000000000400630  00000630
       0000000000000050  0000000000000000   A       0     0     8
  [17] .eh_frame_hdr     PROGBITS         0000000000400680  00000680
       000000000000003c  0000000000000000   A       0     0     4
  [18] .eh_frame         PROGBITS         00000000004006c0  000006c0
       0000000000000114  0000000000000000   A       0     0     8
  [19] .init_array       INIT_ARRAY       0000000000600e10  00000e10
       0000000000000008  0000000000000000  WA       0     0     8
  [20] .fini_array       FINI_ARRAY       0000000000600e18  00000e18
       0000000000000008  0000000000000000  WA       0     0     8
  [21] .jcr              PROGBITS         0000000000600e20  00000e20
       0000000000000008  0000000000000000  WA       0     0     8
  [22] .dynamic          DYNAMIC          0000000000600e28  00000e28
       00000000000001d0  0000000000000010  WA       6     0     8
  [23] .got              PROGBITS         0000000000600ff8  00000ff8
       0000000000000008  0000000000000008  WA       0     0     8
  [24] .got.plt          PROGBITS         0000000000601000  00001000
       0000000000000028  0000000000000008  WA       0     0     8
  [25] .data             PROGBITS         0000000000601028  00001028
       0000000000000010  0000000000000000  WA       0     0     8
  [26] .bss              NOBITS           0000000000601038  00001038
       0000000000000008  0000000000000000  WA       0     0     1
  [27] .comment          PROGBITS         0000000000000000  00001038
       0000000000000034  0000000000000001  MS       0     0     1
  [28] .shstrtab         STRTAB           0000000000000000  000018ea
       000000000000010c  0000000000000000           0     0     1
  [29] .symtab           SYMTAB           0000000000000000  00001070
       0000000000000660  0000000000000018          30    47     8
  [30] .strtab           STRTAB           0000000000000000  000016d0
       000000000000021a  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

There are no section groups in this file.

程序头:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000400040 0x0000000000400040
                 0x00000000000001f8 0x00000000000001f8  R E    8
  INTERP         0x0000000000000238 0x0000000000400238 0x0000000000400238
                 0x000000000000001c 0x000000000000001c  R      1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x00000000000007d4 0x00000000000007d4  R E    200000
  LOAD           0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
                 0x0000000000000228 0x0000000000000230  RW     200000
  DYNAMIC        0x0000000000000e28 0x0000000000600e28 0x0000000000600e28
                 0x00000000000001d0 0x00000000000001d0  RW     8
  NOTE           0x0000000000000254 0x0000000000400254 0x0000000000400254
                 0x0000000000000044 0x0000000000000044  R      4
  GNU_EH_FRAME   0x0000000000000680 0x0000000000400680 0x0000000000400680
                 0x000000000000003c 0x000000000000003c  R      4
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     10
  GNU_RELRO      0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
                 0x00000000000001f0 0x00000000000001f0  R      1

 Section to Segment mapping:
  段节...
   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 .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame 
   03     .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss 
   04     .dynamic 
   05     .note.ABI-tag .note.gnu.build-id 
   06     .eh_frame_hdr 
   07     
   08     .init_array .fini_array .jcr .dynamic .got 

Dynamic section at offset 0xe28 contains 24 entries:
  标记        类型                         名称/值
 0x0000000000000001 (NEEDED)             共享库:[libc.so.6]
 0x000000000000000c (INIT)               0x4003c8
 0x000000000000000d (FINI)               0x400624
 0x0000000000000019 (INIT_ARRAY)         0x600e10
 0x000000000000001b (INIT_ARRAYSZ)       8 (bytes)
 0x000000000000001a (FINI_ARRAY)         0x600e18
 0x000000000000001c (FINI_ARRAYSZ)       8 (bytes)
 0x000000006ffffef5 (GNU_HASH)           0x400298
 0x0000000000000005 (STRTAB)             0x400318
 0x0000000000000006 (SYMTAB)             0x4002b8
 0x000000000000000a (STRSZ)              63 (bytes)
 0x000000000000000b (SYMENT)             24 (bytes)
 0x0000000000000015 (DEBUG)              0x0
 0x0000000000000003 (PLTGOT)             0x601000
 0x0000000000000002 (PLTRELSZ)           48 (bytes)
 0x0000000000000014 (PLTREL)             RELA
 0x0000000000000017 (JMPREL)             0x400398
 0x0000000000000007 (RELA)               0x400380
 0x0000000000000008 (RELASZ)             24 (bytes)
 0x0000000000000009 (RELAENT)            24 (bytes)
 0x000000006ffffffe (VERNEED)            0x400360
 0x000000006fffffff (VERNEEDNUM)         1
 0x000000006ffffff0 (VERSYM)             0x400358
 0x0000000000000000 (NULL)               0x0

重定位节 '.rela.dyn' 位于偏移量 0x380 含有 1 个条目:
  偏移量          信息           类型           符号值        符号名称 + 加数
000000600ff8  000300000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0

重定位节 '.rela.plt' 位于偏移量 0x398 含有 2 个条目:
  偏移量          信息           类型           符号值        符号名称 + 加数
000000601018  000100000007 R_X86_64_JUMP_SLO 0000000000000000 printf@GLIBC_2.2.5 + 0
000000601020  000200000007 R_X86_64_JUMP_SLO 0000000000000000 __libc_start_main@GLIBC_2.2.5 + 0

The decoding of unwind sections for machine type Advanced Micro Devices X86-64 is not currently supported.

Symbol table '.dynsym' contains 4 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND printf@GLIBC_2.2.5 (2)
     2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@GLIBC_2.2.5 (2)
     3: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__

Symbol table '.symtab' contains 68 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000400238     0 SECTION LOCAL  DEFAULT    1 
     2: 0000000000400254     0 SECTION LOCAL  DEFAULT    2 
     3: 0000000000400274     0 SECTION LOCAL  DEFAULT    3 
     4: 0000000000400298     0 SECTION LOCAL  DEFAULT    4 
     5: 00000000004002b8     0 SECTION LOCAL  DEFAULT    5 
     6: 0000000000400318     0 SECTION LOCAL  DEFAULT    6 
     7: 0000000000400358     0 SECTION LOCAL  DEFAULT    7 
     8: 0000000000400360     0 SECTION LOCAL  DEFAULT    8 
     9: 0000000000400380     0 SECTION LOCAL  DEFAULT    9 
    10: 0000000000400398     0 SECTION LOCAL  DEFAULT   10 
    11: 00000000004003c8     0 SECTION LOCAL  DEFAULT   11 
    12: 00000000004003f0     0 SECTION LOCAL  DEFAULT   12 
    13: 0000000000400420     0 SECTION LOCAL  DEFAULT   13 
    14: 0000000000400430     0 SECTION LOCAL  DEFAULT   14 
    15: 0000000000400624     0 SECTION LOCAL  DEFAULT   15 
    16: 0000000000400630     0 SECTION LOCAL  DEFAULT   16 
    17: 0000000000400680     0 SECTION LOCAL  DEFAULT   17 
    18: 00000000004006c0     0 SECTION LOCAL  DEFAULT   18 
    19: 0000000000600e10     0 SECTION LOCAL  DEFAULT   19 
    20: 0000000000600e18     0 SECTION LOCAL  DEFAULT   20 
    21: 0000000000600e20     0 SECTION LOCAL  DEFAULT   21 
    22: 0000000000600e28     0 SECTION LOCAL  DEFAULT   22 
    23: 0000000000600ff8     0 SECTION LOCAL  DEFAULT   23 
    24: 0000000000601000     0 SECTION LOCAL  DEFAULT   24 
    25: 0000000000601028     0 SECTION LOCAL  DEFAULT   25 
    26: 0000000000601038     0 SECTION LOCAL  DEFAULT   26 
    27: 0000000000000000     0 SECTION LOCAL  DEFAULT   27 
    28: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS crtstuff.c
    29: 0000000000600e20     0 OBJECT  LOCAL  DEFAULT   21 __JCR_LIST__
    30: 0000000000400460     0 FUNC    LOCAL  DEFAULT   14 deregister_tm_clones
    31: 00000000004004a0     0 FUNC    LOCAL  DEFAULT   14 register_tm_clones
    32: 00000000004004e0     0 FUNC    LOCAL  DEFAULT   14 __do_global_dtors_aux
    33: 0000000000601038     1 OBJECT  LOCAL  DEFAULT   26 completed.7585
    34: 0000000000600e18     0 OBJECT  LOCAL  DEFAULT   20 __do_global_dtors_aux_fin
    35: 0000000000400500     0 FUNC    LOCAL  DEFAULT   14 frame_dummy
    36: 0000000000600e10     0 OBJECT  LOCAL  DEFAULT   19 __frame_dummy_init_array_
    37: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS hello.c
    38: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS crtstuff.c
    39: 00000000004007d0     0 OBJECT  LOCAL  DEFAULT   18 __FRAME_END__
    40: 0000000000600e20     0 OBJECT  LOCAL  DEFAULT   21 __JCR_END__
    41: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS 
    42: 0000000000600e18     0 NOTYPE  LOCAL  DEFAULT   19 __init_array_end
    43: 0000000000600e28     0 OBJECT  LOCAL  DEFAULT   22 _DYNAMIC
    44: 0000000000600e10     0 NOTYPE  LOCAL  DEFAULT   19 __init_array_start
    45: 0000000000400680     0 NOTYPE  LOCAL  DEFAULT   17 __GNU_EH_FRAME_HDR
    46: 0000000000601000     0 OBJECT  LOCAL  DEFAULT   24 _GLOBAL_OFFSET_TABLE_
    47: 0000000000400620     2 FUNC    GLOBAL DEFAULT   14 __libc_csu_fini
    48: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_deregisterTMCloneTab
    49: 0000000000601028     0 NOTYPE  WEAK   DEFAULT   25 data_start
    50: 0000000000601038     0 NOTYPE  GLOBAL DEFAULT   25 _edata
    51: 0000000000400624     0 FUNC    GLOBAL DEFAULT   15 _fini
    52: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND printf@@GLIBC_2.2.5
    53: 0000000000400526    22 FUNC    GLOBAL DEFAULT   14 max
    54: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@@GLIBC_
    55: 0000000000601028     0 NOTYPE  GLOBAL DEFAULT   25 __data_start
    56: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
    57: 0000000000601030     0 OBJECT  GLOBAL HIDDEN    25 __dso_handle
    58: 0000000000400630     4 OBJECT  GLOBAL DEFAULT   16 _IO_stdin_used
    59: 00000000004005b0   101 FUNC    GLOBAL DEFAULT   14 __libc_csu_init
    60: 0000000000601040     0 NOTYPE  GLOBAL DEFAULT   26 _end
    61: 0000000000400430    42 FUNC    GLOBAL DEFAULT   14 _start
    62: 0000000000601038     0 NOTYPE  GLOBAL DEFAULT   26 __bss_start
    63: 000000000040053c   109 FUNC    GLOBAL DEFAULT   14 main
    64: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _Jv_RegisterClasses
    65: 0000000000601038     0 OBJECT  GLOBAL HIDDEN    25 __TMC_END__
    66: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_registerTMCloneTable
    67: 00000000004003c8     0 FUNC    GLOBAL DEFAULT   11 _init

Version symbols section '.gnu.version' contains 4 entries:
 地址: 0000000000400358  Offset: 0x000358  Link: 5 (.dynsym)
  000:   0 (*本地*)       2 (GLIBC_2.2.5)   2 (GLIBC_2.2.5)   0 (*本地*)    

Version needs section '.gnu.version_r' contains 1 entries:
 地址:0x0000000000400360  Offset: 0x000360  Link: 6 (.dynstr)
  000000: 版本: 1  文件:libc.so.6  计数:1
  0x0010:名称:GLIBC_2.2.5  标志:无  版本:2

Displaying notes found at file offset 0x00000254 with length 0x00000020:
  Owner                 Data size   Description
  GNU                  0x00000010   NT_GNU_ABI_TAG (ABI version tag)
    OS: Linux, ABI: 2.6.32

Displaying notes found at file offset 0x00000274 with length 0x00000024:
  Owner                 Data size   Description
  GNU                  0x00000014   NT_GNU_BUILD_ID (unique build ID bitstring)
    Build ID: debd3d7912be860a432b5c685a6cff7fd9418528

从地方的新闻中我们可以领悟那一个文件的项目是ELF64,
约等于6二个人的可执行程序, 并且有7个程序头和三十五个节头,
种种节的效能大家可以在网上找到资料, 那篇作品中只涉及到以下的节

  • .init 程序初叶化的代码
  • .rela.dyn 必要重一直的变量列表
  • .rela.plt 须求重一向的函数列表
  • .plt 调用动态链接函数的代码
  • .text 保存了非常主要的程序代码
  • .init 保存了先后的初阶化代码, 用于初阶化全局变量等
  • .fini 保存了先后的截至代码, 用于析构全局变量等
  • .rodata 保存了只读的数量,例如字符串(const char*)
  • .data 保存了可读写的数码,例如全局变量
  • .dynsym 动态链接的符号表
  • .dynstr 动态链接的标志名称字符串
  • .dynamic 动态链接所急需的音讯,供程序运维时采纳(不须要拜访节头)

静态链接

在大家的主次中,平常会在贰个文本中引用其他文件中的函数和变量,当链接器将那几个文本组合成一个可执行文件时,就需求对这个引用举办重一向,否则,系统就不能看清出那些引用的地址,程序也就无法平常运作。

举个栗子:

Android
NDK开发中,大家平常采纳动态注册函数的法门,将JAVA中调用的native函数与JNI中某一函数举行关联,一般的源代码如下(节选):

#define LOG_TAG "JNITEST_NATIVE"

#define LOGD(fmt, args...) ;__android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, fmt, ##args)

static JNINativeMethod methods[] = {  
    {"getStringFromNative", "()Ljava/lang/String;", (void*)native_hello}
}; 

int jniRegisterNativeMethods(JNIEnv* env,
                             const char* className,
                             const JNINativeMethod* gMethods,
                             int numMethods)
{
    jclass clazz;
    int tmp;

    LOGD("Registering %s natives\n", className);
    clazz = (*env)->FindClass(env, className);
    if (clazz == NULL) {
        LOGD("Native registration unable to find class '%s'\n", className);
        return -1;
    }
    if ((tmp= (*env)->RegisterNatives(env, clazz, gMethods, numMethods)) < 0) {
        LOGD("RegisterNatives failed for '%s', %d\n", className, tmp);
        return -1;
    }
    return 0;
}

大家在Android.mk上将其编译成共享库SO:

LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
LOCAL_SRC_FILES:= m.c
LOCAL_MODULE := hello
LOCAL_LDLIBS := -llog
LOCAL_CFLAGS := -DDEBUG -O0
include $(BUILD_SHARED_LIBRARY)

运用NDK提供的工具链对转移的.o文件代码段举行反编译
arm-linux-androideabi-objdumop.exe -d m.o

发生的一些代码如下所示:

……  
1a:        a902              add        r1, sp, #8  
1c:        4a10              ldr        r2, [pc, #64]        ; (60 <JNI\_OnLoad+0x60>)  
1e:        4798              blx        r3  
20:        1c03              adds        r3, r0, #0  
22:        2b00              cmp        r3, #0
……  
3e:        4b08              ldr        r3, [pc, #32]        ; (60 <JNI\_OnLoad+0x60>)  
40:        9303              str        r3, [sp, #12]  
42:        4b08              ldr        r3, [pc, #32]        ; (64 <JNI\_OnLoad+0x64>)  
44:        447b              add        r3, pc  
46:        1c19              adds        r1, r3, #0  
48:        4b07              ldr        r3, [pc, #28]        ; (68 <JNI\_OnLoad+0x68>)  
4a:        447b              add        r3, pc 
4c:        1c1a              adds        r2, r3, #0  
4e:        9b03              ldr        r3, [sp, #12]  
50:        2003              movs        r0, #3  
52:        f7ff fffe         bl        0 <__android_log_print> 
……

可以旁观,在对象文件中,对于目的文件之外的函数,例如__android_log_print函数的调用地址为0,很明显,那是多少个地下地址。那标志在对象中,对文本外符号的引用是不法的,而重平昔就是要消除不合法引用的难点。

静态链接指的是在程序加载到内存在此之前,在链接成可执行文件时就对那个引用进行重一向。为了符号符号让链接器可以看清哪些符号须要被重一向以及在代码段和数据段中的具体地方,例如地点最一生成的对象文件中重定位表中一定有一项,记录了__android_log_print符号,并且记录了它要求被重一向的地点为代码段JNI_OnLoad函数中的52字节处。ELF文件用符号表和重定位表分别记录了那些多少。在Android
NDK开发中,可以在Android,mk中添加命令
include $(BUILD_STATIC_LIBRARY)将源文件编译成静态库。

符号表 .symtab

符号表是ELF文件中的非常主要的表,因为符号可以认为是链接三个模块的粘合剂,通过对符号的引用解析,系统才能将三个目的文件组成贰个完完全全的可执行文件。符号表首要记录了大局符号(定义在本目的文件中的全局符号,可以被此外目的文件引用)和外部符号(本目的文件中引用的大局符号,却从不概念在本目的文件中)。符号表的协会是多少个Elf32_Sym类型的数组。Elf32_Sym结构体保存了符号名,符号值,符号大小,符号类型和绑定新闻和标记所在的段。

重定位表

静态链接下必要对符号的引用进行重一直,并且ELF文件中对符号的引用大概出现在代码段,也可能现身在数据段,由此重平素表分为了代码段重定位表和数据段重定位表,分别记录引用的标记名和所在的晃动地址。由此,重定位表的格式就是记录符号名和重定位地方的数组。

2. 线程

  • 线程基础
    • 一个标准的线程由线程 ID当前指令指针(PC)
      寄存器集合堆栈组成
    • 线程私有
      • 栈 局地变量
      • 函数的参数
      • 线程局地存储 TLS 数据
      • 寄存器
    • 线程之间共享
      • 全局变量
      • 堆上的数量
      • 函数里的静态变量
      • 程序代码
      • 打开的文书
    • 线程调度中,线程平日兼有至少三种情状
      • 运行
      • 就绪
      • 等待
  • 线程安全
    • 原子操作
    • 共同与锁
    • 防范 CPU 过度优化
  • 二十四线程的内部景观——两种线程模型
    • 杰出模型 (用户线程 内核线程)
    • 多对一模型
    • 多对多模型

什么是动态链接

地方的次第中调用了printf函数, 但是以此函数的完成并不在./a.out中,
那么printf函数在什么地方, 又是怎么被调用的?

printf函数的实未来glibc库中,
也就是/lib/x86_64-linux-gnu/libc.so.6中,
在执行./a.out的时候会在glibc库中找到这么些函数并进行调用,
大家来看望那段代码

执行以下命令反编译./a.out

objdump -c -S ./a.out

我们得以看看以下的代码

00000000004003f0 <printf@plt-0x10>:
  4003f0:   ff 35 12 0c 20 00       pushq  0x200c12(%rip)        # 601008 <_GLOBAL_OFFSET_TABLE_+0x8>
  4003f6:   ff 25 14 0c 20 00       jmpq   *0x200c14(%rip)        # 601010 <_GLOBAL_OFFSET_TABLE_+0x10>
  4003fc:   0f 1f 40 00             nopl   0x0(%rax)

0000000000400400 <printf@plt>:
  400400:   ff 25 12 0c 20 00       jmpq   *0x200c12(%rip)        # 601018 <_GLOBAL_OFFSET_TABLE_+0x18>
  400406:   68 00 00 00 00          pushq  $0x0
  40040b:   e9 e0 ff ff ff          jmpq   4003f0 <_init+0x28>

000000000040053c <main>:
  40053c:   55                      push   %rbp
  40053d:   48 89 e5                mov    %rsp,%rbp
  400540:   be 41 01 00 00          mov    $0x141,%esi
  400545:   bf 7b 00 00 00          mov    $0x7b,%edi
  40054a:   e8 d7 ff ff ff          callq  400526 <max>
  40054f:   89 c6                   mov    %eax,%esi
  400551:   bf 38 06 40 00          mov    $0x400638,%edi
  400556:   b8 00 00 00 00          mov    $0x0,%eax
  40055b:   e8 a0 fe ff ff          callq  400400 <printf@plt>

在这一段代码中,大家可以见到调用printf会率先调用0x400400printf@plt
printf@plt会顶住在运作时找到实际的printf函数并跳转到该函数
在此处实在的printf函数会保存在0x400406 + 0x200c12 = 0x601018

急需小心的是0x601018一开端并不会指向实际的printf函数,而是会指向0x400406,
为何会如此?
因为Linux的可执行程序为了考虑质量,不会在一初阶就化解全部动态连接的函数,而是精选了推迟消除.
在地点第二次jmpq *0x200c12(%rip)会跳转到下一条指令0x400406,
又会继续跳转到0x4003f0, 再跳转到0x601010本着的地点,
0x601010本着的地点就是延迟化解的贯彻, 第三回推迟化解成功后,
0x601018就会指向实际的printf,
以往调用就会一向跳转到实际的printf上.

怎么是动态链接

地方的顺序中调用了printf函数, 可是以此函数的贯彻并不在./a.out中,
那么printf函数在哪里, 又是怎么被调用的?

printf函数的贯彻在glibc库中,
也就是/lib/x86_64-linux-gnu/libc.so.6中,
在执行./a.out的时候会在glibc库中找到这几个函数并展开调用,
大家来看看那段代码

履行以下命令反编译./a.out

objdump -c -S ./a.out

俺们得以看来以下的代码

00000000004003f0 <printf@plt-0x10>:
  4003f0:   ff 35 12 0c 20 00       pushq  0x200c12(%rip)        # 601008 <_GLOBAL_OFFSET_TABLE_+0x8>
  4003f6:   ff 25 14 0c 20 00       jmpq   *0x200c14(%rip)        # 601010 <_GLOBAL_OFFSET_TABLE_+0x10>
  4003fc:   0f 1f 40 00             nopl   0x0(%rax)

0000000000400400 <printf@plt>:
  400400:   ff 25 12 0c 20 00       jmpq   *0x200c12(%rip)        # 601018 <_GLOBAL_OFFSET_TABLE_+0x18>
  400406:   68 00 00 00 00          pushq  $0x0
  40040b:   e9 e0 ff ff ff          jmpq   4003f0 <_init+0x28>

000000000040053c <main>:
  40053c:   55                      push   %rbp
  40053d:   48 89 e5                mov    %rsp,%rbp
  400540:   be 41 01 00 00          mov    $0x141,%esi
  400545:   bf 7b 00 00 00          mov    $0x7b,%edi
  40054a:   e8 d7 ff ff ff          callq  400526 <max>
  40054f:   89 c6                   mov    %eax,%esi
  400551:   bf 38 06 40 00          mov    $0x400638,%edi
  400556:   b8 00 00 00 00          mov    $0x0,%eax
  40055b:   e8 a0 fe ff ff          callq  400400 <printf@plt>

在这一段代码中,大家可以寓目调用printf会首先调用0x400400printf@plt
printf@plt会承受在运作时找到实际的printf函数并跳转到该函数
在此地其实的printf函数会保存在0x400406 + 0x200c12 = 0x601018

须要留意的是0x601018一先导并不会指向实际的printf函数,而是会指向0x400406,
为何会这么?
因为Linux的可执行程序为了考虑质量,不会在一从头就化解全部动态连接的函数,而是接纳了推迟消除.
在上头首次jmpq *0x200c12(%rip)会跳转到下一条指令0x400406,
又会接二连三跳转到0x4003f0, 再跳转到0x601010本着的地点,
0x601010本着的地址就是延迟消除的已毕, 第②遍推迟消除成功后,
0x601018就会指向实际的printf,
将来调用就会一向跳转到实际的printf上.

动态链接

静态链接消除重定位难点实在程序被载入内存从前,而动态链接消除这一标题标时机是在先后被载入内存之后。那与静态链接比较,有多少个肯定的优势:首先是节约内存空间。固然三个程序行使静态链接的不二法门缓解重定位难题,若是程序用到了库函数,那么每二个主次的虚拟内存空间都具有一份祥和个人的静态库。那么在某一每一日,计算机的情理内存中就会存在内容完全相同的多份静态库,那如实浪费内存空间。其次,静态链接的主次一旦有更新,甚至是轻微的翻新,都亟需重新编译打包链接,用户也急需展开全量的换代。最终,动态链接的艺术,将顺序拆分成了三个模块,种种模块之间可以独立开发,下降了程序各模块之间的耦合度。

为了适应动态链接的须求,ELF文件中追加了四个段。

.dynamic段

typedef struct { 
  Elf32 _Sword d_tag;
  union { 
    Elf32_Word d_val; 
    Elf32_Addr d_ptr; 
  } d_un; 
} Elf32_Dyn; 

d_val 此 Elf32_Word 对象表示一个整数值,可以有多种解释。
d_ptr 此 Elf32_Addr 对象代表程序的虚拟地址。

目的表示二个整数值,可以有二种表明。d_ptr 此 Elf32_Addr
对象表示先后的虚拟地址

.dynamic段是动态链接中最要害的段,它记录了和动态链接有关的段的项目,地址或然数值。

.interp段

Android下的动态链接器本质上就是3个SO文件,我们编辑的SO文件被系统加载之后,为了成功动态链接,系统还会把动态链接器也加载到内存中。所以在.interp段中就记下了动态链接器的门道。

.got段

如上文所说,动态链接可以节省空间,因为整个内存中只需求保留一份运营库即可。可是,那有四个难点。纵然运营库可以在内存中只保留一份,不过对于每1个独自的进程,其运营库在进程虚拟空间的地点可能不雷同,那样就招致了代码段或数据段对库以外的号子的引用不恐怕以相对地址的款式写死,否则的话,整个内存只存一份库是不能落成的。

从而,ELF文件指出了”地方非亲非故代码”的定义,将ELF代码段中对库外函数的引用全体移到.got段中,使得代码段不含有对函数地址的相对化引用,以此保证代码段可以被重复使用,而每贰个进程都保有库数据段的副本。

.got的格式就是存放在地方的数组。那么链接器是何等找到呼应符号在.got表中的地点的啊?那亟需.rel.plt段的支撑。

.plt段

在介绍.rel.plt段之前,相比较静态链接与动态链接大家必要知道.plt段的机能。大家会发觉,动态链接将富有的重定位操作延迟到加载时处理,那么就难防止止的会下落程序执行的频率,试想,如果有一千个对表面模块的函数引用,动态链接器就须求先化解这一千个函数引用,然后才起来执行顺序。为此,链接器为了进步作用,接纳了那般一种政策:仅当函数被调用时,才会唤醒动态链接器化解重定位难点。

.plt段就是为着兑现那种政策增添的段。增添了.plt段之后,代码段中的地址就不再指向.got段而是指向了.plt段,再由.plt段指向.got段,具体经过如下:

.plt段是富含了多少多少的代码片段组成的段,代码段中的地址指向对应函数的.plt代码段,代码片段的首先行代码就是间接调用.got表中对应函数地址,可是此时,.got表中的地址指向的是.plt中代码片段的第壹行代码,而第贰行以往的代码功效就是调用动态链接器处理.got表中的地址。当再一次调用此函数时,.plt中代码段第贰行代码就足以正确的跳入函数地址执行相应的函数了。

不过,在Android平台下,由于包容性的限定,并没有应用那种”延迟加载”的表征,所以一起先加载,.got表中的地址就是真性的函数调用地址。不过,Android下的ELF文件依旧保留了.plt表这种社团。

.rel.plt段

.rel.plt段也是重定位表,因而它的格式和重定位表一样,记录符号表索引和重定位地点。对函数调用的重定位。

.rel.dyn段

与.rel.plt,类似,.rel.dyn对数码援引举办重一向。

.dynsym段

动态链接符号表,专门存放动态里娜姐相关的标志。

.hash段

为了增强符号检索的频率扩充的段。它的构造怎么着所示:

bucket 数组包蕴 **nbucket 个项目,** chain 数组包涵 **nchain
个品类,下标都以从 0 开始。** bucket 和 **chain
中都保留符号表索引。** Chain
表项和标志表存在对应。符号表项的多寡应该和 **nchain
卓殊,所以符号表的目录也可用来采取** chain
表项。哈希函数可以经受符号名还要重返多个得以用来统计bucket的索引。
**因而,如果哈希函数针对某些名字重返了数值
X ,则
bucket[X%nbucket]
交给了2个目录 y
,该索引可用来符号表,也可用以 chain

表。假使符号表项不是所急需的,那么 chain[y]
则交由了具备同样哈希值的下1个标志表项。大家得以顺着 **chain
链一直寻找,直到所选中的符号表项包涵了所急需的号子,恐怕** chain
项中带有值 STN_UNDEF**。

哈希函数如下:

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;
}

二 、编译和链接

程序入口点

Linux程序运转首先会从_start函数伊始,
上面readelf中的入口点地址0x400430就是_start函数的地址,

0000000000400430 <_start>:
  400430:   31 ed                   xor    %ebp,%ebp
  400432:   49 89 d1                mov    %rdx,%r9
  400435:   5e                      pop    %rsi
  400436:   48 89 e2                mov    %rsp,%rdx
  400439:   48 83 e4 f0             and    $0xfffffffffffffff0,%rsp
  40043d:   50                      push   %rax
  40043e:   54                      push   %rsp
  40043f:   49 c7 c0 20 06 40 00    mov    $0x400620,%r8
  400446:   48 c7 c1 b0 05 40 00    mov    $0x4005b0,%rcx
  40044d:   48 c7 c7 3c 05 40 00    mov    $0x40053c,%rdi
  400454:   e8 b7 ff ff ff          callq  400410 <__libc_start_main@plt>
  400459:   f4                      hlt    
  40045a:   66 0f 1f 44 00 00       nopw   0x0(%rax,%rax,1)

接下来_start函数会调用__libc_start_main函数,
__libc_start_main是libc库中定义的开始化函数,
负责开端化全局变量和调用main函数等工作.

__libc_start_main函数还担负安装再次来到值和剥离进度,
可以看来上面调用__libc_start_main后的一声令下是hlt,
那么些命令永远不会被执行.

先后入口点

Linux程序运营首先会从_start函数开首,
上边readelf中的入口点地址0x400430就是_start函数的地方,

0000000000400430 <_start>:
  400430:   31 ed                   xor    %ebp,%ebp
  400432:   49 89 d1                mov    %rdx,%r9
  400435:   5e                      pop    %rsi
  400436:   48 89 e2                mov    %rsp,%rdx
  400439:   48 83 e4 f0             and    $0xfffffffffffffff0,%rsp
  40043d:   50                      push   %rax
  40043e:   54                      push   %rsp
  40043f:   49 c7 c0 20 06 40 00    mov    $0x400620,%r8
  400446:   48 c7 c1 b0 05 40 00    mov    $0x4005b0,%rcx
  40044d:   48 c7 c7 3c 05 40 00    mov    $0x40053c,%rdi
  400454:   e8 b7 ff ff ff          callq  400410 <__libc_start_main@plt>
  400459:   f4                      hlt    
  40045a:   66 0f 1f 44 00 00       nopw   0x0(%rax,%rax,1)

接下来_start函数会调用__libc_start_main函数,
__libc_start_main是libc库中定义的开首化函数,
负责初阶化全局变量和调用main函数等工作.

__libc_start_main函数还负责安装重临值和退出进程,
可以看来地方调用__libc_start_main后的一声令下是hlt,
这些命令永远不会被执行.

其他段

.init/.init_array

动态链接器在进行顺序main函数此前会首先实施那七个段中的代码。先执行.init段中的代码,再执行.init_array中函数指针指向的代码。在Android
NDK中,可以因此添加编译器注释
__attribute(constructor)将某一函数写入这多少个段中。

.fini/.fini_array

恍如的,动态链接器最终会履行那八个段中的代码。在Android
NDK中,通过丰盛编译器注释__attribute(destructor)将某一函数写入那八个段。

1. 被隐形了的进程

  • 预编译 : 首要处理这几个源代码文件中的以 “#”
    开首的预编译指令,进度相当于 gcc -E hello.c -o hello.i
  • 编译 :
    把预处理完的文书举行一多重词法分析、语法分析、语义分析及优化后生产相应的汇编代码文件,过程也等于
    gcc -S hello.c -o hello.s,将来版本的 GCC
    把预编译和编译七个步骤合并成二个手续,使用3个誉为 cc1
    的次第来成功那三个步骤
  • 汇编 :
    将汇编代码转变成机器能够进行的授命,每三个汇编语句都对应一条机器指令,进度约等于
    gcc -c hello.s -o hello.o
  • 链接 : 将一大堆 .o 目标文件链接起来才方可赢得 a.out

布帆无恙Linux程序运维器

在具备上述的知识后大家可以先构想以下的运营器必要做什么.

因为x64的Windows和Linux程序拔取的cpu指令集都是同等的,大家可以一直实施汇编而不必要二个下令模拟器,
与此同时本次本身打算在用户层达成, 所以不恐怕像Bash On Windows一样模拟syscall,
那几个运营器会像下图一律模拟libc库的函数

澳门金沙国际 3

那般运转器需求做的政工有:

  • 解析ELF文件
  • 加载程序代码到钦命的内存地址
  • 加载数据到内定的内存地址
  • 提供动态链接的函数已毕
  • 举行加载的程序代码

那么些工作会在偏下的以身作则程序中各种落成, 完整的源代码可以看小说顶部的链接

率先我们要求把ELF文件格式对应的代码从binutils中复制过来,
它涵盖了ELF头, 程序头和连锁的数据结构,
里面用unsigned char[]是为着以免alignment,
那样结构体可以直接从文件内容中改换过来

ELFDefine.h:

#pragma once

namespace HelloElfLoader {
    // 以下内容复制自
    // https://github.com/aeste/binutils/blob/develop/elfcpp/elfcpp.h
    // https://github.com/aeste/binutils/blob/develop/include/elf/external.h

    // e_ident中各项的偏移值
    const int EI_MAG0 = 0;
    const int EI_MAG1 = 1;
    const int EI_MAG2 = 2;
    const int EI_MAG3 = 3;
    const int EI_CLASS = 4;
    const int EI_DATA = 5;
    const int EI_VERSION = 6;
    const int EI_OSABI = 7;
    const int EI_ABIVERSION = 8;
    const int EI_PAD = 9;
    const int EI_NIDENT = 16;

    // ELF文件类型
    enum {
        ELFCLASSNONE = 0,
        ELFCLASS32 = 1,
        ELFCLASS64 = 2
    };

    // ByteOrder
    enum {
        ELFDATANONE = 0,
        ELFDATA2LSB = 1,
        ELFDATA2MSB = 2
    };

    // 程序头类型
    enum PT
    {
        PT_NULL = 0,
        PT_LOAD = 1,
        PT_DYNAMIC = 2,
        PT_INTERP = 3,
        PT_NOTE = 4,
        PT_SHLIB = 5,
        PT_PHDR = 6,
        PT_TLS = 7,
        PT_LOOS = 0x60000000,
        PT_HIOS = 0x6fffffff,
        PT_LOPROC = 0x70000000,
        PT_HIPROC = 0x7fffffff,
        // The remaining values are not in the standard.
        // Frame unwind information.
        PT_GNU_EH_FRAME = 0x6474e550,
        PT_SUNW_EH_FRAME = 0x6474e550,
        // Stack flags.
        PT_GNU_STACK = 0x6474e551,
        // Read only after relocation.
        PT_GNU_RELRO = 0x6474e552,
        // Platform architecture compatibility information
        PT_ARM_ARCHEXT = 0x70000000,
        // Exception unwind tables
        PT_ARM_EXIDX = 0x70000001
    };

    // 动态节类型
    enum DT
    {
        DT_NULL = 0,
        DT_NEEDED = 1,
        DT_PLTRELSZ = 2,
        DT_PLTGOT = 3,
        DT_HASH = 4,
        DT_STRTAB = 5,
        DT_SYMTAB = 6,
        DT_RELA = 7,
        DT_RELASZ = 8,
        DT_RELAENT = 9,
        DT_STRSZ = 10,
        DT_SYMENT = 11,
        DT_INIT = 12,
        DT_FINI = 13,
        DT_SONAME = 14,
        DT_RPATH = 15,
        DT_SYMBOLIC = 16,
        DT_REL = 17,
        DT_RELSZ = 18,
        DT_RELENT = 19,
        DT_PLTREL = 20,
        DT_DEBUG = 21,
        DT_TEXTREL = 22,
        DT_JMPREL = 23,
        DT_BIND_NOW = 24,
        DT_INIT_ARRAY = 25,
        DT_FINI_ARRAY = 26,
        DT_INIT_ARRAYSZ = 27,
        DT_FINI_ARRAYSZ = 28,
        DT_RUNPATH = 29,
        DT_FLAGS = 30,

        // This is used to mark a range of dynamic tags.  It is not really
        // a tag value.
        DT_ENCODING = 32,

        DT_PREINIT_ARRAY = 32,
        DT_PREINIT_ARRAYSZ = 33,
        DT_LOOS = 0x6000000d,
        DT_HIOS = 0x6ffff000,
        DT_LOPROC = 0x70000000,
        DT_HIPROC = 0x7fffffff,

        // The remaining values are extensions used by GNU or Solaris.
        DT_VALRNGLO = 0x6ffffd00,
        DT_GNU_PRELINKED = 0x6ffffdf5,
        DT_GNU_CONFLICTSZ = 0x6ffffdf6,
        DT_GNU_LIBLISTSZ = 0x6ffffdf7,
        DT_CHECKSUM = 0x6ffffdf8,
        DT_PLTPADSZ = 0x6ffffdf9,
        DT_MOVEENT = 0x6ffffdfa,
        DT_MOVESZ = 0x6ffffdfb,
        DT_FEATURE = 0x6ffffdfc,
        DT_POSFLAG_1 = 0x6ffffdfd,
        DT_SYMINSZ = 0x6ffffdfe,
        DT_SYMINENT = 0x6ffffdff,
        DT_VALRNGHI = 0x6ffffdff,

        DT_ADDRRNGLO = 0x6ffffe00,
        DT_GNU_HASH = 0x6ffffef5,
        DT_TLSDESC_PLT = 0x6ffffef6,
        DT_TLSDESC_GOT = 0x6ffffef7,
        DT_GNU_CONFLICT = 0x6ffffef8,
        DT_GNU_LIBLIST = 0x6ffffef9,
        DT_CONFIG = 0x6ffffefa,
        DT_DEPAUDIT = 0x6ffffefb,
        DT_AUDIT = 0x6ffffefc,
        DT_PLTPAD = 0x6ffffefd,
        DT_MOVETAB = 0x6ffffefe,
        DT_SYMINFO = 0x6ffffeff,
        DT_ADDRRNGHI = 0x6ffffeff,

        DT_RELACOUNT = 0x6ffffff9,
        DT_RELCOUNT = 0x6ffffffa,
        DT_FLAGS_1 = 0x6ffffffb,
        DT_VERDEF = 0x6ffffffc,
        DT_VERDEFNUM = 0x6ffffffd,
        DT_VERNEED = 0x6ffffffe,
        DT_VERNEEDNUM = 0x6fffffff,

        DT_VERSYM = 0x6ffffff0,

        // Specify the value of _GLOBAL_OFFSET_TABLE_.
        DT_PPC_GOT = 0x70000000,

        // Specify the start of the .glink section.
        DT_PPC64_GLINK = 0x70000000,

        // Specify the start and size of the .opd section.
        DT_PPC64_OPD = 0x70000001,
        DT_PPC64_OPDSZ = 0x70000002,

        // The index of an STT_SPARC_REGISTER symbol within the DT_SYMTAB
        // symbol table.  One dynamic entry exists for every STT_SPARC_REGISTER
        // symbol in the symbol table.
        DT_SPARC_REGISTER = 0x70000001,

        DT_AUXILIARY = 0x7ffffffd,
        DT_USED = 0x7ffffffe,
        DT_FILTER = 0x7fffffff
    };;

    // ELF头的定义
    typedef struct {
        unsigned char   e_ident[16];        /* ELF "magic number" */
        unsigned char   e_type[2];      /* Identifies object file type */
        unsigned char   e_machine[2];       /* Specifies required architecture */
        unsigned char   e_version[4];       /* Identifies object file version */
        unsigned char   e_entry[8];     /* Entry point virtual address */
        unsigned char   e_phoff[8];     /* Program header table file offset */
        unsigned char   e_shoff[8];     /* Section header table file offset */
        unsigned char   e_flags[4];     /* Processor-specific flags */
        unsigned char   e_ehsize[2];        /* ELF header size in bytes */
        unsigned char   e_phentsize[2];     /* Program header table entry size */
        unsigned char   e_phnum[2];     /* Program header table entry count */
        unsigned char   e_shentsize[2];     /* Section header table entry size */
        unsigned char   e_shnum[2];     /* Section header table entry count */
        unsigned char   e_shstrndx[2];      /* Section header string table index */
    } Elf64_External_Ehdr;

    // 程序头的定义
    typedef struct {
        unsigned char   p_type[4];      /* Identifies program segment type */
        unsigned char   p_flags[4];     /* Segment flags */
        unsigned char   p_offset[8];        /* Segment file offset */
        unsigned char   p_vaddr[8];     /* Segment virtual address */
        unsigned char   p_paddr[8];     /* Segment physical address */
        unsigned char   p_filesz[8];        /* Segment size in file */
        unsigned char   p_memsz[8];     /* Segment size in memory */
        unsigned char   p_align[8];     /* Segment alignment, file & memory */
    } Elf64_External_Phdr;

    // DYNAMIC类型的程序头的内容定义
    typedef struct {
        unsigned char   d_tag[8];       /* entry tag value */
        union {
            unsigned char   d_val[8];
            unsigned char   d_ptr[8];
        } d_un;
    } Elf64_External_Dyn;

    // 动态链接的重定位记录,部分系统会用Elf64_External_Rel
    typedef struct {
        unsigned char r_offset[8];  /* Location at which to apply the action */
        unsigned char   r_info[8];  /* index and type of relocation */
        unsigned char   r_addend[8];    /* Constant addend used to compute value */
    } Elf64_External_Rela;

    // 动态链接的符号信息
    typedef struct {
        unsigned char   st_name[4];     /* Symbol name, index in string tbl */
        unsigned char   st_info[1];     /* Type and binding attributes */
        unsigned char   st_other[1];        /* No defined meaning, 0 */
        unsigned char   st_shndx[2];        /* Associated section index */
        unsigned char   st_value[8];        /* Value of the symbol */
        unsigned char   st_size[8];     /* Associated symbol size */
    } Elf64_External_Sym;
}

接下去我们定义二个读取和执行ELF文件的类,
这么些类会在伊始化时把文件加载到fileStream_, execute函数会负责实施

HelloElfLoader.h:

#pragma once
#include <string>
#include <fstream>

namespace HelloElfLoader {
    class Loader {
        std::ifstream fileStream_;

    public:
        Loader(const std::string& path);
        Loader(std::ifstream&& fileStream);
        void execute();
    };
}

构造函数如下, 相当于标准的c++打开文件的代码

HelloElfLoader.cpp:

Loader::Loader(const std::string& path) :
    Loader(std::ifstream(path, std::ios::in | std::ios::binary)) {}

Loader::Loader(std::ifstream&& fileStream) :
    fileStream_(std::move(fileStream)) {
    if (!fileStream_) {
        throw std::runtime_error("open file failed");
    }
}

接下去将已毕位置所说的步调, 首先是解析ELF文件

void Loader::execute() {
    std::cout << "====== start loading elf ======" << std::endl;

    // 检查当前运行程序是否64位
    if (sizeof(intptr_t) != sizeof(std::int64_t)) {
        throw std::runtime_error("please use x64 compile and run this program");
    }

    // 读取ELF头
    Elf64_External_Ehdr elfHeader = {};
    fileStream_.seekg(0);
    fileStream_.read(reinterpret_cast<char*>(&elfHeader), sizeof(elfHeader));

    // 检查ELF头,只支持64位且byte order是little endian的程序
    if (std::string(reinterpret_cast<char*>(elfHeader.e_ident), 4) != "\x7f\x45\x4c\x46") {
        throw std::runtime_error("magic not match");
    }
    else if (elfHeader.e_ident[EI_CLASS] != ELFCLASS64) {
        throw std::runtime_error("only support ELF64");
    }
    else if (elfHeader.e_ident[EI_DATA] != ELFDATA2LSB) {
        throw std::runtime_error("only support little endian");
    }

    // 获取program table的信息
    std::uint32_t programTableOffset = *reinterpret_cast<std::uint32_t*>(elfHeader.e_phoff);
    std::uint16_t programTableEntrySize = *reinterpret_cast<std::uint16_t*>(elfHeader.e_phentsize);
    std::uint16_t programTableEntryNum = *reinterpret_cast<std::uint16_t*>(elfHeader.e_phnum);
    std::cout << "program table at: " << programTableOffset << ", "
        << programTableEntryNum << " x " << programTableEntrySize << std::endl;

    // 获取section table的信息
    // section table只给linker用,loader中其实不需要访问section table
    std::uint32_t sectionTableOffset = *reinterpret_cast<std::uint32_t*>(elfHeader.e_shoff);
    std::uint16_t sectionTableEntrySize = *reinterpret_cast<std::uint16_t*>(elfHeader.e_shentsize);
    std::uint16_t sectionTableEntryNum = *reinterpret_cast<std::uint16_t*>(elfHeader.e_shentsize);
    std::cout << "section table at: " << sectionTableOffset << ", "
        << sectionTableEntryNum << " x " << sectionTableEntrySize << std::endl;

ELF文件的的发端部分就是ELF头,和Elf64_External_Ehdr结构体的协会同样,
我们得以读到Elf64_External_Ehdr结构体中,
然后ELF头蕴涵了程序头和节头的偏移值, 大家可以预先获取到这几个参数

节头在运作时不须要采用, 运维时须求遍历程序头

    // 准备动态链接的信息
    std::uint64_t jmpRelAddr = 0; // 重定位记录的开始地址
    std::uint64_t pltRelType = 0; // 重定位记录的类型 RELA或REL
    std::uint64_t pltRelSize = 0; // 重定位记录的总大小
    std::uint64_t symTabAddr = 0; // 动态符号表的开始地址
    std::uint64_t strTabAddr = 0; // 动态符号名称表的开始地址
    std::uint64_t strTabSize = 0; // 动态符号名称表的总大小

    // 遍历program hedaer
    std::vector<Elf64_External_Phdr> programHeaders;
    programHeaders.resize(programTableEntryNum);
    fileStream_.read(reinterpret_cast<char*>(programHeaders.data()), programTableEntryNum * programTableEntrySize);
    std::vector<std::shared_ptr<void>> loadedSegments;
    for (const auto& programHeader : programHeaders) {
        std::uint32_t type = *reinterpret_cast<const std::uint32_t*>(programHeader.p_type);
        if (type == PT_LOAD) {
            // 把文件内容(包含程序代码和数据)加载到虚拟内存,这个示例不考虑地址冲突
            std::uint64_t fileOffset = *reinterpret_cast<const std::uint64_t*>(programHeader.p_offset);
            std::uint64_t fileSize = *reinterpret_cast<const std::uint64_t*>(programHeader.p_filesz);
            std::uint64_t virtAddr = *reinterpret_cast<const std::uint64_t*>(programHeader.p_vaddr);
            std::uint64_t memSize = *reinterpret_cast<const std::uint64_t*>(programHeader.p_memsz);
            if (memSize < fileSize) {
                throw std::runtime_error("invalid memsz in program header, it shouldn't less than filesz");
            }
            // 在指定的虚拟地址分配内存
            std::cout << std::hex << "allocate address at: 0x" << virtAddr <<
                " size: 0x" << memSize << std::dec << std::endl;
            void* addr = ::VirtualAlloc((void*)virtAddr, memSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
            if (addr == nullptr) {
                throw std::runtime_error("allocate memory at specific address failed");
            }
            loadedSegments.emplace_back(addr, [](void* ptr) { ::VirtualFree(ptr, 0, MEM_RELEASE); });
            // 复制文件内容到虚拟内存
            fileStream_.seekg(fileOffset);
            if (!fileStream_.read(reinterpret_cast<char*>(addr), fileSize)) {
                throw std::runtime_error("read contents into memory from LOAD program header failed");
            }
        }
        else if (type == PT_DYNAMIC) {
            // 遍历动态节
            std::uint64_t fileOffset = *reinterpret_cast<const std::uint64_t*>(programHeader.p_offset);
            fileStream_.seekg(fileOffset);
            Elf64_External_Dyn dynSection = {};
            std::uint64_t dynSectionTag = 0;
            std::uint64_t dynSectionVal = 0;
            do {
                if (!fileStream_.read(reinterpret_cast<char*>(&dynSection), sizeof(dynSection))) {
                    throw std::runtime_error("read dynamic section failed");
                }
                dynSectionTag = *reinterpret_cast<const std::uint64_t*>(dynSection.d_tag);
                dynSectionVal = *reinterpret_cast<const std::uint64_t*>(dynSection.d_un.d_val);
                if (dynSectionTag == DT_JMPREL) {
                    jmpRelAddr = dynSectionVal;
                }
                else if (dynSectionTag == DT_PLTREL) {
                    pltRelType = dynSectionVal;
                }
                else if (dynSectionTag == DT_PLTRELSZ) {
                    pltRelSize = dynSectionVal;
                }
                else if (dynSectionTag == DT_SYMTAB) {
                    symTabAddr = dynSectionVal;
                }
                else if (dynSectionTag == DT_STRTAB) {
                    strTabAddr = dynSectionVal;
                }
                else if (dynSectionTag == DT_STRSZ) {
                    strTabSize = dynSectionVal;
                }
            } while (dynSectionTag != 0);
        }
    }

还记得大家地点使用readelf读取到的消息呢?

程序头:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000400040 0x0000000000400040
                 0x00000000000001f8 0x00000000000001f8  R E    8
  INTERP         0x0000000000000238 0x0000000000400238 0x0000000000400238
                 0x000000000000001c 0x000000000000001c  R      1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x00000000000007d4 0x00000000000007d4  R E    200000
  LOAD           0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
                 0x0000000000000228 0x0000000000000230  RW     200000
  DYNAMIC        0x0000000000000e28 0x0000000000600e28 0x0000000000600e28
                 0x00000000000001d0 0x00000000000001d0  RW     8
  NOTE           0x0000000000000254 0x0000000000400254 0x0000000000400254
                 0x0000000000000044 0x0000000000000044  R      4
  GNU_EH_FRAME   0x0000000000000680 0x0000000000400680 0x0000000000400680
                 0x000000000000003c 0x000000000000003c  R      4
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     10
  GNU_RELRO      0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
                 0x00000000000001f0 0x00000000000001f0  R      1

那其中类型是LOAD的头代表要求加载文件的内容到内存,
Offset是文本的偏移值, VirtAddr是虚拟内存地址,
FileSiz是急需加载的文件大小, MemSiz是要求分配的内存大小,
Flags是内存的造访权限,
其一示例不考虑访问权限(统一行使PAGE_EXECUTE_READWRITE).

本条程序有多个LOAD头, 第三个包涵了代码和只读数据(.data, .init,
.rodata等节的始末), 第①个包罗了可写数据(.init_array,
.fini_array等节的内容).

LOAD头对应的始末加载到指定的内存地址后大家就形成了构想中的第②个第三个步骤,
以后代码和多少都在内存中了.

接下去我们还索要处理动态链接的函数,
处理所需的新闻可以从DYNAMIC头得到
DYNAMIC头包罗的音信有

Dynamic section at offset 0xe28 contains 24 entries:
  标记        类型                         名称/值
 0x0000000000000001 (NEEDED)             共享库:[libc.so.6]
 0x000000000000000c (INIT)               0x4003c8
 0x000000000000000d (FINI)               0x400624
 0x0000000000000019 (INIT_ARRAY)         0x600e10
 0x000000000000001b (INIT_ARRAYSZ)       8 (bytes)
 0x000000000000001a (FINI_ARRAY)         0x600e18
 0x000000000000001c (FINI_ARRAYSZ)       8 (bytes)
 0x000000006ffffef5 (GNU_HASH)           0x400298
 0x0000000000000005 (STRTAB)             0x400318
 0x0000000000000006 (SYMTAB)             0x4002b8
 0x000000000000000a (STRSZ)              63 (bytes)
 0x000000000000000b (SYMENT)             24 (bytes)
 0x0000000000000015 (DEBUG)              0x0
 0x0000000000000003 (PLTGOT)             0x601000
 0x0000000000000002 (PLTRELSZ)           48 (bytes)
 0x0000000000000014 (PLTREL)             RELA
 0x0000000000000017 (JMPREL)             0x400398
 0x0000000000000007 (RELA)               0x400380
 0x0000000000000008 (RELASZ)             24 (bytes)
 0x0000000000000009 (RELAENT)            24 (bytes)
 0x000000006ffffffe (VERNEED)            0x400360
 0x000000006fffffff (VERNEEDNUM)         1
 0x000000006ffffff0 (VERSYM)             0x400358
 0x0000000000000000 (NULL)               0x0

一个个看上边代码中涉嫌到的种类

  • DT_JMPREL: 重定位记录的初始地址,
    指向.rela.plt节在内存中保留的地方
  • DT_PLTREL: 重定位记录的门类 RELA或RE, 那里是RELAL
  • DT_PLTRELSZ: 重定位记录的总大小, 那里是24 * 2 = 48

重定位节 '.rela.plt' 位于偏移量 0x398 含有 2 个条目:
  偏移量          信息           类型           符号值        符号名称 + 加数
000000601018  000100000007 R_X86_64_JUMP_SLO 0000000000000000 printf@GLIBC_2.2.5 + 0
000000601020  000200000007 R_X86_64_JUMP_SLO 0000000000000000 __libc_start_main@GLIBC_2.2.5 + 0
  • DT_SYMTAB: 动态符号表的开始地址,
    指向.dynsym节在内存中保留的地点
  • DT_STRTAB: 动态符号名称表的开端地址,
    指向.dynstr节在内存中保留的地点
  • DT_STRSZ: 动态符号名称表的总大小

Symbol table '.dynsym' contains 4 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND printf@GLIBC_2.2.5 (2)
     2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@GLIBC_2.2.5 (2)
     3: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__

在遍历完程序头将来, 大家可以领悟有八个动态链接的函数要求重一直,
它们各自是__libc_start_mainprintf,
其中__libc_start_main担负调用main函数
接下去让大家需求安装那么些函数的地方

    // 读取动态链接符号表
    std::string dynamicSymbolNames(reinterpret_cast<char*>(strTabAddr), strTabSize);
    Elf64_External_Sym* dynamicSymbols = reinterpret_cast<Elf64_External_Sym*>(symTabAddr);

    // 设置动态链接的函数地址
    std::cout << std::hex << "read dynamic entires at: 0x" << jmpRelAddr <<
        " size: 0x" << pltRelSize << std::dec << std::endl;
    if (jmpRelAddr == 0 || pltRelType != DT_RELA || pltRelSize % sizeof(Elf64_External_Rela) != 0) {
        throw std::runtime_error("invalid dynamic entry info, rel type should be rela");
    }
    std::vector<std::shared_ptr<void>> libraryFuncs;
    for (std::uint64_t offset = 0; offset < pltRelSize; offset += sizeof(Elf64_External_Rela)) {
        Elf64_External_Rela* rela = (Elf64_External_Rela*)(jmpRelAddr + offset);
        std::uint64_t relaOffset = *reinterpret_cast<const std::uint64_t*>(rela->r_offset);
        std::uint64_t relaInfo = *reinterpret_cast<const std::uint64_t*>(rela->r_info);
        std::uint64_t relaSym = relaInfo >> 32; // ELF64_R_SYM
        std::uint64_t relaType = relaInfo & 0xffffffff; // ELF64_R_TYPE
        // 获取符号
        Elf64_External_Sym* symbol = dynamicSymbols + relaSym;
        std::uint32_t symbolNameOffset = *reinterpret_cast<std::uint32_t*>(symbol->st_name);
        std::string symbolName(dynamicSymbolNames.data() + symbolNameOffset);
        std::cout << "relocate symbol: " << symbolName << std::endl;
        // 替换函数地址
        // 原本应该延迟解决,这里图简单就直接覆盖了
        void** relaPtr = reinterpret_cast<void**>(relaOffset);
        std::shared_ptr<void> func = resolveLibraryFunc(symbolName);
        if (func == nullptr) {
            throw std::runtime_error("unsupport symbol name");
        }
        libraryFuncs.emplace_back(func);
        *relaPtr = func.get();
    }

地方的代码遍历了DT_JMPREL重定位记录,
并且在加载时设置了这么些函数的地点,
实质上应当通过延迟化解落成的, 然则那里为了简单就直接替换到最后的地点了.

地点拿到函数实际地址的逻辑自个儿写到了resolveLibraryFunc中,那一个函数的实以后别的三个文本,
如下

namespace HelloElfLoader {
    namespace {
        // 原始的返回地址
        thread_local void* originalReturnAddress = nullptr;

        void* getOriginalReturnAddress() {
            return originalReturnAddress;
        }

        void setOriginalReturnAddress(void* address) {
            originalReturnAddress = address;
        }

        // 模拟libc调用main的函数,目前不支持传入argc和argv
        void __libc_start_main(int(*main)()) {
            std::cout << "call main: " << main << std::endl;
            int ret = main();
            std::cout << "result: " << ret << std::endl;
            std::exit(0);
        }

        // 模拟printf函数
        int printf(const char* fmt, ...) {
            int ret;
            va_list myargs;
            va_start(myargs, fmt);
            ret = ::vprintf(fmt, myargs);
            va_end(myargs);
            return ret;
        }

        // 把System V AMD64 ABI转换为Microsoft x64 calling convention
        // 因为vc++不支持inline asm,只能直接写hex
        // 这个函数支持任意长度的参数,但是性能会有损耗,如果参数数量已知可以编写更快的loader代码   
        const char generic_func_loader[]{
            // 让参数连续排列在栈上
            // [第一个参数] [第二个参数] [第三个参数] ...
            0x58, // pop %rax 暂存原返回地址
            0x41, 0x51, // push %r9 入栈第六个参数,之后的参数都在后续的栈上
            0x41, 0x50, // push %r8 入栈第五个参数
            0x51, // push %rcx 入栈第四个参数
            0x52, // push %rdx 入栈第三个参数
            0x56, // push %rsi 入栈第二个参数
            0x57, // push %rdi 入栈第一个参数

            // 调用setOriginalReturnAddress保存原返回地址
            0x48, 0x89, 0xc1, // mov %rax, %rcx 第一个参数是原返回地址
            0x48, 0x83, 0xec, 0x20, // sub $0x20, %rsp 预留32位的影子空间
            0x48, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // movabs $0, %rax
            0xff, 0xd0, // callq *%rax 调用setOriginalReturnAddress
            0x48, 0x83, 0xc4, 0x20, // add %0x20, %rsp 释放影子空间

            // 转换到Microsoft x64 calling convention
            0x59, // pop %rcx 出栈第一个参数
            0x5a, // pop %rdx 出栈第二个参数
            0x41, 0x58, // pop %r8 // 出栈第三个参数
            0x41, 0x59, // pop %r9 // 出栈第四个参数

            // 调用目标函数
            0x48, 0x83, 0xec, 0x20, // sub $0x20, %esp 预留32位的影子空间
            0x48, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // movabs 0, %rax
            0xff, 0xd0, // callq *%rax 调用模拟的函数
            0x48, 0x83, 0xc4, 0x30, // add $0x30, %rsp 释放影子空间和参数(影子空间32 + 参数8*2)
            0x50, // push %rax 保存返回值

            // 调用getOriginalReturnAddress获取原返回地址
            0x48, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // movabs $0, %rax
            0xff, 0xd0, // callq *%rax 调用getOriginalReturnAddress
            0x48, 0x89, 0xc1, // mov %rax, %rcx 原返回地址存到rcx
            0x58, // 恢复返回值
            0x51, // 原返回地址入栈顶
            0xc3 // 返回
        };
        const int generic_func_loader_set_addr_offset = 18;
        const int generic_func_loader_target_offset = 44;
        const int generic_func_loader_get_addr_offset = 61;
    }

    // 获取动态链接函数的调用地址
    std::shared_ptr<void> resolveLibraryFunc(const std::string& name) {
        void* funcPtr = nullptr;
        if (name == "__libc_start_main") {
            funcPtr = __libc_start_main;
        }
        else if (name == "printf") {
            funcPtr = printf;
        }
        else {
            return nullptr;
        }
        void* addr = ::VirtualAlloc(nullptr,
            sizeof(generic_func_loader), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
        if (addr == nullptr) {
            throw std::runtime_error("allocate memory for _libc_start_main_loader failed");
        }
        std::shared_ptr<void> result(addr, [](void* ptr) { ::VirtualFree(ptr, 0, MEM_RELEASE); });
        std::memcpy(addr, generic_func_loader, sizeof(generic_func_loader));
        char* addr_c = reinterpret_cast<char*>(addr);
        *reinterpret_cast<void**>(addr_c + generic_func_loader_set_addr_offset) = setOriginalReturnAddress;
        *reinterpret_cast<void**>(addr_c + generic_func_loader_target_offset) = funcPtr;
        *reinterpret_cast<void**>(addr_c + generic_func_loader_get_addr_offset) = getOriginalReturnAddress;
        return result;
    }
}

知情那段代码须要先明白怎么是x86 calling
conventions,
在汇编中传递函数参数的方法由很两种,
cdecl是把具有参数都位居栈中从低到高排列,
fastcall是把第多少个参数放ecx, 第二个参数放edx, 其余参数放栈中.

大家须求效法的6几位Linux程序,它传参使用了System V AMD64 ABI正式,
先把参数按RDI, RSI, RDX, RCX, R8, R9的逐条设置,倘使有再多参数就位于栈中.
而6三个人的Windows传参使用了Microsoft x64 calling convention规范,
先把参数按RCX, RDX, R8, R9的逐条设置,若是有再多参数就位于栈中,
除此之外还索要预留3个32字节的阴影空间.
如若大家要求让Linux程序调用Windows程序中的函数,
必要对参数的逐条举行转换, 那就是上面的汇编代码所做的事情.

转移前的栈结构如下

[原返回地址 8bytes] [第七个参数] [第八个参数] ...

更换后的栈结构如下

[返回地址 8bytes] [影子空间 32 bytes] [第五个参数] [第六个参数] [第七个参数] ...

因为须求匡助不定个数的参数,
上面的代码用了3个thread local变量来保存原重返地址,
这样的拍卖会影响属性, 如果函数的参数个数已知可以换来更快捷的转换代码.

在安装好动态链接的函数地址后, 大家做到了构想中的第四步,
接下来就可以运营主程序了

    // 获取入口点
    std::uint64_t entryPointAddress = *reinterpret_cast<const std::uint64_t*>(elfHeader.e_entry);
    void(*entryPointFunc)() = reinterpret_cast<void(*)()>(entryPointAddress);
    std::cout << "entry point: " << entryPointFunc << std::endl;
    std::cout << "====== finish loading elf ======" << std::endl;

    // 执行主程序
    // 会先调用__libc_start_main, 然后再调用main
    // 调用__libc_start_main后的指令是hlt,所以必须在__libc_start_main中退出执行
    entryPointFunc();

入口点的地方在ELF头中得以得到到,这几个地址就是_start函数的地方,
我们把它转换到一个void()项目标函数指针再实践即可,
由来示例程序完毕了构想中的全数功效.

实施职能如下图

澳门金沙国际 4

这份演示程序还有为数不少相差, 例如未支持三十四人Linux程序,
不帮衬加载其余Linux动态链接库(so), 不匡助命令行参数等等.
再就是那份演示程序和Bash On Windows的规律有所出入,
因为在用户层是无能为力模拟syscall.
本人愿意它可以让你对怎么样运行其他系统的可执行文件有贰个初步的摸底,
借使你指望更长远的了然哪些模拟syscall,
能够找寻rdmsrwrmsr命令相关的资料.

如何落到实处在Windows上运营Linux程序,SO文件格式。末了附上本人在编写那份演示程序中查阅的链接:

纠错(2017-10-28), 用户层通过vsyscall机制是足以里丑捧心syscall的.

布帆无恙Linux程序运行器

在全体上述的知识后大家可以先构想以下的运营器须要做什么.

因为x64的Windows和Linux程序选取的cpu指令集都是同样的,我们能够直接实施汇编而不要求3个下令模拟器,
并且这一次小编打算在用户层已毕, 所以不可以像Bash On Windows一样模拟syscall,
那几个运维器会像下图一律模拟libc库的函数

澳门金沙国际 5

这么运转器需求做的事务有:

  • 解析ELF文件
  • 加载程序代码到内定的内存地址
  • 加载数据到内定的内存地址
  • 提供动态链接的函数已毕
  • 实施加载的程序代码

那些工作会在以下的以身作则程序中逐一达成, 完整的源代码可以看文章顶部的链接

率先大家需求把ELF文件格式对应的代码从binutils中复制过来,
它涵盖了ELF头, 程序头和连锁的数据结构,
里面用unsigned char[]是为了戒备alignment,
那样结构体可以直接从文件内容中改换过来

ELFDefine.h:

#pragma once

namespace HelloElfLoader {
    // 以下内容复制自
    // https://github.com/aeste/binutils/blob/develop/elfcpp/elfcpp.h
    // https://github.com/aeste/binutils/blob/develop/include/elf/external.h

    // e_ident中各项的偏移值
    const int EI_MAG0 = 0;
    const int EI_MAG1 = 1;
    const int EI_MAG2 = 2;
    const int EI_MAG3 = 3;
    const int EI_CLASS = 4;
    const int EI_DATA = 5;
    const int EI_VERSION = 6;
    const int EI_OSABI = 7;
    const int EI_ABIVERSION = 8;
    const int EI_PAD = 9;
    const int EI_NIDENT = 16;

    // ELF文件类型
    enum {
        ELFCLASSNONE = 0,
        ELFCLASS32 = 1,
        ELFCLASS64 = 2
    };

    // ByteOrder
    enum {
        ELFDATANONE = 0,
        ELFDATA2LSB = 1,
        ELFDATA2MSB = 2
    };

    // 程序头类型
    enum PT
    {
        PT_NULL = 0,
        PT_LOAD = 1,
        PT_DYNAMIC = 2,
        PT_INTERP = 3,
        PT_NOTE = 4,
        PT_SHLIB = 5,
        PT_PHDR = 6,
        PT_TLS = 7,
        PT_LOOS = 0x60000000,
        PT_HIOS = 0x6fffffff,
        PT_LOPROC = 0x70000000,
        PT_HIPROC = 0x7fffffff,
        // The remaining values are not in the standard.
        // Frame unwind information.
        PT_GNU_EH_FRAME = 0x6474e550,
        PT_SUNW_EH_FRAME = 0x6474e550,
        // Stack flags.
        PT_GNU_STACK = 0x6474e551,
        // Read only after relocation.
        PT_GNU_RELRO = 0x6474e552,
        // Platform architecture compatibility information
        PT_ARM_ARCHEXT = 0x70000000,
        // Exception unwind tables
        PT_ARM_EXIDX = 0x70000001
    };

    // 动态节类型
    enum DT
    {
        DT_NULL = 0,
        DT_NEEDED = 1,
        DT_PLTRELSZ = 2,
        DT_PLTGOT = 3,
        DT_HASH = 4,
        DT_STRTAB = 5,
        DT_SYMTAB = 6,
        DT_RELA = 7,
        DT_RELASZ = 8,
        DT_RELAENT = 9,
        DT_STRSZ = 10,
        DT_SYMENT = 11,
        DT_INIT = 12,
        DT_FINI = 13,
        DT_SONAME = 14,
        DT_RPATH = 15,
        DT_SYMBOLIC = 16,
        DT_REL = 17,
        DT_RELSZ = 18,
        DT_RELENT = 19,
        DT_PLTREL = 20,
        DT_DEBUG = 21,
        DT_TEXTREL = 22,
        DT_JMPREL = 23,
        DT_BIND_NOW = 24,
        DT_INIT_ARRAY = 25,
        DT_FINI_ARRAY = 26,
        DT_INIT_ARRAYSZ = 27,
        DT_FINI_ARRAYSZ = 28,
        DT_RUNPATH = 29,
        DT_FLAGS = 30,

        // This is used to mark a range of dynamic tags.  It is not really
        // a tag value.
        DT_ENCODING = 32,

        DT_PREINIT_ARRAY = 32,
        DT_PREINIT_ARRAYSZ = 33,
        DT_LOOS = 0x6000000d,
        DT_HIOS = 0x6ffff000,
        DT_LOPROC = 0x70000000,
        DT_HIPROC = 0x7fffffff,

        // The remaining values are extensions used by GNU or Solaris.
        DT_VALRNGLO = 0x6ffffd00,
        DT_GNU_PRELINKED = 0x6ffffdf5,
        DT_GNU_CONFLICTSZ = 0x6ffffdf6,
        DT_GNU_LIBLISTSZ = 0x6ffffdf7,
        DT_CHECKSUM = 0x6ffffdf8,
        DT_PLTPADSZ = 0x6ffffdf9,
        DT_MOVEENT = 0x6ffffdfa,
        DT_MOVESZ = 0x6ffffdfb,
        DT_FEATURE = 0x6ffffdfc,
        DT_POSFLAG_1 = 0x6ffffdfd,
        DT_SYMINSZ = 0x6ffffdfe,
        DT_SYMINENT = 0x6ffffdff,
        DT_VALRNGHI = 0x6ffffdff,

        DT_ADDRRNGLO = 0x6ffffe00,
        DT_GNU_HASH = 0x6ffffef5,
        DT_TLSDESC_PLT = 0x6ffffef6,
        DT_TLSDESC_GOT = 0x6ffffef7,
        DT_GNU_CONFLICT = 0x6ffffef8,
        DT_GNU_LIBLIST = 0x6ffffef9,
        DT_CONFIG = 0x6ffffefa,
        DT_DEPAUDIT = 0x6ffffefb,
        DT_AUDIT = 0x6ffffefc,
        DT_PLTPAD = 0x6ffffefd,
        DT_MOVETAB = 0x6ffffefe,
        DT_SYMINFO = 0x6ffffeff,
        DT_ADDRRNGHI = 0x6ffffeff,

        DT_RELACOUNT = 0x6ffffff9,
        DT_RELCOUNT = 0x6ffffffa,
        DT_FLAGS_1 = 0x6ffffffb,
        DT_VERDEF = 0x6ffffffc,
        DT_VERDEFNUM = 0x6ffffffd,
        DT_VERNEED = 0x6ffffffe,
        DT_VERNEEDNUM = 0x6fffffff,

        DT_VERSYM = 0x6ffffff0,

        // Specify the value of _GLOBAL_OFFSET_TABLE_.
        DT_PPC_GOT = 0x70000000,

        // Specify the start of the .glink section.
        DT_PPC64_GLINK = 0x70000000,

        // Specify the start and size of the .opd section.
        DT_PPC64_OPD = 0x70000001,
        DT_PPC64_OPDSZ = 0x70000002,

        // The index of an STT_SPARC_REGISTER symbol within the DT_SYMTAB
        // symbol table.  One dynamic entry exists for every STT_SPARC_REGISTER
        // symbol in the symbol table.
        DT_SPARC_REGISTER = 0x70000001,

        DT_AUXILIARY = 0x7ffffffd,
        DT_USED = 0x7ffffffe,
        DT_FILTER = 0x7fffffff
    };;

    // ELF头的定义
    typedef struct {
        unsigned char   e_ident[16];        /* ELF "magic number" */
        unsigned char   e_type[2];      /* Identifies object file type */
        unsigned char   e_machine[2];       /* Specifies required architecture */
        unsigned char   e_version[4];       /* Identifies object file version */
        unsigned char   e_entry[8];     /* Entry point virtual address */
        unsigned char   e_phoff[8];     /* Program header table file offset */
        unsigned char   e_shoff[8];     /* Section header table file offset */
        unsigned char   e_flags[4];     /* Processor-specific flags */
        unsigned char   e_ehsize[2];        /* ELF header size in bytes */
        unsigned char   e_phentsize[2];     /* Program header table entry size */
        unsigned char   e_phnum[2];     /* Program header table entry count */
        unsigned char   e_shentsize[2];     /* Section header table entry size */
        unsigned char   e_shnum[2];     /* Section header table entry count */
        unsigned char   e_shstrndx[2];      /* Section header string table index */
    } Elf64_External_Ehdr;

    // 程序头的定义
    typedef struct {
        unsigned char   p_type[4];      /* Identifies program segment type */
        unsigned char   p_flags[4];     /* Segment flags */
        unsigned char   p_offset[8];        /* Segment file offset */
        unsigned char   p_vaddr[8];     /* Segment virtual address */
        unsigned char   p_paddr[8];     /* Segment physical address */
        unsigned char   p_filesz[8];        /* Segment size in file */
        unsigned char   p_memsz[8];     /* Segment size in memory */
        unsigned char   p_align[8];     /* Segment alignment, file & memory */
    } Elf64_External_Phdr;

    // DYNAMIC类型的程序头的内容定义
    typedef struct {
        unsigned char   d_tag[8];       /* entry tag value */
        union {
            unsigned char   d_val[8];
            unsigned char   d_ptr[8];
        } d_un;
    } Elf64_External_Dyn;

    // 动态链接的重定位记录,部分系统会用Elf64_External_Rel
    typedef struct {
        unsigned char r_offset[8];  /* Location at which to apply the action */
        unsigned char   r_info[8];  /* index and type of relocation */
        unsigned char   r_addend[8];    /* Constant addend used to compute value */
    } Elf64_External_Rela;

    // 动态链接的符号信息
    typedef struct {
        unsigned char   st_name[4];     /* Symbol name, index in string tbl */
        unsigned char   st_info[1];     /* Type and binding attributes */
        unsigned char   st_other[1];        /* No defined meaning, 0 */
        unsigned char   st_shndx[2];        /* Associated section index */
        unsigned char   st_value[8];        /* Value of the symbol */
        unsigned char   st_size[8];     /* Associated symbol size */
    } Elf64_External_Sym;
}

接下去大家定义2个读取和执行ELF文件的类,
那些类会在早先化时把公文加载到fileStream_, execute函数会负责实施

HelloElfLoader.h:

#pragma once
#include <string>
#include <fstream>

namespace HelloElfLoader {
    class Loader {
        std::ifstream fileStream_;

    public:
        Loader(const std::string& path);
        Loader(std::ifstream&& fileStream);
        void execute();
    };
}

构造函数如下, 约等于正式的c++打开文件的代码

HelloElfLoader.cpp:

Loader::Loader(const std::string& path) :
    Loader(std::ifstream(path, std::ios::in | std::ios::binary)) {}

Loader::Loader(std::ifstream&& fileStream) :
    fileStream_(std::move(fileStream)) {
    if (!fileStream_) {
        throw std::runtime_error("open file failed");
    }
}

接下去将贯彻地点所说的步调, 首先是解析ELF文件

void Loader::execute() {
    std::cout << "====== start loading elf ======" << std::endl;

    // 检查当前运行程序是否64位
    if (sizeof(intptr_t) != sizeof(std::int64_t)) {
        throw std::runtime_error("please use x64 compile and run this program");
    }

    // 读取ELF头
    Elf64_External_Ehdr elfHeader = {};
    fileStream_.seekg(0);
    fileStream_.read(reinterpret_cast<char*>(&elfHeader), sizeof(elfHeader));

    // 检查ELF头,只支持64位且byte order是little endian的程序
    if (std::string(reinterpret_cast<char*>(elfHeader.e_ident), 4) != "\x7f\x45\x4c\x46") {
        throw std::runtime_error("magic not match");
    }
    else if (elfHeader.e_ident[EI_CLASS] != ELFCLASS64) {
        throw std::runtime_error("only support ELF64");
    }
    else if (elfHeader.e_ident[EI_DATA] != ELFDATA2LSB) {
        throw std::runtime_error("only support little endian");
    }

    // 获取program table的信息
    std::uint32_t programTableOffset = *reinterpret_cast<std::uint32_t*>(elfHeader.e_phoff);
    std::uint16_t programTableEntrySize = *reinterpret_cast<std::uint16_t*>(elfHeader.e_phentsize);
    std::uint16_t programTableEntryNum = *reinterpret_cast<std::uint16_t*>(elfHeader.e_phnum);
    std::cout << "program table at: " << programTableOffset << ", "
        << programTableEntryNum << " x " << programTableEntrySize << std::endl;

    // 获取section table的信息
    // section table只给linker用,loader中其实不需要访问section table
    std::uint32_t sectionTableOffset = *reinterpret_cast<std::uint32_t*>(elfHeader.e_shoff);
    std::uint16_t sectionTableEntrySize = *reinterpret_cast<std::uint16_t*>(elfHeader.e_shentsize);
    std::uint16_t sectionTableEntryNum = *reinterpret_cast<std::uint16_t*>(elfHeader.e_shentsize);
    std::cout << "section table at: " << sectionTableOffset << ", "
        << sectionTableEntryNum << " x " << sectionTableEntrySize << std::endl;

ELF文件的的先导部分就是ELF头,和Elf64_External_Ehdr结构体的布局同样,
大家能够读到Elf64_External_Ehdr结构体中,
然后ELF头包蕴了程序头和节头的偏移值, 大家可以先行获取到那一个参数

节头在运维时不要求采纳, 运转时索要遍历程序头

    // 准备动态链接的信息
    std::uint64_t jmpRelAddr = 0; // 重定位记录的开始地址
    std::uint64_t pltRelType = 0; // 重定位记录的类型 RELA或REL
    std::uint64_t pltRelSize = 0; // 重定位记录的总大小
    std::uint64_t symTabAddr = 0; // 动态符号表的开始地址
    std::uint64_t strTabAddr = 0; // 动态符号名称表的开始地址
    std::uint64_t strTabSize = 0; // 动态符号名称表的总大小

    // 遍历program hedaer
    std::vector<Elf64_External_Phdr> programHeaders;
    programHeaders.resize(programTableEntryNum);
    fileStream_.read(reinterpret_cast<char*>(programHeaders.data()), programTableEntryNum * programTableEntrySize);
    std::vector<std::shared_ptr<void>> loadedSegments;
    for (const auto& programHeader : programHeaders) {
        std::uint32_t type = *reinterpret_cast<const std::uint32_t*>(programHeader.p_type);
        if (type == PT_LOAD) {
            // 把文件内容(包含程序代码和数据)加载到虚拟内存,这个示例不考虑地址冲突
            std::uint64_t fileOffset = *reinterpret_cast<const std::uint64_t*>(programHeader.p_offset);
            std::uint64_t fileSize = *reinterpret_cast<const std::uint64_t*>(programHeader.p_filesz);
            std::uint64_t virtAddr = *reinterpret_cast<const std::uint64_t*>(programHeader.p_vaddr);
            std::uint64_t memSize = *reinterpret_cast<const std::uint64_t*>(programHeader.p_memsz);
            if (memSize < fileSize) {
                throw std::runtime_error("invalid memsz in program header, it shouldn't less than filesz");
            }
            // 在指定的虚拟地址分配内存
            std::cout << std::hex << "allocate address at: 0x" << virtAddr <<
                " size: 0x" << memSize << std::dec << std::endl;
            void* addr = ::VirtualAlloc((void*)virtAddr, memSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
            if (addr == nullptr) {
                throw std::runtime_error("allocate memory at specific address failed");
            }
            loadedSegments.emplace_back(addr, [](void* ptr) { ::VirtualFree(ptr, 0, MEM_RELEASE); });
            // 复制文件内容到虚拟内存
            fileStream_.seekg(fileOffset);
            if (!fileStream_.read(reinterpret_cast<char*>(addr), fileSize)) {
                throw std::runtime_error("read contents into memory from LOAD program header failed");
            }
        }
        else if (type == PT_DYNAMIC) {
            // 遍历动态节
            std::uint64_t fileOffset = *reinterpret_cast<const std::uint64_t*>(programHeader.p_offset);
            fileStream_.seekg(fileOffset);
            Elf64_External_Dyn dynSection = {};
            std::uint64_t dynSectionTag = 0;
            std::uint64_t dynSectionVal = 0;
            do {
                if (!fileStream_.read(reinterpret_cast<char*>(&dynSection), sizeof(dynSection))) {
                    throw std::runtime_error("read dynamic section failed");
                }
                dynSectionTag = *reinterpret_cast<const std::uint64_t*>(dynSection.d_tag);
                dynSectionVal = *reinterpret_cast<const std::uint64_t*>(dynSection.d_un.d_val);
                if (dynSectionTag == DT_JMPREL) {
                    jmpRelAddr = dynSectionVal;
                }
                else if (dynSectionTag == DT_PLTREL) {
                    pltRelType = dynSectionVal;
                }
                else if (dynSectionTag == DT_PLTRELSZ) {
                    pltRelSize = dynSectionVal;
                }
                else if (dynSectionTag == DT_SYMTAB) {
                    symTabAddr = dynSectionVal;
                }
                else if (dynSectionTag == DT_STRTAB) {
                    strTabAddr = dynSectionVal;
                }
                else if (dynSectionTag == DT_STRSZ) {
                    strTabSize = dynSectionVal;
                }
            } while (dynSectionTag != 0);
        }
    }

还记得大家地点运用readelf读取到的新闻呢?

程序头:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000400040 0x0000000000400040
                 0x00000000000001f8 0x00000000000001f8  R E    8
  INTERP         0x0000000000000238 0x0000000000400238 0x0000000000400238
                 0x000000000000001c 0x000000000000001c  R      1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x00000000000007d4 0x00000000000007d4  R E    200000
  LOAD           0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
                 0x0000000000000228 0x0000000000000230  RW     200000
  DYNAMIC        0x0000000000000e28 0x0000000000600e28 0x0000000000600e28
                 0x00000000000001d0 0x00000000000001d0  RW     8
  NOTE           0x0000000000000254 0x0000000000400254 0x0000000000400254
                 0x0000000000000044 0x0000000000000044  R      4
  GNU_EH_FRAME   0x0000000000000680 0x0000000000400680 0x0000000000400680
                 0x000000000000003c 0x000000000000003c  R      4
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     10
  GNU_RELRO      0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
                 0x00000000000001f0 0x00000000000001f0  R      1

这里面类型是LOAD的头代表必要加载文件的内容到内存,
Offset是文件的偏移值, VirtAddr是虚拟内存地址,
FileSiz是要求加载的文件大小, MemSiz是亟需分配的内存大小,
Flags是内存的走访权限,
以此示例不考虑访问权限(统一行使PAGE_EXECUTE_READWRITE).

以此顺序有七个LOAD头, 第③个包括了代码和只读数据(.data, .init,
.rodata等节的始末), 第三个饱含了可写数据(.init_array,
.fini_array等节的始末).

LOAD头对应的内容加载到内定的内存地址后大家就做到了构想中的第二个第1个步骤,
将来代码和数据都在内存中了.

接下去我们还索要处理动态链接的函数,
处理所需的音信可以从DYNAMIC头得到
DYNAMIC头包涵的信息有

Dynamic section at offset 0xe28 contains 24 entries:
  标记        类型                         名称/值
 0x0000000000000001 (NEEDED)             共享库:[libc.so.6]
 0x000000000000000c (INIT)               0x4003c8
 0x000000000000000d (FINI)               0x400624
 0x0000000000000019 (INIT_ARRAY)         0x600e10
 0x000000000000001b (INIT_ARRAYSZ)       8 (bytes)
 0x000000000000001a (FINI_ARRAY)         0x600e18
 0x000000000000001c (FINI_ARRAYSZ)       8 (bytes)
 0x000000006ffffef5 (GNU_HASH)           0x400298
 0x0000000000000005 (STRTAB)             0x400318
 0x0000000000000006 (SYMTAB)             0x4002b8
 0x000000000000000a (STRSZ)              63 (bytes)
 0x000000000000000b (SYMENT)             24 (bytes)
 0x0000000000000015 (DEBUG)              0x0
 0x0000000000000003 (PLTGOT)             0x601000
 0x0000000000000002 (PLTRELSZ)           48 (bytes)
 0x0000000000000014 (PLTREL)             RELA
 0x0000000000000017 (JMPREL)             0x400398
 0x0000000000000007 (RELA)               0x400380
 0x0000000000000008 (RELASZ)             24 (bytes)
 0x0000000000000009 (RELAENT)            24 (bytes)
 0x000000006ffffffe (VERNEED)            0x400360
 0x000000006fffffff (VERNEEDNUM)         1
 0x000000006ffffff0 (VERSYM)             0x400358
 0x0000000000000000 (NULL)               0x0

多个个看下边代码中涉嫌到的花色

  • DT_JMPREL: 重定位记录的启幕地址,
    指向.rela.plt节在内存中保留的地方
  • DT_PLTREL: 重定位记录的品种 RELA或RE, 那里是RELAL
  • DT_PLTRELSZ: 重定位记录的总大小, 那里是24 * 2 = 48

重定位节 '.rela.plt' 位于偏移量 0x398 含有 2 个条目:
  偏移量          信息           类型           符号值        符号名称 + 加数
000000601018  000100000007 R_X86_64_JUMP_SLO 0000000000000000 printf@GLIBC_2.2.5 + 0
000000601020  000200000007 R_X86_64_JUMP_SLO 0000000000000000 __libc_start_main@GLIBC_2.2.5 + 0
  • DT_SYMTAB: 动态符号表的启幕地址,
    指向.dynsym节在内存中保留的地点
  • DT_STRTAB: 动态符号名称表的开始地址,
    指向.dynstr节在内存中保存的地方
  • DT_STRSZ: 动态符号名称表的总大小

Symbol table '.dynsym' contains 4 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND printf@GLIBC_2.2.5 (2)
     2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@GLIBC_2.2.5 (2)
     3: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__

在遍历完程序头以往, 我们可以精通有多个动态链接的函数须求重一直,
它们各自是__libc_start_mainprintf,
其中__libc_start_main负责调用main函数
接下去让我们必要安装那个函数的地方

    // 读取动态链接符号表
    std::string dynamicSymbolNames(reinterpret_cast<char*>(strTabAddr), strTabSize);
    Elf64_External_Sym* dynamicSymbols = reinterpret_cast<Elf64_External_Sym*>(symTabAddr);

    // 设置动态链接的函数地址
    std::cout << std::hex << "read dynamic entires at: 0x" << jmpRelAddr <<
        " size: 0x" << pltRelSize << std::dec << std::endl;
    if (jmpRelAddr == 0 || pltRelType != DT_RELA || pltRelSize % sizeof(Elf64_External_Rela) != 0) {
        throw std::runtime_error("invalid dynamic entry info, rel type should be rela");
    }
    std::vector<std::shared_ptr<void>> libraryFuncs;
    for (std::uint64_t offset = 0; offset < pltRelSize; offset += sizeof(Elf64_External_Rela)) {
        Elf64_External_Rela* rela = (Elf64_External_Rela*)(jmpRelAddr + offset);
        std::uint64_t relaOffset = *reinterpret_cast<const std::uint64_t*>(rela->r_offset);
        std::uint64_t relaInfo = *reinterpret_cast<const std::uint64_t*>(rela->r_info);
        std::uint64_t relaSym = relaInfo >> 32; // ELF64_R_SYM
        std::uint64_t relaType = relaInfo & 0xffffffff; // ELF64_R_TYPE
        // 获取符号
        Elf64_External_Sym* symbol = dynamicSymbols + relaSym;
        std::uint32_t symbolNameOffset = *reinterpret_cast<std::uint32_t*>(symbol->st_name);
        std::string symbolName(dynamicSymbolNames.data() + symbolNameOffset);
        std::cout << "relocate symbol: " << symbolName << std::endl;
        // 替换函数地址
        // 原本应该延迟解决,这里图简单就直接覆盖了
        void** relaPtr = reinterpret_cast<void**>(relaOffset);
        std::shared_ptr<void> func = resolveLibraryFunc(symbolName);
        if (func == nullptr) {
            throw std::runtime_error("unsupport symbol name");
        }
        libraryFuncs.emplace_back(func);
        *relaPtr = func.get();
    }

下面的代码遍历了DT_JMPREL重定位记录,
并且在加载时设置了那个函数的地方,
实际应当经过延迟化解已毕的, 但是那里为了简单就一贯替换来最后的地方了.

地点得到函数实际地址的逻辑本人写到了resolveLibraryFunc中,那个函数的落到实处在其它叁个文本,
如下

namespace HelloElfLoader {
    namespace {
        // 原始的返回地址
        thread_local void* originalReturnAddress = nullptr;

        void* getOriginalReturnAddress() {
            return originalReturnAddress;
        }

        void setOriginalReturnAddress(void* address) {
            originalReturnAddress = address;
        }

        // 模拟libc调用main的函数,目前不支持传入argc和argv
        void __libc_start_main(int(*main)()) {
            std::cout << "call main: " << main << std::endl;
            int ret = main();
            std::cout << "result: " << ret << std::endl;
            std::exit(0);
        }

        // 模拟printf函数
        int printf(const char* fmt, ...) {
            int ret;
            va_list myargs;
            va_start(myargs, fmt);
            ret = ::vprintf(fmt, myargs);
            va_end(myargs);
            return ret;
        }

        // 把System V AMD64 ABI转换为Microsoft x64 calling convention
        // 因为vc++不支持inline asm,只能直接写hex
        // 这个函数支持任意长度的参数,但是性能会有损耗,如果参数数量已知可以编写更快的loader代码   
        const char generic_func_loader[]{
            // 让参数连续排列在栈上
            // [第一个参数] [第二个参数] [第三个参数] ...
            0x58, // pop %rax 暂存原返回地址
            0x41, 0x51, // push %r9 入栈第六个参数,之后的参数都在后续的栈上
            0x41, 0x50, // push %r8 入栈第五个参数
            0x51, // push %rcx 入栈第四个参数
            0x52, // push %rdx 入栈第三个参数
            0x56, // push %rsi 入栈第二个参数
            0x57, // push %rdi 入栈第一个参数

            // 调用setOriginalReturnAddress保存原返回地址
            0x48, 0x89, 0xc1, // mov %rax, %rcx 第一个参数是原返回地址
            0x48, 0x83, 0xec, 0x20, // sub $0x20, %rsp 预留32位的影子空间
            0x48, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // movabs $0, %rax
            0xff, 0xd0, // callq *%rax 调用setOriginalReturnAddress
            0x48, 0x83, 0xc4, 0x20, // add %0x20, %rsp 释放影子空间

            // 转换到Microsoft x64 calling convention
            0x59, // pop %rcx 出栈第一个参数
            0x5a, // pop %rdx 出栈第二个参数
            0x41, 0x58, // pop %r8 // 出栈第三个参数
            0x41, 0x59, // pop %r9 // 出栈第四个参数

            // 调用目标函数
            0x48, 0x83, 0xec, 0x20, // sub $0x20, %esp 预留32位的影子空间
            0x48, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // movabs 0, %rax
            0xff, 0xd0, // callq *%rax 调用模拟的函数
            0x48, 0x83, 0xc4, 0x30, // add $0x30, %rsp 释放影子空间和参数(影子空间32 + 参数8*2)
            0x50, // push %rax 保存返回值

            // 调用getOriginalReturnAddress获取原返回地址
            0x48, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // movabs $0, %rax
            0xff, 0xd0, // callq *%rax 调用getOriginalReturnAddress
            0x48, 0x89, 0xc1, // mov %rax, %rcx 原返回地址存到rcx
            0x58, // 恢复返回值
            0x51, // 原返回地址入栈顶
            0xc3 // 返回
        };
        const int generic_func_loader_set_addr_offset = 18;
        const int generic_func_loader_target_offset = 44;
        const int generic_func_loader_get_addr_offset = 61;
    }

    // 获取动态链接函数的调用地址
    std::shared_ptr<void> resolveLibraryFunc(const std::string& name) {
        void* funcPtr = nullptr;
        if (name == "__libc_start_main") {
            funcPtr = __libc_start_main;
        }
        else if (name == "printf") {
            funcPtr = printf;
        }
        else {
            return nullptr;
        }
        void* addr = ::VirtualAlloc(nullptr,
            sizeof(generic_func_loader), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
        if (addr == nullptr) {
            throw std::runtime_error("allocate memory for _libc_start_main_loader failed");
        }
        std::shared_ptr<void> result(addr, [](void* ptr) { ::VirtualFree(ptr, 0, MEM_RELEASE); });
        std::memcpy(addr, generic_func_loader, sizeof(generic_func_loader));
        char* addr_c = reinterpret_cast<char*>(addr);
        *reinterpret_cast<void**>(addr_c + generic_func_loader_set_addr_offset) = setOriginalReturnAddress;
        *reinterpret_cast<void**>(addr_c + generic_func_loader_target_offset) = funcPtr;
        *reinterpret_cast<void**>(addr_c + generic_func_loader_get_addr_offset) = getOriginalReturnAddress;
        return result;
    }
}

精通这段代码需求先了然如何是x86 calling
conventions,
在汇编中传递函数参数的艺术由很多种,
cdecl是把持有参数都置身栈中从低到高排列,
fastcall是把第多少个参数放ecx, 第②个参数放edx, 其他参数放栈中.

大家需求效法的六十一人Linux程序,它传参使用了System V AMD64 ABI规范,
先把参数按RDI, RSI, RDX, RCX, R8, R9的顺序设置,如果有再多参数就放在栈中.
而6几人的Windows传参使用了Microsoft x64 calling convention标准,
先把参数按RCX, RDX, R8, R9的次第设置,若是有再多参数就放在栈中,
除此之外还索要预留一个32字节的黑影空间.
只要大家须要让Linux程序调用Windows程序中的函数,
必要对参数的顺序举办更换, 那就是地方的汇编代码所做的事情.

转移前的栈结构如下

[原返回地址 8bytes] [第七个参数] [第八个参数] ...

改换后的栈结构如下

[返回地址 8bytes] [影子空间 32 bytes] [第五个参数] [第六个参数] [第七个参数] ...

因为急需帮忙不定个数的参数,
上边的代码用了3个thread local变量来保存原再次来到地址,
那样的拍卖会影响属性, 如若函数的参数个数已知可以换来更飞速的转换代码.

在安装好动态链接的函数地址后, 大家成功了构想中的第肆步,
接下来就足以运营主程序了

    // 获取入口点
    std::uint64_t entryPointAddress = *reinterpret_cast<const std::uint64_t*>(elfHeader.e_entry);
    void(*entryPointFunc)() = reinterpret_cast<void(*)()>(entryPointAddress);
    std::cout << "entry point: " << entryPointFunc << std::endl;
    std::cout << "====== finish loading elf ======" << std::endl;

    // 执行主程序
    // 会先调用__libc_start_main, 然后再调用main
    // 调用__libc_start_main后的指令是hlt,所以必须在__libc_start_main中退出执行
    entryPointFunc();

入口点的地点在ELF头中可以博得到,这一个地方就是_start函数的地点,
大家把它转换来1个void()品类的函数指针再举行即可,
时至明日示例程序达成了构想中的全部功效.

实践职能如下图

澳门金沙国际 6

这份演示程序还有不少不足, 例如未援助3肆个人Linux程序,
不支持加载其余Linux动态链接库(so), 不帮衬命令行参数等等.
再者那份演示程序和Bash On Windows的原理有所出入,
因为在用户层是心有余而力不足模拟syscall.
自家梦想它可以让你对什么样运作其余系统的可执行文件有三个初阶的摸底,
借使你愿意更深远的理解哪些模拟syscall,
可以找寻rdmsrwrmsr指令相关的资料.

最终附上本人在编写那份演示程序中查阅的链接:

纠错(2017-10-28), 用户层通过vsyscall机制是足以效仿syscall的.

参考:

  1. 《ELF文件格式分析》滕启明 二零零零年二月

  2. POC一期文档资料

2. 编译器

  • 将高级语言翻译成机器语言的二个工具
  • 编译进程相似可分为 6
    步:扫描、语法分析、语义分析、源代码优化、代码生成、目标代码优化

    • 词法分析 :
      源代码程序被输入到扫描器,运用一种恍若于少数状态机的算法轻松地将源代码的字符种类分割成一体系的标记,记号一般分为:关键字、标识符、字面量、特殊符号
    • 语法分析 :
      对由扫描器暴发的标志进行辨析,从而暴发语法树(以表明式为节点的树),整个分析过程接纳了上下文无关语法
    • 语义分析 :
      静态语义和动态语义,经过语义分析阶段后,整个语法树的表明式都被标识了体系
    • 中档语言生成 :
      源代码优化器会在源代码级别举行优化,将全部语法树转换到中间代码,使得编译器可以被分成前端和后端,前端负责产生机器非亲非故的中间代码,后端将中间代码转换到目的机器代码。中间代码有诸多门类,比较广泛的有:三地址码、P –
      代码
    • 对象代码生成与优化 : 编译器后端包括代码生成器(将中间代码转换来目标机器代码) 和
      目标代码优化器(对目的代码举办优化,比如接纳合适的寻址格局、使用位移来代替乘法运算、删除多余的下令等)

3. 静态链接

  • 链接的要害内容 :
    把各种模块之间互相引用的部分都处理好,使得各种模块之间可以科学地链接
  • 经过紧要不外乎 : 地址和空中分配、符号决议、重一向等步骤
  • 各种模块的源代码文件通过编译器编译成目标文件,目的文件和库一起链接形成最后的可执行文件
  • 最广大的库就是运作时库,它是
    襄助程序运营的中坚函数的成团,库其实是一组目的文件的包
  • 重定位 : 编译器无法明确地方的动静下,先将下令的靶子地址置为
    0,等待链接器将目的文件链接起来的时候再将其修正

三 、目的文件

1. 目的文件的格式

  • 对象文件就是源代码编译后但未开展链接的那种中间文件,它跟可执行文件的格式大约是千篇一律的,可广义的作为同一种类型的文本,在
    Windows 下可把它们统称为 PE-COFF 格式,在 Linux 下可把它们统称为 ELF
    文件
  • 可执行文件、动态链接库即静态链接库文件都根据可执行文件格式存储。可执行文件格式
    :紧如若 Windows 下的 PELinux 的 ELF,都以 COFF
    格式的变种
  • 静态链接库和动态链接库都以听从可执行文件格式存储
  • ELF 文件归为 4 类 :
    • 可重一向文件 : 如 .o / .obj
    • 可执行文件 : 如 .exe
    • 共享目的文件 : 如 .so / .dll
    • 基本转储文件 : Linux 下的 core dump

2. 对象文件

  • 含有 : 编译后的机器指令代码、数据、链接时所急需的片段音讯(如
    符号表、调试音信、字符串等)
  • 目的文件将这么些音信以“段”的样式储存
  • 程序源代码被编译后根本分为二种段:
    • 程序指令 : 代码段
    • 次第数据 :
      • 数据段 : 已开首化的 全局变量和一些静态变量
      • .bss 段 : 为 未早先化的 全局变量和有个别静态变量
        预留地点,并不曾内容,所以在文书中也不占用空间
    • 数码和指令分段的功利:
      • 数量区域是可读写的,指令区域是只读的,防止程序的通令被改写
      • 对 CPU 的缓存命中率升高有实益
      • 当系统中运作着多个该程序的副本时,指令等能源都以共享的,只需保留一份,而种种副本的数据区域是不雷同的,是经过私有的,可节省多量空中

3. 打通目的文件

  • objdump -h SimpleSection.o : 查看各样目的文件的结构和内容
  • size SimpleSection.o : 查看 ELF 文件的代码段、数据段和 BSS
    段的长度
  • 代码段
  • 数据段和只读数据段
    • .data 段保留的是那2个早已起首化了的全局变量和一部分静态变量
    • .rodata 段存放的是社会制度数据,一般是程序里面的只读变量和字符串常量
  • BSS 段 : .bss 段存放的是 未先导化的全局变量和一部分静态变量,.bss
    段为它们预留了半空中,但稍事编译器不存放全局的未初阶化变量,只是预留三个未定义的全局变量符号,等到最终链接成可执行文件的时候再在
    .bss 段分配空间,然而编译单元内部可知的静态变量的确是存放在 .bss
    段的
  • 其他段
    • 那么些段的名字皆以由 .
      作为前缀,表示这么些表的名字是系统保留的,应用程序也可以应用一些非系统保留的名字作为段名
    • 自定义段 : 在全局变量或函数从前拉长
      __attribute__((section("name")))
      属性就可以把相应的变量或函数放到以 name 作为段名的段中

4. ELF 文本结构描述

  • 文件头
    • readelf -h 详细查看 ELF 文件
    • 文本头重定义了 ELF 魔数(最前边的 “Magic” 的 16 个字节,被 ELF
      标准规定用来标识 ELF
      文件的阳台属性,用来认可文件的里类型)、文件机器字节长度、数据存储格局、版本、运营平台、ABI
      版本、ELF
      重定位类型、硬件平台、硬件平台版本、入口地址、程序头入口和长度、段表的地点和尺寸、段的数据等
  • 段表
    • 保留各样段的骨干质量的构造,描述了 ELF
      各种段的音讯,如逐个段的段名、长度、在文书中的偏移、读写权限、段的其余属性
    • 是三个以 Elf32_Shdr 结构体为要素的数组,成分的个数等于段的个数
    • 段的名字只是在链接和编译进程中有含义,对于编译器和链接器来说,首要决定段的本性的是
      段的类型段的标志位

      • 段的品种 相关常量以 SHT_ 开头
      • 段的标志位
        表示该段在进程虚拟地址空间中的属性,如是不是可写、是不是可实施等,相关常量以
        SHF_ 开头
  • 重定位表 :
    链接器在拍卖对象文件时,要求对目标文件中的某个部位开展重平昔,即代码段和多少段中那么些对绝对地址的引用的职责
  • 字符串表
    • 把字符串集中起来存放到1个表,然后采纳字符串在表中的撼动来引用字符串
    • 字符串表(保存普通的字符串) 和
      段表字符串表(保存段表中用到的字符串)

5. 链接的接口 —— 符号

  • 在链接中,将函数和变量统称为符号,函数名或变量名就是符号名
  • 每多少个对象文件都会有2个应和的符号表,各种定义的标记有二个对应的值,叫做符号值
  • 标志表中的保有符号举行分类 :
    • 概念在本目的文件的全局符号
    • 在本目的文件中引用的全局符号(外部符号)
    • 段名,它的值就是该段的序曲地址
    • 一部分符号
    • 行号音信
  • ELF 符号表结构 :ELF 符号表往往是文件中的八个段,段名一般叫
    .symtab,是一个 Elf32_Sym 结构的数组

    • st_name 符号名
    • st_value 符号对应的值
    • st_size 符号大小
    • st_info 符号类型和绑定消息
    • st_other (没用)
    • st_shndx 符号所在的段 :
      如果符号定义在本目的文件中,那么那一个成员代表符号所在段表的下表
  • 特殊符号 :使用 ld
    作为链接器来链接生产可执行文件时,他会为大家定义很多与众不一样的符号,可以一向申明同时引用它
  • 标志修饰与函数签名
    • 提防不相同对象文件中的符号名争辨,如 C++
      增添了名称空间的艺术来缓解多模块的标记争论难点
    • C++ 符号修饰
    • 函数签名 :
      包蕴二个函数的新闻,包蕴函数名、参数类型、类和称号空间及其余消息
  • extern "C" : C++ 会将在其的大括号内的代码当做 C 语言代码处理
  • 弱符号与强符号
    • 对此 C/C++
      语言来说,编译器暗中同意函数和初阶化了的全局变量为强符号,未初叶化的全局变量为弱符号,也得以因此GCC 的 __attribute__((weak))
      来定义任何三个强符号为弱引用,强、弱符号不是指向符号的引用,只针对定义
    • 差距意强符号被一再概念、假使二个符号在有些目的文件中是强符号而在任何文件中都是弱符号,那么接纳强符号
    • 弱引用与强引用
      :对于未定义的弱引用,链接器不以为它是贰个张冠李戴,默许为 0
      或一个例外的值,而对于未声明的强引用,链接器会报符号未定义错误

④ 、静态链接

1. 上空与地点分配

  • 链接进度 :将多少个输入目标文件加工后统一成一个输出文件
  • 按序叠加 :会促成内存空间大批量的内部碎片
  • 貌似段合并 :将一律属性的段合并到一块儿
    • 链接器为对象分配地址和空间
      :对于有实际数据的段,两者都要分配空间;对于 “.bss”
      那样的段只局限于分配虚拟地址空间

      • 在输出的可执行文件中的空间
      • 在装载后的虚拟地址中的虚拟地址空间
    • 两步链接
      • 空间与地点分配
      • 标志解析与重一直
  • 符号地址的规定
    :各种符号在段内的周旋地点是固定的,链接器须求给各样符号加二个偏移量,使它们可以调动到科学的虚拟地址

2. 标志解析与重一直

  • 重定位
    • 源代码被编译成目的文件时,编译器不晓得定义在其余目的文件中的符号地址,所以编译器权且把地点0 看作是该变量的地方,该函数的地点也是二个权且的假地址
    • 链接器在形成地点和空中分配之后就足以显然全数符号的虚拟地址了,链接器可以依照富豪的地点对每种需求重平昔的下令举办身份勘误,call
      指令是一条近址相对位移调用命令,它背后跟的是调用指令的下一条指令的偏移量
  • 重定位表
    • objdump -r 各个要被重一向的 ELF
      段都有一个应和的重定位表,也等于 ELF 文件中的三个段
    • 每种要被重一向的地点叫三个重定位入口,重一向入口的偏移表示该入口在要被重定位段中的地点
  • 标志解析
    :重从来进程中,各种重一贯的进口都以对三个标记的引用,重平昔进度中,链接器会去追寻由拥有输入目的文件的符号表组成的全局符号表,找到呼应的符号后展开重一直
  • 一声令下考订格局
    • 纯属寻址改进 S + A :更正后的地方为该符号的其实地址
    • 相持寻址纠正 S + A – P
      :矫正后的地点为该符号距离被革新地点的地方差

3. COMMON 块

  • 编译器将未起先化的全局/静态变量作为弱符号处理,链接时存在多个同名的弱符号,链接后输出文件中以最大的丰裕为准
  • 编译时将弱符号标记为 COMMON
    类型,由于该若符号最后所占的上空尺寸是不解的,所以不大概为该弱符号在
    BSS
    段分配空间,可是链接器在链接进程后鲜明了叁个若符号的终极大小,所以它可以在终极输出文件的
    BSS 段为其分配空间

4. C++ 皮之不存毛将焉附题材

  • 再也代码化解
    • C++
      编译器在不足为奇时候会爆发重复的代码,如模板、外部内联函数、虚函数表都有或然在区其余编译单元生成相同的代码
    • 3个相比灵通的做法就是将各个示例代码都独立地存放在贰个段里,各个段只含有3个实例,链接器在终极链接的时候可以区分这几个相同的实例段,然后将她们合并入最终的代码段
    • 函数级别链接
      :3个编译选项,让抱有的函数都封存到3个段中间,链接器必要用到有些函数时,才将它合并到输出文件中
  • 全局构造与析构
    • 在 main
      函数被调用从前,为了使程序能够顺遂推行,要先初阶化进度执行环境,如堆分配先河化、线程子系统等
    • C++ 的大局对象的构造函数 在 main 在此以前被实践,析构函数在 main
      之后被执行
    • .init 段里保存的是可进行命令,它结合了经过的初叶化代码;
      .fini 段保存着进程终止代码指令
  • C++ 与 ABI
    • 使八个编译器编译出来的靶子文件能相互链接,则七个目的文件须满意:拔取同样的目的文件格式、拥有同等的符号修饰标准、变量的内存分布形式同样、函数的调用形式同样,等等
    • ABI (Application Binary Interface)
      :符号修饰标准、变量内存布局、函数调用方式等这么些跟可实施代码二进制包容性相关的情节
    • 硬件、编程语言、编译器、链接器、操作系统等都会潜移默化 ABI
  • 静态库链接
    • 贰个静态库可以总结地看成一组目的文件的聚众,即许多目的文件通过压缩打包后形成的一个文书
    • 编译和链接一个平日 C 程序的时候,不仅要用到 C 语言库 libc.a
      ,而且还有其余一些帮忙性质的靶子文件和库。中间步骤:

      • 调用 cc1 程序,实际就是 GCC 的 C
        语言编译器,将源文件编译成1个目前的汇编文件
      • 调用 as 程序,as 程序是 GNU
        的编译器,将一时的汇编文件汇编成目前目的文件
      • GCC 调用 collet2 程序来成功最后的链接,collet2 可以看做是 ld
        链接器的3个包裹
  • 链接进程控制 :对于一些特殊须要的主次如操作系统内核、BIOS
    或部分在没有操作系统的情况下运作的次第,以及此外的一对需求特殊的链接进程的顺序,往往受限于一些非正规的原则,对程序的依次段的地点有着异乎平时的渴求,须求举行连接进度控制

    • 链接控制脚本
      • 选取命令行来给链接器指定参数,如 ld 的 -o 、-e
      • 将链接指令放在目的文件之中,编译器京城会由此那种办法想链接器传递指令
      • 动用链接控制脚本
    • “小”程序
      • -fno-builtin :关闭 GCC 内置函数成效
      • -stati :ld 将应用静态方式来链接程序
      • -e nomain :该程序的入口函数为 nomain
    • 采纳 ld 链接脚本
      • 一言以蔽之,链接控制进度就是决定输入端怎么着成为输出段,比如怎么样输入端要统百分之十二个输出段,哪些输入段要抛开;钦命输入段的名字、装载地方、属性等
      • 链接控制脚本是决定链接进程的“程序”,使得链接进程以“程序”要求的法门将输入加工成所急需的出口结果,一般链接脚本都是lds 作为扩充名
    • ld 链接脚本语法简介
      • 链接脚本由一名目繁多语句组成,一种是指令语句,此外一种是赋值语句
        • 命令语句 :ENTRY(symbol)STARTUP(filename)
          SEARCH_DIR(path)INPUT(file,file,...)
          INCLUDE filenamePROVIDE(symbol)SECTIONS
      • 讲话之间采用分号 ; 作为分割符
      • 表达式与运算符 :可以使用 C 语言类似的表明式和运算操作符
      • 评释和字符引用 :使用 /**/ 作为注释
  • BFD 库 (Binary File Descriptor library)
    • 五花八门的软硬件平台基础导致各个平台都有它特其他靶子文件格式,即便同三个格式在区其余软件平台都持有不相同的变种,导致编译器和链接器很难处理不相同平台之间的靶子文件
    • BFD 库 :三个 GNU
      项目,目的是梦想经过一种统一的假说来处理差别的对象文件格式,通过操作抽象的目标文件模型就可以完结操作所有BFD 支持的对象文件格式
    • GNU 汇编器 GAS、链接器 ld、调试器 GBD 及 binutils
      的其他工具都由此 BFD
      库来处理目的文件,而不是直接操作目标文件,将编译器和链接器自己同具体的靶子文件格式隔离开

五、Windows PE / COFF

1. Windows 的二进制文件格式 PE / COFF

  • PE :Protable Executable ,与 ELF 同根同源,都以由 COFF
    格式发展而来的
  • 议论 Windows 平台上的公文结构时,目的文件暗中认同为 COFF
    格式,而可执行文件为 PE 格式
  • 也应用基于段的格式

2. PE 的前身 —— COFF

  • COFF 文件结构
    :由头文件及末端的几何个段组成,再增进文件末尾的符号表、调试新闻的内容

    • 文件头包蕴
      • 叙述文件总体协会和性质的映像头
      • 叙述文件中蕴藏的段的性格的段表 :是二个项目为
        “IMAGE_SECTION_HEADE奥德赛”结构的数组,数组里面各个成分代表二个段,用来讲述每一个段的品质
    • 段的始末与 ELF 中大约如出一辙,多少个 ELF 文件不存在的段
      :“.drectve”段 和 “.debug$S”段

3. 链接指示消息

  • 情节是编译器传递给链接器的授命
  • 段名后边就是段的习性,最后八脾质量是表明位 “flags”,即
    IMAGE_SECTION_HEADERS里面的 Characteristics 成员
  • 输出新闻中紧随其后的是该段在文书中的原始数据

4. 调试音讯

  • COFF 文件中保有以 “.debug” 开头的段都含有着调试音信
    • “.debug$S” 符号相关的调剂音讯段
    • “.debug$P” 包蕴预编译头文件有关的调剂音信段
    • “.debug$T” 包括类型相关的调剂新闻段

5. 符号表

  • COFF 文件的符号表蕴含的内容跟 ELF
    文件的符号表一样,主要就是符号名、符号的品种、所在的义务
  • 符号表的出口结果从左到右
    :符号的号码、符号的高低、符号所在的地点、符号类型、符号的可知范围、符号名

6. Windows 下的 ELF —— PE

  • PE 文件是依照 COFF 的恢宏

    • 文本的最开端有些不是 COFF 文件头,而是 DOS MZ
      可执行文件格式的公文头和桩代码
    • 原来的 COFF 文件头中的 IMAGE_FILE_HEADER 部分增加成了 PE
      文件头结构 IMAGE_NT_HEADERS。包涵了原本的 “Image Header”
      及新增的 PE 扩充底部文件
  • DOS 下的可执行文件格式是 “MZ” 格式,与 Windows 下的 PE
    差别,固然它们利用相同的恢弘名 “.exe”

  • IMAGE_NT_HEADERS 是 PE
    真正的文书头,包含了2个标志和多个结构体,标记是二个常量,结构体是印象头和
    PE 扩张尾部结构

  • 为了不一致,Windows 中把 32 位的 PE 文件格式叫做 PE32,把 64 位的 PE
    文件格式叫做 PE32+

  • PE 数据目录

    • 在 Windows 装载 PE
      文件时必要快捷的找到一些装载所急需的数据结构如导入表、导出表、能源。重定位表等,那些常用的多少的地方和长度都被保存在了2个叫数码目录的社团里
    • DataDirectory
      数组里面每三个要素都对应1个含有一定意义的表,每一个协会有五个分子,是虚拟地址以及长度

六. 可执行文件的装载与经过

1. 进度虚拟地址空间

  • 种种程序被周转起来之后,它将持有和谐独立的虚拟地址空间,大小由总计机的硬件平台决定,具体地说是由
    CPU 的位数决定的,比如 32 位的硬件平台决定了虚拟地址空间的地点为 0
    到 2 ^ 32 – 1,即 0x00000000 ~ 0xFFFFFFFF,约等于常说的 4GB
    虚拟空间尺寸;而 64 位的硬件平台具有 6肆人寻址能力,它的虚拟地址空间达到了 2 ^ 64 – 1,总共 17179864184GB
  • 32 位 Linux 下,整个 4GB
    被剪切成两片段,其中操作系统本身用去了一部分:从地点 0xC0000000 ~
    0xFFFFFFFF,共 1GB。剩下的从 0x00000000 ~ 0xBFFFFFFF 共
    3GB。从规范上讲(其实进度并不可以一心拔取那 3GB
    的虚构空间,其中有局地是预留给任何用途的),我们的经过最多可以运用
    3GB 的虚构空间,整个经过在执行的时候,所有的代码、数据包罗经过 C
    语言 malloc() 的要命方式申请的虚构空间之和不得以超越 3GB
  • 对于 Windows 操作系统,进度虚拟地址空间划分是操作系统占用
    2GB,进度只剩余 2GB
    空间,所以有个运维参数可以将操作系统占用的虚拟地址空间裁减到 1GB
  • PAE :从硬件层面上来讲,原先的 32 位地址线只好访问最多 4GB
    的大体内存,不过一旦增加至 36 位地址线之后, Intel修改了页映射的主意,使得新的投射格局得以访问到越来越多的大体内存,那些地方伸张格局叫做
    PAE

2. 装载的不二法门

  • 覆盖装入
    • 编写程序的时候手工将先后分割成多少块。然后编写贰个小的扶植代码来管理那些模块曾几何时应该驻留内存哪天应该被轮换掉,那些小的帮带代码就是覆盖管理器
    • 在有三个模块的情况下,须要手工将模块依照它们之间的调用依赖关系协会成树状结构
      • 树状结构中从任何3个模块到树的根模块都叫调用路径,当该模块被调用时,整个调用路径上的模块都必须在内存中
      • 禁止跨树间调用,任何三个模块分歧意跨过树状结构举行调用
  • 页映射
    • 将内存和有着磁盘中的数据和下令依据“页”为单位划分为多少个页,今后全体的装载和操作的单位就是页,近年来硬件规定的页的轻重有
      4096 字节、8192 字节、2MB、4MB 等,最广大的 速龙 IA32
      处理器一般都利用 4096 字节的页
    • 有多如牛毛算法化解选取哪位页来替换,如 FIFO、LU奥德赛 等

3. 从操作系统角度看可执行文件的装载

  • 经过的确立
    • 从操作系统的角度来看,三个经过最要害的特点是它具备独立的虚拟地址空间
    • 创设2个经过,然后装载相应的可执行文件并且实施
      的进度最开端做的事分三步 :

      • 创建虚拟地址空间 :虚拟空间到大体内存的照射关系
      • 读取可执行文件头,并且创制虚拟空间与可执行文件的照耀关系
        :虚拟空间与可执行文件的照耀关系

        • 炫耀关系是保存在操作系统内部的一个数据结构,Linux
          元帅进度虚拟空间中的1个段叫做虚拟内存区域(VMA),Windows
          校官以此名叫虚拟段
      • 将 CPU 指令寄存器设置成可执行文件入口,运维运作
  • 页错误
    • 上面的步子之后,操作系统只是经过可执行文件底部的音信建立起可执行文件和经过虚存之间的映照关系而已
    • CPU
      真正初叶实践时,会发现先后的进口地址是一个空页面,认为那是一个页错误,操作系统有特别的页错误处理例程来拍卖,将查询前边提到的装载进度的第一步建立的数据结构,然后找到空页面所在的
      VMA,统计出相应的页面在可执行文件中的偏移,在情理内存中分配2个大体页面,将经过中该虚拟页与分配的物理页之间确立映射关系,然后把控制权再还给进度,进度从刚刚页错误的职位再次伊始履行

4. 进程虚存空间分布

  • ELF 文件链接视图和举办视图

    • ELF
      文件被映射时,是以体系的页长度作为单位的,如若每种段都占据整数倍个页的长短,浪费内存空间。由此,装载时,对于一样权限的段,把它们统一到一同作为贰个段展开映射
    • ELF 可执行文书引入了 “segment” 的定义,3个 “segment”
      包蕴一个或七个属性类似的
      “section”,装载时将他们作为三个总体一并映射,使得映射以往在进度虚存空间中只有三个应和的
      VMA,减弱了页面内部碎片,节省了内存空间
    • 叙述 “segment” 的布局叫做程序头,描述了 ELF
      文件该怎么被操作系统映射到进度的杜撰空间
  • 堆和栈

    • 操作系统通过运用 VMA
      来对经过的地方空间拓展管制,很多气象下,三个经过中的栈和堆分别有二个一拍即合的
      VMA
    • 操作系统通过给进程空间划分出2个个 VMA
      来治本进度的杜撰空间,基本标准是将一律权限属性、有平等映像文件的投射成三个VMA,三个进程基本上可以分成如下两种 VMA 区域 :代码 VMA、数据
      VMA、堆 VMA、栈 VMA
  • 堆得最大申请数量

    • Linux 下虚拟地址空间分给进度自己的是 3GB(Windows 私行认同是 2GB)
    • 具体数值会受到操作系统版本、程序本身尺寸、用到的动态/共享库数量、大小、程序栈数量、大小等,甚至或许每回运维的结果都不可同日而语
  • 段地址对齐

    • 可执行文件最后是要被操作系统装载运营的,这些装载的历程相似是透过虚拟内存的页映射机制成功的,映射进度中,页是映射的小不点儿单位
    • 为了消除每一个段分开映射所带来的浪费磁盘空间的题目,可以让各样段接壤部分共享3个大体页面,然后将该物理页面分别被映射四遍,系统将她们映射到两份虚拟地址空间,其余的页都依据常规的页粒度举办映射,系统将
      ELF
      文件头也作为是系统的1个段,将其映射到进度的地方空间,好处是经过中的某一段区域就是整整
      ELF 文件的印象,对于某个须访问 ELF
      头文件的操作可以一向通过读写内存地址空间举行
    • 从某种角度看,好像是整整 ELF
      文件从文件最开首到某些点甘休,被逻辑上分为了以 4096
      字节为单位的好八个块,各种块都被装载到大体内存中,对于这些坐落五个段中间的快,它们将会被映射两遍
    • 在 ELF 文件中,对于其余一个可装载的 “segment”,它的 p_vaddr
      除以对其性能的余数等于 p_offset 除以对齐属性的余数
  • 经过栈早先化

    • 经过刚初始起步的时候,须知道有个别进度运转的条件,最基本的就是系统环境变量和经过的周转参数,常见的做法是操作系统在进度运转前将那个新闻超前保存到进度的杜撰空间的栈中
    • 进度在运行之后,程序的库部分会把堆栈里的开始化音讯中的参数消息传递给
      main() 函数,约等于 main() 函数的三个 argc 和 argv
      八个参数,那四个参数分别对应那里的命令行参数数量和下令行参数字符串指针数组
  • Linux 内核装载 ELF 进度简介

    • 在用户规模,bash 进程会调用 fork()
      系统调用穿件1个新的长河,然后新的长河调用 execve()
      系统调用执行钦点的 ELF 文件,原先的 bash
      进度继续回来等待刚才运转的新进度停止 ,然后继续伺机用户输入指令
    • 在进入 execve() 系统调用之后,Linux 内核就起头进行真正的装载工作
    • 首要步骤 :
      • 反省 ELF 可执行文件格式的管用
      • 检索动态链接的 “.interp” 段,设置动态链接路径
      • 基于 ELF 可执行文书的次序头表的描述,对 ELF 文件举办映射
      • 初阶化 ELF 进程环境
      • 将系统调用的回来地址修改成 ELF
        可执行文书的入口,这一个进口取决于程序的链接方式,对于静态链接
        ELF 可执行文书,这么些程序入口就是 ELF 文件的文书头中的
        e_entry 所指的地方;对于动态链接的 ELF
        可执行文书,程序入口点是动态链接器
  • Windows PE 的装载

    • PE
      文件中,链接器在转变可执行文件时,往往将具有的段尽只怕合并,所以一般只有代码段、数据段、只读数据段和
      BSS 等为数不多的多少个段
    • QashqaiVA 相对虚拟地址,是相对于 PE 文件的装载集散地址的一个偏移地址
    • 驻地址 :每一种 PE 文件在装载时都会有3个装载目的地址

7、动态链接

1. 怎么要动态链接

  • 动态链接 :链接进度推迟到了运维时再开展
  • 缓解了共享目标文件多个副本浪费磁盘和内存空间的难点
  • 方升级程序库或程序共享某些模块时,新本子的目标文件会被电动装载到内存并且链接起来,使得各种模块越发独立,耦合度更小
  • 增强程序的可增加性,程序在运转时得以动态地采取加载种种程序模块,后来被大千世界选择来创设程序的插件
  • 做实程序的包容性,动态链接库也就是在先后和操作系统之间增添了2个中间层,从而解除了先后对两样平台之间以来的差距性
  • 着力考虑
    :把程序依照模块拆分为各种相对独立部分,在程序运转时才将它们连接在联合形成3个总体的主次,而不是像静态链接一样,把具备程序模块都链接成3个独门的可执行文件
  • Linux 系统中,ELF
    动态链接文件被称为动态共享对象,简称共享对象,一般都是以
    “.so” 为扩充名的一些文本;Windows
    系统中,动态链接文件被叫做动态链接库,日常是以 “.dll”
    为扩大名的文件
  • 先后被装载的时候,系统的动态链接器会将次第所急需的装有动态链接库装载到进度的地址空间,并且将先后中兼有未决定的号子绑定到相应的动态链接库中,并开展重一向工作。动态链接把链接那个历程从自然的程序装载钱被推移到了装载的时候。

2. 简练的动态链接

  • 动态链接下,一个先后被分成若干个文本,有先后的基本点部分,即客户性文件和次序所依靠的共享对象,把那么些有些号称模块,即动态链接下的可执行文件和共享对象都足以看成是先后的三个模块
  • 假定函数是1个定义在有些动态共享对象中的函数,那么链接器就会将以此符号的引用标记为一个动态链接的记号,不对它举办地址重一直,把这几个进度留到装载时再运转
  • 共享对象的末梢装载地方在编译时是不显然的,而是在装载时,装载器依据当前地点空间的空余意况,动态分配一块丰盛大小的虚拟地址空间给相应的共享对象

3. 地点非亲非故代码

  • 定位装载地方的干扰

    • 静态共享库的做法是将先后的各样模块统一交由操作系统来保管,操作系统在有个别特定的地址划分出一些地址块,为那八个已知的模块预留丰裕的上空
    • 地点争持难题、静态共享库升级难点
    • 解决:让共享对象在自由地址加载,共享对象在编译时无法假若本身在进度虚拟地址空间中的地点
  • 装载时重平素

    • 在链接时,对具有相对地址的引用不作重平昔,而把这一步推迟到装载时再形成,一旦模块装载地方显然,即目的地方分明,那么系统就对先后中的相对地址引用进行重向来
    • 静态链接时的重定位名叫链接时重一直,将来那种称为装载时重平昔,在
      Windows 中又被称之为基址重置
    • 装载时重从来不吻合用来解决共享对象所存在的难点
  • 地址无关代码 PIC

    • 装载时重一直是焚林而猎动态模块中的有相对地址引用的法门之一,但是指令部分不能在多个进程之间共享,失去了动态链接节省里存的优势
    • 地点无关代码
      :把指令中如何必要修改的局地分离出来,跟数据部分放在一起,那样指令部分可以维持不变,而数据部分可以在各样进度中拥有一个副本,程序模块中共享的指令部分在装载时不须要因为装载地方的变更而更改
    • 把共享对象模块中的地址引用根据是不是为跨模块分为模块内部引用和模块外部引用,依据分裂的引用格局又可以分为指令引用和数目访问
      • 花色一 模块内部调用或伸张:可以使相对地址调用,大概是依照寄存器的相对调用,那种指令是不要求重一向的
      • 项目二 模块内部数据访问
        :任何一条指令与它要求拜访的模块内部数据里面的相对地点是一定的,只须求对此当下命令加上一定的偏移量就可以达成访问相应变量的目的(PC
        值加上2个偏移量)。模块在编译时可以分明模块内部变量相对于近年来下令的撼动
      • 品种三 模块间数据访问 :ELF
        的做法是在数据段里面建立多个指向那么些变量的指针数组,也被叫作大局偏移表
        GOT
        ,当代码须求引用全局变量时,也足以经过 GOT
        中相呼应的项间接引用。在编译时规定 GOT
        相对于当下下令的撼动,然后通过获取 PC
        值后增加四个偏移量,按照变量地址在 GOT
        中的偏移就可以拿走变量的地点,是的 GOT 做到指令的地方非亲非故
      • 模块四 模块间调用、跳转 :与数量访问类似,GOT
        中相应的项保存的是目标函数的地方,当模块要调用目的函数时,可以经过
        GOT 中的项举办直接跳转
    • 使用 “-fPIC” 和 “-fpic” 参数来发出地址非亲非故代码
  • 共享模块的全局变量难题

    • 当三个模块引用了贰个概念在共享对象的全局变量的时候,编译器在编译这几个模块时,不或者依照上下文判断变量是概念在同3个模块的其余目的文件恐怕定义在此外1个共享对象之中,即不只怕判读是还是不是为跨模块间的调用
    • 化解办法
      :把装有应用这么些变量的一声令下都针对位于可执行文件中的那些副本,ELF
      共享库在编译时,暗中同意都把定义在模块内部的全局变量当做定义在此外模块的全局变量,通过
      GOT 来促成变量的访问,该变量在运行时实际上最后就只有一个实例
    • 极度须要:多进度共享全局变量叫做“共享数据段”、五个线程访问区其他全局变量副本叫做“线程私有存储”
  • 数据段地址非亲非故性

    • 对此数据段来说,它在种种进程都是一份独立的副本,并不担心被进度改变,可以选取装载时重平素的措施来缓解数量段中相对地址引用难点
    • 对此共享对象的话,若是数量段有绝对地址引用,那么编译器和链接器就会爆发2个重定位表。当动态链接器装载共享对象时,即便发现该共享对象有重定位入口,那么动态链接器就会对该共享对象开展重一贯
    • 对此可执行文件来说,默许景况下,假设可执行文件是动态链接的,那么
      GCC 会使用 PIC
      方法来发生可执行文件的代码段部分,一边与差其他进程可以共享代码段

4. 逐渐悠悠绑定 PLT

  • 动态链接比静态链接慢

    • 动态链接下对于全局和静态的数据访问都要开展复杂的 GOT
      定位,然后直接寻址;对于模块间的调用也要先固定
      GOT,然后开展直接跳转
    • 动态链接的链接工作在运作时做到,即程序开首推行时
  • 延期绑定完成

    • 当函数第三遍被用到时才开展绑定,假使没有使用则不举办绑定
    • ELF 使用 PLT
      的方法来促成,调用有个别外部模块的函数时,经常做法是由此 GOT
      中相应的项举办直接跳转,PLT
      在那一个进程当中又扩展了一层直接跳转,唯有利用到该函数才跳转到 GOT
      来形成符号解析和重定位工作

5. 动态链接相关社团

  • 动态链接处境下,可执行文件的装载与静态链接情形基本一样,先是操作系统会读取可执行文件的头顶,检查文件的合法性,然后从底部中的
    “Program Header” 中读取各个 “segment”
    的虚拟地址、文件地方和本性,并将它们映射到进度虚拟空间的对应地方。
    唯独,可执行文件依赖于广大共享对象,对于广大外表符号的引用还地处低效地址的意况,即还尚未跟相应的共享对象中的实际地方链接起来。所以在炫耀完可执行文件之后,操作系统会先运营三个动态链接器。操作系统同样通过炫耀的不二法门将它将它加载到进度的地点空间中,加载完动态链接之后,就将控制权交给动态链接器的入口地址,当动态链接器得到控制权之后,它开头履行一多重本身的初叶化操作,然后依照当下的环境参数,早先对可执行文件举行动态链接工作。当有着动态链接工作成功之后,动态链接器会将控制权转交到可执行文件的输入地址,程序开始推行

  • “.interp” 段

    • 动态链接器是由是由 ELF 可执行文书决定的,在动态链接的 ELF
      可执行文书中,有叁个专门的段叫做 “.interp” 段
    • “.interp”
      里面保存的是3个字符串,就是可执行文件所须求的动态链接器的不二法门,在
      Linux
      中,操作系统在对可执行文件的进展加载的时候,它回到寻找装载盖可执行文件所急需相应的动态链接器,即
      “.interp” 段钦定的门路的共享对象
  • “.dynamic” 段

    • 动态链接 ELF 中最要紧的构造应当是 “.dynamic”
      段,那些段中间保存了动态链接器所急需的骨干新闻,比如借助于怎么样共享对象、动态链接符号表的地点、动态链接重定位表的岗位、共享对象开始化代码的位置
    • “.dynamic” 段可以看成是动态链接下 ELF 文件的“文件头”
  • 动态符号表

    • 为了表示动态链接模块之间的符号导入导出关系,ELF
      专门有1个称为动态符号表的段来保存那一个音讯,那一个段的段名平时叫做
      “.dynsym”,只保留了与动态链接相关的记号,对于那么些模块内部的符号,比如模块私有变量则不保留,而
      “.symtab” 中往往保存了具备的标记
    • 动态符号字符串表,用于保存符号名的字符串表,类似于静态链接时的标记字符串表
      “.strtab”
    • 动态链接下,必要在程序运转时寻找符号,为了加紧符号的物色进度,往往还有帮忙的标记哈希表
      “.hash”
  • 动态链接重定位表

    • 对于使用 PIC
      技术的可执行文件或共享对象的话,纵然它们的代码段不须求重一向(因为地点毫不相关),但是多少段还富含了相对地址的引用,因为代码段中与相对地址相关的一部分被分手了出来,变成了
      GOT,而 GOT 实际上是数据段的一片段
    • 动态链接重定位有关社团
      • 动态链接的公文中,有重一直表叫做 “.rel.dyn” 和
        “.rel.plt”,前者实际上是对数据援引的匡正,它所校订的岗位放在
        “.got”
        以及数据段,后者是对函数引用的校对,它所校订的地点放在
        “.got.plt”
      • 共享对象的数据段是不曾章程做到地址毫无干系的,它恐怕会包蕴相对地址的引用,对于那种纯属地址的引用,我们不只怕不在装载时将其重一直
      • 导入函数从 “.rel.plt” 到了
        “.rel.dyn”,参数字符串常量的地址在 PIC 时不须求重向来而非
        PIC 时需求重一直,因为 PIC
        时,那个字符串可以当作是欧痛的全局变量,地址是可以透过 PIC
        中相对当前命令的职责加上三个固定偏移计算出来的;而在非 PIC
        中,代码段不再选用那种相对于当下命令的 PIC
        方法,而是采纳相对地址寻址,所以它须求重一向
  • 动态链接时经过堆栈初步化音信

    • 动态链接器必要精晓关于可执行文件和本进度的有个别消息,这个消息往往由操作系统传递给动态链接器,保存在经过的库房里面,在经过开头化的时候,堆栈里面保存了关于进度执行环境和命令行参数等音讯,还保存了动态链接器所须要的一些支持音信数组
    • 资助音讯数组位于环境变量指针的前边

6. 动态链接步骤和落实

  • 启航动态链接器本身

    • 动态链接器不得以凭借于任何任何共享对象
    • 动态链接器本身所急需的大局和静态变量的重定位有它自个儿已毕,动态链接器必须在起步的时候有一段卓殊精致的代码可以达成那项工作又无法用到全局和静态变量,那种有着自然限制条件的启航代码往往被称作自举
    • 动态链接器入口地址即是自举代码的进口,当操作系统将经过控制权交给动态链接器时,动态链接器的自举代码即起来推行实施。自举代码首先找到本身的
      GOT,而 GOT 的第三个入口保存的即是 “.dynamic”
      段的撼动地址,由此找到了动态链接器本人的 “.dynamic”
      段,拿到动态链接器本人的重定位表和标志表等,从而取得动态链接器自身的重定位入口,先将它们整个重一直
    • 骨子里在动态链接器的自举代码中,动态链接器自个儿的函数也不或许调用,因为使用
      PIC
      情势演进的共享对象,对于模块内部的函数调用接纳的跟模块外部函数调用一样的措施,即利用
      GOT/PLT 的艺术,所以在 GOT/PLT
      没有被重平昔以前,自举代码不得以动用其余全局变量,也不只怕调用函数
  • 装载共享对象

    • 完毕大旨自举以往,动态链接器将可执行文件和链接器自个儿的号子表都合并到2个符号表当中,可称为全局符号表

    • 链接器伊始摸索可执行文件所看重的共享对象,“.dynamic”
      段中有一种类型的入口是
      DT_NEEDED,它所指出的是该可执行文件(或贡共享对象)所依赖的共享对象,因此链接器可以列出可执行文件所急需的具有共享对象,并将这几个共享对象的名字放入三个装载集合中

    • 链接器初步从集合中取出一个所急需的共享对象的名字,然后将它对应的代码段和数据段映射到进程空间中

    • 如若这些 ELF
      共享对象还凭借于其余共享对象,则将所依靠的共享对象的名字放到装载集合中,循环直到全体所正视的共享对象都被装载进来甘休,可以看成3个图

    • 大局符号加入:当2个符号须要被插手全局符号表时,就算同样的标记名曾经存在,则后加盟的标志被忽略 *
      全局符号参预与地点毫不相关代码
      :模块内部调用或跳转的处理时,即使中间函数由于全局符号出席被别的模块的同名函数覆盖,要是使用绝对地址调用,这一个绝对地址部分就须要重平昔,与共享对象的地点非亲非故性争执,所以只能够当做模块外部符号处理。化解办法
      :把里面函数编程编译单元私有函数,即使用 “static” 关键字

  • 重一直和开头化

    • 链接器起头再度遍历可执行文件和每种共享对象的重定位表,将它们的
      GOT/PLT 中的每种须要重平昔的岗位展开矫正
    • 重定位成功之后,若是某些共享对象有 “.init”
      段,那么动态链接器会履行 “.init”
      段中的代码,用以完毕共享对象特有的伊始化进度,相应的还只怕有
      “.finit” 段,进度退出时会执行
    • 姣好重平昔和开首化之后,全部的备选干活就完了可,所急需的共享对象已经装载并链接落成了,动态链接器将控制权转交给程序的进口并起头执行
  • Linux 动态链接器达成

    • 对于静态链接的可执行文件来说,程序的进口就是 ELF 文件头重的
      e_entry
      钦定的入口;对于动态连接的可执行文件,内核会分析它的动态链接器地址(在
      “.interp”
      段),将动态链接器映射至进度地址空间,然后把控制权交给动态链接器
    • 动态链接器是个拾分出格的共享对象,它不只是个共享对象,照旧个可举办的程序
    • Linux 内核在进行 execve() 时不关怀目标 ELF
      文件是还是不是可实施,直接是粗略依据程序头表里的叙述对文本举办装载然后把控制权转交给
      ELF
      入口地址,所以共享课和可执行文件实际上没什么不同,除了文件头的标志位和伸张名有所不一致
    • 动态链接器自己是静态链接的
    • 动态链接器自个儿可以是 PIC 也可以不是,可是用 PIC 会不难一些
    • 动态链接器可以被作为可执行文件运转,装载地点跟一般的共享对象没分化,即为
      0x00000000。那是一个失效的装载地方,内核在装载它时,会为其选取八个十三分的装载地方

7. 显式运营时链接

  • 显式运转时链接,有时候也叫作运维时加载,就是让程序自个儿在运营时控制加载钦命的模块,并且能够在不必要该模块时将其卸载。一般的共享对象不必要开展任何修改就可以拓展运维服装载,那种共享对象往往被号称动态装载库
  • 使得程序的模块协会变得灵活,可以用来已毕部分诸如插件、驱动等作用,唯有程序须求选择有些插件或驱动的时候才会将相应的模块装载进来,而不需求在一始发就见它们整个装载进来,收缩了先后运转时间和内存使用;并且程序可以在运转的时候加载有个别模块,使得程序本人不必再一次启航而落到实处模块的扩展、删除、更新等
  • 动态装载库的装载是透过一多重动态链接器提供的 API,具体的讲共有 陆个函数:打开动态库(dlopen)、查找符号(dlsym)、错误处理(dlerror)、关闭动态库(dlclose)
    • dlopen()
      • 用来打开二个动态库,并将其加载到进度的地点空间,已毕开始化进度
      • 参数
        :filename(被加载动态库的不二法门,为空则重回全局符号表的句柄)、flag(函数符号的分析方法)
      • 归来值 :被加载模块的句柄,后边使用 dlsym 可能 dlclose
        时要用到
    • dlsym()
      • 是运营时装载的主干部分,通过这么些函数来找到所需求的符号
      • 参数
        :handle(动态库的句柄)、symbol(所要查找的记号的名字)
      • 归来值 :查找到的号子
      • 标记优先级
        :四个同名符号冲突时,现装入的记号优先,那种事先级艺术叫做装载体系
      • 倘诺在大局符号表中进行标记查找,则 dlsym()
        使用的是装载系列,若是对某些 dlsym()
        打开的共享对象开展标记查找,那么拔取一种名叫依靠种类的先行级
    • dlerror()
      • 在调用其余多少个函数时,用来判定上二回调用是不是成功
      • 比方回去 NULL,则象征上二回调用成功;否则再次回到相应的失实音讯
    • dlclose()
      • 将1个已经加载的模块卸载
      • dlopen() 和 dlclose() 使用计数器

⑧ 、Linux 共享库的集团

1. 共享库版本

  • 共享库包容性

    • 相当更新

    • 不合作更新

      • 导出函数的行事发出变动,调用这么些函数今后时有发生的结果和在此以前不雷同
      • 导出函数被删除
      • 导出数据的布局爆发变化
      • 导出函数的接口发生变化
    • 导出接口为 C++ 的共享库包容拾贰分不方便

  • 共享库版本命名

    • Linux 规定共享库的公文命名规则必须如下 :libname.so.x.y.z
    • 最前面使用前缀 “lib”、中间是库的名字和后缀
      “.so”,最终边跟着的是两个数字构成的版本号。“x”
      表示主版本号(库的重大升级),“y”
      表示次版本号(库的增量升级),“z”
      表示公告版本号(库的失实的匡正、品质的改革等)
  • SO-NAME

    • 先后必须记录被依赖的共享库的名字和主版本号
    • SO-NAME :共享库的文书名去掉次版本号和揭橥版本号,保留主版本号
    • “SO-NAME” 的五个相同共享库,次版本号大的合营次版本号小的
    • 系统会为各样共享库在它所在的目录创立3个跟 “SO-NAME”
      相同的还要指向它的软链接,实际上那个软链接会指向目录中主版本号相同、次版本号和公告版本号最新的共享库。目的:使得全数看重某些共享库的模块,在编译、链接和运作时,都使用共享库的
      SO-NAME,而不应用详细的本子号
    • 编译输出 ELF 文件时,将被倚重的共享库的 SO-NAME 保存到
      “.dynamic”
      中,那样当链接器进行共享库看重文件查找时,就会依照系统中各类共享库目录中的
      SO-NAME 软链接活动定向到最新版本的共享库
    • SO-NAME 代表多少个库的接口,接口不向后非凡,SO-NAME 就发生变化

2. 符号版本

  • 基于符号的本子机制

    • 次版本号交会难点没有因为 SO-NAME
      而化解,当有些程序依赖于较高的次版本号的共享库,而运行于较低次版本号的共享库系统时,就大概暴发缺少某个符号的失实
    • 缓解次版本号交会难题:让各类导出和导入的符号都有三个相关联的本子号,做法类似于名称修饰的不二法门,八个共享库每三回次版本号升级,都能给那多少个在新的次版本号中增加的全局符号打上相应的记号,可以了然地看出共享库中的每种符号都享有同等的竹签
  • Solaris 中的符号版本机制

    • ld 链接器为共享库新增了本子机制和范围机制
    • 本子机制定义一些标记的汇合,这么些聚集本人都有名字,逐个集合都饱含部分点名的记号,除了可以具有符号之外,八个凑合还足以涵盖其余2个会见
    • 范围机制
      :共享库外部的应用程序或其余的共享库将不能访问这几个标记,可以维护那几个共享库内部的公用实用函数,可是共享库的小编又不希望共享库的使用者可以有意或下意识地走访这个函数
    • 是对 SO-NAME 机制确保共享库主版本号一致的一种相当好的补偿
  • Linux 中的符号版本 :允许同叁个名称的记号存在五个本子

3. 共享库系统路径

  • FHS(File Hierarchy
    Standard)标准规定了1个系统中的系统文件应该怎么存放,包括各类目录的布局、协会和法力

    • /lib :存放最重大和底蕴的共享库
    • /usr/lib
      :保存一些非系统运作时所急需的基点的共享库,还包括了支付时或者会用到的静态库、目的文件等
    • /usr/local/lib :放置一些跟操作系统本身并不拾壹分有关的库

4. 共享库查找进程

  • 开行动态链接器
  • 动态链接的模块所依赖的模块路径保存在 “.dynamic” 段里面,由 DT_NEED
    类型的项表示
  • Linux 系统中有二个叫做 ldconfig
    的次第,为共享目录下的逐一共享库创制、删除或更新相应的
    SO-NAME,将这几个收集起来,集中存放,建立缓存,大大加速了共享库的物色进度

5. 环境变量

  • LD_LIBRARY_PATH
    :可以目前改变某些应用程序的共享库查找路径,而不会潜移默化系统中的其他程序。默许意况为空,假如为某些进度设置了,那么进度运行时,动态链接器在寻找共享库时,会首先查找内定的目录
  • LD_PRELOAD :钦定预先装载的一对共享库甚或是目标文件
  • LD_DEBUG
    :打开动态链接器的调剂功用,会在运行时打印出各样有效的音讯

6. 共享库的开创和装置

  • 共享库的创办 :与创设一般共享对象的进度基本一致,最重大的是利用 GCC
    的三个参数,即 “-shared” 和 “-fPIC”
  • 扫除符号音信 :使用三个叫 “strip”
    的工具清除掉共享库或可执行文件的兼具符号和调试音信,也可应用 ld 的
    “-s” 和 “-S” 参数使得链接器生成输出文件时就不发出符号新闻
  • 共享库的装置 :将共享库复制到某些专业的共享库目录,如 /lib、/usr/lib
    等,然后运转 ldconfig 即可。可是须要系统的 root
    权限,可以透过建立相应的 SO-NAME
    软链接,告诉编译器和顺序怎么样寻找该共享库等
  • 共享库构造和析构函数
    • 在函数注解时抬高 __attribute__((constructor))
      的性质,内定为共享库构造函数,会在共享库加载时被执行,即在先后的
      main 函数从前实施
    • 在函数表明时增加 __attribute__((destructor))
      的属性,内定为共享库析构函数,会在程序的 main()
      函数执行已毕之后执行
    • 假设有八个构造函数,执行各样是尚未鲜明的,可以钦点某些组织或析构函数的优先级,构造函数优先级小的先运维,析构函数相反
  • 共享库版本
    • 共享库还是能是适合自然格式的链接脚本文件
    • 3个或四个输入文件以自然的格式经过变换之后形成七个出口文件

玖 、Windows 下的动态链接

1. DLL 简介

  • DLL 即动态链接库,约等于 Linux 下打共享对象

  • Windows 下的 DLL 文件和 EXE 文件实际上是1个定义,都以 PE
    格式的二进制文件

  • 进程地址空间和内存管理 :DLL
    的代码并不是地点非亲非故的,所以它在少数情形下能够被三个经过共享

  • 基地址和 奥德赛VA(相对地址)

    • 当三个 PE
      文件被装载时,其经过地址空间中的初叶值就是集散地址,对于任何1个PE 文件来说,它都有贰个预先装载的基地址,那几个值就是 PE
      文件头中的 Image Base
    • WIndows 在装载 DLL 时,会先尝试把它装载到由 Image Base
      指定的虚拟地址,若该地点已经被其余模块占用,那 PE
      装载器会采纳其他空闲地址,而相对地址就是一个地方绝对于集散地址的撼动
  • DLL 共享数据段

    • 采取 DLL 来落到实处进度间通讯
    • Windows 允许将 DLL 的数据段设置成共享的,即任何进度都得以共享该
      DLL 的平等份数据段
    • 普遍的做法是将某些索要经过间共享的变量分离出来,放到其余七个数码段中,然后将以此数据段设置成进度间可共享的,约等于说3个DLL 中有多个数据段,二个进度间共享,其余二个个体
    • 为平安考虑,DLL 共享数据段来贯彻进度间通信应该尽量防止
  • DLL 的简便例子

    • DLL
      须求显式地告诉编译器要求导出某些符号,否则编译器暗中认同全体符号都不导出
    • 可以因而 __declspec 属性关键字来修饰某些函数或然变量,当使用
      __declspec(dllexport) 时表示该符号是从本 DLL
      导出的号子,__declspec(dllimport) 表示该符号是从其他 DLL
      导入的符号
    • 可以使用 “.def” 文件来声称导入导出符号,类似于 ld
      链接器的链接脚本文件
  • 成立 DLL :使用编译器 cl 举行编译 :参数 /LDd 表示生产 Debug 版的
    DLL,不加任何参数则代表生产 EXE 可执行文件,可以采取 /LD 来编译生成
    Release 版的 DLL

  • 使用 DLL

    • 先后行使 DLL 的进度实际上是援引 DLL
      中的导出函数和标志的历程,即导入进度
    • “.lib” 文件中并不确实带有 “.c” 文件的代码和多少,是用来讲述
      “.dll” 的导出符号,包括了
      链接时所必要的导入符号以及一些“桩代码”,以便将顺序与 DLL
      粘在一道,那样的 “.lib” 文件被称作导入库
  • 采纳模块定义文件

    • .def 文件在链接进程中的功用与链接脚本文件在 ld
      链接进程中的成效类似,是用于控制链接进度,为链接器提供关于链接程序的导出符号、属性以及任何音讯
    • 便宜 :能够操纵导出符号的号子名;可以将导出函数重新命名;当二个DLL
      语言被八个语言编写的模块使用时,拔取那种方法导出一个函数往往会很有用
      ;可以操纵一些链接的经过,可以决定输出文件的暗中同意堆大小、输出文件名、种种段的质量、默许堆栈大小、版本号等
  • DLL 显式运营时链接

    • LoadLibrary :用来装载三个 DLL 到进度的地方空间,功用与 dlopen
      类似
    • GetProcAddress :用来寻找有个别符号的地址,与 dlsym 类似
    • FreeLibrary :用来卸载某些已加载的模块,与 dlclose 类似

2. 符号导出导入表

  • 导出表

    • 当1个 PE 须求将一部分函数或变量提必要任何 PE
      文件使用时,把那种表现称为符号导出,最特异的气象就是三个 DLL
      将标志导出给 EXE 文件使用
    • 持有的号子被集中存放在了被称作导出表的结构中,提供了二个符号名与符号地址的照耀关系
    • 导出表的末梢 3 个分子指向的是 一个数组,他们是大街小巷地址表(EAT)、符号名表、名字序号对应表
      • 序号 :二个函数导出的号子就是函数在 EAT 中的地址下标加上2个Base 值
      • 运用序号导入导出省去了函数名查找进程,函数名表也不须要保存在内存中了,不过二个函数的序号或然会变动
      • 近年来 DLL
        基本都平昔利用标志名作为导入导出,举行动态链接时,动态链接器在函数名表中举办二分查找,找到后在名字序号对应表中找到所对应的序号,减去
        Base 值,然后在 EAT 中找到呼应下标下标的成分
  • EXP 文件

    • 链接器在开立 DLL 时与静态链接一样使用三次扫描进程
    • 首先遍会遍历全部的靶子文件同时收集全数导出符号消息并且创办 DLL
      的导出表,链接器会把这么些导出表放到一个一时半刻的靶子文件叫做
      “.edata” 的段中,这么些目的文件就是 EXP 文件
    • 第①回,链接器把这些 EXP
      文件作为平日目的文件一律,与其他输入的靶子文件链接在同步还要输出
      DLL,这时 EXP 文件中的 “.edata” 段也就会被输出到 DLL
      文件中还要变成导出表
  • 导出重定向

    • 将有个别符号重定向到其它二个 DLL
    • 例市场价格状下,导出表的地点数组中含有的是函数的 奥迪Q5VA,可是即使这一个安德拉VA
      指向的职位位于导出表中,那么表示这么些标记被重定向了,被重定向了的标志的
      SportageVA 并不意味着该函数的地点,而是举办一个 ASCII
      的字符串,这几个字符串在导出表中,是标志重定向后的 DLL
      文件名和标志名
  • 导入表

    • 设若有些程序中拔取到了来自 DLL
      的函数或然变量,那么那种行为称作符号导入
    • 某些 PE 文件被加载时,Windows
      加载器的中间二个义务就是将有着要求导入的函数地址显然并且将导入表中的因素调整到正确的地址,以完结动态链接
    • 导入地址数组 IAT
      :各种成分对应一个被导入的记号,成分的值在不相同的图景下有不一致的含义,动态链接器刚达成映射还不曾起来重一直和标志解析时,IAT
      中的成分值表示相呼应的导入符号的序号可能是符号名;当 Windows
      的动态链接器在成功该模块的链接时,成分值会被动态链接器改写成真的的号子地址。导入地址数组与
      ELF 中的 GOT 极度相像
    • 对此 32 位的 PE 来说,如若最高位被置 1,那么低 30个人值就是导入符号的序号值;尽管没有,那么成分的值是指向2个 瑞鹰VA
    • 对于 Windows 来说,它的动态链接器其实是 Windows
      内核的一片段,所以它可以随心所欲地修改 PE
      装载未来的人身自由一部分故事情节,包含内容和它的页面属性;在装载时,将导入表所在的岗位的页面改写成可读写的,一旦导入表的
      IAT 被改写完,再将那些页面设回只读属性
  • 导入函数的调用

    • PE DLL 的代码段并不是地方毫不相关的,使用了一种叫做重定集散地址的主意
    • 链接器在链接时会将导入函数的靶子地址导向一小段桩代码,由那些桩代码再将控制权交给
      IAT 中的确的靶子地方
    • 编译器在发出导入库的时候,同2个导出函数会时有爆发多少个记号的定义,2个指向桩代码,二个针对性函数在
      IAT 中的地方

3. DLL 优化

  • DLL 的代码段和数量段本身并不是地方无关的,默许须求被装载到由
    ImageBase 钦定的目的地方中,被占用就须要装载到其余得知,引起整个 DLL
    的 Rebase

  • 重定集散地址

    • PE 的 DLL
      中的代码段并不是地点毫无干系的,约等于说它在被装载时有一个稳住的对象地址,就是
      PE 里面所谓的基地址。默许情形 PE
      文件将被装载到那么些基地址,一般的话,EXE 的集散地址暗中同意为
      0x00五千00,而 DLL 文件集散地址暗许为 0x一千0000
    • 消除共享对象的地方争辨难题 :Windows PE
      接纳的是装载时重一向的办法,在 DLL
      模块装载时,借使目的地址被占用,那么操作系统就会为它分配一块新的空间,并且将
      DLL
      装载到该地方,对于每一个相对地址引用都进展重一向,全体那几个要求重一向的地点只必要加上八个稳住的差值,约等于说加上二个指标装载地方与实际装载地点的差值
    • 是因为 DLL 内部的地点皆以基于基地址的,或许是相持于基地址的
      PRADOVA,那么具有须求重一贯的地点都只需求丰富一个定点差值,PE
      里面把这种奇异的重定位进程又称作重定基地址,好处是比 ELF 的
      PIC 机制有着更快的运作速度,因为 PE 的 DLL
      对数据段的访问不必要经过类似于 GOT
      的体制,对于外部数据和函数的引用不要求每一回都盘算 GOT 的岗位
    • 变动私行认同基地址 :MSVC
      的链接器提供了内定输出文件的集散地址的效应,可以在链接时拔取 link
      命令中的 “/BASE” 参数内定集散地址
    • 系统 DLL :Windows 在设置时就把一块地方分配给了系统
      DLL,调整那些 DLL
      的基地址使得它们互相不抵触,从而在装载时就不要求举办举行重定基址了
  • 序号

    • 2个 DLL
      中每多个导出的函数都有二个一面如旧的序号,一个导出函数甚至能够没有函数名,但无法不有一个唯一的序号
    • 貌似的话,那一个仅供内部使用的导出函数,只有序号没有函数名,外部使用者无法想见它的意义和行使方法,避防止误用
    • 至今的 DLL
      中,导出函数表的函数名是通过排序的,所以寻找可以行使二分查找法,所以综合来看,一般意况下不推荐使用序号作为导入导出的手腕
  • 导入函数绑定

    • DLL 绑定
      :对绑定的程序的导入符号举行遍历查找,找到今后就把符号的周转时的目的地方写入到被绑定程序的导入表内
    • DLL 绑定的地点失效 :被正视的 DLL 更新导致 DLL
      的导出函数地址爆发变化;被依赖的 DLL 在装载时发生重定基址,导致
      DLL 的装载地点与被绑定时不雷同

4. C++ 与动态链接

  • 动用 C++ 编写 DLL 时很不难碰着包容性难题
  • 应用 C++ 编写动态链接库,要尽量坚守 :
    • 拥有的几口函数都应该是虚幻的,全数的主意都应当是纯虚的
    • 富有的全局函数都应有利用 extern “C” 来严防名字修饰的不合作
    • 不用使用 C++ 标准库 STL
    • 并非采用十二分
    • 毫无采纳虚析构函数,可以创造2个 destroy() 方法并且重载 delete
      操作符并且调用 destroy()
    • 不用再 DLL 里面申请内存,而且在 DLL 外释放
    • 毫不在接口中应用重载方法(Overloaded Methods,1个艺术多重参数)

5. DLL HELL

  • 总的看,有三种大概的来由导致了 DLL Hell 的暴发 :

    • 由使用旧版本的 DLL 替代原先2个新本子的 DLL
      而滋生,在设置时将多个旧版的 DLL 覆盖掉1个更新版本的 DLL
    • 由新版 DLL 中的函数无意暴发变更而滋生
    • 由新版 DLL 的安装引入八个新 BUG
  • 解决 DLL Hell 的方法 :

    • 静态链接 :防止采取动态链接,运营程序是就不再依靠 DLL
      了,不过会丧失动态链接带来的益处
    • 谨防 DLL 覆盖 :使用 Windows 文件敬爱技巧来消除
    • 防止 DLL 顶牛 :让各种程序有所一份祥和依靠的 DLL,把难点 DLL
      的不一致版本放到该应用程序的文本夹中,而不是系统文件夹中
    • .NET 下 DLL Hell 的解决

十、 内存

1. 顺序的内存布局

  • 32 位的系统里,内存空间拥有
    4GB(2^32)的寻址能力,被称作平坦的内存模型,整个内存是呀个联合的地址空间
  • 实际内存如故在区其他地方区间上拥有差其他地方,例如大部分 OS 会将
    4GB
    的内存空间中的一有的挪给基础使用,应用程序不可以直接访问这一段内存,这一片段内存地址被称呼水源空间,Windows
    在暗许意况下会将高地址的 2GB 空间分配给基础(也可安排为 1GB),Linux
    默许情形下将高地址的 1GB 空间分配给基础
  • 用户使用的剩余的 2GB 或 3GB
    的内存空间称为用户空间,一般来讲,应用程序使用的内存空间里有如下“暗中同意”的区域


    • :用于维护函数调用的上下文,离开了栈函数调用就无奈完毕;日常在用户空间的万丈地址处分配,常常有数兆字节的轻重
    • 堆 :用来包容应用程序动态分配的内存区域,当程序行使 malloc
      或 new
      分配内存时,得到的内存来自堆里;经常位于栈的花花世界(低地址方向),在有些时候,堆也有大概没有永恒统一的储存区域。堆一般比栈大很多
    • 可执行文件映像 :存储着可执行文件在内存里的影象
    • 保留区
      :不是二个单一的内存区域,而是对内存中受到保安而禁止访问的内存区域的总称

2. 栈与调用惯例

  • 哪些是栈

    • 栈被定义为3个非正规的器皿,先进后出
    • 栈保存了3个函数调用所急需的保险音讯,这一场被叫作堆栈帧或移动记录,堆栈帧一般包蕴如下几方面内容

      • 函数的归来地址和参数
      • 目前变量
        :包蕴函数的非静态局部变量以及编译器自动生成的其余一时半刻变量
      • 封存的上下文 :包蕴在函数调用前后必要保险不变的寄存器
    • 在 i386 中,三个函数的移动记录用 ebp 和 esp 那多少个寄存器划定范围

      • esp
        寄存器始终指向栈的顶部没同时也就执行了当前函数活动记录的顶部

      • ebp
        寄存器指向了函数活动记录的一个固定地方,又被改为帧指针,在参数之后的多少(包蕴参数)即是当前函数的移位记录,ebp
        固定在哪些地点,不随函数的推行而转变;固定不变的 ebp
        可以用来恒定函数活动记录中的各样数据,在 ebp
        在此以前率先是以此函数的回到地址,再往前是压入栈中的参数;ebp
        所平昔指向的数据是调用该函数前 ebp
        的值,那样在函数重临的时候,ebp
        可以通过读取那个值復苏到调用前的值

      • 把 ebp 压入栈中,是为着在函数再次来到的时候便与回复原先的 ebp
        值;之所以恐怕要保存一些寄存器,在于编译器可能要求某个寄存器在调用前后保持不变,那么函数就可以在调用初始时将那么些寄存器的值压入栈中,截至后再取出

      • i386 标准函数进入和退出指令系列,基本的花样为 :

          push ebp      // 保存 ebp
          mov ebp, esp  // 让 ebp 指向目前的栈顶
          sub esp, x    // 在栈上开辟一块空间
          [push reg1]   // 保存寄存器
          ...
          [push regn]
        
          // 函数实际内容
          mov eax, x   // 通过寄存器传递返回值
        
          [pop reg1]    // 从栈上恢复寄存器
          ...
          [pop regn]
           mov esp, ebp // 恢复进入函数前的 esp 和 ebp
           pop ebp       
           ret          // 返回
        
  • 调用惯例
    :函数的调用方和被调用方对于函数怎样调用要有1个斐然的规定,唯有双方都坚守,函数才能被正确地调用

    • 函数参数的传递顺序和章程 :最广泛的一种是经过栈传递
    • 栈的保养格局:函数将参数压栈之后,函数体会被调用,此后亟需将被压入栈的参数全体弹出,以使得栈在函数调用前后保持一致
    • 名字修饰的策略 :不相同的调用惯例有例外的名字修饰策略
    • cdecl 是 C 语言暗中认同的调用惯例
      :参数传递顺序为从右至左的顺序压参数入栈,出栈方为函数调用方,名字修饰为直接在函数名称前加一个下划线
  • 函数重回值传递

    • eax 是传递再次来到值的大路,函数将重回值存储在 eax
      中,重临后函数的调用方再读取 eax
    • eax 本身只有 4 个字节,重返大的再次来到值需要 :
      • 先是 main
        函数在栈上额外开发了一片空间,并将那块空间的一有的作为传递重临值的目前对象,那里名为
        temp
      • 将 temp 对象的地点作为隐藏参数传递给函数
      • 函数将数据拷贝给 temp 对象,并将 temp 对象的地址用 eax 传出
      • 函数再次回到之后,main 函数将 eax 指向的 temp 对象的始末拷贝给 n
    • 假使回去值类型尺寸太大,C
      语言在函数重回时会使用二个一时半刻的栈上内存区域作为中转,结果回到值会被拷贝三遍

3. 堆与内存管理

  • 哪些是堆
    • 堆是一块高大的内存空间,平日挤占整个虚拟空间的三头,在那片空间里,程序可以请求一块三番五次内存,并随机地行使,那块内存在程序积极甩掉此前都会一贯维持有效
    • 比方老是程序提请照旧释放对空中都急需展开系统调用,系统调用的性质开支是一点都不小的,频仍操作严重影响属性,相比好的做法就是程序向操作系统申请一块适当大小的堆空间,然后程序本人管理那块空间,管理者堆空间分配的一再是先后的运行库
  • Linux 进程堆管理
    • 提供八个系统调用 :
      • 一个是 brk()
        系统调用,功用实际上是安装进度数据段的扫尾地址,可以增加恐怕缩短数据段(Linux
        下数据段和 BSS 合并在同步统称数据段)
      • 另二个是
        mmap(),功效是向操作系统申请一段虚拟地址空间,那段虚拟地址空间可以映射到某些文件,当它不将地点空间映射到有个别文件时,又称那块空间为匿名,匿名空间就能够拿来作为堆空间
    • 从理论可以测算,2.6 版的 Linux 的 malloc 的最大空间申请数应该在
      2.9G 左右(可执行文件占去一部分、0x080四千0
      在此之前的地址占去一部分、栈占去一部分、共享库占去一部分)
  • Windows 进度堆管理
    • Windows 的历程将地方空间分配给了各样 EXE、DLL 文件、堆、栈
    • 各样线程的栈都是单独的,所以二个经过中有个别许个线程,就应当有多少个照应的栈,对于
      Windows 来说,各个线程暗许的栈大小是
      1MB,在线程运维时,系统会为它在经过地址空间中分红相应的半空中作为栈
    • Windows 系统提供了多少个 API 叫做
      VirtualAlloc(),用来向系统报名空间,与 Linux 下的 mmap
      格外相似,实际上申请的半空中不肯定只用于堆,仅仅是想系统留住了一块虚拟地址,应用程序可以坚守需求自由动用
    • 拔取 VirtualAlloc()
      函数申请空间时,系统须要空间大小必须为页的平头倍
    • 在 Windows
      中,有依据对管理器落成的分红的算法,对管理器提供了一套与堆相关的
      API 可以用来制造、分配、释放和销毁堆空间
    • 透过 Windows
      进度地址空间分布,可见二个历程中可见分配给堆用的长空不是接二连三的,所以当1个堆的半空中已经力不从心再扩充时,必须创制3个新的堆,运维库的
      malloc 函数已经解决了那整个
    • 经过中或然存在七个堆,不过八个经过中可见分配的最大堆空间在于最大的要命堆
  • 堆分配算法
    • 没事链表
      • 实际上就是把堆中相继空闲的块依照链表的艺术连接起来,当用户请求一块空间时,可以遍历整个列表,直到找到适合大小的块并且将它拆分;当用户自由空间时将它合并空闲链表中
      • 而是即使链表被毁掉,或然记录长度的那 4
        字节被弄坏,整个对就无法不奇怪办事
    • 位图
      • 主题情想是将全体堆分配为大气的块,逐个块的轻重缓急一样,当用户请求内存的时候,总是分配整数个块的长空给用户,第二个块称为已分配区域的头,其他的称之为已分配区域的基点
      • 可以用2个平头数组来记录块的使用状态,由于各类块唯有头/主体/空闲二种意况,由此仅要求两位即可表示三个块,因而称为位图
      • 优点 :
        • 速度快
        • 稳定好
        • 块不必要十三分音讯
      • 缺点
        • 分配内存的时候简单爆发碎片
        • 即使对非常的大,只怕设定贰个快十分的小,那么位图将会十分大,只怕错过
          cache 命中率的优势,也会浪费一定的上空
    • 对象池
      • 倘若每三回分配的上空大小都如出一辙,那么就可以依照那些每趟请求分配的大大小小作为一个单位,把一切堆空间划分为大气的小块,每一回请求的时候只要求找到一个小块就足以了
      • 每回一而再只请求一个单位的内存,由此请求得到满意的进程特别快,无需寻找2个充足大的空中

十一、 运行库

1. 入口函数和顺序开首化

  • 程序从 main 开始吗

    • 操作系统装载程序未来,首先运转的代码并不是 main
      的首先行,而是某个其他代码,这一个代码负责准备好 main
      函数执行所急需的环境,并且负责调用 main 函数那时才方可在 main
      函数里大胆地写各类代码 :申请内存、使用系统调用、触发非常、访问
      I/O。在 main 再次回到之后,会记录 main 函数的重临值,调用 atexit
      注册的函数,然后甘休进度
    • 运维这么些代码的函数称为入口函数或入口点,程序的入口点实际上是3个顺序的初叶化和竣事部分,往往是运维库的一局地
    • 顶级的次序运作步骤大约如下 :
      • 操作系统在开创进程后,把控制权交到了先后的进口,那些进口往往是运营库中的有个别入口函数
      • 入口函数对运营库和顺序环境展开早先化,包括堆、I/O
        、线程、全局变量构造
        ,等等
      • 入口函数在成功开头化之后,调用 main
        函数,正式开班推行顺序主体部分
      • main
        函数执行落成之后,再次来到到入口函数,入口函数进行清理工作,包涵全局变量析构、堆销毁、关闭
        I/O 等,然后进行系统调用截止进度
  • 入口函数如何贯彻

    • GLIBC 入口函数
    • MSVC C帕杰罗T 入口函数
      • 次第3起始堆还不曾被先河化,alloca
        是绝无仅有可以不拔取堆得动态分配机制,能够在栈上分配任意大小的上空,并在函数再次来到的时候会自行释放,就类似有个别变量一样
  • 运转库与 I/O

    • 一个程序的 I/O
      指代了程序与外边的互相,包涵文件、管道、网络、命令行、信号等
    • 广义地讲,I/O 指代任何操作系统了然为“文件”的东西,许多 OS
      都将各个具有输入和出口概念的实业——包涵设备、磁盘文件、命令行等——统称为文件
    • C 语言文件操作是透过三个 FILE 结构的指针来拓展的
    • OS 层面上,文件操作也有接近于 FILE 的3个概念,在 Linux
      里,叫做文本讲述符,在 Windows
      里,叫做句柄。用户通过有个别函数打开文件以博取句柄,然后用户操纵文件皆通过该句柄进行,因为句柄可以免患用户私行读写操作系统内核的文本对象,文件句柄总是和水源的公文对象相关联的
  • MSVC CTiggoT 的入口函数初步化

    • 系统堆初始化 :由函数 _heap_init 完成,调用了 HeapCreate
      那么些 API 制造了三个体系堆
    • I/O 初始化
      • 在 MSVC 中,FILE 结构中最重视的二个字段 _file_file
        是三个平头,通过它可以访问到里头文件句柄表中的某一项
      • 在 Windows 中,用户态使用句柄来访问基本文件对象
      • 走访文件时,必需求从 FILE 结构转换来操作系统的句柄
      • MSVC 的 I/O 开头化就是要布局二维的开辟文件表
      • _ioinit 函数起始化了 _pioinfo
        数组的首先个二级数组,接下去,将部分预约义的开拓文件给开头化,包罗:

        • 从父进程继续的开拓文件句柄,可以采用继续自个儿的开辟文件句柄
        • OS 提供的正统输入输出
      • MSVC 的 I/O 先导化首要举办了如下多少个工作 :
        • 确立打开文件表
        • 假定可以持续自父进程,那么从父进度取得继承的句柄
        • 开首化标准输入输出

2. C/C++ 运行库

  • C 语言运营库

    • 使程序可以健康运营,至少包含入口函数,及其所倚重的函数所构成的函数集合,还应当包罗种种专业库函数的贯彻,那样的1个代码集合称之为运维时库,C
      语言的运营库被喻为 C 运行库
    • 1个 C 语言运营库大约包含了如下效果 :
      • 开行与脱离
      • 业内函数
      • I/O
      • 言语落成
      • 调试
  • C 语言标准库

    • 例如
      :标准输入输出、文件操作、字符操作、字符串操作、数学函数、财富管理、格式转换、时间/日期、断言、各体系型上的常数,还有局地非同平日的操作如:变长参数、非局地跳转
  • glibc 与 MSVC CRT

3. 运营库与多线程

  • C纳瓦拉T 的八线程干扰

    • 线程的造访权限

      • 实际上使用中线程也拥有本人的个体存储空间
        • 线程局地存储 TLS
        • 寄存器
      • 从 C 程序员的角度来看 :
        • 线程私有 :
          • 一对变量
          • 函数的参数
          • TLS 数据
        • 线程之间共享(进度具有):
          • 全局变量
          • 堆上的多寡
          • 函数里的静态变量
          • 程序代码
          • 开辟文件
    • 二十四线程运维库

      • 提供十二线程操作的接口
      • C 运营库本身要力所能及在多线程的环境下正确运营
  • CRT 改进

    • 使用 TLS
    • 加锁
    • 句酌字斟函数调用格局:修改全数线程不安全的函数的参数列表,改成某种线程安全的版本
  • 线程局地存储完毕

    • 假使3个全局变量被定义成 TLS
      类型的,那么各个线程都会怀有那几个变量的叁个副本,然和线程对该变量的改动都不会潜移默化别的线程中该变量的副本
    • 使用 __declspec(thread)
      定义3个线程私有变量的时候,编译器会把这个变量放到 PE 文件的
      “.tls”
      段中。当系统运行1个新的线程时,它会从进程的堆中,分配一块充足大小的半空中,然后把
      “.tls” 段中的内容复制到那块空间,于是每种线程都有和好独自的三个“.tls” 副本
    • 对于每一种 Windows
      线程来说,系统都会树立1个关于线程新闻的社团,叫做线程环境块,保存了现成的库房地址、线程
      ID 等相关音讯
    • 显式 TLS
    • _beginthread() 是对 CreateThread() 的卷入,当使用 CRAV4T
      时,尽量使用
      _beginthread/_beginthreadex()/_endthread()/_endthreadex()
      那组函数来制造线程

4. C++ 大局构造函数与析构

  • C++ 入口函数要求在 main 的前后已毕全局变量的布局与析构

  • glibc 全局构造与析构

    • _start -> __libc_start_main -> __libc_csu_init ->
      _init -> __do_global_ctors_aux
    • _init 调用了 __do_global_ctors_aux 函数,它不属于
      glibc,而是来自于 GCC 提供的二个目标文件
      crtbegin.o,负责协会的函数来自于 GCC
    • __CTOR_LIST__ 数组里面存放的就是大局对象的构造函数的指针
    • 对此逐个编译单元,GCC
      编译器会遍历其中装有的全局对象,生成二个出奇的函数,那个极度函数的左右就是对本编译单元里的保有全局对象开展开首化
    • 把各种指标文件的复杂全局/静态对象协会的函数地址位于一个不一致日常的段中间,让链接器把这么些优异的段收集起来,收集齐全部的全局构造函数后就足以在开始化的时候举办布局了
    • 各个目的文件的 .ctors 段会被统一为1个 .ctors 段,拼接起来的
      .ctors
      段成为了三个函数指针数组,每一个因素都针对三个目的文件的大局构造函数
    • glibc 的全局构造函数是停放在 .ctors 段里的
    • 为了确保全局对象社团和析构的逐条(先构造后析构),链接器必须包装全数的
      “.dtor” 段的联合顺序必须是 “.ctors”
      的阴毒反序,后来采纳一种新的做法是经过__cxa_atexit()
      exit() 函数中注册进度退出回调函数来贯彻析构
    • 全局对象的创设和析构都是由运转库达成的
  • MSVC COdysseyT 的大局构造与析构

    • mainCRTStartup -> _initterm
    • _initterm 遍历全体的函数指针并且调用
    • MSVC CENCORET 的全局构造实以后编制上与 Glibc
      基本是均等的,但是名字略有分化
    • MSVC C兰德RT 析构 :通过 atexit() 完毕全局析构

5. fread 实现

  • fread 最后是透过 Windows 的系统 API :ReadFile()
    来兑现对文本的读取的;fread 有 4 个参数,作用是尝尝从文件流 stream
    里读取 count 个轻重缓急为 elementSize 个字节的数额,存储在 buffer
    里,重回实际读取的字节数

  • 缓冲

    • 如若每一遍读或写多少都进展一次系统调用,让内核读写多少,系统开发一点都不小,要开展上下文切换、内核参数检查、复制等,会严重影响程序和序列的性质
    • 行缓冲和全缓冲
  • fread_s

    • fread 将有着的行事都传送给了 _fread_s
    • fread_s 的参数比 fread 多了叁个 bufferSize,用于钦点参数
      buffer 的深浅,而 fread 只有 SIZE_MAXfread_s
      可以钦点这么些参数以幸免越界
    • fread_s 首先对各样参数检查,然后使用 _lock_str
      对文件进行加锁,以免止多个线程同时读取文件而导致缓冲区不等同
  • fread_nolock_s

    • 拥有的头脑最终都指向 _read 函数,它根本担负两件事 :
      • 从文件中读取数据
      • 对文件格局打开的文书,转换回车符
  • _read

    • _read 函数在历次读取管道和装置数量的时候必须先反省
      pipech,以防漏掉二个字节
    • ReadFile 是1个 Windows API 函数,由 Windows
      系统提供,功用和_read 类似,用于从文件里读取数据
  • 文件换行

    • _read 要为以文件方式打开的文本转换回车符
    • 首先检查文件是不是以文件格局打开,再展开 “\r\澳门金沙国际 ,n” 之类的更换
  • fread 回顾

    • fread
    • fread_s 增添缓冲溢出爱抚,加锁
    • _fread_nolock_s 循环读取、缓冲
    • _read 换行符转换
    • ReadFile Windows 文件读取 API

十② 、 系统调用与 API

1. 系统调用介绍

  • 什么是系统调用
    • 为了让应用程序有力量访问系统财富,也为了让程序借助操作系统做一些无法不由操作系统协助的作为,逐个操作系统都会提供一套接口,以供应用程序行使,那些接口往往通过暂停来兑现
    • 富含程序运营所不可不的支撑、系统能源的访问、对图形界面的操作支持等
    • 亟待保持安澜和向后非常
    • Windows 与应用程序的最终接口是 API
  • Linux 系统调用
    • x86 下,系统调用有 0x80
      中断达成,各样通用寄存器用于传递参数,EAX
      寄存器用于表示系统调用的接口号
    • 席卷进度处理、读写文件、权限管理、定时器、信号、网络等
  • 系统调用的弊病
    • 拔取不便
    • 逐条操作系统之间系统调用不匹配
    • 化解办法 :使用运转库为系统调用和顺序之间的贰个抽象层

2. 系统调用原理

  • 特权级与中断
    • 当代 OS 中,经常有两种特权级别
      :用户方式和水源情势,也被号称用户态和内核态
    • 系统调用是运作在内核态的,而应用程序基本都以运转在用户态的
    • 操作系统一般通过暂停来从用户态切换成内核态
    • 停顿是多少个硬件或软件暴发的伸手,须求 CPU
      暂停当前的办事转眼去处理越发主要的事
    • 暂停一般装有八本性子:中断号和刹车处理程序;不一致的中断全数不一致的中断号,二个抛锚处理程序一一对应三个抛锚号
  • 基于 int 的 Linux 的经典系统调用完成
    • 接触中断 :利用宏
    • 切换堆栈
      :调用中断时,程序的实践会在用户态和内核态之间切换,程序的脚下栈也在用户栈和内核栈之间切换,当前栈指的是
      ESP 的值所在的栈空间,寄存器 SS 的值还应该本着当前栈所在的页
    • 暂停处理程序
    • 用户调用系统调用时,依据系统调用参数数量不等,依次将参数放入
      EBX、ECX、EDX、ESI、EDI、EBP 那 六个寄存器中传送,进入系统调用的服务程序 system_call
      的时候,调用了3个宏 SAVE_ALL 来保存各样寄存器
  • Linux 的风靡系统调用机制
    • 行使 ldd 来拿到三个可执行文件的共享库的倚重情状,会看出
      linux-gate.so.1
      没有与其它实际的公文相呼应,是用于辅助新型系统调用的“虚拟”共享库,并不存在实际的文本,只是操作系统生成的一个虚拟动态共享库
    • 新式系统调用指令
      :sysenter,调用之后系统会直接跳转到有个别寄存器钦命的函数执行,并自行完结特权级转换、堆栈切换等功效;在参数传递方面,新型的种类调用与运用
      int 的系统调用完全等同

3. Windows API

  • Windows API 是指 Windows
    操作系统提必要应用程序开发者最底部的、最直接与 Windows
    打交道的接口,在 Windows OS 下,C纳瓦拉T 是确立在 Windows API 之上的,MFC
    是很闻名的一种以 C++ 格局封装的库
  • 概述 :Windows API 是以 DLL 导出函数的样式暴露给应用程序开发者的
  • 为啥要选择 Windows API(放着系统调用不用,在 C本田UR-VT
    和系统调用之间增加一层 Windows API
    层):系统调用实际上是可怜依赖于硬件结构的一种接口,受到硬件的严谨限定,比如寄存器的数目、调用时的参数传递、中断号、堆栈切换等,都与硬件密切相关,假如硬件结构有个别发生变更,大量的应用程序只怕就会师世难题。
    所以 Windows OS 把系统调用包装了四起,使用 DLL
    导出函数作为应用程序的唯一可用的接口暴光给用户
  • API 与子系统
    • 子系统又称之为 Windows 环境子系统,简称子系统
    • 子系统又是 Windows 架设在 API
      和应用程序之间的另八个中间层,是用来为各个不同平台的应用程序创制与它们包容的周转条件

十叁 、 运转库完成

1. C 语言运维库

  • 兑现 Mini C凯雷德T,它应当享有 C福特ExplorerT 的基本功效以及根据几个核心陈设规范 :
    • 有道是以 ANSI C 的标准库为目的,尽量做到与其接口一致
    • 富有温馨的入口函数
    • 宗旨的进度有关操作
    • 支撑堆操作
    • 支撑大旨的公文操作
    • 支撑宗旨的字符串操作
    • 支撑格式化字符串和出口操作
    • 支持 atexit() 函数
    • 应当是跨平台的
    • 兑现应有尽或许简单
  • 开始
    • 从入口函数初叶
      • 先后的初期入口点不是 main
        函数,而是由运维库为其提供的入口函数,首要担负
        :准备好程序运维环境及初叶化运营库、调用 main
        函数执行顺序主体、清理程序运维后的各样财富
      • 运作库为全部程序提供的入口函数应该同等,在链接程序时必须钦赐该入口函数名
      • 要求规定入口函数的函数原型,包蕴函数名、输入参数及重返值
      • 初步化首要承担好程序运转的条件,包蕴准备 main
        函数的参数、早先化运维库,包括堆、IO
        等,甘休部分重大承担清理程序运维能源
    • main 函数 :argc、argv
    • C中华VT 初阶化 :紧假若堆和 IO 部分
    • 截至部分 :调用由 atexit() 注册的退出回调函数、已毕竣事进度
  • 堆的兑现
    • 落到实处一个以空闲链表算法为根基的堆空间分配算法
    • 为了不难,堆空间大小固定为 32MB,伊始化之后空间不再扩展或减弱
    • 选拔 VirtualAlloc 向系统一直申请 32MB,由友好的对分配算法完毕malloc
    • Linux 平台下,使用 brk 将数据段告竣地址向后调整
      32MB,将那块空间作为堆空间
  • IO 与公事操作
    • 仅达成核心的文本操作,包涵 fopen、fread、fwrite、fclose、fseek
    • 不已毕缓冲机制
    • 不对 Windows 下的换行机制举办转移
    • 支撑八个正式的输入输出 stdin、stdout、stderr
    • 在 Windows 下,文件基本操作可以运用 API
    • Linux 不像 Windows 那样有 API 接口,必须利用内嵌汇编已毕open、read、write、close、seek 那多少个种类调用
    • fopen
      时只分化“r”、“w”、“+”那两种方式及它们的构成,不对文件形式和二进制方式举办区分,不扶助追加方式(“a”)
  • 字符串相关操作 :无须涉及其余与基础交互
  • 格式化字符串 :完成 printf

2. 怎么着利用 Mini C奥迪Q3T

  • Mini CHighlanderT 也将以库文件和头文件的样式提要求用户,可以建立三个minicrt.h 的头文件,然后将持有有关的常数定义、宏定义,以及 Mini C本田CR-VT
    所达成的函数表明等位居该头文件里,当用户使用时,仅要求
    #include "minicrt.h" 即可
  • MiniC冠道T 仅凭借与 Kernel32.DLL,的确绕过了 MSVC CRubiconT 的运维库
    msvcr90.dll

3. C++ 运维库完结

  • 在 Mini C奇骏T 的根基上落到实处一个支撑 C++ 的运营库
  • 依据以下原则 :
    • 尽量简化设计,尽量符合 C++ 标准库的正经
    • 对此可以一贯在头文件落到实处的模块尽量在头文件中落实
    • 可以在 Windows 和 Linux
      上同事运维,由此对于平台相关部分要利用规则编译分别完成
    • 模板是不要求运维库资助的,它的落到实处依靠于编译器和链接器
  • new 与 delete :在堆上分配空间
  • C++ 全局构造与析构
    :完结依靠于编译器、链接器和运转库三者共同的支撑和合营
  • atexit 完结 :由它注册的函数会在进程退出前,在 exit()
    函数中调用;落成的缘由是 全体全局对象的析构函数都以经过 atexit()
    或其类似函数来注册的,以高达在程序退出时实施的目标
  • 入口函数修改 :把对 do_global_ctors()
    mini_crt_call_exit_routine 的调用插手到 entry() 和 exit() 函数中去
  • stream 和 string

4. 怎么样使用 Mini CPRADOT++

相关文章