C/C++的编译过程
以GCC编译器为例
总览
使用gcc命令行一步一步编译的命令为:
gcc [option] -o 用于保存结果的文件名 输入文件名
|
其中option即为图中的-E -S -c
一个源代码文件要经过上述过程才能被编译成可执行文件,下面对其中每个步骤做一些介绍
预处理
一个源代码中包含了#include、#ifdef等预处理语句以及宏定义等内容,而在编译时,它们都会被展开,例如如下的源程序
#include <stdio.h> #include "add.h"
#define PI 3.14 int main(int argc, char const* argv[]) { char temp[80]; double ss = PI; printf("Hello,World!\n"); printf("%d\n", add(1, 2)); return 0; }
|
#ifndef __ADD_H__ #define __ADD_H__
int add(int a, int b);
#endif
|
#include "add.h"
int add(int a, int b) { return a + b; }
|
我们include了stdio.h,add.h这些头文件,宏定义了一个PI,并在main函数中进行了一些简单的操作,在add.c中实现了一个简单的加法函数,而对main.c预处理之后,它变成了这样
# 1 "./main.c" # 1 "<built-in>"
const char *__mingw_get_crt_info (void); # 11 "D:/Tools/GCC/x86_64-w64-mingw32/include/crtdefs.h" 2 3
#pragma pack(push,_CRT_PACKING) # 35 "D:/Tools/GCC/x86_64-w64-mingw32/include/crtdefs.h" 3 __extension__ typedef unsigned long long size_t; # 45 "D:/Tools/GCC/x86_64-w64-mingw32/include/crtdefs.h" 3 __extension__ typedef long long ssize_t;
typedef size_t rsize_t; # 62 "D:/Tools/GCC/x86_64-w64-mingw32/include/crtdefs.h" 3 __extension__ typedef long long intptr_t; # 75 "D:/Tools/GCC/x86_64-w64-mingw32/include/crtdefs.h" 3 __extension__ typedef unsigned long long uintptr_t; # 88 "D:/Tools/GCC/x86_64-w64-mingw32/include/crtdefs.h" 3 __extension__ typedef long long ptrdiff_t; # 98 "D:/Tools/GCC/x86_64-w64-mingw32/include/crtdefs.h" 3 typedef unsigned short wchar_t;
typedef unsigned short wint_t; typedef unsigned short wctype_t;
typedef int errno_t;
# 2 "./main.c" 2
# 1 "./add.h" 1
# 4 "./add.h" int add(int a, int b); # 4 "./main.c" 2
int main(int argc, char const* argv[]) { char temp[80]; double ss = 3.14; printf("Hello,World!\n"); printf("%d\n", add(1, 2)); return 0; }
|
这种看着像C语言,但又有些不同的语法,便是编译器对源文件预处理后得到的中间产物。对比源文件我们可以发现,我们#include进来的<stdio.h> “add.h” 文件的内容被替换在引入头文件的位置了,且add.h中的预处理命令#ifndef、#define、#endif等等都被处理完了。
而add.c在预处理后变成了这样
# 1 "./add.c" # 1 "<built-in>" # 1 "<command-line>" # 1 "./add.c" # 1 "./add.h" 1
int add(int a, int b); # 2 "./add.c" 2
int add(int a, int b) { return a + b; }
|
编译
此处的编译是将预处理后的那种带有奇怪标记的中间文件编译成汇编语言文件
此处得到的结果为
//main.s .file "main.c" .text .def __main; .scl 2; .type 32; .endef .section .rdata,"dr" .LC1: .ascii "Hello,World!\0" .LC2: .ascii "%d\12\0" .text .globl main .def main; .scl 2; .type 32; .endef .seh_proc main main: pushq %rbp .seh_pushreg %rbp movq %rsp, %rbp .seh_setframe %rbp, 0 addq $-128, %rsp .seh_stackalloc 128 .seh_endprologue movl %ecx, 16(%rbp) movq %rdx, 24(%rbp) call __main movsd .LC0(%rip), %xmm0 movsd %xmm0, -8(%rbp) leaq .LC1(%rip), %rcx call puts movl $2, %edx movl $1, %ecx call add movl %eax, %edx leaq .LC2(%rip), %rcx call printf movl $0, %eax subq $-128, %rsp popq %rbp ret .seh_endproc .section .rdata,"dr" .align 8 .LC0: .long 1374389535 .long 1074339512 .ident "GCC: (x86_64-posix-seh-rev0, Built by MinGW-W64 project) 8.1.0" .def puts; .scl 2; .type 32; .endef .def add; .scl 2; .type 32; .endef .def printf; .scl 2; .type 32; .endef
|
没学过汇编语言的人可能会想:“这啥啊,看都看不懂,我写的不是C语言吗,怎么变成这样了”
在编译过程中,将C语言编译成汇编语言,我们的工作就成功大半了。因为汇编语言与机器语言(计算机可以识别、执行的命令)是一一对应的,接下来再进行一些小步骤,就能把我们的文本文件变成计算机可以执行的文件了。
不过这里让我们略微看一看这个汇编代码,在这里
call puts movl $2, %edx movl $1, %ecx call add movl %eax, %edx leaq .LC2(%rip), %rcx call printf
|
是不是看到了一些熟悉的字眼?puts、printf这些都是C语言提供的函数,而add函数是我们自己定义的。call的意思就是去调用这些函数。
汇编
得到了汇编代码就可以简单地把它翻译成机器语言了
编译得到main.o文件,使用著名反汇编分析程序IDA打开它,可以发现,它已经不再是简单的文本文件,但当使用软件将它从机器语言再翻译回汇编语言
还是能从中发现一些端倪,比如main函数调用了puts、add、printf函数等
但当我们追踪下去,却发现
它们无一例外地,全都只是一个空入口,此时,编译得到的文件虽然已经是计算机认识的代码了,但有些函数调用只是在原地留了个坑,程序还不知道如何去调用这些程序。
而汇编add.s文件后,得到了add函数。
那么如何让我们main.o与add.o以及printf、puts函数的实现文件联合起来呢?快进到链接。
链接
链接不需要加额外参数,只要执行
gcc -o main ./main.o ./add.o
|
便可以得到可执行文件main.exe了,执行试试
嗯,不错。
此时我们再用IDA来分析main.exe文件
找不同时间到,这与刚刚得到的main.o文件有什么不同呢?
答案是,几乎没有区别,但是!puts、add、printf等字眼变成蓝色了,此时我们再追踪这些函数,终于找到了它们真实的函数入口
例如add函数
至此,我们便完成了一个分文件的项目的编译,由于项目简单,仅有两个源文件,采用gcc命令行编译&链接的方式也不算繁琐。但当项目变得庞大起来,这种方法还靠谱吗?