目标格式里有什么?

文章目录

    • 1 目标文件格式
    • 2 目标文件是什么样的
    • 3 挖掘SimpleSection.o
      • 3.1 代码段
      • 3.2 数据段和只读数据段
      • 3.3 BSS段
      • 3.4 其他段
    • 4 ELF文件结构描述
      • 4.1 文件头
      • 4.2 段表
      • 4.3 重定位表
      • 4.4 字符串表
    • 5 链接的接口----符号
      • 5.1 ELF符号表结构
      • 5.2 特殊符号
      • 5.3 符号修饰与函数签名
      • 5.4 extern "C"
      • 5.5 弱符号与强符号
    • 6 调试信息

1 目标文件格式

目标文件就是源代码编译后但未进行链接的那些中间文件(windows的.obj和linux下的.o),它跟可执行文件的内容与结构很相似,所以一般跟可执行文件格式一起采用一个格式存储。

平台 可执行文件格式 目标文件格式 动态链接库 静态链接库
Linux ELF .o .so .a
Windows PE .obj .dll .lib
ELF文件类型 说明
可重定位文件(Relocatable File) 这类文件包含了代码和数据,可以被用来链接成可执行文件或共享目标文件,静态链接库也可以归为这一类
可执行文件(executable file) 这类文件包含了可以直接执行的程序,它的代表就是ELF可执行文件,他们一般都没有扩展名
共享目标文件(Shared Object File) 这种文件包含了代码和数据,可以在一下2种情况下使用。一种是链接器可以使用这种文件跟其他的可重定位文件和共享目标文件链接,产生新的目标文件。第二种是动态链接器可以将几个这种共享目标文件与可执行文件结合,作为进程映像的一部分来运行。
核心转储文件(Core Dump File) 当进程意外终止时,系统可以将该进程的地址空间的内容及终止时的一些其他信息转储到核心转储文件。

在Linux中,可以用file命令来查看相应的类型。

2 目标文件是什么样的

目标格式里有什么?_第1张图片
ELF文件的开头是一个文件头(File Header),它描述了整个文件的文件属性,包括文件是否可执行、是静态链接还是动态链接及入口地址(如果是可执行文件)、目标硬件、目标操作系统等信息,文件头还包括了一个段表(Section Table)。

段表其实是一个描述文件中各个段的数组。段表描述了文件中各个段在文件中的偏移位置及段的属性等,从段表里面可以得到每个段的所有信息。

对照上图,一般C语言的编译后执行语句都编译成机器代码,保存在.text section;已初始化的全局变量和局部静态变量一般放在.data section;未初始化的全局变量和局部静态变量一般放在一个叫.bss section

我们知道未初始化的全局变量和局部变量默认值都是0,本来它们也可以被放在.data section,但是因为它们都是0,所以为它们在.data section分配空间并存放数据0是没有必要的。程序运行的时候它们的确是要占内存空间,并且可执行文件必须记录所有未初始化的全局变量和局部静态变量的大小总和,记为.bss section。所以.bss section只是为未初始化的全局变量和局部静态变量预留位置而已,它并没有内容,所以它在文件中也不占据空间。

总体来说,程序源代码被编译以后主要分成两种段:程序指令和程序数据。代码段属于程序指令,而数据段和.bss段属于程序数据。

为什么要将程序的指令和数据分开存放,原因有:

  1. 当程序被装载后,数据和指令分别被映射到两个虚存区域。由于数据区域对于进程来说是可读写的,而指令区域对于进程来说是只读的,所以这两个虚存区域的权限可以被分别设置成可读写和只读。这样可以防止程序的指令被有意或无意地改写。
  2. 对于现代CPU来说,它们有着极为强大的缓存体系。由于缓存在现代的计算机中地位非常重要,所以程序必须尽量提高缓存的命中率。指令区和数据区的分离有利于提高程序的局部性。现代CPU的缓存一般都被设计成数据缓存和指令缓存(一级),所以程序的指令和数据被分开存放对CPU缓存命中率提高有好处。
  3. 当系统中运行着多个该程序的副本时,它们的指令都是一样的,所以内存中只须保持一份该程序的指令部分。这样可以节省大量的内存。

3 挖掘SimpleSection.o

代码3-1

/*
 * SimpleSection.c
 * 
 * Linux:
 *      gcc -c -m32 SimpleSection.c
 *      -m32编译成32位
 * 
 * Windows:
 *      c1 SimpleSection /c /Za
 */
int printf(const char *format, ...);

int global_init_var = 84;
int global_uninit_var;

void func1(int i)
{
    printf("%d\n", i);
}

int main(void)
{
    static int static_var = 85;
    static int static_var2;

    int a = 1;
    int b;

    func1(static_var + static_var2 + a + b);

    return a;
}

objdump -h SimpleSection.o查看目标文件的结构和内容:

SimpleSection.o:     file format elf32-i386

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .text         00000062  00000000  00000000  00000034  2**0
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  1 .data         00000008  00000000  00000000  00000098  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000004  00000000  00000000  000000a0  2**2
                  ALLOC
  3 .rodata       00000004  00000000  00000000  000000a0  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  4 .comment      00000036  00000000  00000000  000000a4  2**0
                  CONTENTS, READONLY
  5 .note.GNU-stack 00000000  00000000  00000000  000000da  2**0
                  CONTENTS, READONLY
  6 .eh_frame     00000064  00000000  00000000  000000dc  2**2
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA

