02--编译过程
[TOC]
通过本章的学习,要了解到以下几个问题
- 被隐藏了的过程
- 编译器做了什么
- 链接器和编译器
- 模块拼装——静态链接
被隐藏的过程
对于平常的应用程序开发,我们很少选用关注编译和链接过程,因为通常的开发环境都是流行的集成开发环境(IDE),比如xcode。IDE一般都将编译和链接的过程一步完成,这个过程被成为构建(Build)。
为何要深入了解这些被隐藏的过程?
IDE和编译器提供的默认配置、编译和链接参数对于大部分的应用程序开发而言已经足够使用了。但是在这样的开发过程中,我们往往被这些复杂的集成工具所提供的强大的功能所迷惑,很多系统软件的运行机制与机理被掩盖,
- 其程序运行的很多莫名其妙的错误让我们无所适从;
- 面对程序运行时种种性能瓶颈让我们束手无策;
- 我们看到的是这些问题的现象,但是无法看清本质;
所以要了解软件运行背后的机理及支撑软件运行的各种平台和工具,这样就能在解决问题的时候能够游刃有余、收放自如。 - 文件:
hello.c
,文件内容如下
#include <stdio.h>
int main()
{
printf("Hello World\n");
return 0;
}
譬如说,下面的一个简单的编译命令:
gcc hello.c
编译结果会生成一个a.out文件,虽然只有简单的命令,但是它的过程并不简单。这个过程可以分解为四个步骤:预处理(Processing)、编译(Compilation)、汇编(Assembly)、链接(Linking)
预编译
预编译命令:( -E
表示只进行预编译)
gcc -E hello.c -o hello.i
-
处理源代码文件
hello.c
相关的头文件。例如:stdio.h
等被预编译器cpp预编译成一个.i
文件。(在c++中,源文件扩展名为.cpp/.cxx
,头文件扩展为.hpp
,预编译后的文件扩展名为.ii
。 -
处理源代码文件中,以”#“开头的预编译指令。例如 #include、#define 等。处理规则有以下几点:
- 将所有的
#define
删除,并且扩展所有的宏定义。 - 处理所有条件预编译指令,例如
#if、#ifdef、#elif、#else、#endif
。 - 处理
#include
预编译指令,将被包含的文件插入到该预编译指令的位置。(这个过程是递归进行的,也就是被包含的文件可能还包含其他文件) - 删除所有的注释
//
和/**/
; - 添加行号和文件标识,比如:
# 2 "hello.c" 2
,以便编译时编译器产生调试用的行号信息及用于编译时产生编译错误或警告时能够显示行号。 - 保留所有的
#pragma
编译器指令,因为编译器需要使用他们。
- 将所有的
查看预编译后的文件,可以确定:
- 宏定义是否正确
- 头文件包含是否正确
编译
编译过程就是把所有预处理完的文件进行一些列词法分析、语法分析、语义分析及优化后产生相应的汇编代码文件。这个过程是整个程序构建的核心部分,也是最复杂的部分之一。
- 编译上面预编译之后的文件
hello.i
gcc -S hello.i -o hello.s
来看看结果,感受一下汇编的魅力😏
.section __TEXT,__text,regular,pure_instructions
.build_version macos, 10, 15 sdk_version 10, 15, 6
.globl _main ## -- Begin function main
.p2align 4, 0x90
_main: ## @main
.cfi_startproc
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
subq $16, %rsp
movl $0, -4(%rbp)
leaq L_.str(%rip), %rdi
movb $0, %al
callq _printf
xorl %ecx, %ecx
movl %eax, -8(%rbp) ## 4-byte Spill
movl %ecx, %eax
addq $16, %rsp
popq %rbp
retq
.cfi_endproc
## -- End function
.section __TEXT,__cstring,cstring_literals
L_.str: ## @.str
.asciz "Hello World\n"
.subsections_via_symbols
- 也可以使用 gcc 直接编译
hello.c
文件
gcc -S hello.c -o hello2.s
hello2.s
文件内容与上面贴出来的内容一模一样,我们要知道这个过程经历了 预编译+编译
两个过程即可
gcc是什么?
这个命令其实是包装过的命令,会根据不同的参数去调用预编译程序cc1、汇编器as、链接器ld
。
- C文件:gcc 是 cc1
- C++文件:gcc 是 cc1plus
- Objective-C:gcc 是 cc1obj
- fortran:gcc 是 f771
- java:gcc 是 jc1
这个内容了解即可,不需要深究 gcc的原理
汇编
汇编器的定义
汇编器是将汇编代码转变成可以执行的指令,每一个汇编语句几乎都对应一条机器指令。
汇编器的特点
- 汇编过程相对于编译器来讲比较简单,没有复杂的语法
- 没有语义
- 不需要做指令优化
汇编的指令 - 调用汇编器 as 来完成
as hello.s -o hello.o - 使用 gcc 来完成
或者是直接从c源码输出目标文件(Object File)gcc -c hello.s -o hello.o
gcc -c hello.c -o hello.o
链接
需要搞懂的几个问题
- 为什么汇编器不直接输出可执行文件而是输出一个目标文件?
- 链接过程到底包含什么内容?
- 为什么要链接?
ld 链接示例
ls -static /usr/lib/crt1.o /usr/lib/crti.o /usr/lib/gcc/i486-linux-gnu/4.1.3 -L/usr/lib -L/lib hello.o --start-group -lgcc -lgcc_he -lc --end-group /usr/lib/gcc/i486-linux-gnu/4.1.3/crtend.o /user/lib/crtn.o
这是从书中手敲下来的一段示例,书中介绍的是Linux环境下的链接器,而我的电脑是Macos,示例中的一些目标文件无法获取,所以这行代码是没有运行的,但这并不影响我们来理解 链接器ld。
在终端中执行:man ld,可以查看 链接器ld 的用法,截取前面一段内容如下所示
NAME
ld -- linker
SYNOPSIS
ld files... [options] [-o outputfile]
DESCRIPTION
The ld command combines several object files and libraries, resolves references, and pro-
duces an ouput file. ld can produce a final linked image (executable, dylib, or bundle), or
with the -r option, produce another object file. If the -o option is not used, the output
file produced is named "a.out".
- 名称:链接器
- 概要(用法):
ld files... [options] [-o outputfile]
-
files...
:表示可以链接多个文件 -
[options]
:表示一些可选配置 -
[-o outputfile]
:表示可以设置输出文件
-
- 描述:ld命令组合多个目标文件和库,解析引用并生成一个输出文件(可执行文件)。ld可以生成最终的镜像(可执行文件、动态库或包),也可使用 -r 选项生成另一个目标文件。如果没有使用 -o 指定输出文件,则会默认生成输出文件 a.out。
看到这么多内容,基本上可以对 链接器ld 有一个大概的印象了,具体的用法可以往后查阅 options 的含义。 -
-static
:生成不使用 dyld的 mach-o文件,仅用于构建内核。
其他的命令后面再具体分析,这里就不做过多的解释了。