1 Introduction
Just refer to the following docs when needed:
C standards: https://www.c-language.org/.
The C Programming Language by K&R.
C Programing: A Modern Approach by K. N. King
2 Basics
This part is based on the classic K&R book. After you’ve got the basics down, the best way to improve is to read the original chapters over and over. I haven’t listed every single detail here because C and its ecosystem is incredibly deep — you’ll find something new every time you go back to the source and other materials.
2.1 C standards evolution
Classical C \(\rightarrow\) C89 \(\rightarrow\) C99 \(\rightarrow\) C11 \(\rightarrow\) C18 \(\rightarrow\) C23.
2.2 Prerequisites
2.2.1 Writing and running a C program
The basic structure of a simple C program:
/* Directives */
int main(void) {
/* Statements */
return 0;
}
e.g.,
#include <stdio.h>
int main(void) {
printf("hello, world!\n");
return 0;
}
Write the above source code into a file named hello.c.
Install Clang/LLVM using the following commands for Debian-based Linux distributions:
sudo apt update
sudo apt install clang lldb lld
Compile and run hello.c:
# Syntax checking
clang -fsyntax-only hello.c
# Compiling with the highest optimization (-O3)
# and reporting all warnings (-Wall)
clang -Wall -O3 -o hello hello.c
# Run the executable file
./hello
2.2.2 Concepts behind writing and running a C program
2.2.2.1 编译过程
在将 C 源码 hello.c 编译为可执行程序 hello 的过程中,经历了以下几个主要步骤:
预处理(Preprocessing):预处理器(Preprocessor)根据
#开头的指令(Directive)修改源文件(文本级别的操作),如文本替换、头文件插入、条件编译等。生成后缀为.i的中间文件。编译(Compilation):
编译(Compilation):基于后缀为
.i的中间文件,将 C 源码翻译为汇编码(生成后缀为.s的中间文件)。汇编(Assembly):通过汇编器(Assembler)将汇编码翻译为机器码(生成后缀为
.o或.obj的目标文件)。
- 链接(Linking):
符号解析(Symbol resolution):建立全局符号表,确保所有引用都已被定义。
重定位(Relocation):在可执行文件的虚拟地址空间中,为所有来源的代码和数据(源文件,库文件等)分配全局统一的虚拟内存地址。最终生成一个可执行文件(Executable file)。
其中链接分为:
静态链接(Static linking):从静态库(
.a/.lib,本质为一堆.o文件的打包集合)中将实际调用到的.o模块复制到可执行文件里。动态链接(Dynamic linking):仅在可执行文件中记录来源库。在程序运行时,动态链接器将需要的库加载到内存中。
2.2.2.2 编译时与运行时
编译时(Compilation time):编译器将源码编译成可执行文件的过程。
运行时(Runtime):从操作系统加载程序到程序执行完毕的过程。
运行时环境(Runtime environment):程序运行所依赖的“基础设施”。
2.2.2.3 Memory model
当程序被运行时,操作系统会为其分配一定大小的虚拟内存空间。为了高效管理,该虚拟内存空间会被严格划分为几块不同的区域(Segment):
Code segment:存放程序执行代码。
Data segment & BSS segment:存放全局变量(Global variable)和静态变量(Static variable)。
- Data segment:存放已初始化且初始值非 0 的全局/静态变量。
这些变量的具体数值已经保存在可执行文件中,如 int g_var = 100;。
- BSS segment:存放未初始化或初始值为 0 的全局/静态变量。
这些变量在可执行文件中不占用磁盘空间,只记录一个总大小,如 int g_arr[100];。对于 BSS segment,在程序真正被执行之前(main 函数被调用之前),C 运行时环境自动把这块内存清零(即填充零值),因为 C 语言标准规定:全局/静态变量如果未被初始化,其默认值必须为 0(保证程序行为可预测,因为这块内存区域可能存在随机的非零值)。
- 栈(Stack):存放局部变量,如函数参数和函数调用上下文。遵循 “last in, first out” 的原则。函数结束,自动销毁。这块内存的分配和释放由编译器自动生成的指令完成(即由编译器负责),其空间通常很小(默认几 MB,Linux 可通过命令
ulimit -s查看)。如果在栈上存放过大数据会导致栈溢出(Stack overflow),如超大数组和无限递归。
栈内存不会预先清零,因为其分配极其频繁。如果每次都清零,CPU 的性能损耗非常大。
为了实现栈内存的高性能,栈内存一般连续且容量有限且一般从高地址向低地址增长。这样 CPU 只需通过简单的指针移动(偏移量加减法)就能完成内存的分配和回收。另外,连续的内存有助于 CPU 预取与缓存(利用 CPU 自带的高速缓存)。CPU 的运行速度远远高于内存,否则 CPU 一直在等待内存。
栈溢出:程序申请使用的栈空间超过了操作系统的最大允许限度。
- 堆(Heap):用于动态内存分配的巨大空间,完全由程序员手动管理,一般遵循谁申请(
malloc/calloc)、谁释放(free)的原则。适合处理大型数据和未知大小的数据。如果申请后忘了释放,就会导致内存泄漏(Memory leak);如果释放后继续访问,就会导致悬空指针(Dangling pointer)。
内存泄漏:用完不释放。在程序运行的整个生命周期中,该内存区域将一直处于占用状态。对于长期运行的程序,可能导致内存耗尽(Out of Memory,OOM)。
悬空指针:一个指针指向的内存已经被释放了(free(p)),但是指针并未置空(p = NULL)。随后,程序又通过该指针读写内存,可能读到错误的数据或修改不该修改的数据。
2.2.2.4 Clang/LLVM compiler
- Preview preprocessed C:
# -E: only run the preprocessor
# -S: only run the preprocess and compilation steps
# -c: only run the preprocess, compile and assemble steps
clang -E -o - hello.c
- Preview AST in text or binary mode:
AST:即抽象语法树(Abstract Syntax Tree),去除源码中与计算机逻辑无关的信息,以树的形式来表示程序源码及其逻辑。
# in text mode
clang -Xclang -ast-dump -E -o - hello.c
# in binary mode
# -emit-ast: emit Clang AST files for source inputs
# only run the parsing step and generate binary AST
clang -emit-ast -o hello.ast hello.c
- Preview LLVM IR
# -emit-llvm: use the LLVM representation for assembler and object files
# unoptimized
clang -S -emit-llvm -o - hello.c
# optimized
clang -S -emit-llvm -O3 -o - hello.c
- Preview native machine code
clang -S -O3 -o - hello.c