SimpleSection.o除了最基本的代码段,数据段和BSS段外,还有其他的只读数据段(.rodata)、注释信息段(.comment)和堆栈提示段(.note.GNU-stack)。
目标格式里有什么?_第2张图片

3.1 代码段

objdump -s -d SImpleSection.o查看代码段的内容:

SimpleSection.o:     file format elf32-i386

Contents of section .text:
 0000 5589e583 ec0883ec 08ff7508 68000000  U.........u.h...
 0010 00e8fcff ffff83c4 1090c9c3 8d4c2404  .............L$.
 0020 83e4f0ff 71fc5589 e55183ec 14c745f0  ....q.U..Q....E.
 0030 01000000 8b150400 0000a100 00000001  ................
 0040 c28b45f0 01c28b45 f401d083 ec0c50e8  ..E....E......P.
 0050 fcffffff 83c4108b 45f08b4d fcc98d61  ........E..M...a
 0060 fcc3                                 ..

......

Disassembly of section .text:

00000000 :
   0:   55                      push   %ebp
   1:   89 e5                   mov    %esp,%ebp
   3:   83 ec 08                sub    $0x8,%esp
   6:   83 ec 08                sub    $0x8,%esp
   9:   ff 75 08                pushl  0x8(%ebp)
   c:   68 00 00 00 00          push   $0x0
  11:   e8 fc ff ff ff          call   12 
  16:   83 c4 10                add    $0x10,%esp
  19:   90                      nop
  1a:   c9                      leave
  1b:   c3                      ret

0000001c 
: 1c: 8d 4c 24 04 lea 0x4(%esp),%ecx 20: 83 e4 f0 and $0xfffffff0,%esp 23: ff 71 fc pushl -0x4(%ecx) 26: 55 push %ebp 27: 89 e5 mov %esp,%ebp 29: 51 push %ecx 2a: 83 ec 14 sub $0x14,%esp 2d: c7 45 f0 01 00 00 00 movl $0x1,-0x10(%ebp) 34: 8b 15 04 00 00 00 mov 0x4,%edx 3a: a1 00 00 00 00 mov 0x0,%eax 3f: 01 c2 add %eax,%edx 41: 8b 45 f0 mov -0x10(%ebp),%eax 44: 01 c2 add %eax,%edx 46: 8b 45 f4 mov -0xc(%ebp),%eax 49: 01 d0 add %edx,%eax 4b: 83 ec 0c sub $0xc,%esp 4e: 50 push %eax 4f: e8 fc ff ff ff call 50 54: 83 c4 10 add $0x10,%esp 57: 8b 45 f0 mov -0x10(%ebp),%eax 5a: 8b 4d fc mov -0x4(%ebp),%ecx 5d: c9 leave 5e: 8d 61 fc lea -0x4(%ecx),%esp 61: c3 ret

对照下面的反汇编内容,可以很明显地看到,.text段里面所包含的正是SimpleSection.c里两个函数func1()main()的指令。.text段的第一个字节0x55就是func1()函数的第一条push %ebp指令,而最后一个字节0xc3正是main()函数的最后一条指令ret

3.2 数据段和只读数据段

# objdump -x -s -d SimpleSection.o

SimpleSection.o:     file format elf32-i386
SimpleSection.o
architecture: i386, flags 0x00000011:
HAS_RELOC, HAS_SYMS
start address 0x00000000

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  1 .data         00000008  00000000  00000000  00000098  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  3 .rodata       00000004  00000000  00000000  000000a0  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
                  
......

Contents of section .data:
 0000 54000000 55000000                    T...U...
Contents of section .rodata:
 0000 25640a00                             %d..

......

.data段保存的是那些已经初始化了的全局变量和局部静态变量SimpleSection.c代码中一共有2个这样的变量,分别是global_init_varabalstatic_var,一共8个字节,所以.data段的大小为8个字节。

调用printf的时候,用到了一个字符串常量%d\n,它是一种只读数据,所以它被放到了.rodata段。该段大小为4个字节,刚好是这个字符串常量的ASCII字节序,最后以\0结尾。

我们注意到,.data段里的前4个字节,从低到高分别为0x54 0x00 0x00 0x00,该值刚好是global_init_varabal,即84。这里使用的是小端存储。

3.3 BSS段

.bss段存放的是没初始化的全局变量和局部静态变量。如上述代码中global_uninit_varstatic_var2就是被存放在.bss段,其实更准确的说法是为它们预留了空间。但是,该段大小只有4个字节,2个变量的大小为8个字节?

在随后的符号表(Symbol Table)中,只有static_var2被存放在.bss段,而global_uninit_var却没有被存放在任何段中,只有一个未定义的COMMON符号。

SYMBOL TABLE:
00000000 l    df *ABS*  00000000 SimpleSection.c
00000000 l    d  .text  00000000 .text
00000000 l    d  .data  00000000 .data
00000000 l    d  .bss   00000000 .bss
00000000 l    d  .rodata        00000000 .rodata
00000004 l     O .data  00000004 static_var.1488
00000000 l     O .bss   00000004 static_var2.1489
00000000 l    d  .note.GNU-stack        00000000 .note.GNU-stack
00000000 l    d  .eh_frame      00000000 .eh_frame
00000000 l    d  .comment       00000000 .comment
00000000 g     O .data  00000004 global_init_var
00000004       O *COM*  00000004 global_uninit_var
00000000 g     F .text  0000001c func1
00000000         *UND*  00000000 printf
0000001c g     F .text  00000046 main

