编译说明
步骤 | 未编译 | 预编译 | 编译 | 汇编 | 链接 |
---|---|---|---|---|---|
文件 | hello.c | hello.i | hello.s | helle.o | hello |
详解
1. 预编译
预编译过程主要做四件事
- 展开头文件,在写有#include 或#include "filename"的文件中,将文件filename展开,通俗来说就是将filename文件中的代码写入到当前文件中:
- 宏替换
- 去掉注释
- 条件编译,即对#ifndef #define #endif进行判断检查,也正是在这一步,#ifndef #define #endif的作用体现出来,即防止头文件被多次重复引用
2. 编译
将代码转换成汇编代码,并且在这个步骤中做了两件重要的的工作:
- 编译器在每个文件中保存一个函数地址符表,该表中存储着当前文件内包含的各个函数的地址;
- 因为这步要生成汇编代码,即一条一条的指令,而调用函数的代码会被编译成一条call指令,call指令后面跟的是jmp指令的汇编代码地址,而jmp指令后面跟的才是(被调用的函数编译成汇编代码后的第一条指令“的地址,但是给call指令后面补上地址的工作是在链接的时候做的事情。
3. 汇编
将汇编代码转成机器码
4.链接
编译器将生产的多个.o文件链接到一起生成也给可执行.exe文件;
但是这个过程中,编译器做的一个重要的事情是将每个文件中call指令后面的地址补充上;
方式是从当前文件的函数地址符表中开始找,如果没有,继续向别的文件的函数地址符表中找,找到后补在call指令后面,如果找不到,则链接失败
综述
从一个源文件(.c)到可执行程序到底经历了哪几步,我想大多数的人都知道,到时到底每一步都做了什么,我估计也没多少人能够说得清清楚楚,明明白白。
其实总的流程是这样的。
.c文件—>预编译—>编译—>链接(包括库文件)—>可执行程序
实验操作
编辑hello.c
#include <stdio.h>
int main(int argc, char **argv)
{
printf("Hello World!\n");
return 0;
}
【第一步】预处理
预处理过程实质上是处理“#”,将#include包含的头文件直接拷贝到hell.c当中;将#define定义的宏进行替换,同时将代码中没用的注释部分删除等
具体做的事儿如下:
- 将所有的#define删除,并且展开所有的宏定义。说白了就是字符替换
- 处理所有的条件编译指令,#ifdef #ifndef #endif等,就是带#的那些
- 处理#include,将#include指向的文件插入到该行处
- 删除所有注释
- 添加行号和文件标示,这样的在调试和编译出错的时候才知道是是哪个文件的哪一行
- 保留#pragma编译器指令,因为编译器需要使用它们。
gcc -E hello.c -o hello.i
先将.c文件预编译为.i,可以生成预处理后的文件。通过查看文件内容和文件大小可以得知a.c讲stdio.h和stdlib.h包含了进来。
zhou@ubuntu:~/eclipse-workspace/hello$ gcc -E hello.c -o hello.i
zhou@ubuntu:~/eclipse-workspace/hello$ ls -l
total 28
-rwxr--r-- 1 zhou zhou 95 Feb 14 10:50 hello.c
-rw-rw-r-- 1 zhou zhou 17177 Feb 14 10:59 hello.i
-rwxr--r-- 1 zhou zhou 754 Feb 14 10:58 Makefile
【第二步】编译
gcc -S hello.i -o hello.s
编译的过程实质上是把高级语言翻译成机器语言的过程,即对hello.i做了这些事儿
- 词法分析,
- 语法分析
- 语义分析
- 优化后生成相应的汇编代码
从 高级语言->汇编语言->机器语言(二进制)
zhou@ubuntu:~/eclipse-workspace/hello$ gcc -S hello.i -o hello.s
zhou@ubuntu:~/eclipse-workspace/hello$ ls -l
total 32
-rwxr--r-- 1 zhou zhou 95 Feb 14 10:50 hello.c
-rw-rw-r-- 1 zhou zhou 17177 Feb 14 10:59 hello.i
-rw-rw-r-- 1 zhou zhou 502 Feb 14 11:01 hello.s
-rwxr--r-- 1 zhou zhou 754 Feb 14 10:58 Makefile
汇编代码如下:
.file "hello.c"
.section .rodata
.LC0:
.string "Hello World!"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movl %edi, -4(%rbp)
movq %rsi, -16(%rbp)
movl $.LC0, %edi
call puts
movl $0, %eax
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 4.8.4-2ubuntu1~14.04.4) 4.8.4"
.section .note.GNU-stack,"",@progbits
【第三步】汇编
gcc -c hello.s -o hello.o
,将源文件翻译成二进制文件。类Uinx系统编译的结果生生成.o文件,Windows系统是生成.obj文件。- 编译的过程就是把hello.c翻译成二进制文件
zhou@ubuntu:~/eclipse-workspace/hello$ gcc -c hello.s -o hello.o
zhou@ubuntu:~/eclipse-workspace/hello$ ls -l
total 36
-rwxr--r-- 1 zhou zhou 95 Feb 14 10:50 hello.c
-rw-rw-r-- 1 zhou zhou 17177 Feb 14 10:59 hello.i
-rw-rw-r-- 1 zhou zhou 1512 Feb 14 11:04 hello.o
-rw-rw-r-- 1 zhou zhou 502 Feb 14 11:01 hello.s
-rwxr--r-- 1 zhou zhou 754 Feb 14 10:58 Makefile
【第四步】链接
就像刚才的hello.c它使用到了C标准库的东西“printf”,但是编译过程只是把源文件翻译成二进制而已,这个二进制还不能直接执行,这个时候就需要做一个动作,
将翻译成的二进制与需要用到库绑定在一块。打个比方编译的过程就向你对你老婆说,我要吃雪糕。你只是给你老婆发出了你要吃雪糕的诉求而已,但是雪糕还没有到。
绑定就是说你要吃的雪糕你的老婆已经给你买了,你可以happy。
gcc hello.c -o hello可以生成可执行程序。即gcc不带任何参数。ldd就可以看到你的可执行程序依赖的库。
zhou@ubuntu:~/lvhui/eclipse-workspace/hello$ ls -lh
total 48K
-rwxrwxr-x 1 zhou zhou 8.4K Feb 14 12:26 hello
-rwxr--r-- 1 zhou zhou 95 Feb 14 10:50 hello.c
-rw-rw-r-- 1 zhou zhou 17K Feb 14 10:59 hello.i
-rw-rw-r-- 1 zhou zhou 1.5K Feb 14 11:04 hello.o
-rw-rw-r-- 1 zhou zhou 502 Feb 14 11:01 hello.s
-rwxr--r-- 1 zhou zhou 754 Feb 14 10:58 Makefile
zhou@ubuntu:~/lvhui/eclipse-workspace/hello$ ./hello
Hello World!
zhou@ubuntu:~/lvhui/eclipse-workspace/hello$ ldd hello
linux-vdso.so.1 => (0x00007ffcc26b4000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f9a27f37000)
/lib64/ld-linux-x86-64.so.2 (0x0000562d05411000)
可以看到hello.o的大小是1.5k,毕竟他只是把源文件翻译成二进制文件。hello却有8.4k,应该是他多了很多“绳子”吧。在运行的时候这些“绳子”就将对应的库函数“牵过来”。很形象的比喻是不是?哈哈。libc.so.6 中就对咱们用的printf进行了定义。
这就是编写的整个流程,(⊙o⊙)。谢谢各位看官。不足的地方请不吝赐教。