目录
- 1.程序从源文件到结果输出的执行过程
- 2.预处理
- 3.编译
- 3.1 词法分析
- 3.2 语法分析
- 3.3 语义分析
- 3.4 生成test.s文件
- 4.汇编
- 5.链接
- 6.运行
1.程序从源文件到结果输出的执行过程
2.预处理
预处理阶段的执行操作:
预处理阶段会将#define定义的常量或宏进行替换,经过预处理后的文件#define的语句就不存在;
预处理阶段会将注释进行删除,并对行号进行标识;
预处理阶段会处理#include包含的头文件,将其内容进行插入,此过程是可以递归进行的,因为包含的头文件中可能包含其它头文件;
预处理阶段会处理#if #endif #elif #ifdef #else的条件编译指令,对表达式进行判断处理;
预处理阶段会保留所有#pragma指令。
以下为简单例子展示,创建三个文件:Add.h,Add.c,test.c
//Add.h文件
#pragma once//防止头文件重复包含//类似效果
//#ifndef C //判断是否没有定义符号M
//
//#define C //定义符号C
//
//#include<stdio.h>//重复包含多次
//#include<stdio.h>
//#include<stdio.h>
//
//#endif//结束#ifenf条件判断指令#include<stdio.h>#ifndef M //判断是否没有定义符号M#define M 100//定义符号M#endif//结束#ifenf条件判断指令#define N 200int Add(int x, int y);//函数声明
//Add.c文件
#include"Add.h"//包含头文件
//函数定义
int Add(int x, int y)
{return (x + y);
}
//test.c文件
#include "Add.h"//包含头文件int main()
{//输出两个数的和printf("%d", Add(M, N));return 0;
}
将以上代码在VS环境下输入后Ctrl + S保存,打开文件保存的位置,点击输入cmd后回车,打开cmd.
打开cmd后需要输入指令,将源文件转变为.i为后缀的中间文件
gcc -E test.c -o test.i
gcc是一个编译器,需要下载相应的插件才可以使用,具体流程可以参考以下文章:
https://blog.csdn.net/qq_36318563/article/details/140336690
以上文章可能介绍的网站打不开,可以使用以下链接:MinGW下载
输入以上指令后会生成一个test.i的文件,可以在VS2022中打开观察
因为头文件的插入有400多行代码,以上就不做过多的展示,最后可以看到处理后的#define定义的常量和条件编译指令进行了处理和替换,并将注释删除。
3.编译
编译阶段会进行词法分析,语法分析和语义分析及优化,以下面例子为参考
arr[n] = i + 1;
3.1 词法分析
词法分析阶段会将语句的标识符,操作符,数字进行标记,以上语句可以得到以下标记
3.2 语法分析
语法分析阶段会根据标记生成语法树,语法树是根据表达式为节点的数。
3.3 语义分析
语义分析阶段是对语法层面的意思进行转换,包括声明,类型的匹配和转换等,此时会报告错误信息,经过语义分析后的语法树。
3.4 生成test.s文件
对test.i的中间文件通过以下指令编译生成test.s的文件
gcc -S test.i -o test.s
将生成的test.s文件在VS2022中打开,文件内容是汇编代码。
.file "test.c".text.globl _Add.def _Add; .scl 2; .type 32; .endef
_Add:
LFB10:.cfi_startprocpushl %ebp.cfi_def_cfa_offset 8.cfi_offset 5, -8movl %esp, %ebp.cfi_def_cfa_register 5movl 8(%ebp), %edxmovl 12(%ebp), %eaxaddl %edx, %eaxpopl %ebp.cfi_restore 5.cfi_def_cfa 4, 4ret.cfi_endproc
LFE10:.def ___main; .scl 2; .type 32; .endef.section .rdata,"dr"
LC0:.ascii "%d\0".text.globl _main.def _main; .scl 2; .type 32; .endef
_main:
LFB11:.cfi_startprocpushl %ebp.cfi_def_cfa_offset 8.cfi_offset 5, -8movl %esp, %ebp.cfi_def_cfa_register 5andl $-16, %espsubl $16, %espcall ___mainmovl $200, 4(%esp)movl $100, (%esp)call _Addmovl %eax, 4(%esp)movl $LC0, (%esp)call _printfmovl $0, %eaxleave.cfi_restore 5.cfi_def_cfa 4, 4ret.cfi_endproc
LFE11:.ident "GCC: (MinGW.org GCC-6.3.0-1) 6.3.0".def _printf; .scl 2; .type 32; .endef
4.汇编
汇编过程是将汇编代码转换为二进制代码,每一条汇编语句都代表一条指令,将汇编语句与机器语句通过对照表转换,不做任何优化,通过以下指令将test.s的文件转换为test.o的目标文件。
gcc -c test.s -o test.o
打开test.o的文件,可以观察到都是二进制的编码
L?.textH? 0`.data@0?bss€0?rdataL@0@/4$P@0@/15Xt?@0@U夊婾婨衇肬夊冧饍?枨D$惹$d柩塂$?$韪擅悙%dGCC: (MinGW.org GCC-6.3.0-1) 6.3.0zR|?
A?B
I?<9A?B
u?6; @.file?gtest.c_Add _main.textF.data.bss.rdata#$X___main _printf ..rdata$zzz.eh_frame.rdata$zzz.eh_frame
5.链接
链接过程主要处理地址和空间分配,符号决议和符号重定位等操作,最后通过链接库链接生成可执行程序。
例子:
add.c文件int n = 100;//全局变量int Add(int x,int y)
{return x + y;
}
test.c文件extern int n;//声明外部符号
extern int Add(int ,int)//声明外部符号int main()
{int r = Add(2,3);printf("r = %d",r);printf("n = %d",n);return 0;
}
地址和空间分配在链接过程,每一个.o为后缀的文件生成后都会有对应的符号表,对于add.o文件和test.o文件如下:一般符号标记录的是函数名和全局变量或静态变量。
因为test.c文件中的n和Add是外部符号,在链接前并不确定这些外部符号的具体地址,系统会随机分配一个虚假的地址;有了各自文件的符号表就可以进行符号的决议和重定位。
通过以上过程确定最终的符号表,再通过链接库的链接就可以生成可执行程序,可以通过以下的指令
//因为需要test.o和Add.o文件进行链接,此时有了test.o文件,再生成一个Add.o的文件gcc -c Add.c -o Add.o
有了test.o和Add.o文件就可以通过链接库链接生成可执行程序,使用以下指令
//生成一个test.exe的可执行程序
gcc test.o Add.o -o test
6.运行
运行过程需要在运行时环境下进行,可执行程序的运行必须先将程序植入内存,在操作系统中,一般由操作系统完成;调用函数时会开辟运行时堆栈(即函数栈帧的创建),一般函数是从main函数进入,在开辟过程会保存局部变量和函数的地址,全局或静态变量会存储于静态区,直到函数销毁而销毁,最后随main函数的结束而终止程序 ,并输出结果。
对应可执行程序在cmd指令中直接输入文件名后回车即可输出结果:
test.exe
以上内容只是编译和链接的大概介绍,如果对编译链接想要进一步的了解,可以参考以下书籍:
《程序员的自我修养》
其它推荐书籍:高质量c/c++编程