Linux——gcc编译过程详解与ACM时间和进度条的制作
gcc编译过程详解与ACM时间和进度条的制作
文章目录
- gcc编译过程详解与ACM时间和进度条的制作
- 1. 编译详细过程与ACM时间
- 1.1 预处理
- 1.2 编译
- 1.3 汇编
- 1.4 链接
- 1.5 C语言和C++在函数符号形成上的区别
- 1.6 编译器与ACM时间的关系
- 2. 进度条的制作
- \r
- C语言标准输出的缓冲区
- sleep()和usleep()
- 倒计时
- 简单的进度条
- 带颜色的进度条
1. 编译详细过程与ACM时间
我们知道编译分为四大步:预处理、编译、汇编、链接,但是详细的情况并不像我们想的那样,因为编译器会做很多事情,比如:宏替换、头文件展开、语法检查、语义检查、代码优化、生成汇编代码、生成可执行文件等。
1.1 预处理
gcc -E main.c -o main.i
解析:
- gcc:编译器
- -E:将源代码文件进行预编译处理,形成.i文件
- main.c:源代码文件
- -o:指定目标文件的名称或者存放路径
- main.i:预处理后的目标文件
预处理主要完成以下工作:
- 头文件展开
- 处理所有#include预处理指令
- 将头文件的内容复制到当前文件
- 可以递归展开(头文件中包含的头文件)
- 宏定义替换
- 展开所有#define宏定义
- 处理条件编译指令(#if、#ifdef、#ifndef等)
- 展开所有宏调用
#define Max(a, b) ((a) > (b) ? (a) : (b))
int max = Max(3, 4); //展开为:int max = ((3) > (4) ? (3) : (4))
-
条件编译处理
- #if、#ifdef、#ifndef:条件判断
- #elif、#else:分支处理
- #endif:结束条件编译
#ifdef DEBUG printf("debug info\n"); #endif
-
删除注释
- 删除所有//和/* */格式的注释
- 每个注释都替换为一个空格
-
添加行号和文件名标识
- 使用#line指令标记行号和文件名
- 用于编译器产生调试信息和编译错误提示
-
处理特殊预处理指令
- #pragma:编译器指令
- #error:产生编译错误
- #warning:产生编译警告
-
保留所有的换行符
- 确保错误提示的行号正确
- 方便调试
-
字符串常量化
- 处理#运算符,将宏参数转换为字符串
#define STR(s) #s STR(hello) // 展开为: "hello"
-
宏连接操作
- 处理##运算符,连接两个记号
#define CONCAT(a,b) a##b CONCAT(x,y) // 展开为: xy
示例:
// 源文件 main.c
#include <stdio.h>
#define MAX 100
#define SQUARE(x) ((x)*(x))int main() {int value = SQUARE(MAX);return 0;
}// 预处理后 main.i(简化版)
// ... stdio.h的内容 ...
int main() {int value = ((100)*(100));return 0;
}
预处理的作用:
- 将源文件转换为完整的C/C++代码
- 处理编译器不关心的细节
- 提供代码复用和条件编译的机制
- 简化程序的编写和维护
注意事项:
- 预处理是编译的第一步,不检查语法
- 宏定义要注意括号的使用
- 头文件要防止重复包含
- 条件编译要注意匹配
- 预处理指令必须独占一行
- 宏不会检查类型,因此一定要注意类型匹配
1.2 编译
gcc -S main.i -o main.s
解析:
- gcc:编译器
- -S:将预处理后的文件进行编译处理,形成.s文件
- main.i:预处理后的目标文件
- -o:指定目标文件的名称或者存放路径
- main.s:编译后的目标文件
编译主要完成以下工作:
-
词法分析
- 将源代码分解成记号(token)
- 识别关键字、标识符、常量、运算符等
int main() { return 0; } // 分解为: int(关键字) main(标识符) ((符号) )(符号) {(符号) // return(关键字) 0(常量) ;(符号) }(符号)
-
语法分析
- 根据语法规则分析记号序列
- 构建抽象语法树(AST)
- 检查语法错误
if (x > 0) { y = 1; } // 构建为: // if // / \ // > = // / \ / \ // x 0 y 1
-
语义分析
- 类型检查
- 变量声明检查
- 类型转换
- 检查语义错误
int x; float y = x + 1.5; // 需要将x从int转换为float
-
中间代码生成
- 生成平台无关的中间表示(IR)
- 方便后续优化
// 源代码 x = a + b * c;// 中间代码(三地址码形式) t1 = b * c t2 = a + t1 x = t2
-
代码优化
- 常量折叠
int x = 3 + 4; // 优化为: int x = 7;
- 死代码消除
if (0) { // 这段代码永远不会执行x = 1; // 可以被消除 }
- 循环优化
// 循环展开 for (i=0; i<2; i++) { a[i] = i; } // 优化为: a[0] = 0; a[1] = 1;
- 公共子表达式消除
// 原代码 x = a + b; y = (a + b) * c; // 优化后 t = a + b; x = t; y = t * c;
编译可以带上优化选项来指定想要优化的级别,如:
gcc -O2 main.c -o main
,-O2表示中等优化,-O3表示最大优化。 -
目标代码生成
- 生成特定平台的汇编代码
- 考虑目标机器的特性
- 寄存器分配
# x86汇编示例 movl $1, %eax # 将1移动到eax寄存器 addl %ebx, %eax # 将ebx的值加到eax
注意事项:
- 编译错误必须全部修复才能继续
- 警告可以忽略但最好处理
- 优化可能改变代码行为
- 不同编译器可能有不同结果
- Debug版本应该使用-O0
1.3 汇编
gcc -c main.s -o main.o
解析:
- gcc:编译器
- -c:将汇编代码转换为目标文件
- main.s:汇编代码文件
- -o:指定输出文件
- main.o:目标文件
汇编阶段主要工作:
-
指令转换
- 将汇编指令转换为机器码
- 生成目标文件(二进制格式)
# 汇编代码 movl $1, %eax # 转换为机器码(十六进制) # B8 01 00 00 00
-
符号表生成
- 记录全局符号
- 记录未解析的外部符号
- 记录调试信息
-
重定位表生成
- 标记需要重定位的地址
- 为链接阶段做准备
1.4 链接
gcc main.o -o main
链接阶段主要工作:
-
符号解析
- 解析所有外部符号引用
- 检查符号重定义
- 建立全局符号表
-
重定位处理
- 计算符号的最终地址
- 修正代码中的地址引用
- 合并各个段(代码段、数据段等)
-
生成可执行文件
- ELF格式(Linux)
- PE格式(Windows)
- Mach-O格式(macOS)
注意事项:
- 静态链接和动态链接的选择
- 注意符号冲突
- 库的链接顺序很重要
- 链接错误通常与符号解析相关
1.5 C语言和C++在函数符号形成上的区别
C语言是没有函数重载的,因此不存在重名函数,那么一个函数名就可以表示一个唯一的函数,在建立符号表时,一个函数名就对应一个符号,因此链接时不会出现符号冲突。
C++是有函数重载的,因此存在重名函数,在建立符号表时,多个函数名可以对应一个符号,因此链接时会出现符号冲突。
那么该如何解决呢?
函数重载,它要求一个函数名可以有多个不同的定义,但是这些定义的参数类型不能完全相同,因此C++编译器在链接时会对函数名进行改编,在函数名后加上函数参数类型,这样就可以区分重名函数,从而解决符号冲突。
比如:
void f(int, int);
void f(double, double);
// 编译后
// f_i_i
// f_d_d
各种参数类型的符号:
- v - void
- b - bool
- c - char
- a - signed char
- h - unsigned char
- s - short
- t - unsigned short
- i - int
- j - unsigned int
- l - long
- m - unsigned long
- x - long long
- y - unsigned long long
- f - float
- d - double
- e - long double
- z - … (varargs)
复合类型的符号:
- P - 指针 (pointer)
- R - 引用 (reference)
- K - const
- V - volatile
- A - 数组 (array)
示例:
void func(int); // _Z4funci
void func(int*); // _Z4funcPi
void func(const int); // _Z4funcKi
void func(int&); // _Z4funcRi
void func(const int*); // _Z4funcPKi
void func(int* const); // _Z4funcKPi
//_Z是C++编译器改编符号的标志,4表示函数名长度
1.6 编译器与ACM时间的关系
当我们使用makefile管理项目时,makefile中会有一个all目标,它通常会调用所有的编译命令,根据依赖文件,然后逐步的将源代码文件预编译、编译、汇编形成.o文件,最后链接生成可执行文件。
但是这样就会引申出一个问题,我们在链接阶段,实际上就是将.o文件链接到一起,可以每次我们修改代码,并不一定所有的源代码文件都会修改,那么那些没有修改过的源文件最终预编译编译汇编形成的.o文件和之前完全一样,这样链接时就会造成很多不必要的开销。
那么该如何解决呢?
这时我们就要了解一个概念——ACM(Access Time、Modify Time、Create Time)。
对于任何一个文件来说,它都有三个时间戳:
- 创建时间(Create Time):文件被创建的时间
- 修改时间(Modify Time):文件内容被修改的时间
- 访问时间(Access Time):文件被访问的时间
创建时间,顾名思义,就是文件被创建的时间,这个时间戳在文件创建时被设置,并且在文件的整个生命周期中保持不变。
修改时间,这个时间戳在文件内容被修改时被设置,比如我们使用vim编辑一个文件时,每敲一个键,这个时间戳就会被更新。
访问时间,这个时间戳在文件被访问时被设置,比如我们使用ls查看一个文件时,这个时间戳就会被更新。
由于.o文件是由源代码文件编译形成的,因此.o文件的创建时间总是比源代码文件的修改时间要早,且修改时间一开始就是自己的创建时间。
而当我们修改源代码文件时,源代码文件的修改时间就会被更新,而.o文件的修改时间不会被更新,此时.c的修改时间就要比.o的创建时间要早,这就表示.o文件需要重新编译。
所以,当编译器在进行编译时,如果检查到源代码文件的修改时间比.o文件的修改时间要早,那么就会重新编译源代码文件形成新的.o文件,反之则不会重新编译,使用修改过的.o文件和没修改的.o文件链接生成可执行文件,大大节省了时间。
2. 进度条的制作
在学习制作进度条之前,我们需要认识一些概念:
\r
在C语言中,我们用的最多的转义字符大概就是\n了,\n叫做换行符,作用是将光标移动到下一行,而\r是回车符,作用是将光标移动到当前行的行首。(但是终端在解释的时候会让\n同时拥有回车换行的功能)
C语言标准输出的缓冲区
当我们使用printf输出时,我们认为调用完函数就立即输出到终端,但实际上是先输出到一个语言层面的缓冲区(非系统层面),然后缓冲区再刷新到终端。
一般来说,有两种缓冲区模式:行缓冲和全缓冲
对于行缓冲来说,当遇到\n时,缓冲区就会刷新;
而对于全缓冲来说,当缓冲区满时,缓冲区才会刷新;
而标准输出默认是行缓冲。
因此,如果我们想立即看到输出,可以使用\n刷新缓冲区,或者使用fflush(stdout)刷新缓冲区。
由于我们想在终端的同一行看到输出,所以我们采用fflush(stdout)刷新缓冲区。
而对于\r来说,虽然它不能刷新缓冲区,但是我们可以让光标回到行首,然后输出新的内容,这样就实现了终端上覆盖的效果。
sleep()和usleep()
这两个函数是C语言标准库提供的,用于使程序休眠。
- sleep(int n):使系统休眠n秒。
- usleep(int n):使系统休眠n微秒
如果我们不想让进度条刷新的太快,就可以使用这两个函数进行控制。
倒计时
#include<stdio.h>
#include<unisted.h>int main()
{int n = 10;while(n){printf("%-2d\r", n);n--;fflush(stdout);sleep(1);}return 0;
}
简单的进度条
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#define bar '#'#define MaxSize 101
int main()
{printf("简单的进度条:\n");char arr[MaxSize];char spindle[] = {'|', '/', '-', '\\'};memset(arr, '\0', MaxSize);int i = 0;while (i < MaxSize){printf("[%-100s][%-3d%%][%c]\r", arr, i, spindle[i % 4]);arr[i++] = bar;fflush(stdout);usleep(100000);}printf("\n");return 0;
}
带颜色的进度条
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#define bar '#'#define MaxSize 101
int main()
{srand((unsigned)time(NULL));printf("简单的进度条:\n");char arr[MaxSize];char spindle[] = {'|', '/', '-', '\\'};memset(arr, '\0', MaxSize);int i = 0;while (i < MaxSize){printf("\033[%d;%dm[%-100s]\033[0m\033[40;%dm[%-3d%%]\033[0m\033[40;%dm[%c]\033[0m\r", rand() % 10 + 40, rand() % 10 + 30, arr, rand() % 10 + 30, i, rand() % 10 + 30, spindle[i % 4]);arr[i++] = bar;fflush(stdout);usleep(100000);}printf("\n");return 0;
}