程序员的自我修养阅读笔记

编译和链接

将编译和链接合并到一起的过程称为构建(Build)

从源文件生成最终可执行目标文件共有4个步骤:

  • 预处理(Prepressing)
  • 编译(Compilation)
  • 汇编(Assembly)
  • 链接(Linking)

预处理

命令行指令:

gcc -E hello.c -o hello.i

预处理实际上使用的是cpp程序:

cpp hello.c > hello.i

预编译过程主要处理那些源代码文件中的以#开始的预编译指令。处理规则如下:

  • 将所有的#define删除,并且展开所有的宏定义
  • 处理所有条件预编译指令,比如#if#ifdef#elif#else#endif
  • 处理#include预编译指令,将被包含的文件插入到该预编译指令的位置。这个过程是递归进行的。
  • 删除所有的注释///* */
  • 添加行号和文件名标识,比如#2 hello.c 2,便于编译器产生调试信息。
  • 保留所有的#pragma编译器指令,因为编译器需要使用它们。

编译

命令行指令:

gcc -S hello.c -o hello.s
# 或者基于预处理得到的.i文件
gcc -S hello.i -o hello.s

编译实际上使用的是cc1程序:

/usr/lib/gcc/x86_64-linux-gnu/9/cc1 hello.i

汇编

命令行指令:

gcc -c hello.s -o hello.o

汇编实际上使用的是as程序:

as hello.s -o hello.o

链接

链接使用的是ld程序(命令过于复杂,不再列出)。

实际上gcc这个命令只是对cpp、cc1、as、ld程序的封装。

编译器

不予记录,感兴趣可以选修《编译原理》课程。

链接器

当程序修改时,一些指令的地址会发生改变。重新计算各个目标的地址的过程叫做重定位

链接的主要工作就是把各个模块之间相互引用的部分都处理好,使得各个模块之间能够正确地衔接。

链接过程主要包括了地址和空间分配符号决议重定位等这些步骤。

符号决议有时也被称为符号绑定

决议的意思偏向于静态链接,而绑定的意思偏向于动态链接

每个模块的源代码文件经过编译器编译成目标文件(.o文件),目标文件和库(Library)一起链接形成最终可执行文件。

最常见的库是运行时库(Runtime Library)

库其实是一组目标文件的包。

目标文件里有什么

目标文件的格式

现在PC平台流行的可执行文件格式主要是:

  • Windows下的PE(Portable Executable)。
  • Linux的ELF(Executable Linkable Format)。

它们都是COFF(Common file format)格式的变种。

除此之外,动态链接库(Dynamic Linking Library)和静态链接库(Static Linking Library)文件也都按照可执行文件格式存储。

Windows下的动态链接库文件后缀为**.dll**,静态链接库文件后缀为**.lib**。

Linux下的静态链接库文件后缀为**.so**,静态链接库文件后缀为**.a**。

ELF文件的分类

ELF文件可以分为以下几类:

  1. 可重定位文件:包含代码和数据,可以用来链接成可执行文件或共享目标文件。静态链接库可以归为该类。Windows下的文件后缀为**.obj**,Linux下的文件后缀为**.o**。

  2. 可执行文件:包含了可以直接执行的程序。Linux下一般没有拓展名,Windows下后缀为**.exe**。

  3. 共享目标文件:包含了代码和数据。用途有以下两种:

    • 链接器使用这种文件和其他的可重定位文件和共享目标文件链接,产生新的目标文件。
    • 动态链接器可以将几个共享目标文件与可执行文件结合,作为进程映像的一部分来运行。

    Linux下的文件后缀为**.so**,Windows下文件后缀为**.dll**。

  4. 核心转储文件:进程意外终止时,系统可将进程相关的一些信息转储到核心转储文件。Linux有一种core dump行为(WSL1似乎并未实现该功能)。

我们可以通过file <filename>命令来查看相应的文件格式信息。

目标文件是什么样的

目标文件中包含了编译后的机器指令代码、数据,还包括了链接时所须的一些信息,比如符号表、调试信息、字符串等。

目标文件将这些信息按不同的属性以节(Section)的形式存储,有时候也叫做段(Segment)

  • 程序源代码编译后的机器指令经常被放在代码段里,代码段一般叫.text
  • 全局变量和局部静态变量数据经常放在数据段,数据段一般叫.data
  • 未初始化的全局变量和局部静态变量(默认值均为0)一般放在一个叫.bss的段里。
  • .rodata段用来存放只读数据,如const常量和字符串常量等。

.bss段只是为未初始化的全局变量和局部静态变量预留位置,它并没有内容,在文件中也不占据空间。

有些编译器会将全局的未初始化变量存放在目标文件.bss段,有些则不存放,只是预留一个未定义的全局变量符号,等到最终链接成可执行文件的时候再在.bss段分配空间。

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

