目标文件有什么(ELF)

目标文件的格式

  • 目标文件:已经编译后的可执行文件格式,还没有经过链接的过程,其中可能有一些符号和地址没有调整。它跟可执行文件的内容与结构很相似,所以一般和可执行文件以同一种格式存储。

  • 现在PC平台流行的可执行文件格式主要是PE(Windows平台)ELF(Linux平台)

  • 动态链接库(Window的.dll和Linux的.so)和静态链接库(Windows的.lib和Linux的.a)文件也按照可执行文件格式存储

  • 静态链接库是把很多目标文件捆绑在一起形成一个文件

ELF格式的文件

  • 可重定位文件:Linux中的.o, Windows的.obj,这类文件包含代码和数据,可被链接成可执行文件或共享目标文件,例如静态链接库。

  • 可执行文件:可以直接执行的文件,如/bin/bash文件。

  • 共享目标文件:Linux中的.so,包含代码和数据,一种是链接器可以使用这种文件和其它的可重定位文件和共享目标文件链接,另一种是动态链接器可以将几个这种共享目标文件和可执行文件结合,作为进程映像的一部分来执行。

  • core dump文件:进程意外终止时,系统可以将该进程的地址空间的内容和其它信息存到coredump文件用于调试,如gdb。

我们可以通过file命令查看相应的文件格式

file sum.o
sum.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

relocatable:可重定位文件

file /bin/bash 
/bin/bash: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=12f73d7a8e226c663034529c8dd20efec22dde54, stripped

shared object:共享目标文件

目标文件是什么样的

  • 目标文件内容有编译后的机器指令代码、数据。

  • 还有链接时所需要的一些信息,比如符号表、调试信息、字符串等

目标文件将这些信息按不同的属性以段Segment的形式存储

  • 程序源代码编译后的机器指令放在代码段.text
  • 全局变量和局部静态变量放在数据段.data
  • 未初始化的或者初始化为0的全局变量和局部静态变量放在.bss,不占文件的空间
int gdata1 = 10;      //.data
int gdata2 = 0;		 //.bss
int gdata3;	         //.bss

static int gdata4 = 11;//.data
static int gdata5 = 0;//.bss
static int gdata6;//.bss

int main()
{
	int a = 10;
	int b = 0;
	int c;
	static int d = 12;//.data
	static int e = 0;//.bss
	static int f;//.bss
	sum(a, b);

	return 0;
}

image-20220109003956867

