计算机系统篇之链接(2):目标文件

Author: stormQ

Created: Saturday, 21. December 2019 11:08AM

Last Modified: Tuesday, 26. January 2021 11:32PM



摘要

本文描述了目标文件的基础知识,其中详细介绍了ELF-64目标文件格式,并提供了 Linux 系统中目标文件处理工具——readelfobjdump的常见用法。

目标文件类型

从技术角度来看,目标文件就是一个字节序列(Technically, object file is a sequence of bytes)。

目标文件可以分为三类:可重定位目标文件、可执行目标文件和可共享目标文件(可共享目标文件是一种特殊的可重定位目标文件)。

三者之间的关系:可重定位目标文件和可共享目标文件可用于生成可执行目标文件;可重定位目标文件可用于生成可共享目标文件。

目标文件类型 生成者(特指直接参与者) 是否可以直接加载到内存中执行 Linux 中的目标文件后缀(习惯上)
可重定位目标文件 汇编器 不可以 .o
可执行目标文件 链接器 可以 无后缀
可共享目标文件 链接器 不可以 .so

注:

注意: CS:APP(Third Edition) 7.3 节中所描述的是,可共享目标文件的生成者为编译器和汇编器。其原文为:

Compilers and assemblers generate relocatable object files (including shared object files). Linkers generate
executable object files.

但经过笔者实际验证(验证方法见上一篇:《计算机系统篇之链接(1):gcc / g++的编译流程》),链接器参与了生成可共享目标文件的过程。这一点需要注意。

1) 如何生成可重定位目标文件(On X86-64 Linux):

$ g++ -c main.cpp -o main.o
# 或 g++ -c main.cpp
# 这两个命令都会生成名称为 main.o 的可重定位目标文件

注:-c选项表示只执行编译和汇编过程,但不执行链接过程(Compile or assemble the source files, but do not link.)。

查看 main.cpp 的内容:

$ cat main.cpp
int main()
{
    return 0;
}

2) 如何生成可执行目标文件(On X86-64 Linux):

# 默认生成的可执行目标文件为 a.out
$ g++ main.cpp
# 指定生成的可执行目标文件为 main
$ g++ main.cpp -o main

3) 如何生成可共享目标文件(On X86-64 Linux):

# 默认生成的可共享目标文件为 a.out
$ g++ -shared -fPIC sum.cpp
# 指定生成的可共享目标文件为 sum.so
$ g++ -shared -fPIC sum.cpp -o sum.so

注:

注意: 含有main函数的源文件不能用于生成可共享目标文件,但可用于生成可重定位目标文件(即 .o 文件)。

查看 sum.cpp 的内容:

$ cat sum.cpp 
int sum(int a, int b)
{
  return a + b;
}

目标文件格式

目标文件格式是指目标文件的组织方式。目标文件格式因系统而异。

系统 目标文件格式
Windows PE(Portable Executable)
Mac OS-X Mach-O(Mach Object)
X86-64 Linux ELF-64(Executable and Linkable Format)
aarch64 Linux ELF-64

注:aarch64 表示 ARM 64-bit 系统。

1) 如何生成目标文件格式为PE的可执行目标文件(On X86-64 Linux):

$ /usr/bin/x86_64-w64-mingw32-g++ -o main_w64.exe main.cpp

在 Windows 64-bit 系统上的命令行窗口中执行 main_w64.exe 程序:

C:\Users\x\Desktop>main_w64.exe
g_val_1=0x0, g_val_2=0x10

查看 main.cpp 的内容:

$ cat main.cpp 
#include <stdio.h>

int g_val_1;
int g_val_2 = 16;
const int g_val_3 = 2;

int main()
{
  printf("g_val_1=0x%x, g_val_2=0x%x\n", g_val_1, g_val_2);
  return 0;
}

注:要在 X86-64 Linux 系统上生成在 Windows 64-bit 系统上运行的可执行目标文件,需要使用交叉编译器,比如:g++-mingw-w64(GNU C++ compiler for MinGW-w64)。安装命令为sudo apt-get install g++-mingw-w64

2) 如何生成目标文件格式为ELF-64的可重定位目标文件(On X86-64 Linux):

$ g++ -c sum.cpp -o sum.o

查看 sum.cpp 的内容:

$ cat sum.cpp
extern int g_val;
int g_val_1 = 0;
int g_val_2 = 1;
int g_val_3;

int sum(int a, int b)
{
  static int val_1;
  static int val_2 = 0;
  static int val_3 = 1;
  static int val_4 = 0;
  static int val_5 = 2;
  const static int val_6 = 0;
  return a + b;
}