Quiz 变量存放位置

static int x1 = 0;
static int x2 = 1;
x1和x2会被放在什么段中呢?

x1会被放在.bss中,x2会被放在.data中。x1为0,可以认为是没初始化的,因为没初始化的都是0,所以被优化掉了可以放在.bss,这样可以节省磁盘空间,因为.bss不占磁盘空间。另一个变量x2初始值为1,是初始化的,所以放在.data段中。

3.4 其他段

常用的段名 说明
.rodata1 Read only Data,这种段里存放的是只读数据,比如字符串常量、全局const变量。跟.rodata 一样
.comment 存放的是编译器版本信息,比如字符串:GCC:(GNU) 4.2.0
.debug 调试信息
.dynamic 动态链接信息
.hash 符号哈希表
.line 调试时的行号表,即源代码行号与编译后指令的对应表
.note 额外的编译器信息。比如程序的公司名,发布版本号
.strtab String Table。字符串表,用于存储ELF文件中用到的各种字符串
.symtab Symbol Table。符号表
.shstrtab Section String Table。段名表
.plt .got 动态链接的跳转表和全局入口表
.init .fini 程序初始化与终结代码段

Q:如果我们要将一个二进制文件,比如图片,MAP3音乐一类的东西作为目标文件中的一个段,该怎么做?
A:可以使用objcopy工具,比如我们有一个图片文件image.jpg,大小为242,434 bytes

$ objcopy -I binary -O elf32-i386 -B i386 image.jpg image.o
$ objdump -ht image.o

image.o:     file format elf32-i386

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .data         0003b302  00000000  00000000  00000034  2**0
                  CONTENTS, ALLOC, LOAD, DATA
SYMBOL TABLE:
00000000 l    d  .data  00000000 .data
00000000 g       .data  00000000 _binary_image_jpg_start
0003b302 g       .data  00000000 _binary_image_jpg_end
0003b302 g       *ABS*  00000000 _binary_image_jpg_size

_binary_image_jpg_start_binary_image_jpg_end_binary_image_jpg_size表示图片文件在内存中的起始地址、结束地址和大小,我们可以在程序里面直接声明并使用它们。


自定义段
有时候你希望变量或某些代码能够放到你指定的段中,以实现某些特定的功能。比如为了满足硬件的内存和I/O的地址布局,或者是像Linux操作系统内核中用来完成一些初始化和用户空间复制时,出现页错误异常等。

__attribute__((section("FOO"))) int global = 42;
__attribute__((section("BAR"))) void foo()
{
}

4 ELF文件结构描述

ELF目标文件格式的最前部是ELF文件头(ELF Header),它包含了描述整个文件的基本属性,比如ELF文件版本、目标机器型号、程序入口地址等。紧接着是ELF文件各个段。其中ELF文件中与段有关的重要结构就是段表(Section Header Table),该表描述了ELF文件的所有段的信息,比如每个段的段名、段的长度、在文件中的偏移、读写权限及段的其他属性。

4.1 文件头

readelf -h SimpleSection.o来查看ELF文件:

$ readelf -h SimpleSection.o
ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              REL (Relocatable file)
  Machine:                           Intel 80386
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          0 (bytes into file)
  Start of section headers:          832 (bytes into file)
  Flags:                             0x0
  Size of this header:               52 (bytes)
  Size of program headers:           0 (bytes)
  Number of program headers:         0
  Size of section headers:           40 (bytes)
  Number of section headers:         13
  Section header string table index: 10              

ELF文件头结构及相关参数被定义在/usr/include/elf.h,有32版本和64为版本。

/* Type for a 16-bit quantity.  */
typedef uint16_t Elf32_Half;
typedef uint16_t Elf64_Half;

/* Types for signed and unsigned 32-bit quantities.  */
typedef uint32_t Elf32_Word;
typedef int32_t Elf32_Sword;
typedef uint32_t Elf64_Word;
typedef int32_t Elf64_Sword;

/* Types for signed and unsigned 64-bit quantities.  */
typedef uint64_t Elf32_Xword;
typedef int64_t Elf32_Sxword;
typedef uint64_t Elf64_Xword;
typedef int64_t Elf64_Sxword;

/* Type of addresses.  */
typedef uint32_t Elf32_Addr;
typedef uint64_t Elf64_Addr;

/* Type of file offsets.  */
typedef uint32_t Elf32_Off;
typedef uint64_t Elf64_Off;

/* Type for section indices, which are 16-bit quantities.  */
typedef uint16_t Elf32_Section;
typedef uint16_t Elf64_Section;

/* Type for version symbol information.  */
typedef Elf32_Half Elf32_Versym;
typedef Elf64_Half Elf64_Versym;

32位版本的文件头结构Elf32_Ehdr作为例子来描述:

/* The ELF file header.  This appears at the start of every ELF file.  */

#define EI_NIDENT (16)