程序员自我修养的例子

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DPBNJKzD-1650725716571)(https://syz-picture.oss-cn-shenzhen.aliyuncs.com/D:%5CPrograme%20Files(x86)]%5CPicGoimage-20220130194413822.png)

为什么未初始化的或者初始化为0的全局变量和局部静态变量要放在.bss

他们默认值都为0,本来也可以放到.data,但因为我们已经知晓他们的值都为0,所以在.data段分配空间并且存放数据是没必要的,这样也方便减小ELF文件大小。既然这些数据值都相同,所以干脆把他们统一放到另一个段里,且不分配空间单独记录数据,只要在这个段里就知道他的数据为0。 但是程序运行期间这个是要占内存的,所以我们的.bss段需要记录这些数据的大小,仅仅作为一个占用空间的标记,为未初始化的和初始化为0的全局变量和静态局部变量预留位置而已,并无内容。

为什么要把数据和指令分开放

  • 程序被加载后,数据和指令分别被映射到两个虚存内存区域,数据区域对进程是可读写的,而指令区域是只读的,这样可以防止程序的指令被改写
  • 另一方面对于现代CPU,它们有着强大的缓存体系。程序必须尽量提高缓存(Cache)的命中率,而指令区和数据区的分离有利于提高程序的局部性。
  • 当系统运行多个程序的副本时,指令是一样的,所以只要保存一份指令部分即可。是属于共享的资源。但是数据区域是不一样的,是进程私有的。这个共享指令的概念,特别是在有动态链接的系统中可以节省大量的内存。(多进程状态下不用保存多份指令)

使用命令探索ELF文件格式

我们可以使用objdump命令打印ELF的信息

objdump命令是Linux下的反汇编目标文件或者可执行文件的命令,它以一种可阅读的格式让你更多地了解二进制文件可能带有的附加信息。 不过它只会打印关键段的信息,像是符号表段之类的辅助段不会打印

参考视频例子

#include <stdio.h>

int global_1;
int global_2 = 0;
int global_3 = 10;

static int global_4;
static int global_5 = 0;
static int global_6 = 10;

char *str = "Hello World";

int main()
{
    int global_7;
    int global_8 = 0;
    int global_9 = 10;

    static int global_10;
    static int global_11 = 0;
    static int global_12 = 10;

    return 0;
}
gcc test.c -c
  • -h打印各个段的基本信息
  • -x打印各个段更多的信息
objdump -h test.o

test.o:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         00000019  0000000000000000  0000000000000000  00000040  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .data         0000000c  0000000000000000  0000000000000000  0000005c  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000014  0000000000000000  0000000000000000  00000068  2**2
                  ALLOC
  3 .rodata       0000000c  0000000000000000  0000000000000000  00000068  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  4 .data.rel.local 00000008  0000000000000000  0000000000000000  00000078  2**3
                  CONTENTS, ALLOC, LOAD, RELOC, DATA
  5 .comment      0000002a  0000000000000000  0000000000000000  00000080  2**0
                  CONTENTS, READONLY
  6 .note.GNU-stack 00000000  0000000000000000  0000000000000000  000000aa  2**0
                  CONTENTS, READONLY
  7 .eh_frame     00000038  0000000000000000  0000000000000000  000000b0  2**3
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
  • .text:代码段
  • .data:数据段
  • .bss:…
  • .rodata:只读数据段
  • .comment:注释信息段
  • .note.GNU-stack:堆栈提示段

对照一下.data,其大小为Size = 0000000c,将16进制转换得到12,正好是3个变量的大小,符合我们的判断。

File off:段偏移量

CONTENTS: 代表该段在文件中存在,.bss段就没有CONTENTS,说明.bss段在文件中不存在;.note.GNU-stack段有CONTENTS,但是值为0,我们暂时认为它不存在。

objdump -x test.o

test.o:     file format elf64-x86-64
test.o
architecture: i386:x86-64, flags 0x00000011:
HAS_RELOC, HAS_SYMS
start address 0x0000000000000000

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         00000019  0000000000000000  0000000000000000  00000040  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .data         0000000c  0000000000000000  0000000000000000  0000005c  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000014  0000000000000000  0000000000000000  00000068  2**2
                  ALLOC
  3 .rodata       0000000c  0000000000000000  0000000000000000  00000068  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  4 .data.rel.local 00000008  0000000000000000  0000000000000000  00000078  2**3
                  CONTENTS, ALLOC, LOAD, RELOC, DATA
  5 .comment      0000002a  0000000000000000  0000000000000000  00000080  2**0
                  CONTENTS, READONLY
  6 .note.GNU-stack 00000000  0000000000000000  0000000000000000  000000aa  2**0
                  CONTENTS, READONLY
  7 .eh_frame     00000038  0000000000000000  0000000000000000  000000b0  2**3
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
SYMBOL TABLE:	//符号表
0000000000000000 l    df *ABS*  0000000000000000 test.c
0000000000000000 l    d  .text  0000000000000000 .text
0000000000000000 l    d  .data  0000000000000000 .data
0000000000000000 l    d  .bss   0000000000000000 .bss
0000000000000004 l     O .bss   0000000000000004 global_4
0000000000000008 l     O .bss   0000000000000004 global_5
0000000000000004 l     O .data  0000000000000004 global_6
0000000000000000 l    d  .rodata        0000000000000000 .rodata
0000000000000000 l    d  .data.rel.local        0000000000000000 .data.rel.local
0000000000000008 l     O .data  0000000000000004 global_12.2261
000000000000000c l     O .bss   0000000000000004 global_11.2260
0000000000000010 l     O .bss   0000000000000004 global_10.2259
0000000000000000 l    d  .note.GNU-stack        0000000000000000 .note.GNU-stack
0000000000000000 l    d  .eh_frame      0000000000000000 .eh_frame
0000000000000000 l    d  .comment       0000000000000000 .comment
0000000000000004       O *COM*  0000000000000004 global_1
0000000000000000 g     O .bss   0000000000000004 global_2
0000000000000000 g     O .data  0000000000000004 global_3
0000000000000000 g     O .data.rel.local        0000000000000008 str
0000000000000000 g     F .text  0000000000000019 main


RELOCATION RECORDS FOR [.data.rel.local]:
OFFSET           TYPE              VALUE 
0000000000000000 R_X86_64_64       .rodata


RELOCATION RECORDS FOR [.eh_frame]:
OFFSET           TYPE              VALUE 
0000000000000020 R_X86_64_PC32     .text

继续探索——SimpleSection.o

代码示例

#include <stdio.h>

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;
}
gcc -c SimpleSection.c

