当前位置: 首页 > article >正文

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:预处理后的目标文件

预处理主要完成以下工作:

  1. 头文件展开
  • 处理所有#include预处理指令
  • 将头文件的内容复制到当前文件
  • 可以递归展开(头文件中包含的头文件)
  1. 宏定义替换
  • 展开所有#define宏定义
  • 处理条件编译指令(#if、#ifdef、#ifndef等)
  • 展开所有宏调用
#define Max(a, b) ((a) > (b) ? (a) : (b))
int max = Max(3, 4); //展开为:int max = ((3) > (4) ? (3) : (4))
  1. 条件编译处理

    • #if、#ifdef、#ifndef:条件判断
    • #elif、#else:分支处理
    • #endif:结束条件编译
    #ifdef DEBUG
    printf("debug info\n");
    #endif
    
  2. 删除注释

    • 删除所有//和/* */格式的注释
    • 每个注释都替换为一个空格
  3. 添加行号和文件名标识

    • 使用#line指令标记行号和文件名
    • 用于编译器产生调试信息和编译错误提示
  4. 处理特殊预处理指令

    • #pragma:编译器指令
    • #error:产生编译错误
    • #warning:产生编译警告
  5. 保留所有的换行符

    • 确保错误提示的行号正确
    • 方便调试
  6. 字符串常量化

    • 处理#运算符,将宏参数转换为字符串
    #define STR(s) #s
    STR(hello)  // 展开为: "hello"
    
  7. 宏连接操作

    • 处理##运算符,连接两个记号
    #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;
}

预处理的作用:

  1. 将源文件转换为完整的C/C++代码
  2. 处理编译器不关心的细节
  3. 提供代码复用和条件编译的机制
  4. 简化程序的编写和维护

注意事项:

  1. 预处理是编译的第一步,不检查语法
  2. 宏定义要注意括号的使用
  3. 头文件要防止重复包含
  4. 条件编译要注意匹配
  5. 预处理指令必须独占一行
  6. 宏不会检查类型,因此一定要注意类型匹配

1.2 编译

gcc -S main.i -o main.s

解析:

  • gcc:编译器
  • -S:将预处理后的文件进行编译处理,形成.s文件
  • main.i:预处理后的目标文件
  • -o:指定目标文件的名称或者存放路径
  • main.s:编译后的目标文件

编译主要完成以下工作:

  1. 词法分析

    • 将源代码分解成记号(token)
    • 识别关键字、标识符、常量、运算符等
    int main() { return 0; }
    // 分解为: int(关键字) main(标识符) ((符号) )(符号) {(符号) 
    //         return(关键字) 0(常量) ;(符号) }(符号)
    
  2. 语法分析

    • 根据语法规则分析记号序列
    • 构建抽象语法树(AST)
    • 检查语法错误
    if (x > 0) { y = 1; }
    // 构建为:
    //   if
    //  /  \
    // >    =
    // / \  / \
    // x 0  y 1
    
  3. 语义分析

    • 类型检查
    • 变量声明检查
    • 类型转换
    • 检查语义错误
    int x;
    float y = x + 1.5;  // 需要将x从int转换为float
    
  4. 中间代码生成

    • 生成平台无关的中间表示(IR)
    • 方便后续优化
    // 源代码
    x = a + b * c;
    
    // 中间代码(三地址码形式)
    t1 = b * c
    t2 = a + t1
    x = t2
    
  5. 代码优化

    • 常量折叠
    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表示最大优化。

  6. 目标代码生成

    • 生成特定平台的汇编代码
    • 考虑目标机器的特性
    • 寄存器分配
    # x86汇编示例
    movl    $1, %eax    # 将1移动到eax寄存器
    addl    %ebx, %eax  # 将ebx的值加到eax
    

注意事项:

  1. 编译错误必须全部修复才能继续
  2. 警告可以忽略但最好处理
  3. 优化可能改变代码行为
  4. 不同编译器可能有不同结果
  5. Debug版本应该使用-O0

1.3 汇编

gcc -c main.s -o main.o

解析:

  • gcc:编译器
  • -c:将汇编代码转换为目标文件
  • main.s:汇编代码文件
  • -o:指定输出文件
  • main.o:目标文件

汇编阶段主要工作:

  1. 指令转换

    • 将汇编指令转换为机器码
    • 生成目标文件(二进制格式)
    # 汇编代码
    movl $1, %eax
    # 转换为机器码(十六进制)
    # B8 01 00 00 00
    
  2. 符号表生成

    • 记录全局符号
    • 记录未解析的外部符号
    • 记录调试信息
  3. 重定位表生成

    • 标记需要重定位的地址
    • 为链接阶段做准备

1.4 链接

gcc main.o -o main

链接阶段主要工作:

  1. 符号解析

    • 解析所有外部符号引用
    • 检查符号重定义
    • 建立全局符号表
  2. 重定位处理

    • 计算符号的最终地址
    • 修正代码中的地址引用
    • 合并各个段(代码段、数据段等)
  3. 生成可执行文件

    • ELF格式(Linux)
    • PE格式(Windows)
    • Mach-O格式(macOS)

注意事项:

  1. 静态链接和动态链接的选择
  2. 注意符号冲突
  3. 库的链接顺序很重要
  4. 链接错误通常与符号解析相关

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语言标准库提供的,用于使程序休眠。

  1. sleep(int n):使系统休眠n秒。
  2. 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;
}

http://www.kler.cn/a/389652.html

相关文章:

  • 强推未发表!3D图!Transformer-LSTM+NSGAII工艺参数优化、工程设计优化!
  • 微软宣布Win11 24H2进入新阶段!设备将自动下载更新
  • 内网渗透测试工具及渗透测试安全审计方法总结
  • 2025年1月17日(点亮三色LED)
  • LabVIEW桥接传感器数据采集与校准程序
  • 【人工智能】:搭建本地AI服务——Ollama、LobeChat和Go语言的全方位实践指南
  • 批量从Excel某一列中找到符合要求的值并提取其对应数据
  • 【笔记】LLC电路工作频点选择 2-1 输出稳定性的限制
  • 数学建模-----假设性检验引入+三个经典应用场景(三种不同的假设性检验类型)
  • 【React】深入理解 JSX语法
  • ReactPress 安装指南:从 MySQL 安装到项目启动
  • Pr 视频过渡:沉浸式视频 - VR 随机块
  • 去中心化存储:Web3中的数据安全新标准
  • linux网络管理基本知识
  • dapp获取钱包地址,及签名
  • 阿里公告:停止 EasyExcel 更新与维护
  • LlamaIndex RAG实践 | 书生大模型
  • 【RabbitMQ】04-发送者可靠性
  • Spark中给读取到的数据 的列 重命名的几种方式!
  • 如何使用 Web Scraper API 高效采集 Facebook 用户帖子信息
  • 跨域及解决跨域
  • 使用腾讯地图的 IP 定位服务。这里是正确的实现方式
  • 字节青训-游戏排名第三大的分数、补给站最优花费问题
  • vite-plugin-electron 库作用
  • 细说STM32单片机USART中断收发RTC实时时间并改善其鲁棒性的另一种方法
  • 5G NR:各物理信道的DMRS配置