typedef struct
{
  unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
  Elf32_Half e_type;                /* Object file type */
  Elf32_Half e_machine;             /* Architecture */
  Elf32_Word e_version;             /* Object file version */
  Elf32_Addr e_entry;               /* Entry point virtual address */
  Elf32_Off e_phoff;                /* Program header table file offset */
  Elf32_Off e_shoff;                /* Section header table file offset */
  Elf32_Word e_flags;               /* Processor-specific flags */
  Elf32_Half e_ehsize;              /* ELF header size in bytes */
  Elf32_Half e_phentsize;           /* Program header table entry size */
  Elf32_Half e_phnum;               /* Program header table entry count */
  Elf32_Half e_shentsize;           /* Section header table entry size */
  Elf32_Half e_shnum;               /* Section header table entry count */
  Elf32_Half e_shstrndx;            /* Section header string table index */
} Elf32_Ehdr;
成员 readelf输出结果与含义
e_ident 包括:Magic Class Data Version OS/ABI ABI Version
e_type Type ELF文件类型
e_machine MachineELF文件的CPU平台属性。相关常量以EM_开头
e_version VersionELF版本号。一般为常数1
e_entry Entry point address 入口地址,规定ELF程序的入口虚拟地址,操作系统在加载完该程序后从这个地址开始执行的指令。可重定位文件一般没有入口地址,则这个值为0
e_phoff Start of program headers 略过
e_shoff Start of section headers段表在文件中的偏移,上面的例子里这个值是832,也就是段表从文件的第833个字节开始
e_flags Flags ELF标志位,用来标识平台相关属性
e_ehsize Size of this header ELF头文件本身的大小
e_phentsize Size of program headers略过
e_phnum Number of program headers略过
e_shentsize Size of section headers段表描述符的大小,一般为sizeof(Elf32_Shdr)
e_shnum Number of section headers段表描述符数量。这个值等于ELF文件中拥有的段的数量
e_shstrndx Section header string table index段表字符串表所在的段在段表中的下标

ELF魔数

         1  2  3  4  5  6  7  8  9  10 11 12 13 14 15 16
         ------------------------------------------------
Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
  • 最开始的4个字节是所有ELF文件都必须相同的标识码。分别对应ASCII字符里面的DEL E L F
  • 第5个字节用来标识ELF的文件类,0x01表示32位的,0x02标识64位。
  • 第6个字是字节序。0无效格式,1小端,2大端
  • 第7个字节规定ELF文件的主版本号,一般为1
  • 后面的9个字节ELF标准中没有定义,一般全为0。有些平台会使用这9个字节作为扩展标志。

文件类型

/* Legal values for e_type (object file type).  */

#define ET_NONE 0        /* No file type */
#define ET_REL 1         /* Relocatable file */
#define ET_EXEC 2        /* Executable file */
#define ET_DYN 3         /* Shared object file */
#define ET_CORE 4        /* Core file */
#define ET_NUM 5         /* Number of defined types */
#define ET_LOOS 0xfe00   /* OS-specific range start */
#define ET_HIOS 0xfeff   /* OS-specific range end */
#define ET_LOPROC 0xff00 /* Processor-specific range start */
#define ET_HIPROC 0xffff /* Processor-specific range end */

机器类型
ELF文件格式被设计成可以在多个平台下使用。这并不是表示同一个ELF文件可以在不同的平台下使用,而是表示不同平台下的ELF文件都遵循同一套ELF标准。

常量 含义
EM_M32 1 AT&T WE 32100
EM_SPARC 2 SPARC
EM_386 3 Intel x86
EM_68K 4 Motorola 68000
EM_88K 5 Motorola 88000
EM_860 6 Intel 80860

4.2 段表

段表描述了ELF的各个段的信息,比如每个段的段名、段的长度、在文件中的偏移量、读写权限及段的其他属性。段表在ELF、文件中的位置有ELF文件头的e_shoff

$ readelf -S SimpleSection.o
There are 13 section headers, starting at offset 0x340:

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .text             PROGBITS        00000000 000034 000062 00  AX  0   0  1
  [ 2] .rel.text         REL             00000000 0002a8 000028 08   I 11   1  4
  [ 3] .data             PROGBITS        00000000 000098 000008 00  WA  0   0  4
  [ 4] .bss              NOBITS          00000000 0000a0 000004 00  WA  0   0  4
  [ 5] .rodata           PROGBITS        00000000 0000a0 000004 00   A  0   0  1
  [ 6] .comment          PROGBITS        00000000 0000a4 000036 01  MS  0   0  1
  [ 7] .note.GNU-stack   PROGBITS        00000000 0000da 000000 00      0   0  1
  [ 8] .eh_frame         PROGBITS        00000000 0000dc 000064 00   A  0   0  4
  [ 9] .rel.eh_frame     REL             00000000 0002d0 000010 08   I 11   8  4
  [10] .shstrtab         STRTAB          00000000 0002e0 00005f 00      0   0  1
  [11] .symtab           SYMTAB          00000000 000140 000100 10     12  11  4
  [12] .strtab           STRTAB          00000000 000240 000066 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings)
  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

段表的结构是一个以Elf32_Shdr结构体为元素的数组。数组元素的个数等于段的个数,每个Elf32_Shdr结构体对应一个段。Elf32_Shdr又被称为段描述符(Section Descriptor)。第一个类型为NULL表示无效。

typedef struct
{
  Elf32_Word sh_name;      /* Section name (string tbl index) */
  Elf32_Word sh_type;      /* Section type */
  Elf32_Word sh_flags;     /* Section flags */
  Elf32_Addr sh_addr;      /* Section virtual addr at execution */
  Elf32_Off sh_offset;     /* Section file offset */
  Elf32_Word sh_size;      /* Section size in bytes */
  Elf32_Word sh_link;      /* Link to another section */
  Elf32_Word sh_info;      /* Additional section information */
  Elf32_Word sh_addralign; /* Section alignment */
  Elf32_Word sh_entsize;   /* Entry size if section holds table */
} Elf32_Shdr;