我们得到了SimpleSection.o文件

使用objdump命令探索

objdump -h SimpleSection.o	//打印目标文件的基本段信息
SimpleSection.o:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         00000057  0000000000000000  0000000000000000  00000040  2**0
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  1 .data         00000008  0000000000000000  0000000000000000  00000098  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000004  0000000000000000  0000000000000000  000000a0  2**2
                  ALLOC
  3 .rodata       00000004  0000000000000000  0000000000000000  000000a0  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  4 .comment      0000002a  0000000000000000  0000000000000000  000000a4  2**0
                  CONTENTS, READONLY
  5 .note.GNU-stack 00000000  0000000000000000  0000000000000000  000000ce  2**0
                  CONTENTS, READONLY
  6 .eh_frame     00000058  0000000000000000  0000000000000000  000000d0  2**3
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
  • .text
  • .data
  • .bss
  • .rodata
  • .comment
  • .note.GNU-stack

代码段

objdump -s -d SimpleSection.o	//-s可以将所有段内容以十六进制打印 -d可以将所有指令反汇编
SimpleSection.o:     file format elf64-x86-64

Contents of section .text:
 0000 554889e5 4883ec10 897dfc8b 45fc89c6  UH..H....}..E...
 0010 488d3d00 000000b8 00000000 e8000000  H.=.............
 0020 0090c9c3 554889e5 4883ec10 c745f801  ....UH..H....E..
 0030 0000008b 15000000 008b0500 00000001  ................
 0040 c28b45f8 01c28b45 fc01d089 c7e80000  ..E....E........
 0050 00008b45 f8c9c3                      ...E...         
Contents of section .data:
 0000 54000000 55000000                    T...U...        
Contents of section .rodata:
 0000 25640a00                             %d..            
Contents of section .comment:
 0000 00474343 3a202855 62756e74 7520372e  .GCC: (Ubuntu 7.
 0010 352e302d 33756275 6e747531 7e31382e  5.0-3ubuntu1~18.
 0020 30342920 372e352e 3000               04) 7.5.0.      
Contents of section .eh_frame:
 0000 14000000 00000000 017a5200 01781001  .........zR..x..
 0010 1b0c0708 90010000 1c000000 1c000000  ................
 0020 00000000 24000000 00410e10 8602430d  ....$....A....C.
 0030 065f0c07 08000000 1c000000 3c000000  ._..........<...
 0040 00000000 33000000 00410e10 8602430d  ....3....A....C.
 0050 066e0c07 08000000                    .n......        

Disassembly of section .text:

0000000000000000 <func1>:
   0:   55                      push   %rbp
   1:   48 89 e5                mov    %rsp,%rbp
   4:   48 83 ec 10             sub    $0x10,%rsp
   8:   89 7d fc                mov    %edi,-0x4(%rbp)
   b:   8b 45 fc                mov    -0x4(%rbp),%eax
   e:   89 c6                   mov    %eax,%esi
  10:   48 8d 3d 00 00 00 00    lea    0x0(%rip),%rdi        # 17 <func1+0x17>
  17:   b8 00 00 00 00          mov    $0x0,%eax
  1c:   e8 00 00 00 00          callq  21 <func1+0x21>
  21:   90                      nop
  22:   c9                      leaveq 
  23:   c3                      retq   

0000000000000024 <main>:
  24:   55                      push   %rbp
  25:   48 89 e5                mov    %rsp,%rbp
  28:   48 83 ec 10             sub    $0x10,%rsp
  2c:   c7 45 f8 01 00 00 00    movl   $0x1,-0x8(%rbp)
  33:   8b 15 00 00 00 00       mov    0x0(%rip),%edx        # 39 <main+0x15>
  39:   8b 05 00 00 00 00       mov    0x0(%rip),%eax        # 3f <main+0x1b>
  3f:   01 c2                   add    %eax,%edx
  41:   8b 45 f8                mov    -0x8(%rbp),%eax
  44:   01 c2                   add    %eax,%edx
  46:   8b 45 fc                mov    -0x4(%rbp),%eax
  49:   01 d0                   add    %edx,%eax
  4b:   89 c7                   mov    %eax,%edi
  4d:   e8 00 00 00 00          callq  52 <main+0x2e>
  52:   8b 45 f8                mov    -0x8(%rbp),%eax
  55:   c9                      leaveq 
  56:   c3                      retq   

