转帖自:http://hi.baidu.com/lru52777/blog/item/244aa913b9faaf0e5aaf537c.html
总体来说,C/C++源代码要经过:预处理、编译、汇编和连接四步才能变成相应平台下的可执行文件。大多数时候,程序员通过一个命令就能完成上述四个步骤。比如下面这段C的“Hello world!”代码:
File: hw.c
#include <stdio.h>
int main(int argc, char *argv[]) { printf("Hello World!/n");
return 0; }
|
如果用gcc编译,只需要一个命令就可以生成可执行文件hw:
xiaosuo@gentux hw $ gcc -o hw hw.c xiaosuo@gentux hw $ ./hw Hello World! |
我们可以用-v参数来看看gcc到底在背后都做了些什么动作:
稍微整理一下,去掉一些冗余信息后,如下:
cc1 hw.c -o /tmp/ccYB6UwR.s as -o /tmp/ccq8uGED.o /tmp/ccYB6UwR.s ld -o hw /tmp/ccq8uGED.o |
以上三个命令分别对应于编译步骤中的预处理+编译、汇编和连接。预处理和编译还是放在了一个命令(cc1)中进行的,可以把它再次拆分为以下两步:
cpp -o hw.i hw.c cc1 hw.i -o /tmp/ccYB6UwR.s |
接下来我们按照编译顺序看看编译器每一步都做了什么。
首先是预处理,预处理后的文件hw.i:
# 1 "hw.c" # 1 "<built-in>" # 1 "<command line>"
... __extension__ typedef __quad_t __off64_t; __extension__ typedef int __pid_t; __extension__ typedef struct { int __val[2]; } __fsid_t;
... extern int remove (__const char *__filename) __attribute__ ((__nothrow__));
extern int rename (__const char *__old, __const char *__new) __attribute__ ((__nothrow__));
...
int main(int argc, char *argv[]) { printf("Hello World!/n");
return 0; } |
注:由于文件比较大,所以只留下了少部分具有代表性的内容。
可以看见预处理器把所有要包含(include)的文件(包括递归包含的文件)的内容都添加到了原始的C源文件中,然后把其输出到输出文件,除此之外,它还展开了所有的宏定义,所以在预处理器的输出文件中你将找不到任何宏。这也提供了一个查看宏展开结果的简便方法。
第二步“编译”,就是把C/C++代码“翻译”成汇编代码:
.file "hw.c" .section .rodata .LC0: .string "Hello World!/n" .text .globl main .type main, @function main: pushl %ebp movl %esp, %ebp subl $8, %esp andl $-16, %esp movl $0, %eax addl $15, %eax addl $15, %eax shrl $4, %eax sall $4, %eax subl %eax, %esp subl $12, %esp pushl $.LC0 call printf addl $16, %esp movl $0, %eax leave ret .size main, .-main .section .note.GNU-stack,"",@progbits .ident "GCC: (GNU) 3.4.6 (Gentoo 3.4.6-r2, ssp-3.4.6-1.0, pie-8.7.10)"
|
这个汇编文件比预处理后的C/C++文件小了很多,去除了很多不必要的东西,比如说没用到的类型声明和函数声明等。
第三步“汇编”,将第二步输出的汇编代码翻译成符合一定格式的机器代码,在Linux上一般表现为ELF目标文件。
xiaosuo@gentux hw $ file hw.o hw.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped
|
最后一步“连接”,将上步生成的目标文件和系统库的目标文件和库文件连接起来,最终生成了可以在特定平台运行的可执行文件。为什么还要连接系统库中的某些目标文件(crt1.o, crti.o等)呢?这些目标文件都是用来初始化或者回收C运行时环境的,比如说堆内存分配上下文环境的初始化等,实际上crt也正是C RunTime的缩写。这也暗示了另外一点:程序并不是从main函数开始执行的,而是从crt中的某个入口开始的,在Linux上此入口是_start。以上Makefile生成的是动态连接的可执行文件,如果要生成静态连接的可执行文件需要将Makefile中的相应段修改:
hw: hw.o ld -m elf_i386 -static -o hw /usr/lib/crt1.o / /usr/lib/crti.o / /usr/lib/gcc/i686-pc-linux-gnu/3.4.6/crtbeginT.o / -L/usr/lib/gcc/i686-pc-linux-gnu/3.4.6 / -L/usr/i686-pc-linux-gnu/lib / -L/usr/lib/ / hw.o --start-group -lgcc -lgcc_eh -lc --end-group / /usr/lib/gcc/i686-pc-linux-gnu/3.4.6/crtend.o / /usr/lib/gcc/i686-pc-linux-gnu/3.4.6/../../../crtn.o
|
至此,一个可执行文件才最终创建完成。通常的项目中并不需要把编译过程分得如此之细,前三步一般是合为一体的,在Makefile中表现如下:
hw.o: hw.c gcc -o hw.o -c hw.c
|
实际上,如果对hw.c进行了什么更改,那么前三步大多数情况下都是不可避免的。所以把他们写在一起也并没有什么坏处,相反倒可以用--pipe参数告诉编译器用管道替代临时文件,从而提升编译的效率。