段的类型(Sh_type)
段的名字只有在链接和编译过程中有意义,但是它不能真正的表示段的类型。我们也可以将一个数据段命名为.text。对于编译器和链接器来说,主要决定段的属性的是段的类型sh_type和段的标志位sh_flags。段的类型相关常量以SHT_开头。

/* Legal values for sh_type (section type).  */

#define SHT_NULL 0                    /* Section header table entry unused */
#define SHT_PROGBITS 1                /* Program data */
#define SHT_SYMTAB 2                  /* Symbol table */
#define SHT_STRTAB 3                  /* String table */
#define SHT_RELA 4                    /* Relocation entries with addends */
#define SHT_HASH 5                    /* Symbol hash table */
#define SHT_DYNAMIC 6                 /* Dynamic linking information */
#define SHT_NOTE 7                    /* Notes */
#define SHT_NOBITS 8                  /* Program space with no data (bss) */
#define SHT_REL 9                     /* Relocation entries, no addends */
#define SHT_SHLIB 10                  /* Reserved */
#define SHT_DYNSYM 11                 /* Dynamic linker symbol table */
#define SHT_INIT_ARRAY 14             /* Array of constructors */
#define SHT_FINI_ARRAY 15             /* Array of destructors */
#define SHT_PREINIT_ARRAY 16          /* Array of pre-constructors */
#define SHT_GROUP 17                  /* Section group */
#define SHT_SYMTAB_SHNDX 18           /* Extended section indeces */
#define SHT_NUM 19                    /* Number of defined types.  */
#define SHT_LOOS 0x60000000           /* Start OS-specific.  */
#define SHT_GNU_ATTRIBUTES 0x6ffffff5 /* Object attributes.  */
#define SHT_GNU_HASH 0x6ffffff6       /* GNU-style hash table.  */
#define SHT_GNU_LIBLIST 0x6ffffff7    /* Prelink library list */
#define SHT_CHECKSUM 0x6ffffff8       /* Checksum for DSO content.  */
#define SHT_LOSUNW 0x6ffffffa         /* Sun-specific low bound.  */
#define SHT_SUNW_move 0x6ffffffa
#define SHT_SUNW_COMDAT 0x6ffffffb
#define SHT_SUNW_syminfo 0x6ffffffc
#define SHT_GNU_verdef 0x6ffffffd  /* Version definition section.  */
#define SHT_GNU_verneed 0x6ffffffe /* Version needs section.  */
#define SHT_GNU_versym 0x6fffffff  /* Version symbol table.  */
#define SHT_HISUNW 0x6fffffff      /* Sun-specific high bound.  */
#define SHT_HIOS 0x6fffffff        /* End OS-specific type */
#define SHT_LOPROC 0x70000000      /* Start of processor-specific */
#define SHT_HIPROC 0x7fffffff      /* End of processor-specific */
#define SHT_LOUSER 0x80000000      /* Start of application-specific */
#define SHT_HIUSER 0x8fffffff      /* End of application-specific */

段的标志位(sh_flag)

/* Legal values for sh_flags (section flags).  */

#define SHF_WRITE (1 << 0)            /* Writable */
#define SHF_ALLOC (1 << 1)            /* Occupies memory during execution */
#define SHF_EXECINSTR (1 << 2)        /* Executable */
#define SHF_MERGE (1 << 4)            /* Might be merged */
#define SHF_STRINGS (1 << 5)          /* Contains nul-terminated strings */
#define SHF_INFO_LINK (1 << 6)        /* `sh_info' contains SHT index */
#define SHF_LINK_ORDER (1 << 7)       /* Preserve order after combining */
#define SHF_OS_NONCONFORMING (1 << 8) /* Non-standard OS specific handlin required */
#define SHF_GROUP (1 << 9)            /* Section is member of a group.  */
#define SHF_TLS (1 << 10)             /* Section hold thread-local data.  */
#define SHF_COMPRESSED (1 << 11)      /* Section with compressed data. */
#define SHF_MASKOS 0x0ff00000         /* OS-specific.  */
#define SHF_MASKPROC 0xf0000000       /* Processor-specific */
#define SHF_ORDERED (1 << 30)         /* Special ordering requirement (Solaris).  */
#define SHF_EXCLUDE (1U << 31)        /* Section is excluded unless referenced or allocated (Solaris).*/

对于系统保留段,下面列举了它们的属性:

Name sh_type sh_flag
.bss SHT_NOBITS SHF_ALLOC + SHF_WRITE
.comment SHT_PROGBITS none
.data SHT_PROGBITS SHF_ALLOC+SHF_WRITE
.data1 SHT_PROGBITS SHF_ALLOC+SHF_WRITE
.debug SHT_PROGBITS none
.dynamic SHT_DYNAMIC SHF_ALLOC+SHF_WRITE。在有些系统下.dynamic段可能是只读的,所以没有SHF_WRITE
.hash SHT_HASH SHF_ALLOC
.line SHT_PROGBITS none
.note SHT_NOTE none
.rodata SHT_PROGBITS SHF_ALLOC
.rodata1 SHT_PROGBITS SHF_ALLOC
.shstrtab SHT_STRTAB none
.strtab SHT_STRTAB 如果该ELF文件中有可能装载的段须要用到该字符串表,那么该字符串表也将被装载到进程空间,那么有SHF_ALLOC标志位
.symtab SHT_SYMTAB 同字符串表
.text SHT_PROGBITS SHF_ALLOC+SHF_EXECINSTR

