源程序
对于一个最简单的程序:
int main(){
int a = 1;
int b = 2;
int c = a + b;
return 0;
}
预处理
处理源代码中的宏指令,例如#include等
clang -E test.c
处理结果:
# 1 "test.c"
# 1 "<built-in>" 1
# 1 "<built-in>" 3
# 343 "<built-in>" 3
# 1 "<command line>" 1
# 1 "<built-in>" 2
# 1 "test.c" 2
int main(){
int a=1;
int b=2;
int c=a+b;
return 0;
}
预处理输出中多出来的这些行,被称为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)的信息。
生成结果:
词法分析把原文件划分为一个个的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)。抽象语法树是编译器在语法分析阶段生成的一种树形结构,它反映了源代码的语法结构,是编译器进行进一步分析和优化的基础
输出【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: 这个选项告诉分析器以文本形式输出分析结果。换句话说,它指示分析器将结果以易读的文本形式打印到终端。
输出:
中间代码生成
生成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
输出结果:
--- !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'
...
目标代码生成
clang -S test.c --target=riscv32-linux-gnu
生成的代码:
.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
还可以使用ftime-report来查看编译中都做了什么,每个流程都花了多少时间。
clang -S test.c --target=riscv32-linux-gnu -ftime-report
汇编
这一步已经和编译原理不怎么搭边了
clang -c test.s
输出文件为一个.o文件,这个文件是二进制的,无法用cat等工具来看,cat和vim这些工具都是需要有相应的给人看的编码方式,例如UTF-8等。.o文件是为了给计算机看的,使用的编码方式是ISA规定的,具体可以看ISA的手册,每一个汇编语句对应一段二进制代码。
但是我们可以用objdump来看:
llvm-objdump -d test.o
#GNU也有objdump工具,但是需要给出二进制所对应的硬件ISA,llvm的objdump会自动识别ISA
输出结果:
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又实现了一遍。