预处理
1 2 3 4 5 6 7 8 9 10 11 12
| #include <stdio.h> #define MSG \ "Hello \ World!\n" int main() { printf(MSG ); #ifdef __riscv printf("Hello RISC-V!\n"); #endif return 0; }
|
可以通过 gcc -E a.c 来查看预处理结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| ... extern int __uflow (FILE *); extern int __overflow (FILE *, int);
int main() { printf("Hello World!\n" );
return 0; }
|
这个预处理结果非常多,就是因为 include 了一个头文件,它展开之后就会非常大。因此可以说,预处理本质上就是文本替换,包括上面定义的宏 MSG。
头文件怎么查找
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
| gcc -E a.c --verbose > /dev/null Using built-in specs. COLLECT_GCC=/usr/bin/gcc OFFLOAD_TARGET_NAMES=nvptx-none:amdgcn-amdhsa OFFLOAD_TARGET_DEFAULT=1 Target: x86_64-linux-gnu Configured with: ../src/configure -v --with-pkgversion='Ubuntu 11.4.0-1ubuntu1~22.04' --with-bugurl=file:///usr/share/doc/gcc-11/README.Bugs --enable-languages=c,ada,c++,go,brig,d,fortran,objc,obj-c++,m2 --prefix=/usr --with-gcc-major-version-only --program-suffix=-11 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --enable-bootstrap --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-plugin --enable-default-pie --with-system-zlib --enable-libphobos-checking=release --with-target-system-zlib=auto --enable-objc-gc=auto --enable-multiarch --disable-werror --enable-cet --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none=/build/gcc-11-XeT9lY/gcc-11-11.4.0/debian/tmp-nvptx/usr,amdgcn-amdhsa=/build/gcc-11-XeT9lY/gcc-11-11.4.0/debian/tmp-gcn/usr --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu --with-build-config=bootstrap-lto-lean --enable-link-serialization=2 Thread model: posix Supported LTO compression algorithms: zlib zstd gcc version 11.4.0 (Ubuntu 11.4.0-1ubuntu1~22.04) COLLECT_GCC_OPTIONS='-E' '-v' '-mtune=generic' '-march=x86-64' /usr/lib/gcc/x86_64-linux-gnu/11/cc1 -E -quiet -v -imultiarch x86_64-linux-gnu a.c -mtune=generic -march=x86-64 -fasynchronous-unwind-tables -fstack-protector-strong -Wformat -Wformat-security -fstack-clash-protection -fcf-protection -dumpbase a.c -dumpbase-ext .c ignoring nonexistent directory "/usr/local/include/x86_64-linux-gnu" ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/11/include-fixed" ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/11/../../../../x86_64-linux-gnu/include"
/usr/lib/gcc/x86_64-linux-gnu/11/include /usr/local/include /usr/include/x86_64-linux-gnu /usr/include End of search list. COMPILER_PATH=/usr/lib/gcc/x86_64-linux-gnu/11/:/usr/lib/gcc/x86_64-linux-gnu/11/:/usr/lib/gcc/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/11/:/usr/lib/gcc/x86_64-linux-gnu/ LIBRARY_PATH=/usr/lib/gcc/x86_64-linux-gnu/11/:/usr/lib/gcc/x86_64-linux-gnu/11/../../../x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/11/../../../../lib/:/lib/x86_64-linux-gnu/:/lib/../lib/:/usr/lib/x86_64-linux-gnu/:/usr/lib/../lib/:/usr/lib/gcc/x86_64-linux-gnu/11/../../../:/lib/:/usr/lib/ COLLECT_GCC_OPTIONS='-E' '-v' '-mtune=generic' '-march=x86-64'
|
这段信息非常详细,比如它优先搜索include “…” 路径,其次才是include <>。以下是详细解释:
Using built-in specs, COLLECT_GCC, OFFLOAD_TARGET_NAMES, etc.:
- 这些信息描述了编译器的配置和环境。
COLLECT_GCC=/usr/bin/gcc
表示 gcc 的执行路径。
OFFLOAD_TARGET_NAMES
和 OFFLOAD_TARGET_DEFAULT
涉及到目标设备,用于代码卸载至特定的加速器(如 GPU)。
Target: x86_64-linux-gnu
指明目标平台是 x86_64 架构的 Linux 系统。
Configured with:
- 这一行列出了编译 GCC 时的配置选项。它包括支持的语言(如 C, C++),目标架构,优化选项等。
Thread model, Supported LTO compression algorithms, gcc version:
Thread model: posix
表明 GCC 使用 POSIX 线程模型。
Supported LTO compression algorithms: zlib zstd
列出了链接时优化(LTO)支持的压缩算法。
gcc version 11.4.0
显示 GCC 的版本。
COLLECT_GCC_OPTIONS:
- 列出了传递给 gcc 的选项,如
-E
(只进行预处理),-v
(详细输出),以及与目标机器有关的优化设置。
/usr/lib/gcc/x86_64-linux-gnu/11/cc1:
- 这是实际执行预处理的程序路径。
cc1
是 GCC 的一个内部组件,用于处理 C 语言文件。
ignoring nonexistent directory:
- 这些行显示编译器在查找头文件时跳过了不存在的目录。
#include “<…>” search starts here:
- 列出了编译器在处理包含文件时搜索的目录。这对于解决包含文件的依赖关系非常重要。
COMPILER_PATH, LIBRARY_PATH:
COMPILER_PATH
显示了编译器相关工具链的位置。
LIBRARY_PATH
列出了链接器搜索库文件的路径。
怎么验证呢?重新写一下代码:
1 2 3 4 5 6 7 8 9 10 11
| #include "stdio.h" #define MSG \ "Hello \ World!\n" int main() { printf(MSG ); #ifdef __riscv printf("Hello RISC-V!\n"); #endif return 0; }
|
之后,在文件下创建一个 aaa 目录并创建一个 stdin.h,然后预编译并查看预处理信息:
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
| gcc -E a.c -Iaaa -v > /dev/null Using built-in specs. COLLECT_GCC=/usr/bin/gcc OFFLOAD_TARGET_NAMES=nvptx-none:amdgcn-amdhsa OFFLOAD_TARGET_DEFAULT=1 Target: x86_64-linux-gnu Configured with: ../src/configure -v --with-pkgversion='Ubuntu 11.4.0-1ubuntu1~22.04' --with-bugurl=file:///usr/share/doc/gcc-11/README.Bugs --enable-languages=c,ada,c++,go,brig,d,fortran,objc,obj-c++,m2 --prefix=/usr --with-gcc-major-version-only --program-suffix=-11 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --enable-bootstrap --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-plugin --enable-default-pie --with-system-zlib --enable-libphobos-checking=release --with-target-system-zlib=auto --enable-objc-gc=auto --enable-multiarch --disable-werror --enable-cet --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none=/build/gcc-11-XeT9lY/gcc-11-11.4.0/debian/tmp-nvptx/usr,amdgcn-amdhsa=/build/gcc-11-XeT9lY/gcc-11-11.4.0/debian/tmp-gcn/usr --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu --with-build-config=bootstrap-lto-lean --enable-link-serialization=2 Thread model: posix Supported LTO compression algorithms: zlib zstd gcc version 11.4.0 (Ubuntu 11.4.0-1ubuntu1~22.04) COLLECT_GCC_OPTIONS='-E' '-I' 'aaa' '-v' '-mtune=generic' '-march=x86-64' /usr/lib/gcc/x86_64-linux-gnu/11/cc1 -E -quiet -v -I aaa -imultiarch x86_64-linux-gnu a.c -mtune=generic -march=x86-64 -fasynchronous-unwind-tables -fstack-protector-strong -Wformat -Wformat-security -fstack-clash-protection -fcf-protection -dumpbase a.c -dumpbase-ext .c ignoring nonexistent directory "/usr/local/include/x86_64-linux-gnu" ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/11/include-fixed" ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/11/../../../../x86_64-linux-gnu/include"
aaa /usr/lib/gcc/x86_64-linux-gnu/11/include /usr/local/include /usr/include/x86_64-linux-gnu /usr/include End of search list. In file included from a.c:1: aaa/stdio.h:1:10: error: 1 |
|
可以看到它确实在本目录下搜索,并且报错。
预处理的其他工作:
- 去掉注释;
- 连接因断行符(行尾的)而拆分的字符串;
- 处理条件编译 #ifdef/#else/#endif;
- 字符串化 #
- 标识符连接 ##
如果用riscv64-linux-gnu-gcc -E a.c
来处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| ... extern int __uflow (FILE *); extern int __overflow (FILE *, int);
int main() { printf("Hello World!\n" );
printf("Hello RISC-V!\n");
return 0; }
|
编译
词法分析
对于示例代码:
1 2 3 4 5 6 7 8
| #include <stdio.h> int main() { int x = 1, y = 2; int z = x + y; printf("z = %d\n", z); return 0; }
|
clang -fsyntax-only -Xclang -dump-tokens b.c
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| ... equal '=' [LeadingSpace] Loc=<b.c:3:18> numeric_constant '2' [LeadingSpace] Loc=<b.c:3:20> semi ';' Loc=<b.c:3:21> int 'int' [StartOfLine] [LeadingSpace] Loc=<b.c:4:5> identifier 'z' [LeadingSpace] Loc=<b.c:4:9> equal '=' [LeadingSpace] Loc=<b.c:4:11> identifier 'x' [LeadingSpace] Loc=<b.c:4:13> plus '+' [LeadingSpace] Loc=<b.c:4:15> identifier 'y' [LeadingSpace] Loc=<b.c:4:17> semi ';' Loc=<b.c:4:18> identifier 'printf' [StartOfLine] [LeadingSpace] Loc=<b.c:5:5> l_paren '(' Loc=<b.c:5:11> string_literal '"z = %d\n"' Loc=<b.c:5:12> comma ',' Loc=<b.c:5:22> identifier 'z' [LeadingSpace] Loc=<b.c:5:24> r_paren ')' Loc=<b.c:5:25> semi ';' Loc=<b.c:5:26> return 'return' [StartOfLine] [LeadingSpace] Loc=<b.c:6:5> numeric_constant '0' [LeadingSpace] Loc=<b.c:6:12> semi ';' Loc=<b.c:6:13> r_brace '}' [StartOfLine] Loc=<b.c:7:1> eof '' Loc=<b.c:7:2>
|
可以看到,词法分析本质上就是识别并记录源文件中的每一个token:
- 标识符, 关键字, 常数, 字符串, 运算符, 大括号, 分号…;
- 还记录了token的位置(文件名:行号:列号)。
词法分析器本质上是一个字符串匹配程序。
语法分析
clang -fsyntax-only -Xclang -ast-dump a.c
:
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
| ... | |-ParmVarDecl 0x11a0378 <col:24, col:29> col:30 'FILE *' | `-ParmVarDecl 0x11a03f8 <col:32> col:35 'int' `-FunctionDecl 0x11a0610 <b.c:2:1, line:7:1> line:2:5 main 'int ()' `-CompoundStmt 0x11a0ae8 <col:12, line:7:1> |-DeclStmt 0x11a0808 <line:3:5, col:21> | |-VarDecl 0x11a06c8 <col:5, col:13> col:9 used x 'int' cinit | | `-IntegerLiteral 0x11a0730 <col:13> 'int' 1 | `-VarDecl 0x11a0768 <col:5, col:20> col:16 used y 'int' cinit | `-IntegerLiteral 0x11a07d0 <col:20> 'int' 2 |-DeclStmt 0x11a0930 <line:4:5, col:18> | `-VarDecl 0x11a0838 <col:5, col:17> col:9 used z 'int' cinit | `-BinaryOperator 0x11a0910 <col:13, col:17> 'int' '+' | |-ImplicitCastExpr 0x11a08e0 <col:13> 'int' <LValueToRValue> | | `-DeclRefExpr 0x11a08a0 <col:13> 'int' lvalue Var 0x11a06c8 'x' 'int' | `-ImplicitCastExpr 0x11a08f8 <col:17> 'int' <LValueToRValue> | `-DeclRefExpr 0x11a08c0 <col:17> 'int' lvalue Var 0x11a0768 'y' 'int' |-CallExpr 0x11a0a40 <line:5:5, col:25> 'int' | |-ImplicitCastExpr 0x11a0a28 <col:5> 'int (*)(const char *, ...)' <FunctionToPointerDecay> | | `-DeclRefExpr 0x11a0948 <col:5> 'int (const char *, ...)' Function 0x1185648 'printf' 'int (const char *, ...)' | |-ImplicitCastExpr 0x11a0a88 <col:12> 'const char *' <NoOp> | | `-ImplicitCastExpr 0x11a0a70 <col:12> 'char *' <ArrayToPointerDecay> | | `-StringLiteral 0x11a09a8 <col:12> 'char[8]' lvalue "z = %d\n" | `-ImplicitCastExpr 0x11a0aa0 <col:24> 'int' <LValueToRValue> | `-DeclRefExpr 0x11a09c8 <col:24> 'int' lvalue Var 0x11a0838 'z' 'int' `-ReturnStmt 0x11a0ad8 <line:6:5, col:12> `-IntegerLiteral 0x11a0ab8 <col:12> 'int' 0
|
可以看到,语法分析就是将 token 组织成树形结构,AST(Abstract Syntax Tree), 可以反映出源程序的层次结构,还报告语法错误, 例如漏了分号。
语义分析
按照C语言的语义确定AST中每个表达式的类型:
- 相容的类型将根据C语言标准规范进行类型转换
- 报告语义错误
- 未定义的引用
- 运算符的操作数类型不匹配(如struct + int)
- 函数调用参数的类型和数量不匹配
- …
静态程序分析
本质上就是分析 AST 的信息,比如语法错误、代码风格、规范、性能、漏洞、逻辑错误等等等等。
中间代码生成
clang -S -emit-llvm a.c
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| ; Function Attrs: noinline nounwind optnone uwtable define dso_local i32 @main() %1 = alloca i32, align 4 %2 = alloca i32, align 4 %3 = alloca i32, align 4 %4 = alloca i32, align 4 store i32 0, i32* %1, align 4 store i32 1, i32* %2, align 4 store i32 2, i32* %3, align 4 %5 = load i32, i32* %2, align 4 %6 = load i32, i32* %3, align 4 %7 = add nsw i32 %5, %6 store i32 %7, i32* %4, align 4 %8 = load i32, i32* %4, align 4 %9 = call i32 (i8*, ...) @printf(i8* noundef getelementptr inbounds ([8 x i8], [8 x i8]* @.str, i64 0, i64 0), i32 noundef %8) ret i32 0 }
|
中间代码很关键,它是编译器定义的,它可以面向不同的平台、不同的指令集等等。它作为代码抽象层,可以支持多种源语言和目标语言(硬件指令集)。
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
| gcc -c b.c $ objdump -d b.o
b.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>: 0: f3 0f 1e fa endbr64 4: 55 push %rbp 5: 48 89 e5 mov %rsp,%rbp 8: 48 83 ec 10 sub $0x10,%rsp c: c7 45 f4 01 00 00 00 movl $0x1,-0xc(%rbp) 13: c7 45 f8 02 00 00 00 movl $0x2,-0x8(%rbp) 1a: 8b 55 f4 mov -0xc(%rbp),%edx 1d: 8b 45 f8 mov -0x8(%rbp),%eax 20: 01 d0 add %edx,%eax 22: 89 45 fc mov %eax,-0x4(%rbp) 25: 8b 45 fc mov -0x4(%rbp),%eax 28: 89 c6 mov %eax,%esi 2a: 48 8d 05 00 00 00 00 lea 0x0(%rip),%rax 31: 48 89 c7 mov %rax,%rdi 34: b8 00 00 00 00 mov $0x0,%eax 39: e8 00 00 00 00 call 3e <main+0x3e> 3e: b8 00 00 00 00 mov $0x0,%eax 43: c9 leave 44: c3 ret
|
未指定行为、未定义行为、ABI
C语言标准并没有明确定义类型的长度, 而是全部交给具体实现来定义,不过定义了类型的最小范围:
1 2 3 4 5 6 7 8 9 10 11
| sizeof( signed char ) >= 1 sizeof(unsigned char ) >= 1 sizeof( signed short ) >= 2 sizeof(unsigned short ) >= 2 sizeof( int ) >= 2 sizeof(unsigned int ) >= 2 sizeof( long ) >= 4 sizeof(unsigned long ) >= 4 sizeof( long long) >= 8 sizeof(unsigned long long) >= 8
|
可以看到,事实上C 标准连 1 个字节是多少都没有定义,历史上 1~48bit 都用过。
- 3.6 byte
- NOTE 2 A byte is composed of a contiguous sequence of bits, the number of which is
implementation-defined
函数调用时参数求值顺序是unspecified B。
实现定义行为(Implementation-defined Behavior):一类特殊的未指定行为, 具体实现需要将选择写到文档里。
未定义行为:不确定的、错误的行为。比如有符号数溢出,比如 uint8_t a = 128 经常被错误的理解为 -1,事实上,这是 UB,没有这样定义,这也是为什么 PA1中为什么要用无符号数,不产生超范围的 UB。
ABI: 在真实的系统中, 这相当于编译器+(处理器+操作系统+运行库)。