段的链接信息

sh_type sh_link sh_info
SHT_DYNAMIC 该段所使用的字符串表在段表中的下标 0
SHT_HASH 该段所使用的符号表在段表中的下标 0
SHT_REL SHT_RELA 该段所使用的相应符号表在段表中的下标 该重定位表所作用的段在段表中的下标
SHT_SYMTAB SHT_DYNSYM 操作系统相关 操作系统相关
other SHN_UNDEF 0

4.3 重定位表

(见静态链接过程)

4.4 字符串表

常见的做法是把字符串集中起来存放到一个表,然后使用字符串在表中的偏移来引用字符串。

偏移 0 1 2 3 4 5 6 7 8 9
+0 \0 h e l l o w o r l
+10 d \0 M y v a r i a b
+20 l e \0
偏移 字符串
0 空字符串
1 helloworld
6 world
12 Myvariable

一般字符串表在ELF文件中也以段的形式保存,常见的段名为.strtab.shstrtab。这两个字符串表分别为字符串表(String Table)和段字符串表(Section Header String Table)。字符串表用来保存普通的字符串,比如符号的名字;段表字符串表用来保存段表中用到的字符串,最常见的就是段名。


我们在前面提到过,e_shstrndxElf32_Ehdr的最后一个成员,它是Section Header string Table index的缩写。我们知道段表字符串本身也是ELF文件中的一个普通的段,知道它的名字往往叫做.shstrtab。那么这个e_shstrndx就表示.shstrtab在段表中的下标,即段表字符串在段表中的下标。我们可以得出结论,只有分析ELF文件头,就可以得到段表和段表字符串表的位置,从而解析整个ELF文件。

5 链接的接口----符号

在链接中,目标文件直接相互拼合实际上是目标文件之间对地址的引用,即对函数和变量的地址的引用。比如目标文件B要用到目标文件A中的函数foo,那么我们就称目标文件A定义了函数foo,称目标文件B引用了目标文件A中的函数foo。这两个概念同意适用于变量。每个函数或变量都有自己独特的名字,才能避免链接过程中不同变量和函数之间的混淆。在链接中,我们将函数和变量统称为符号(Symbol),函数名和变量名就是符号名(Symbol Name)

链接过程中很关键的一部分就是符号的管理,每一个目标文件都会有一个相应的符号表(Symbol Table),这个表里面记录了目标文件中所用到的所有符号。每个定义的符号有一个对应的值,叫做符号值(Symbol Value),对于变量和函数来说,符号值就是它们的地址。


符号分类

  • 定义在本目标文件的全局符号,可以被其他文件目标文件引用。比如SimpleSection.o里面的func1 main global_init_var
  • 在本目标文件中引用的全局符号,却没有定义在本目标文件,这一般叫外部符号(External Symbol),也就是符号引用。比如SimpleSection.o里面的printf
  • 段名,这种符号往往有编译器产生,它的值就是该段的起始地址。比如SimpleSection.o里面的.text .data
  • 局部符号,这类符号只在编译单元内部可见。比如SimpleSection.o里面的static_varstatic_var2
  • 行号信息,即目标文件指令与源代码中代码行的对于关系。

查看符号:

$ nm SimpleSection.o
00000000 T func1
00000000 D global_init_var
00000004 C global_uninit_var
0000001c T main
         U printf
00000004 d static_var.1488
00000000 b static_var2.1489

5.1 ELF符号表结构

符号表的段名为.symbol。它的结构就是一个Elf32_Sym数组,每个元素对于一个符号。第一个元素为无效或者没定义的符号。

/* Symbol table entry.  */

typedef struct
{
  Elf32_Word st_name;     /* Symbol name (string tbl index) */
  Elf32_Addr st_value;    /* Symbol value */
  Elf32_Word st_size;     /* Symbol size */
  unsigned char st_info;  /* Symbol type and binding */
  unsigned char st_other; /* Symbol visibility */
  Elf32_Section st_shndx; /* Section index */
} Elf32_Sym;

符号类型和绑定信息(st_info)
该成员低4位表示符号的类型Symbol Type,高28为表示符号绑定信息Symbol Binding

/* Legal values for ST_BIND subfield of st_info (symbol binding).  */

#define STB_LOCAL 0       /* Local symbol */
#define STB_GLOBAL 1      /* Global symbol */
#define STB_WEAK 2        /* Weak symbol */
#define STB_NUM 3         /* Number of defined types.  */
#define STB_LOOS 10       /* Start of OS-specific */
#define STB_GNU_UNIQUE 10 /* Unique symbol.  */
#define STB_HIOS 12       /* End of OS-specific */
#define STB_LOPROC 13     /* Start of processor-specific */
#define STB_HIPROC 15     /* End of processor-specific */

/* Legal values for ST_TYPE subfield of st_info (symbol type).  */