数据和指令分段的好处有:

  • 程序被装载后,数据和指令分别被映射到两个虚存区域。这样就可以分别设置这两个区域的读写权限。进而防止程序的指令被有意或无意的更改。
  • 指令区和数据区的分离有利于提高程序的局部性。(现代CPU中的缓存也被设计为数据缓存和指令缓存分离)。
  • 系统中运行多个该程序的副本时,可以共享一些只读区域,如指令部分,进而大量的节省空间。这也是最重要的原因。

挖掘.o文件

我们可以使用如下命令来查看一个.o文件的分段情况;

objdump -h SimpleSection.o

使用-x选项可以查看更多的信息。

可以使用size <object-file>命令来查看ELF文件的各个段的长度。

objdump的-s参数可以将所有段的内容以十六进制的方式打印出来,-d参数可以将所有包含指令的段反汇编。

除了前面提到的一些最常用的段之外,还有一些其他常见的段:

image-20210529211127040

自定义段

gcc提供了一个拓展机制,使程序员可以定义变量所处的段:

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

全局变量或函数之前加上__attribute__((section("name")))属性就可以把相应的变量或函数放到以"name"作为段名的段中。

ELF文件结构描述

ELF的大致结构如图所示:

image-20210529211518340

我们可以使用readelf命令来查看ELF文件的信息。

ELF文件头(ELF Header)

ELF的文件头中定义了ELF魔数、文件机器字节长度、数据存储方式、版本、运行平台、ABI版本、ELF重定位类型、硬件平台、硬件平台版本、入口地址、程序头入口和长度、段表的位置和长度及段的数量等信息。

我们可以使用如下命令来查看ELF文件头:

readelf -h SimpleSection.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:          488 (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 index: 11

32位版本的文件头结构Elf32_Ehdr如下:

typedef struct {
   
    unsigned char e_ident[16];
    Elf32_Half e_type;			// ELF文件类型,ET_REL表示可重定位文件、ET_EXEC表示可执行文件,ET_DYN表示共享目标文件
    Elf32_Half e_machine;		// ELF文件的CPU平台属性
    Elf32_Word e_version;		// ELF版本号
    Elf32_Addr e_entry;			// ELF程序的入口地址(操作系统加载完程序后,从该地址开始执行指令)
    Elf32_Off  e_phoff;			
    Elf32_Off  e_shoff;			// 段表在文件中的偏移
    Elf32_Word e_flags;			// ELF标志位,用来标识ELF文件平台的属性
    Elf32_Half e_ehsize;		// ELF文件头本身的大小
    Elf32_Half e_phentsize;		
    Elf32_Half e_phnum;
    Elf32_Half e_shentsize;		// 段表描述符的大小,一般等于sizeof(Elf32_Shdr)
    Elf32_Half e_shnum;			// 段表描述符数量,即ELF文件中段的数量。
    Elf32_Half e_shstrndx;		// 段表字符串表所在段在段表中的下标
} Elf32_Ehdr;
ELF魔数

最开始的4个字节是所有ELF文件都必须相同的标识码,分别为0x7F、0x45、0x4c、0x46,第一个字节对应ASCII字符里面的DEL控制符,后面3个字节刚好是ELF这3个字母的ASCII码。

第5个字节用于标识ELF的文件类,0x01表示是32位的,0x02表示是64位的;第6个字是字节序,规定该ELF文件是大端的还是小端的。第7个字节规定ELF文件的主版本号,一般是1,因为ELF标准自1.2版以后就再也没有更新了。后面的9个字节ELF标准没有定义,一般填0,有些平台会使用这9个字节作为扩展标志。

段表

段表是以Elf32_Shdr结构体为元素的数组,数组元素的个数等于段的个数。

typedef struct {
   
  Elf32_Word    sh_name;		// 段名
  Elf32_Word    sh_type;		// 段的类型
  Elf32_Word    sh_flags;		// 段的标志位
  Elf32_Addr    sh_addr;		// 段虚拟地址,即该段被加载后在进程地址空间中的虚拟地址
  Elf32_Off     sh_offset;		// 段偏移,该段在文件中的偏移
  Elf32_Word    sh_size;		// 段的长度
  Elf32_Word    sh_link;		// 段链接信息
  Elf32_Word    sh_info;		// 段链接信息
  Elf32_Word    sh_addralign;	// 段地址对其
  Elf32_Word    sh_entsize;		// 项的长度。如符号表每一项的长度。
} Elf32_Shdr;

这些字段的可选值请参考程序员的自我修养P77-79。

可以使用以下命令查看目标文件的段表结构:

readelf -S SimpleSection.o
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值