• C语言程序编译全流程,从源代码到二进制


    源程序

    对于一个最简单的程序:

    int main(){
    	int a = 1;
    	int b = 2;
    	int c = a + b;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    预处理

    处理源代码中的宏指令,例如#include等

    clang -E test.c
    
    • 1

    处理结果:

    # 1 "test.c"
    # 1 "" 1
    # 1 "" 3
    # 343 "" 3
    # 1 "" 1
    # 1 "" 2
    # 1 "test.c" 2
    int main(){
        int a=1;
        int b=2;
        int c=a+b;
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    预处理输出中多出来的这些行,被称为linemarkers,
    格式为:# linenum filename flags
    具体意思可以看gnu的文档以及Cppreference的文档

    RTFM:reading the f**k manual

    编译过程

    词法分析

    clang -fsyntax-only -Xclang -dump-tokens test.c
    # Clang编译器的一个选项,它告诉编译器只进行语法分析而不生成目标代码。这意味着编译器将检查源文件中的语法错误,但不会生成可执行文件或目标文件。
    #-Xclang: 这是Clang编译器的一个选项,它允许传递额外的选项给Clang编译器的底层部分。
    #-dump-tokens 告诉编译器在对源代码进行词法分析后输出标记(tokens)的信息。
    
    • 1
    • 2
    • 3
    • 4

    生成结果:
    在这里插入图片描述
    词法分析把原文件划分为一个个的token,记录下每个token的类型,内容,位置(所在文件,行号列号)
    类型有:

    • 关键字(Keywords):例如int、for、if等。
    • 标识符(Identifiers):由用户定义的名称,用于表示变量、函数等。
    • 常量(Constants):包括整数常量、浮点数常量、字符常量、字符串常量等。
    • 字符(Characters):例如单引号括起来的字符常量。
    • 字符串(Strings):例如双引号括起来的字符串常量。
    • 运算符(Operators):例如+、-、*等。
    • 分隔符(Delimiters):例如逗号,、分号;、括号(、)等。
    • 注释(Comments):包括单行注释//和多行注释/* */。
    • 空白符(Whitespace):例如空格、制表符、换行符等。
      这一步一般不会报错

    语法分析

    将函数

    clang -fsyntax-only -Xclang -ast-dump test.c
    # -ast-dump: 这个选项告诉Clang编译器在语法分析之后输出抽象语法树(Abstract Syntax Tree,AST)。抽象语法树是编译器在语法分析阶段生成的一种树形结构,它反映了源代码的语法结构,是编译器进行进一步分析和优化的基础
    
    • 1
    • 2

    输出【clang的语法树输出把语义信息也输出了】
    在这里插入图片描述
    这一步将识别出来的结果组合成树型结构,称为语法树,包括每个节点的类型,节点间的关系等。
    上面的TranslationUnitDecl就是指一个翻译单元,一般一个源文件就是一个翻译单元,上面的一些invalid sloc的是一些C语言内置的东西,不用看。
    从FuncitonDecl开始,是我们写的代码的语法树
    VarDecl指变量声明。
    BinaryOperator是二进制操作符,也就是+号,
    LValueToRValue是指C语言中的左值和右值机制,a和b是左值,先转化为右值,然后再相加把结果赋给c。
    在这一步会报告一些语法错误,例如缺了分号。
    关于Clang的AST的详细信息可以看官方文档

    语义分析

    按照C语言的语义确定AST中每个表达式的类型,相容的类型将根据C语言标准规范进行类型转换,例如两个不同类型的数相加这种,会进行隐式转换。
    报告一些语义错误,例如:未定义的引用,运算符的操作数类型不匹配(如struct + int),函数调用与定义的参数数量不一致等。

    静态程序分析

    对代码进行静态分析,检查其中的语法错误, 代码风格和规范, 潜在的软件缺陷, 安全漏洞, 性能问题等

    clang --analyze -Xanalyzer -analyzer-output=text ./test.c
    #--analyze: 这是Clang编译器的一个选项,指示编译器执行静态代码分析。它告诉Clang不仅要编译代码,还要分析代码中潜在的问题。
    #-Xanalyzer: 这个选项允许将后续参数传递给分析器。
    #-analyzer-output=text: 这个选项告诉分析器以文本形式输出分析结果。换句话说,它指示分析器将结果以易读的文本形式打印到终端。
    
    • 1
    • 2
    • 3
    • 4

    输出:
    在这里插入图片描述

    中间代码生成

    生成IR中间代码。
    IR:编译器定义的, 面向编译场景的指令集,与源代码编程语言和后端运行平台架构都无关得指令集。
    Clang使用的是LLVM IR,gcc使用的是GIMPLE。
    在这里插入图片描述

    优化

    在确保代码可观测行为一致的情况下,对代码进行优化,如果把程序看作状态机,那么优化就是指使用尽可能简单的状态机来代替复杂的状态机。
    对volatile修饰变量的访问需要严格执行
    程序结束时, 写入文件的数据需要与严格执行时一致
    交互式设备的输入输出(stdio.h)需要与严格执行时一致
    Clang or Gcc中的优化等级都是可控的,优化等级通过-O选项指定,其取值范围为0到3,其中0表示不进行优化,1表示基本优化,2表示更多的优化,3表示更加激进的优化。

    clang -O1 ./test.c
    #-O1表示基本优化
    clang -S -foptimization-record-file=- a.c -O1
    #-foptimization-record-file选项表示输出优化记录,-表示直接输出到终端,或者可以输入一个文件名,表示输出到文件中,格式为yaml
    
    • 1
    • 2
    • 3
    • 4

    输出结果:

    --- !Analysis
    Pass:            prologepilog
    Name:            StackSize
    DebugLoc:        { File: test.c, Line: 1, Column: 0 }
    Function:        main
    Args:
      - NumStackBytes:   '0'
      - String:          ' stack bytes in function'
    ...
    --- !Analysis
    Pass:            asm-printer
    Name:            InstructionMix
    DebugLoc:        { File: test.c, Line: 6, Column: 1 }
    Function:        main
    Args:
      - String:          'BasicBlock: '
      - BasicBlock:      ''
      - String:          "\n"
      - String:          retq
      - String:          ': '
      - INST_retq:       '1'
      - String:          "\n"
      - String:          'xorl      '
      - String:          ': '
      - INST_xorl:       '1'
      - String:          "\n"
    ...
    --- !Analysis
    Pass:            asm-printer
    Name:            InstructionCount
    DebugLoc:        { File: test.c, Line: 1, Column: 0 }
    Function:        main
    Args:
      - NumInstructions: '2'
      - String:          ' instructions in function'
    ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    目标代码生成

    clang -S test.c --target=riscv32-linux-gnu
    
    • 1

    生成的代码:

    .text
            .attribute      4, 16
            .attribute      5, "rv32i2p0_m2p0_a2p0_f2p0_d2p0_c2p0"
            .file   "test.c"
            .globl  main                            # -- Begin function main
            .p2align        1
            .type   main,@function
    main:                                   # @main
    # %bb.0:
            addi    sp, sp, -32
            sw      ra, 28(sp)                      # 4-byte Folded Spill
            sw      s0, 24(sp)                      # 4-byte Folded Spill
            addi    s0, sp, 32
            addi    a0, zero, 1
            sw      a0, -12(s0)
            addi    a0, zero, 2
            sw      a0, -16(s0)
            lw      a0, -12(s0)
            lw      a1, -16(s0)
            mul     a0, a0, a1
            sw      a0, -20(s0)
            mv      a0, zero
            lw      s0, 24(sp)                      # 4-byte Folded Reload
            lw      ra, 28(sp)                      # 4-byte Folded Reload
            addi    sp, sp, 32
            ret
    .Lfunc_end0:
            .size   main, .Lfunc_end0-main
                                            # -- End function
            .ident  "clang version 13.0.0 (https://github.com/llvm/llvm-project/ 24c8eaec9467b2aaf70b0db33a4e4dd415139a50)"
            .section        ".note.GNU-stack","",@progbits
            .addrsig
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    还可以使用ftime-report来查看编译中都做了什么,每个流程都花了多少时间。

    clang -S test.c --target=riscv32-linux-gnu -ftime-report
    
    • 1

    汇编

    这一步已经和编译原理不怎么搭边了

    clang -c test.s
    
    • 1

    输出文件为一个.o文件,这个文件是二进制的,无法用cat等工具来看,cat和vim这些工具都是需要有相应的给人看的编码方式,例如UTF-8等。.o文件是为了给计算机看的,使用的编码方式是ISA规定的,具体可以看ISA的手册,每一个汇编语句对应一段二进制代码。
    但是我们可以用objdump来看:

    llvm-objdump -d test.o
    #GNU也有objdump工具,但是需要给出二进制所对应的硬件ISA,llvm的objdump会自动识别ISA
    
    • 1
    • 2

    输出结果:
    在这里插入图片描述
    objdump的原理跟汇编器的原理正好反过来的,objdump根据二进制指令反猜汇编代码,得到上图所示。

    相关工具

    clang是基于LLVM作为后端的,clang本身只是一个前端,LLVM不止是一个工具,而是多个工具集合到一起的,clang本身只负责将C或C++翻译成LLVM IR,再往后由LLVM IR到目标代码并不是由clang完成的,而是clang调用LLVM相应的工具。
    clang中主要包括:
    编译要用到的:

    • clang,从程序员的角度看就是C/C++编译器
    • llc:将LLVM IR的代码转化为目标代码
    • llvm-as:将LLVM IR的“汇编”转化为LLVM的“bitcode”【二者是一个东西不同形式,都是LLVM IR】
    • llvm-link:将多个LLVM bitcode文件链接为一个bitcode文件
    • lld链接器,为了替代系统链接器,输入多个.o文件和.a文件,链接成可执行文件eg:ELF。
      • 目前Linux系统自带的链接器一般都是GNU开发的ld链接器,lld文档里号称比ld要快很多。

    配套的工具:

    • static analyzer:静态代码分析
    • llvm-objdump:objdump是一个用于分析目标文件(包括可执行文件、共享库、目标文件等)的工具。它可以显示目标文件的各种信息,包括可执行代码的汇编指令、符号表、段信息等。objdump通常与GNU Binutils软件包一起提供,是开发和调试工具链中的一部分。通过objdump,开发人员可以深入了解目标文件的结构和内容,有助于调试和优化程序。
    • llvm-strace:系统调用跟踪
    • llvm-size:输出二进制文件的大小信息
    • llvm-nm:列出二进制文件中的符号名

    配套的工具就那么几种,GNU Binutils里面已经都实现过一遍了,只不过LLVM又实现了一遍。
    在这里插入图片描述

  • 相关阅读:
    代码随想录算法训练营第二天(C) | 977.有序数组的平方 209.长度最小的子数组 59.螺旋矩阵
    微信小程序与idea后端如何进行数据交互
    Linux学习-44-虚拟内存、物理内存和swap分区的作用
    RabbitMq:RabbitMq 主从镜像模式②
    Debian linux系统更换apt源
    消息队列 RocketMQ 5.0:从消息服务到云原生事件流平台
    基于typescript开发的threejs3D动画项目
    Java 设计模式——抽象工厂模式
    如何使用yum 安装php7.2
    uniapp 使用mqtt 报错 socketTask onOpen is not a function
  • 原文地址:https://blog.csdn.net/qq_45983373/article/details/137405081