#define STT_NOTYPE 0     /* Symbol type is unspecified */
#define STT_OBJECT 1     /* Symbol is a data object */
#define STT_FUNC 2       /* Symbol is a code object */
#define STT_SECTION 3    /* Symbol associated with a section */
#define STT_FILE 4       /* Symbol's name is file name */
#define STT_COMMON 5     /* Symbol is a common data object */
#define STT_TLS 6        /* Symbol is thread-local data object*/
#define STT_NUM 7        /* Number of defined types.  */
#define STT_LOOS 10      /* Start of OS-specific */
#define STT_GNU_IFUNC 10 /* Symbol is indirect code object */
#define STT_HIOS 12      /* End of OS-specific */
#define STT_LOPROC 13    /* Start of processor-specific */
#define STT_HIPROC 15    /* End of processor-specific */

符号所在段(st_shndx)

/* Special section indices.  */

#define SHN_UNDEF 0          /* Undefined section */
#define SHN_LORESERVE 0xff00 /* Start of reserved indices */
#define SHN_LOPROC 0xff00    /* Start of processor-specific */
#define SHN_BEFORE 0xff00    /* Order section before all others (Solaris).  */
#define SHN_AFTER 0xff01     /* Order section after all others (Solaris).  */
#define SHN_HIPROC 0xff1f    /* End of processor-specific */
#define SHN_LOOS 0xff20      /* Start of OS-specific */
#define SHN_HIOS 0xff3f      /* End of OS-specific */
#define SHN_ABS 0xfff1       /* Associated symbol is absolute */
#define SHN_COMMON 0xfff2    /* Associated symbol is common */
#define SHN_XINDEX 0xffff    /* Index is in extra table.  */
#define SHN_HIRESERVE 0xffff /* End of reserved indices */

符号值(st_value)

  • 在目标文件中,如果是符号的定义并且该符号不是COMMON块类型的(即st_shndex不为SHN_COMMON),则st_value表示该符号在段中的偏移。即符号所对应的函数或变量位于st_shndex指定的段,偏移st_value的位置。这也是目标文件中定义全局变量的符号的最常见情况。比如,SimpleSection.o中的func1 main global_init_var
  • 在目标文件中,如果符号是COMMON块类型,则st_value表示该符号的对其属性。比如SimpleSection.o中的global_uninit_var
  • 在可执行文件中,st_value表示符号的虚拟地址。这个虚拟地址对于动态链接器来说十分有用。

分析各个符号在符号表中的状态:

$ readelf -s SimpleSection.o

Symbol table '.symtab' contains 16 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
     0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 00000000     0 FILE    LOCAL  DEFAULT  ABS SimpleSection.c
     2: 00000000     0 SECTION LOCAL  DEFAULT    1
     3: 00000000     0 SECTION LOCAL  DEFAULT    3
     4: 00000000     0 SECTION LOCAL  DEFAULT    4
     5: 00000000     0 SECTION LOCAL  DEFAULT    5
     6: 00000004     4 OBJECT  LOCAL  DEFAULT    3 static_var.1488
     7: 00000000     4 OBJECT  LOCAL  DEFAULT    4 static_var2.1489
     8: 00000000     0 SECTION LOCAL  DEFAULT    7
     9: 00000000     0 SECTION LOCAL  DEFAULT    8
    10: 00000000     0 SECTION LOCAL  DEFAULT    6
    11: 00000000     4 OBJECT  GLOBAL DEFAULT    3 global_init_var
    12: 00000004     4 OBJECT  GLOBAL DEFAULT  COM global_uninit_var
    13: 00000000    28 FUNC    GLOBAL DEFAULT    1 func1
    14: 00000000     0 NOTYPE  GLOBAL DEFAULT  UND printf
    15: 0000001c    70 FUNC    GLOBAL DEFAULT    1 main
$ readelf -a SimpleSection.o
.......

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .text             PROGBITS        00000000 000034 000062 00  AX  0   0  1
  [ 2] .rel.text         REL             00000000 0002a8 000028 08   I 11   1  4
  [ 3] .data             PROGBITS        00000000 000098 000008 00  WA  0   0  4
  [ 4] .bss              NOBITS          00000000 0000a0 000004 00  WA  0   0  4
  [ 5] .rodata           PROGBITS        00000000 0000a0 000004 00   A  0   0  1
  [ 6] .comment          PROGBITS        00000000 0000a4 000036 01  MS  0   0  1
  [ 7] .note.GNU-stack   PROGBITS        00000000 0000da 000000 00      0   0  1
  [ 8] .eh_frame         PROGBITS        00000000 0000dc 000064 00   A  0   0  4
  [ 9] .rel.eh_frame     REL             00000000 0002d0 000010 08   I 11   8  4
  [10] .shstrtab         STRTAB          00000000 0002e0 00005f 00      0   0  1
  [11] .symtab           SYMTAB          00000000 000140 000100 10     12  11  4
  [12] .strtab           STRTAB          00000000 000240 000066 00      0   0  1
.......
  • func1main函数都是定义在SimpleSection.c里面,它们所在的位置都为代码段,所以Ndx为1,即SimpleSection.o里面,.text段的下标为1。它们是函数,所以类型是SHT_FUNC;它们是全局可见的,所以是STB_GLOBALSize表示函数指令所占的字节数;Value表示函数相对于代码段起始位置的偏移量。
  • printf这个符号,该符号在SimpleSection.c里面被引用,但是没有被定义。所以它的NdxSHN_UNDEF
  • global_init_var是已初始化的全局变量,它被定义在.data段,即下标为3。
  • global_uninit_var是未初始化的全局变量,它是一个SHN_COMMON类型的符号,它本身并没有存在于.bss段。
  • static_var.1488 static_var2.1489是两个静态变量,它们的绑定属性是STB_LOCAL,即只是在编译单元内部可见。这里它们被符号修饰了。
  • 对于那些STT_SECTION类型的符号,它们表示下标为Ndx的段的段名。它们的符号名没有显示,其实它们的符号名即它们的段名。比如2号的符号的Ndx为1,那么它即表示.text段的段名,该符号的符号名应该就是.text
  • SimpleSection.c这个符号表示编译单元的源文件名。