注:在 X86-64 Linux 上,g++ 默认的目标文件格式为 ELF-64(对应的缺省编译选项为-m64)。如果要在 X86-64 Linux 上使用 g++ 生成 ELF-32 的目标文件格式,需要添加-m32选项,如下所示:

$ g++ -c sum.cpp -o sum_elf32.o -m32
$ readelf -h sum_elf32.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:          740 (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:         12
  Section header string table index9

从上面的打印结果中可以看出,可重定位目标文件 sum_elf32.o 的目标文件格式为 ELF32。


可重定位目标文件(ELF-64 格式)

典型的 ELF-64 可重定位目标文件格式:

组成部分 描述 如何查看
ELF header 描述生成该目标文件的系统的字大小和字节顺序、目标文件类型、机器类型等信息。
  • readelf -h <object file>
  • .text 已编译程序的机器代码。
  • 方式1:objdump -d <object file>(只输出汇编,输出内容包括:.init、.plt、.plt.got、.text、.fini 等 sections)
  • 方式2:objdump -S <object file>(输出汇编和对应的源码。需要编译时加 -g 选项,才会打印源码。)
  • 方式3:objdump -d --section .text sum.so(只输出汇编,且只打印 .text section)
  • .rodata 只读数据,比如:常量字符串、带 const 修饰的全局变量和静态变量等。
    .data 已初始化的且初始值非0的全局变量和静态变量。
    .bss 未初始化的或初始值为0的全局变量和静态变量。
    .symtab 一个符号表,它存放着在目标文件中定义和引用的函数和全局变量、静态变量的信息。
  • readelf -s <object file>(注:删除目标文件中 .symtab section 的命令为:strip <object file>
  • .rel<name> <name> section 的重定位信息。
  • readelf -r <object file>
  • .debug 一个调试符号表,其条目是程序中定义的局部变量和类型定义(typedefs),程序中定义和引用的全局变量,以及原始的 C 源文件。(只有带 -g 编译时才会产生)
  • 方式1:readelf --debug-dump <object file>
  • 方式2:objdump -g <object file>
  • (注:删除目标文件中 .debug 等与调试相关 sections 的命令为:strip --strip-debug <object file>(只删除调试信息相关的 sections)或 strip <object file>(除调试信息相关的 sections 以外,还删除 .symtab 和 .strtab) )
    .comment 版本控制信息。
  • readelf -p .comment <object file>
  • .shstrtab 一个字符串表,其内容包括所有 section 的名称。
  • readelf -p .shstrtab <object file>
  • (注:不能通过 strip 命令删除 .shstrtab section )
    .strtab 一个字符串表,其内容包括所有符号的名称。
  • readelf -p .strtab <object file> (注:删除目标文件中 .strtab section 的命令为:strip <object file>
  • Section header table 描述目标文件的所有节(sections)的位置和大小等信息。
  • readelf -S <object file>
  • 注:

    注意: CS:APP(Third Edition) 7.4 节中所描述的是,节头表名称包含在.strtabsection 中。其原文为:

    .strtab: A string table for the symbol tables in the .symtab and .debug sections, and for the section
    names in the section headers.

    但经过笔者实际验证,在 X86-64 Linux(Ubuntu 20.04)中,节头表名称包含在.shstrtab而不是.strtabsection 中。这一点需要注意。


    可执行目标文件(ELF-64 格式)

    在可执行目标文件和可共享目标文件中,section 被分组成 segment 以供加载。每个 segment 中包含的 sections 是连续的。

    典型的 ELF-64 可执行目标文件格式:

    组成部分 描述 如何查看 属于哪个 Segment Segment 的访问权限 程序执行时是否加载到内存
    ELF header 描述生成该目标文件的系统的字大小和字节顺序、目标文件类型、机器类型等信息。
  • readelf -h <object file>
  • The First Segment 只读
    Program header table 描述可执行目标文件中所有段(Segments)的位置和大小等信息。
  • 方式1:readelf -l <object file>
  • 方式2:objdump -p <object file>
  • The First Segment 只读
    .interp 该 section 中保存了可执行目标文件所需要的动态链接器的路径。
  • 方式1:readelf -p .interp <object file>
  • 方式2:readelf -l <object file> | grep interpreter
  • 方式3:objdump -s --section .interp <object file>
  • The First Segment 只读
    .dynsym 包含需要动态链接的符号
  • 方式1:readelf --dyn-syms <object file>
  • 方式2:readelf -s <object file>
  • The First Segment 只读
    .dynstr 动态链接符号表,仅包括需要动态链接的符号名称
  • readelf -p .dynstr <object file>
  • The First Segment 只读
    .rela.dyn 动态链接中的数据重定位表。类似于静态链接中的 .rela.data。
  • readelf -r <object file>
  • The First Segment 只读
    .rela.plt 动态链接中的代码重定位表。类似于静态链接中的 .rela.text。
  • readelf -r <object file>
  • The First Segment 只读
    .init 该 section 中定义了一个名称为 _init 的函数,会被程序初始化代码调用。 objdump -d <object file> Code Segment( The Second Segment) 可读,可执行
    .text 已编译程序的机器代码。
  • 方式1:objdump -d <object file>(只输出汇编)
  • 方式2:objdump -S <object file>(输出汇编和对应的源码。需要编译时加-g选项,才会打印源码。)
  • 方式3:objdump -d --section .text sum.so(只输出汇编,且只打印 .text section)
  • Code Segment 可读,可执行
    .rodata 只读数据,比如:常量字符串、带 const 修饰的全局变量和静态变量等。 The Third Segment 只读
    .dynamic 该 section 中保存了动态链接的基本信息。
  • 方式1:readelf -d <object file>
  • 方式2:objdump -p <object file>
  • The Fouth Segment 只读
    .data 已初始化的且初始值非0的全局变量和静态变量。 Data Segment( The Fifth Segment) 可读,可写
    .bss 未初始化的或初始值为0的全局变量和静态变量。 Data Segment 可读,可写
    .symtab 一个符号表,它存放着在目标文件中定义和引用的函数和全局变量、静态变量的信息。
  • readelf -s <object file>(注:删除目标文件中 .symtab section 的命令为:strip <object file>
  • 不属于任何一个 Segment
    .debug 一个调试符号表,其条目是程序中定义的局部变量和类型定义(typedefs),程序中定义和引用的全局变量,以及原始的 C 源文件。(只有带 -g 编译时才会产生)
  • 方式1:readelf --debug-dump <object file>
  • 方式2:objdump -g <object file>
  • 不属于任何一个 Segment
    .comment 版本控制信息。
  • readelf -p .comment <object file>
  • 不属于任何一个 Segment
    .shstrtab 一个字符串表,其内容包括所有 section 的名称。
  • readelf -p .shstrtab <object file>
  • 不属于任何一个 Segment
    .strtab 一个字符串表,其内容包括所有符号的名称。
  • readelf -p .strtab <object file> (注:删除目标文件中 .strtab section 的命令为:strip <object file>
  • 不属于任何一个 Segment
    Section header table 描述目标文件的所有节(sections)的位置和大小等信息。
  • readelf -S <object file>
  • 不属于任何一个 Segment

    注:

    注意: CS:APP(Third Edition) 7.8 节中所描述的是,ELF HeaderProgram Header Table.rodatasection 都位于Code Segment(代码段)中。但经过笔者实际验证,在 X86-64 Linux(Ubuntu 20.04)中,ELF HeaderProgram Header Table位于同一个段中(根据先后顺序,在这里称之为The First Segment),但不是与.text所在的代码段。另外,.rodatasection 所在的段(在这里称之为The Third Segment)也不是代码段。这两点需要注意。


    查看 ELF Header

    查看可重定位目标文件(ELF-64 格式)的ELF Header的内容:

    $ readelf -h sum.o
    ELF Header:
      Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
      Class:                             ELF64
      Data:                              2's complement, little endian
      Version:                           1 (current)
      OS/ABI:                            UNIX - System V
      ABI Version:                       0
      Type:                              REL (Relocatable file)
      Machine:                           Advanced Micro Devices X86-64
      Version:                           0x1
      Entry point address:               0x0
      Start of program headers:          0 (bytes into file)
      Start of section headers:          928 (bytes into file)
      Flags:                             0x0
      Size of this header:               64 (bytes)
      Size of program headers:           0 (bytes)
      Number of program headers:         0
      Size of section headers:           64 (bytes)
      Number of section headers:         12
      Section header string table index9

    注:


    查看 Section Header Table

    查看可重定位目标文件(ELF-64 格式)的Section Header Table的内容:

    $ readelf -S sum.o
    There are 12 section headers, starting at offset 0x3a0:

    Section Headers:
      [Nr] Name              Type             Address           Offset
           Size              EntSize          Flags  Link  Info  Align
      [ 0]                   NULL             0000000000000000  00000000
           0000000000000000  0000000000000000           0     0     0
      [ 1] .text             PROGBITS         0000000000000000  00000040
           0000000000000014  0000000000000000  AX       0     0     1
      [ 2] .data             PROGBITS         0000000000000000  00000054
           000000000000000c  0000000000000000  WA       0     0     4
      [ 3] .bss              NOBITS           0000000000000000  00000060
           0000000000000014  0000000000000000  WA       0     0     4
      [ 4] .rodata           PROGBITS         0000000000000000  00000060
           0000000000000004  0000000000000000   A       0     0     4
      [ 5] .comment          PROGBITS         0000000000000000  00000064
           0000000000000033  0000000000000001  MS       0     0     1
      [ 6] .note.GNU-stack   PROGBITS         0000000000000000  00000097
           0000000000000000  0000000000000000           0     0     1
      [ 7] .eh_frame         PROGBITS         0000000000000000  00000098
           0000000000000038  0000000000000000   A       0     0     8
      [ 8] .rela.eh_frame    RELA             0000000000000000  00000328
           0000000000000018  0000000000000018   I      10     7     8
      [ 9] .shstrtab         STRTAB           0000000000000000  00000340
           000000000000005c  0000000000000000           0     0     1
      [10] .symtab           SYMTAB           0000000000000000  000000d0
           00000000000001c8  0000000000000018          11    15     8
      [11] .strtab           STRTAB           0000000000000000  00000298
           0000000000000090  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)

    注:


    查看 .comment

    查看可重定位目标文件的.commentsection 的内容:

    $ readelf -p .comment sum.o

    String dump of section '.comment':
      [     1]  GCC: (Ubuntu 6.5.0-2ubuntu1~16.046.5.0 20181026

    注意: 这里.commentsection 的第一个字节的值为 0x00(二进制的零,\0),但不代表.commentsection 在其他类型的目标文件中第一个字节的值也为 0x00(\0)。比如:在可执行目标文件中该 section 的第一个字节的值不为 0x00。

    查看可执行目标文件main.commentsection 的内容:

    $ readelf -p .comment main

    String dump of section '.comment':
      [     0]  GCC: (Ubuntu 6.5.0-2ubuntu1~16.046.5.0 20181026

    查看 .shstrtab

    查看可重定位目标文件的.shstrtabsection 的内容:

    $ readelf -p .shstrtab sum.o

    String dump of section '.shstrtab':
      [     1]  .symtab
      [     9]  .strtab
      [    11]  .shstrtab
      [    1b]  .text
      [    21]  .data
      [    27]  .bss
      [    2c]  .rodata
      [    34]  .comment
      [    3d]  .note.GNU-stack
      [    4d]  .rela.eh_frame

    注:

    注意: 可重定位目标文件sum.o中,其.shstrtabsection 的内容未显式地包含.eh_framesection 的名称。从表面上看,这不符合“.shstrtabsection 的作用:一个字符串表,其内容包括所有 section 的名称”。实际上,.shstrtabsection 中隐式地包含.eh_framesection 的名称,即复用了.rela.eh_framesection 的后半部分的名称。

    验证过程:

    查看可重定位目标文件的.shstrtabsection 的内容(十六进制):

    $ readelf -x .shstrtab sum.o

    Hex dump of section '.shstrtab':
      0x00000000 002e7379 6d746162 002e7374 72746162 ..symtab..strtab
      0x00000010 002e7368 73747274 616200274657874 ..shstrtab..text
      0x00000020 002e6461 746100262737300 2e726f64 ..data..bss..rod
      0x00000030 61746100 2e636f6d 6d656e74 002e6e6f ata..comment..no
      0x00000040 74652e47 4e552d73 7461636002e7265 te.GNU-stack..re
      0x00000050 6c612e65 685f6672 616d6500          la.eh_frame.

    最左侧一列为地址,后面的为 .shstrtabsection 中的内容。从上面结果中可以看出:


    查看 .strtab

    查看可重定位目标文件的.strtabsection 的内容:

    $ readelf -p .strtab sum.o

    String dump of section '.strtab':
      [     1]  sum.cpp
      [     9]  _ZZ3sumiiE5val_1
      [    1a]  _ZZ3sumiiE5val_2
      [    2b]  _ZZ3sumiiE5val_3
      [    3c]  _ZZ3sumiiE5val_4
      [    4d]  _ZZ3sumiiE5val_5
      [    5e]  _ZZ3sumiiE5val_6
      [    6f]  g_val_1
      [    77]  g_val_2
      [    7f]  g_val_3
      [    87]  _Z3sumii

    注:


    查看 .symtab

    每个目标文件(包括可重定位目标文件、可共享目标文件和可执行目标文件)中都有一个符号表(.symtabsection),符号表中包含了被该目标文件定义和引用的符号的信息。

    符号表(.symtabsection)是由汇编器产生的,跟编译时是否添加-g选项无关。

    符号表中的符号可以分为三类:

    符号类型 定义者 可见性 哪些属于这类符号
    内部定义的全局符号(Global Symbols) 本目标文件 本目标文件和其他目标文件都可见 由本目标文件定义的非静态函数和非静态全局变量
    引用外部的全局符号(External Symbols) 其他目标文件 本目标文件和其他目标文件都可见 由本目标文件引用的但定义在其他目标文件中的非静态函数和非静态全局变量
    内部定义的局部符号(Local Symbols) 本目标文件 仅本目标文件可见 由本目标文件定义的静态函数、静态全局变量和静态局部变量

    注:严格地讲,静态局部变量不仅对于其他目标文件不可见,而且对于本目标文件的其他函数也不可见。

    查看 sum.cpp 的内容:

    $ cat sum.cpp 
    extern int g_val;
    int g_val_1 = 0;
    int g_val_2 = 1;

    int sum(int a, int b)
    {
      static int val_1;
      static int val_2 = 0;
      static int val_3 = 1;
      static int val_4 = 0;
      static int val_5 = 2;
      const static int val_6 = 0;
      return a + b;
    }

    查看 sum.o 的.symtabsection 的内容:

    $ readelf -s sum.o

    Symbol table '.symtab' contains 19 entries:
       Num:    Value          Size Type    Bind   Vis      Ndx Name
         00000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
         10000000000000000     0 FILE    LOCAL  DEFAULT  ABS sum.cpp
         20000000000000000     0 SECTION LOCAL  DEFAULT    1 
         30000000000000000     0 SECTION LOCAL  DEFAULT    3 
         40000000000000000     0 SECTION LOCAL  DEFAULT    4 
         50000000000000004     4 OBJECT  LOCAL  DEFAULT    4 _ZZ3sumiiE5val_1
         60000000000000008     4 OBJECT  LOCAL  DEFAULT    4 _ZZ3sumiiE5val_2
         70000000000000004     4 OBJECT  LOCAL  DEFAULT    3 _ZZ3sumiiE5val_3
         8000000000000000c     4 OBJECT  LOCAL  DEFAULT    4 _ZZ3sumiiE5val_4
         90000000000000008     4 OBJECT  LOCAL  DEFAULT    3 _ZZ3sumiiE5val_5
        100000000000000000     0 SECTION LOCAL  DEFAULT    5 
        110000000000000000     4 OBJECT  LOCAL  DEFAULT    5 _ZZ3sumiiE5val_6
        120000000000000000     0 SECTION LOCAL  DEFAULT    7 
        130000000000000000     0 SECTION LOCAL  DEFAULT    8 
        140000000000000000     0 SECTION LOCAL  DEFAULT    6 
        150000000000000000     4 OBJECT  GLOBAL DEFAULT    4 g_val_1
        160000000000000000     4 OBJECT  GLOBAL DEFAULT    3 g_val_2
        170000000000000000    28 FUNC    GLOBAL DEFAULT    1 _Z3sumii
        180000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND g_val

    注:

    另外,关于readelf输出结果的Name列的值,有两点需要注意

    注意: CS:APP(Third Edition) 7.5 节中所描述的是,伪节:ABSUNDCOM只可能出现在可重定位目标文件中,不会出现在可执行目标文件中。其原文为:

    Note that these pseudosections exist only in relocatable object files; they do not exist in executable object files.

    但经过实际验证,在动态链接的可执行目标文件中,伪节ABSUND可能出现。这一点需要注意。

    验证过程:

    1) 验证“在可重定位目标文件(不包括可共享目标文件)中,Value 列,表示符号在其 section 中的偏移量”

    150000000000000000     4 OBJECT  GLOBAL DEFAULT    4 g_val_1
     50000000000000004     4 OBJECT  LOCAL  DEFAULT    4 _ZZ3sumiiE5val_1
     60000000000000008     4 OBJECT  LOCAL  DEFAULT    4 _ZZ3sumiiE5val_2
     8000000000000000c     4 OBJECT  LOCAL  DEFAULT    4 _ZZ3sumiiE5val_4

    上述内容表示的含义:

    2) 验证“Ndx 列,表示符号所关联对象位于哪个 section”

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

    Section Headers:
      [Nr] Name              Type             Address           Offset
           Size              EntSize          Flags  Link  Info  Align
      [ 0]                   NULL             0000000000000000  00000000
           0000000000000000  0000000000000000           0     0     0
      [ 1] .text             PROGBITS         0000000000000000  00000040
           000000000000001c  0000000000000000  AX       0     0     1
      [ 2] .rela.text        RELA             0000000000000000  00000330
           0000000000000018  0000000000000018   I      11     1     8
      [ 3] .data             PROGBITS         0000000000000000  0000005c
           000000000000000c  0000000000000000  WA       0     0     4
      [ 4] .bss              NOBITS           0000000000000000  00000068
           0000000000000010  0000000000000000  WA       0     0     4
      [ 5] .rodata           PROGBITS         0000000000000000  00000068
           0000000000000004  0000000000000000   A       0     0     4
      [ 6] .comment          PROGBITS         0000000000000000  0000006c
           0000000000000033  0000000000000001  MS       0     0     1
      [ 7] .note.GNU-stack   PROGBITS         0000000000000000  0000009f
           0000000000000000  0000000000000000           0     0     1
      [ 8] .eh_frame         PROGBITS         0000000000000000  000000a0
           0000000000000038  0000000000000000   A       0     0     8
      [ 9] .rela.eh_frame    RELA             0000000000000000  00000348
           0000000000000018  0000000000000018   I      11     8     8
      [10] .shstrtab         STRTAB           0000000000000000  00000360
           0000000000000061  0000000000000000           0     0     1
      [11] .symtab           SYMTAB           0000000000000000  000000d8
           00000000000001c8  0000000000000018          12    15     8
      [12] .strtab           STRTAB           0000000000000000  000002a0
           000000000000008e  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)

    全局变量 g_val1,静态变量 val_1、val_2、val_4 要么未初始化,要么初始值为 0。所以,这些变量会存放到.bss section 中。.bss section 在 Section header table 中的索引为 4([Nr] 列),与Ndx列的值相同。从而,验证了“readelf -s 输出结果中的Ndx列即为上面结果中Nr列的值,表示符号所关联对象所在的 section”。

    3) 验证“Name 列,表示符号在.strtab section 中的名称”

    $ readelf sum.o -p .strtab

    String dump of section '.strtab':
      [     1]  sum.cpp
      [     9]  _ZZ3sumiiE5val_1
      [    1a]  _ZZ3sumiiE5val_2
      [    2b]  _ZZ3sumiiE5val_3
      [    3c]  _ZZ3sumiiE5val_4
      [    4d]  _ZZ3sumiiE5val_5
      [    5e]  _ZZ3sumiiE5val_6
      [    6f]  g_val_1
      [    77]  g_val_2
      [    7f]  _Z3sumii
      [    88]  g_val

    .strtab section 中可以看到符号名称。

    4) 验证“Size 列,表示符号所关联对象的大小”

    a)测试代码 test.cpp

    int g_val_1 = 1;
    int g_val_2[] = {123};

    int add(int a, int b)
    {
      return a + b;
    }

    b)生成可重定位目标文件 test.o

    $ g++ -c -o test.o test.cpp

    c)查看 test.o 的符号表

    $ readelf -s test.o

    Symbol table '.symtab' contains 11 entries:
       Num:    Value          Size Type    Bind   Vis      Ndx Name
         00000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
         10000000000000000     0 FILE    LOCAL  DEFAULT  ABS test.cpp
         20000000000000000     0 SECTION LOCAL  DEFAULT    1 
         30000000000000000     0 SECTION LOCAL  DEFAULT    2 
         40000000000000000     0 SECTION LOCAL  DEFAULT    3 
         50000000000000000     0 SECTION LOCAL  DEFAULT    5 
         60000000000000000     0 SECTION LOCAL  DEFAULT    6 
         70000000000000000     0 SECTION LOCAL  DEFAULT    4 
         80000000000000000     4 OBJECT  GLOBAL DEFAULT    2 g_val_1
         90000000000000008    12 OBJECT  GLOBAL DEFAULT    2 g_val_2
        100000000000000000    20 FUNC    GLOBAL DEFAULT    1 _Z3addii

    d)反汇编 add 函数

    $ objdump -d test.o

    test.o:     file format elf64-x86-64


    Disassembly of section .text:

    0000000000000000 <_Z3addii>:
       0:    55                      push   %rbp
       1:    48 89 e5                mov    %rsp,%rbp
       4:    89 7d fc                mov    %edi,-0x4(%rbp)
       7:    89 75 f8                mov    %esi,-0x8(%rbp)
       a:    855 fc                mov    -0x4(%rbp),%edx
       d:    845 f8                mov    -0x8(%rbp),%eax
      10:    01 d0                   add    %edx,%eax
      12:    5d                      pop    %rbp
      13:    c3                      retq

    从上面可以看出:


    查看 Program Header Table

    查看测试程序 main.cpp 的内容:

    $ cat main.cpp
    #include <stdio.h>

    int g_val_1;
    int g_val_2 = 16;
    const int g_val_3 = 2;

    int main()
    {
      printf("g_val_1=%x, g_val_2=%x\n", g_val_1, g_val_2);
      return 0;
    }

    查看可执行目标文件mainProgram Header Table

    $ readelf -l main

    Elf file type is EXEC (Executable file)
    Entry point 0x400430
    There are 9 program headers, starting at offset 64

    Program Headers:
      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
                     0x000000000000071c 0x000000000000071c  R E    200000
      LOAD           0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
                     0x000000000000022c 0x0000000000000238  RW     200000
      DYNAMIC        0x0000000000000e28 0x0000000000600e28 0x0000000000600e28
                     0x00000000000001d0 0x00000000000001d0  RW     8
      NOTE           0x0000000000000254 0x0000000000400254 0x0000000000400254
                     0x0000000000000044 0x0000000000000044  R      4
      GNU_EH_FRAME   0x00000000000005f0 0x00000000004005f0 0x00000000004005f0
                     0x0000000000000034 0x0000000000000034  R      4
      GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                     0x0000000000000000 0x0000000000000000  RW     10
      GNU_RELRO      0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
                     0x00000000000001f0 0x00000000000001f0  R      1

     Section to Segment mapping:
      Segment Sections...
       00     
       01     .interp 
       02     .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .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 

    注:

    验证过程:

    1) 程序入口点

    查看可执行目标文件main.textsection 的内容:

    $ objdump -d main
    # 省略...
    Disassembly of section .text:

    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 c0 05 40 00    mov    $0x4005c0,%r8
      400446:    48 c7 c1 50 05 40 00    mov    $0x400550,%rcx
      40044d:    48 c7 c7 26 05 40 00    mov    $0x400526,%rdi
      400454:    e8 b7 ff ff ff          callq  400410 <__libc_start_main@plt>
      400459:    f4                      hlt    
      40045a:    66 0f 144 00 00       nopw   0x0(%rax,%rax,1)
    # 省略...

    从上面结果中可以看出,程序入口点即为.textsection 的第一条指令。

    程序执行时,程序入口点的地址仍为 0x400430,如下所示:

    $ gdb -q ./main
    Reading symbols from ./main...(no debugging symbols found)...done.
    (gdb) start
    Temporary breakpoint 1 at 0x40052a
    Starting program: /home/test/tx/main 

    Temporary breakpoint 10x000000000040052a in main ()
    (gdb) disas 0x400430
    Dump of assembler code for function _start:
       0x0000000000400430 <+0>:    xor    %ebp,%ebp
       0x0000000000400432 <+2>:    mov    %rdx,%r9
       0x0000000000400435 <+5>:    pop    %rsi
       0x0000000000400436 <+6>:    mov    %rsp,%rdx
       0x0000000000400439 <+9>:    and    $0xfffffffffffffff0,%rsp
       0x000000000040043d <+13>:    push   %rax
       0x000000000040043e <+14>:    push   %rsp
       0x000000000040043f <+15>:    mov    $0x4005c0,%r8
       0x0000000000400446 <+22>:    mov    $0x400550,%rcx
       0x000000000040044d <+29>:    mov    $0x400526,%rdi
       0x0000000000400454 <+36>:    callq  0x400410 <__libc_start_main@plt>
       0x0000000000400459 <+41>:    hlt
    End of assembler dump.

    查看 .interp

    查看可执行目标文件的.interpsection 的内容:

    $ readelf -p .interp main

    String dump of section '.interp':
      [     0]  /lib64/ld-linux-x86-64.so.2

    在动态链接的 ELF 可执行目标文件中,其.interpsection 里面保存了可执行目标文件所需要的动态链接器的路径。

    在 X86-64 Linux 下,上面结果中的动态链接器在笔者的机器上实际指向/lib/x86_64-linux-gnu/ld-2.31.so,如下所示:

    $ ll /lib64/ld-linux-x86-64.so.2
    lrwxrwxrwx 1 root root 32 Aug 18 04:02 /lib64/ld-linux-x86-64.so.2 -> /lib/x86_64-linux-gnu/ld-2.31.so*

    查看 .dynsym

    查看可执行目标文件的.dynsymsection 的内容:

    $ readelf --dyn-syms main

    Symbol table '.dynsym' contains 7 entries:
       Num:    Value          Size Type    Bind   Vis      Ndx Name
         00000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
         10000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_deregisterTMCloneTab
         20000000000000000     0 FUNC    GLOBAL DEFAULT  UND printf@GLIBC_2.2.5 (2)
         30000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@GLIBC_2.2.5 (2)
         40000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
         50000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_registerTMCloneTable
         60000000000000000     0 FUNC    WEAK   DEFAULT  UND __cxa_finalize@GLIBC_2.2.5 (2)

    上述输出结果中各字段的含义与.symtabsection 中的相同。这里不再赘述。


    查看 .dynamic

    查看可执行目标文件的.dynamicsection 的内容:

    $ readelf -d main

    Dynamic section at offset 0x2dc8 contains 27 entries:
      Tag        Type                         Name/Value
     0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
     0x000000000000000c (INIT)               0x1000
     0x000000000000000d (FINI)               0x11f8
     0x0000000000000019 (INIT_ARRAY)         0x3db8
     0x000000000000001b (INIT_ARRAYSZ)       8 (bytes)
     0x000000000000001a (FINI_ARRAY)         0x3dc0
     0x000000000000001c (FINI_ARRAYSZ)       8 (bytes)
     0x000000006ffffef5 (GNU_HASH)           0x3a0
     0x0000000000000005 (STRTAB)             0x470
     0x0000000000000006 (SYMTAB)             0x3c8
     0x000000000000000a (STRSZ)              132 (bytes)
     0x000000000000000b (SYMENT)             24 (bytes)
     0x0000000000000015 (DEBUG)              0x0
     0x0000000000000003 (PLTGOT)             0x3fb8
     0x0000000000000002 (PLTRELSZ)           24 (bytes)
     0x0000000000000014 (PLTREL)             RELA
     0x0000000000000017 (JMPREL)             0x5e8
     0x0000000000000007 (RELA)               0x528
     0x0000000000000008 (RELASZ)             192 (bytes)
     0x0000000000000009 (RELAENT)            24 (bytes)
     0x000000000000001e (FLAGS)              BIND_NOW
     0x000000006ffffffb (FLAGS_1)            Flags: NOW PIE
     0x000000006ffffffe (VERNEED)            0x508
     0x000000006fffffff (VERNEEDNUM)         1
     0x000000006ffffff0 (VERSYM)             0x4f4
     0x000000006ffffff9 (RELACOUNT)          3
     0x0000000000000000 (NULL)               0x0

    注:

    Tag 的值 Tag 的类型 Name/Value 的值 Name/Value 的含义
    1 NEEDED 所依赖的共享库名称在 .dynstr section 中的下标 依赖的共享库名称(仅包含直接依赖的
    5 STRTAB .dynstr section 在目标文件中的偏移量(字节) 动态链接字符串表的地址
    6 SYMTAB .dynsym section 在目标文件中的偏移量(字节) 动态链接符号表的地址
    7 RELA .rela.dyn section 在目标文件中的偏移量(字节) 动态链接重定位表(特指 rela 类型的)的地址
    8 RELASZ .rela.dyn section 在目标文件中占用的空间大小(字节) 动态链接重定位表(特指 rela 类型的)的大小
    9 RELAENT .rela.dyn section 中每个条目的大小(字节) 动态链接重定位表(特指 rela 类型的)中每个条目的大小
    10 STRSZ .dynstr section 在目标文件中占用的空间大小(字节) 动态链接字符串表的大小
    11 SYMENT .dynsym section 中每个条目的大小(字节) 动态链接符号表中每个条目的大小
    12 INIT .init section 在目标文件中的偏移量(字节) 初始化代码地址
    13 FINI .fini section 在目标文件中的偏移量(字节) 结束代码地址
    14 SONAME 本共享库名称在 .dynstr section 中的下标 本共享对象的“SO-NAME”,即本共享库的名称
    30 FLAGS 常见选项:
  • SYMBOLIC,符号解析从这开始
  • TEXTREL,本目标文件包含 .text 的重定位表
  • BIND_NOW,本目标文件不采用延迟绑定
  • 加载对象的标志
    0x6ffffffb FLAGS_1 常见选项:
  • NOW,为本目标文件设置了 RTLD_NOW 标志位
  • GLOBAL,为本目标文件设置了 RTLD_GLOBAL 标志位
  • 状态标志

    .symtab 和 .dynsym 之间的区别

    .symtab.dynsymsection 之间的区别为:

    Section 名称 内容 是否会被加载到内存 是否可以通过 strip 命令删除该 section
    .symtab 包含所有的符号
    .dynsym 只包含需要动态链接的符号

    下一篇:计算机系统篇之链接(3):静态链接(上)

    上一篇:计算机系统篇之链接(1):gcc / g++的编译流程

    首页