数据段和只读数据段

观察示例代码的函数部分

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

SimepleSection.c里面使用了printf,里面用到了字符串常量%d\n,它是一个只读数据,所以放到了.rodata段。

使用objdump -x SimpleSection.o命令查看

objdump -x SimpleSection.o	//-x打印更多段的信息,包括了辅助段符号表等
SimpleSection.o:     file format elf64-x86-64
SimpleSection.o
architecture: i386:x86-64, flags 0x00000011:
HAS_RELOC, HAS_SYMS
start address 0x0000000000000000

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  2 .bss          00000004  0000000000000000  0000000000000000  000000a0  2**2
                  ALLOC
  3 .rodata       00000004  0000000000000000  0000000000000000  000000a0  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
//省略了许多
Contents of section .data:
 0000 54000000 55000000                    T...U...        
Contents of section .rodata:
 0000 25640a00                             %d..          

我们观察.rodata,正好只有四个字节,符合那个int变量。还有那个Contents of section .rodata: 的字节序也能对应的上。

rodata段的意义

  • 语义上支持了C++的const关键字
  • 操作系统在加载时候可以将.rodata段的属性映射成只读,保证安全性

ELF文件结构描述

ELF Header
.text
.data
.bss
…other sections
Section header table
String Tables
Symbol Tables
  • ELF目标文件格式的最前部是ELF文件头,它包含了描述整个文件的基本属性。
  • 紧接着是各个段,其中ELF文件中与段有关的重要结构就是段表,该表描述了ELF文件包含的所有段的信息。

使用readelf命令来详细查看ELF文件

查看文件头

