本文主要讲解编写完源代码之后,是如何生成可执行文件的。
一个程序的的生命周期从编写源代码开始,在编写完源代码之后,就可以进行代码的构建了。其中第一步就是编译, 编译阶段会生成各文件的目标文件,将生成的目标文件和系统库文件进行链接,最终生成了可以在特定平台运行的可执行文件。最后操作系统装载器会加载、解包这些数据,并将这些数据用于程序的执行阶段。
程序的生命周期简化
- 编写源代码
- 编译
- 链接
- 装载
- 执行
编译阶段
从广义上来讲,编译就是讲一种编程语言源代码转换为另一种编程语言描述的源代码。
- 编译器负责编译程序。
- 编译器的输入时一个编译单元。通常编译单元指的是一个包含源代码的文本文件。
- 一个程序通常包含多个编译单元。
- 编译过程的输出是一系列二进制目标文件的集合,其中每一个目标文件对应一个作为输入的编译单元。
我们通常会遇到以下几个相关概念:
- 编译:从严格意义上讲,编译指的是讲高级语言编写的源代码翻译成低级语言描述代码的过程。
- 交叉编译:如果在一个平台(相同CPU或者操作系统)上进行的编译,生成的代码可以在其他平台上面执行,那么这种编译过程就称为交叉编译。
- 反编译:降低级语言编写的源代码转换成高级语言描述的源代码的过程。
编译的各个阶段
- 预处理阶段
- 语言分析阶段
- 汇编阶段
- 优化阶段
- 代码生成阶段
目标文件属性
编译阶段的输出是一个或者多个目标文件,以下分析这些目标文件的结构。
- 目标文件是通过其对应的源代码编译得到。
- 符号(symbol)和节(section)是目标文件的基本组成部分,其中符号表示的是程序中的内存地址或者数据内存。绝大多数的目标文件中包含代码节(.text)、初始化数据节(.data)、未初始化数据节(.bss)以及一些特殊节(比如试调信息等)。
- 构建程序的目的在于将编译的每个独立的源代码文件生成节拼接到一个二进制可执行文件中。最终生成的二进制文件中包含了多个相同类型的节(.text、.data 和 .bss节等),而这些节是从每个独立的目标文件中拼接得到的。
- 目标文件中独立的节都可能包含在最终的程序内存映射中,因此目标文件中每个节的起始地址都会被临时设置成0,等待连接时调整。在程序构建过程的后续阶段(链接阶段)中会确认程序内存映射中每个独立节的实际地址范围。
- 在将目标文件的节拼接到程序内存映射的过程中。其中唯一重要的参数是节的长度,准确的说是节的地址范围。
- 目标文件中不包含专门的节会影响堆和栈中的数据。内存映射中的堆和栈内容完全在运行时确定,除了需要指定堆和栈的默认长度以外,并不需要程序指定任何其他初始化设置。
- 目标文件只包含了程序.bss(未初始化数据)节的基本信息, 而.bss节本身也仅仅只有字节的长度信息。装载器会利用这有限的数据为.bss节建立足够其数据存储的内存空间。
通常来讲,目标文件中的信息是根据一组特定的二进制格式规范集合进行存储的,其中范围定义了多种不同平台的细节信息。
二进制格式规范的设计通常是为了支持C/C++语言结构并帮助其解决实现问题。二进制格式规范常常会涵盖各种各样的文件类型,比如可执行文件、静态库和动态库等。
在Linux上,可执行和可链接格式(Executable and Linkable Format, ELF)已经得到了普遍运用。在Windows上,二进制文件通常遵循PE/COFF格式规范。
为什么要进行编译?
答:
目前很多语言实现了一阶段式构建过程, 比如:python,javaScripts。
为了程序构建阶段支持复用(此处指的是二进制级别的复用),所以将程序构建分为两个阶段(编译阶段和链接阶段)。
链接阶段
重定位
链接过程第一个阶段仅仅进行拼接,其过程是将分散在单独目标文件中不同类型的节拼接到程序内存映射节中。
如下图所示:
为了完成该任务,需要将之前预留的空间,也就是节中从0开始的地址范围转换成最终程序的内存映射中更具体的地址范围。
解析引用
将节的地址范围转换成程序内存映射地址范围,
1 |
或者
1 |
反汇编main.o文件:
1 |
对含有未初始化数据的.bss节进行反汇编操作,发现变量nCompletionStatus
1 |
可执行文件属性
链接过程的最终结果是二进制可执行文件,其结构布局遵循特定目标平台的可执行文件格式。
启动代码有两种不同形式:
- crt0 是程序入口点,这是程序代码的第一部分,在内核控制下执行。
- crt1 是启动例程(startuproutine),可以在main函数执行前与程序终止后完成一些任务。
程序可执行文件的整体结构大致如下图:
各种节的类型
待补充…
各种符号类型
待补充…