5.2 特殊符号

当我们使用ld作为链接器来链接生产可执行文件时,它会为我们定义很多特殊的符号,这些符号并没有在你的程序中定义,但是你可以直接声明并且引用它,称为特殊符号。链接器会在将程序最终链接成可执行文件的时候将其解析成正确的值,注意,只有使用ld链接生成最终可执行文件的时候这些符号才会存在。代表性的有:

  • _executable_start,该符号为程序起始地址,注意,不是入口地址,是程序的最开始的地址。
  • __etext_etextetext,该符号为代码段结束地址,即代码段最末尾的地址。
  • _edataedata,该符号为数据段结束地址,即数据段最末尾的地址。
  • _endend,该符号为程序结束地址。
/*
 * gcc -m32 SpecialSymbol.c -o SpecialSymbol 编译不成功,提示:
 * fatal error: sys/cdefs.h: No such file or directory #include 
 * 
 * 解决(https://stackoverflow.com/questions/23498237/compile-program-for-32bit-on-64bit-linux-os-causes-fatal-error):
 *      sudo apt-get install libx32gcc-4.8-dev libc6-dev-i386
 */
#include 

extern char __executable_start[];
extern char etext[], _etext[], __etext[];
extern char edata[], _edata[];
extern char end[], _end[];

int main()
{
    printf("Executable Start %X\n", __executable_start);
    printf("Text End %X %X %X\n", etext, _etext, __etext);
    printf("Data End %X %X\n", edata, _edata);
    printf("Executable End %X %X\n", end, _end);

    return 0;
}
$ gcc -m32 SpecialSymbol.c -o SpecialSymbol
$ ./SpecialSymbol
Executable Start 8048000
Text End 8048508 8048508 8048508
Data End 804A01C 804A01C
Executable End 804A020 804A020

5.3 符号修饰与函数签名

5.4 extern “C”

5.5 弱符号与强符号

多个目标文件中含有相同名字全局符号的定义,那么这些目标文件链接的时候将或出现符号重复定义的错误。

对于C/C++语言来说,编译器默认函数和初始化的全局变量为强符号,为初始化的全局变量为弱符号。我们可以通过GCC的__attribute__((weak))来定义任何一个强符号为弱符号。注意,强符号和弱符号都是针对定义来说,不是针对符号的引用。

extern int ext;

int week;
int strong = 1;
__attribute__((weak)) weak2 = 2;

int main()
{
    return 0;
}

上面这段代码中,weakweak2是弱符号,strongmain是强符号,而ext即非强符号也弱符号,因为它是一个外部变量的引用。针对强弱符号的概念,链接器会根据如下规则处理与选择被多次定义的全局符号:

  • 规则1:不允许强符号被定义多次(即不同的目标文件中不能有同名的强符号);如果存在,则链接器报符号重复定义错误。
  • 规则2:如果一个符号在某个目标中都是强符号,在其他文件中都是弱符号,那么选择强符号。
  • 规则3:如果一个符号在所有目标文件中都是弱符号,那么选择其中占用空间最大的一个。

弱引用和强引用
目前我们所看到的对外部目标文件的符号引用在目标文件被最终链接成可执行文件时,它们须要被正确决议,如果没有找到该符号的定义,链接器就会报符号未定义错我,这种被称为强引用。与之相对应的有一种弱引用。在处理弱引用时,如果该符号有定义,则链接器将该符号的引用决议;如果该符号未定义,则链接器对于该引用不报错。

一般对于未定义的弱引用,链接器默认其为0,或者是一个特殊值,以便于程序代码能够识别。弱引用和弱符号主要用于库的链接过程。


/*
 * Weakref1.c
 */
__attribute__((weak)) void foo();

int main()
{
    foo();
}
$ gcc -m32 Weakref1.c -o Weakref1
$ ./Weakref1
Segmentation fault (core dumped)

可以看到,编译没报错,执行时报错。

/*
 * Weakref2.c
 */
void foo() __attribute__((weak));

int main()
{
    if (foo)
        foo();
}
$ gcc -m32 Weakref2.c -o Weakref2
$ ./Weakref2

编译及运行都没有报错。


这种弱符号和弱引用对于库来说十分有用。比如库中定义的弱符号可以被用户定义的强符号所覆盖,从而使得程序可以使用自定义版本的库函数;或者程序可以对某些扩展功能模块的引用定义了弱引用,当我们将扩展模块与程序链接在一起时,功能模块就可以正常使用;
如果我们去掉了某些功能模块,那么程序也可以正常链接,只是缺少了相应的功能。

6 调试信息

我们在GCC编译时加上-g参数,则会在产生的目标文件上加上调试信息。
在Linux下,我们可以使用strip命令来去掉ELF文件中的调试信息:

$ strip foo

你可能感兴趣的:(程序员的自我修养)