readelf -h test.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:          1024 (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:         14

ELF文件头定义了ELF魔数、文件机器字节长度、数据存储方式、版本、运行平台…

段表

ELF文件中有各种各样的段,段表就是保存这些段信息的基本结构。它描述了各个段的信息,比如段名、段长、在文件中的偏移、读写权限等

之前的objdump -h只是打印了ELF文件中关键的段,辅助段等被省略了。

使用readelf细致探索段表

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

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
       0000000000000057  0000000000000000  AX       0     0     1
  [ 2] .rela.text        RELA             0000000000000000  00000340
       0000000000000078  0000000000000018   I      10     1     8
  [ 3] .data             PROGBITS         0000000000000000  00000098
       0000000000000008  0000000000000000  WA       0     0     4
  [ 4] .bss              NOBITS           0000000000000000  000000a0
       0000000000000004  0000000000000000  WA       0     0     4
  [ 5] .rodata           PROGBITS         0000000000000000  000000a0
       0000000000000004  0000000000000000   A       0     0     1
  [ 6] .comment          PROGBITS         0000000000000000  000000a4
       000000000000002a  0000000000000001  MS       0     0     1
  [ 7] .note.GNU-stack   PROGBITS         0000000000000000  000000ce
       0000000000000000  0000000000000000           0     0     1
  [ 8] .eh_frame         PROGBITS         0000000000000000  000000d0
       0000000000000058  0000000000000000   A       0     0     8
  [ 9] .rela.eh_frame    RELA             0000000000000000  000003b8
       0000000000000030  0000000000000018   I      10     8     8
  [10] .symtab           SYMTAB           0000000000000000  00000128
       0000000000000198  0000000000000018          11    11     8
  [11] .strtab           STRTAB           0000000000000000  000002c0
       000000000000007c  0000000000000000           0     0     1
  [12] .shstrtab         STRTAB           0000000000000000  000003e8
       0000000000000061  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  l (large), p (processor specific)

段表结构简单,就是一个以ELf32_Shdr结构体为元素的数据,每一个元素对应一个段

以下为32位系统下的ELF32_Shdr结构体信息

typedef struct 
{
    ELF32_Word	sh_name;	//段名:是一个字符串,位于.shstrtab字符串表,sh_name是在字符串表中的偏移
    ELF32_Word	sh_type;	//段类型
    ELF32_Word	sh_flags;	//段标志位
    ELF32_Addr	sh_addr;	//段虚拟地址:如果可加载,则此为该段被加载后在进程虚拟空间的地址,否则为0
    ELF32_Off	sh_offset;	//段偏移:如果该段存于文件,则表示在文件中的偏移,否则无意义(比如.bss)
    ELF32_Word	sh_size;	//段长度
    ELF32_Word	sh_link;	//段链接信息
    ELF32_Word	sh_info;
    ELF32_Word	sh_addralign;//段地址对齐:表示是地址对齐数量中的指数
    ELF32_Word	sh_entsize;	 //项长度:有些段包含了固定大小的项,比如符号表,它包含每个符号所占大小一样
} ELF32_Shdr;
  • 段名对于编译器,链接器有意义,对于OS无意义。OS只在意段的属性和权限,即段的类型和标志位

段类型sh_type

SHT_NULL		0	无效段;
SHT_PROGBITS	1	程序段、代码段、数据段都为这种类型;
SHT_SYMTAB		2	表示该段的内容为符号表;

链接的接口——符号

链接过程的本质是要把多个不同的目标文件之间相互粘在一起,目标文件之间相互拼合实际上是目标文件之间对地址的引用(对函数和变量的地址的引用)。

比如文件A用到了文件B的地址,它得知道这个地址在哪里吧,编译过程只是单独的对本文件编译,而不知道其他文件的情况。而函数和变量应该有不同的名字,否则链接过程会混淆报错。(函数内的不算,那不属于链接器管辖)

在链接中,我们将函数和变量统称为符号,函数名和变量名就是符号名

每一个目标文件都会有一个相应的符号表Symbol Table,这个表里面记录了目标文件中所用到的符号,符号对应有符号值(变量和函数的地址)。对于变量和函数来说,符号值就是它们的地址

符号的分类

  • 定义在本目标文件的全局符号,比如func1mainglobal_init_var
  • 在本目标文件中引用的全局符号,却没有定义在本目标文件中,比如printf
  • 段名,这种由编译器产生,它的值就是该段的起始地址,比如.text
  • 局部符号,这类符号只在编译单元内可见。这些局部符号对链接过程无用,链接器不会注意,比如static_varstatic_var2
  • 行号信息,即目标文件指令与源代码中代码行的对应关系
nm SimpleSection.o	//查看SimpleSection.o中的符号
0000000000000000 T func1
0000000000000000 D global_init_var
                 U _GLOBAL_OFFSET_TABLE_
0000000000000004 C global_uninit_var
0000000000000024 T main
                 U printf
0000000000000000 b static_var2.2258
0000000000000004 d static_var.2257

使用readelf命令查看符号属性

readelf -s SimpleSection.o
Symbol table '.symtab' contains 17 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS SimpleSection.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3 
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 
     5: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 
     6: 0000000000000004     4 OBJECT  LOCAL  DEFAULT    3 static_var.2257
     7: 0000000000000000     4 OBJECT  LOCAL  DEFAULT    4 static_var2.2258
     8: 0000000000000000     0 SECTION LOCAL  DEFAULT    7 
     9: 0000000000000000     0 SECTION LOCAL  DEFAULT    8 
    10: 0000000000000000     0 SECTION LOCAL  DEFAULT    6 
    11: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    3 global_init_var
    12: 0000000000000004     4 OBJECT  GLOBAL DEFAULT  COM global_uninit_var
    13: 0000000000000000    36 FUNC    GLOBAL DEFAULT    1 func1
    14: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _GLOBAL_OFFSET_TABLE_
    15: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND printf
    16: 0000000000000024    51 FUNC    GLOBAL DEFAULT    1 main
  • 第一列Num表示符号表数组的下标;第二列为符号值;第三列为符号大小;第四列和第五列分别为符号类型和绑定信息;第七列Ndx表示该符号所属的段;最后一列为符号名称
  • func1main函数都是在代码段,所以Ndx都为1,可以通过readelf -a 或者 objdump -x来验证。他们是函数,所以类型为STT_FUNC。他们是全局可见的,所以是STL_GLOBALSize表示函数指令所占的字节数,Value表示函数相对于代码其实位置的偏移量
  • printf这个符号被引用,但是该目标文件没有定义,所以它的NdxSHN_UNDEF
  • global_init_var是已初始化的全局变量,位于

重定位表

  [ 2] .rela.text        RELA             0000000000000000  00000340
       0000000000000078  0000000000000018   I      10     1     8

我们注意到一个.rel.text段,它的类型为sh_type = SHT_REL,说明这是一个重定位表(Relocation Table)

链接器处理目标文件,需要对目标文件中某些部位进行重定位,即代码段和数据段中那些对绝对地址的引用的位置。这些重定位的信息都记录在ELF文件的重定位表里面,对于每个须要重定位的代码段和数据段,都会有一个对应的重定位表。

SimpleSection.o.rel.text就是针对.text段的重定位表,因为.text段中有一个绝对地址的引用,即对printf函数的调用。

  • 一个重定位表同时也是ELF的一个段,这个段的类型就是SHT_REL,他的sh_link表示符号表的下标,它的sh_info表示它作用于哪个段

符号修饰与函数签名

  • 早期编译器编译源代码产生目标文件时,符号名与相应的变量和函数名相同。如果程序员自己定义的变量函数和库中定义的重复,就会产生冲突
  • 为了防止类似的符号名冲突,UNIX下的C语言规定,C语言源代码文件中所有全局的变量和函数经过编译之后,对应的符号名前加上下划线_
  • 后来,不同人开发不同模块还是有可能产生冲突,在C++中引入了名称空间namespace的方法解决了多模块的符号冲突问题

C++符号修饰

C++拥有类,继承,虚机制,重载,名称空间这些特性,这会使得符号管理变得困难。比如函数重载,它们的名字就是相同的,如果没有特殊处理,符号名应该也一样,这会造成冲突。

为了支持C++这些复杂特性,人们发明了符号修饰的机制。

int func(int);
float func(float);

class C {
  int func(int);
  class C2 {
      int func(int);  
    };
};

namespace N {
    int func(int);
    class C {
        int func(int);
    };
};
  • 上述代码有六个同名函数叫做func,我们引入一个叫做函数签名的术语,函数签名包含了一个函数的信息(函数名,参数类型,所在的类和命名空间等)
  • 编译器及链接器处理符号时,它们使用名称修饰的方法,使得函数签名对应一个修饰后名称。
  • image-20220329184547979
  • GCC基本C++名称修饰方法
    • 所有符号都以_Z开头
    • 对于嵌套的名字,后面紧跟N,然后是各个名称空间和类的名字,每个名字前是名字字符串长度,再以E结尾,后面再跟参数类型的首字母
  • 不同编译器采用不同的名字修饰方法

extern ”C“

C++为了与C兼容,在符号的管理上,C++有一个用来声明或定义一个C的符号的extern C关键字用法

extern "C" {
    int func(int);
    int var;
}

C++编译器会将extern "C"的大括号内部的代码当作C语言代码处理。那么C++的修饰机制将不会起作用

弱符号与强符号

我们经常遇到的一种情况叫做符号重复定义。多个目标文件中含有相同名字全局符号的定义,那么这些目标文件链接的时候将会出现符号重复定义的错误。

对于C++来说

  • 编译器默认函数和初始化了的全局变量为强符号
  • 未初始化的全局变量为弱符号
  • 我们可以使用_attribute_((weak))来定义任何一个强符号为弱符号

强弱符号规则

  • 不允许强符号被多次定义,否则链接器报重复定义错误
  • 如果一个符号在某个目标文件是强符号,在其他文件都是弱符号,那么选择强符号
  • 如果一个符号在所有目标文件中都是弱符号,那么选择其中占用空间最大的一个

强引用和弱引用

  • 我们所看到的对外部目标文件中的符号引用在目标文件中被最终链接成可执行文件时,它们必须被正确决议,如果没有找到该符号的定义,链接器就会报符号未定义错误,这种被称为强引用。
  • 在处理弱引用时,如果该符号有定义,则链接器将该符号的引用决议,如果该符号未被定义,则链接器对于该引用不报错,链接器默认其值为0。只不过其可被覆盖

强弱符号作用

  • 比如库中定义的弱符号可以被用户定义的强符号所覆盖,从而使用自定义版本的库函数。
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值