建立简单的网站/哪里有培训网

目录
- 7、C 语言程序编译的背后
- 7.1、我们如何编译运行 C 语言程序
- 7.2、整个编译过程的内部发生了什么
- 7.2.1 预处理
- 7.2.2 编译
- 7.2.3 汇编
- 7.2.4 链接
7、C 语言程序编译的背后
C 语言是一种高级编程语言,它需要一个编译器将其转换为可执行代码才能在我们的机器上运行。
7.1、我们如何编译运行 C 语言程序
在下面的示例中,我们使用 带有 gcc 编译器的 Ubuntu 系统作为演示,我们首先打开编辑器,编写我们的 C 语言程序,并保存为 filename.c 。
利用下面 vim 命令,创建一个新文本:
vim filename.c
//program for the addition of two numbers
#include<stdio.h>
#define add(a, b) (a+b) //using macrosint main()
{int a=5, b=4;printf("Addition is: %d\n", add(a, b));return 0;
}
上面的程序实现了一个简单的加法运算,我们接下来用下面的命令对源代码进行编译:
gcc -Wall filename.c -o filename # 编译源代码
ls -F # 展示当前目录下的文件# filename* filename.c 第一个文件后面的 * 表示这是一个可执行文件(* 不是文件名的一部分)
参数 -Wall 能够获取编译过程的所有警告信息,这个可选参数被推荐用来生成更好的代码。
参数 -o 用来指定输出文件的名字,如果我们不加该参数,默认的输出文件名称为 a.out
在编译完成后,我们可以直接运行得到的可执行文件:
./filename
# Addition is: 9
7.2、整个编译过程的内部发生了什么
编译器将一个 C 语言源码转换为一个可执行文件,这中间涉及到了 4 个阶段:
- 预处理
- 编译
- 汇编(Assembly)
- 链接
通过执行下面的命令,我们将会得到编译过程中所有的中间文件:
gcc –Wall –save-temps filename.c –o filename ls -F# filename* filename.c filename.i filename.o filename.s
接下来,我们挨个看看这些文件都是什么内容
7.2.1 预处理
这是第一阶段,是基于源代码处理的,这个阶段的工作包括:
- 移除所有的注释内容
- 宏扩展
- 头文件扩展
- 有条件的编译
预处理阶段输出的内容存储在 filename.i 文件中,我们看看这个文件里的内容
1 # 1 "filename.c"2 # 1 "<built-in>"3 # 1 "<command-line>"4 # 31 "<command-line>"5 # 1 "/usr/include/stdc-predef.h" 1 3 46 # 32 "<command-line>" 27 # 1 "filename.c"...
853 # 5 "filename.c"
854 int main()
855 {
856 int a=5, b=4;
857 printf("Addition is: %d\n", (a+b));
858 return 0;
859 }
在上面的输出内容中,源文件被塞满了很多很多信息,但是在文件的最后我们会看到我们编写的源代码。
分析:
- 输出语句中,现在是 a+b 而不是 add(a,b),这是因为宏定义已经被展开替换
- 注释内容全部不见了
- #inclue 部分不见了,被替换为许多代码,这是因为头文件中的代码被复制进源代码中间中了
7.2.2 编译
接下来我们对 filename.i 进行编译并产生一个中间结果文件 filename.s 。这个文件包含一些汇编级别的指令,我们可以打开看一下:
.file "filename.c".section .rodata
.LC0:.string "Addition is: %d\n".text.globl main.type main, @function
main:
.LFB0:.cfi_startprocpushq %rbp.cfi_def_cfa_offset 16.cfi_offset 6, -16movq %rsp, %rbp.cfi_def_cfa_register 6subq $16, %rspmovl $5, -8(%rbp)movl $4, -4(%rbp)movl -8(%rbp), %edxmovl -4(%rbp), %eaxaddl %edx, %eaxmovl %eax, %esimovl $.LC0, %edimovl $0, %eaxcall printfmovl $0, %eaxleave.cfi_def_cfa 7, 8ret.cfi_endproc
.LFE0:.size main, .-main.ident "GCC: (Ubuntu 6.5.0-2ubuntu1~16.04) 6.5.0 20181026".section .note.GNU-stack,"",@progbits
7.2.3 汇编
在这个阶段,filename.s 作为输入文件,通过汇编程序转成 filename.o 文件,这个文件包含了一些机器级别的指令。在这个阶段,只是将已存在的代码转换成机器语言,但是像 printf 这种函数调用依然没有得到解决(注意,我们在预处理阶段从 stdio.h 文件中只是把 printf 函数的声明拿了过来,具体的实现交给链接器),我们看一下 filename.o 文件中的内容,你会看到一些类似乱码一样的符号。
7.2.4 链接
这是最后一个阶段,在这个阶段中,所有函数调用与其定义的链接都完成了。链接器知道所有这些函数的实现位置。链接器也做一些额外的工作,它添加一些额外的代码到我们的程序中,这是程序开始和结束时所必需的。例如,有一个设置环境(如传递命令行参数)所需的代码。使用 $size filename.o
和 $size filename
可以轻松验证此任务。通过这些命令,我们知道输出文件如何从对象文件增加到可执行文件。这是因为链接器在我们的程序中添加了额外的代码。
(base) root@ml:/ml/home/dfsj# size filename.otext data bss dec hex filename127 0 0 127 7f filename.o
(base) root@ml:/ml/home/dfsj# size filenametext data bss dec hex filename1221 552 8 1781 6f5 filename
注意 GCC 默认实现了动态链接,所以 printf() 是动态链接到上面程序中的,关于静态与动态链接的更多内容可以参考:这里, 这里 还有 这里 。
This article is contributed by Vikash Kumar.