操作系统进入 32 位模式以后,我们后面的代码就是用 C 语言来写了,这样写起来会轻松很多也会快很多。但是我们需要了解 C 语言的本质和原理,后面会遇到很多问题,如果不理解其中的本质,有些问题我们查起来会很慢或者根本查不出来,很多同学可能也会因此而放弃。
语言分类介绍
我自己学过这么些客户端语言:汇编、C/C++、Java、kotlin、OC、swift、flutter 。刚开始做 Android 用的就是 Java 语言,基本要了我半条命,后面做跨平台做 iOS 学 OC 和 Swift 就是一遍上班一边学,当时就用了两周,一周学语言本身的原理,一周学语法,那就很快了。获得一个普世的智慧以后,对于语言那么一通应该就要百通。我们一起来看下:
首先计算机 CPU 是硬件晶体管做的,只能执行硬件指令也就是机器码指令,汇编是助记型语言基本是直接转机器码。所以汇编可以说是一切语言的基础,比如我们做 C/C++ 开发的,能看懂汇编代码才能真正了解 C/C++ 的本质和原理。
再往上走是 C/C++ 属于编译器语言,编译器帮我转汇编,汇编再转机器码,C/C++ 可以说是一切其他语言的基础了,比如 Java 和 kotlin 编译出来的 class 是跑在 Android/Java 虚拟机上的,虚拟机就是 C/C++ 和汇编来实现的,所以如果我们看不懂虚拟机源码,原则上不能说精通 Java 。还有像 flutter 的引擎就是 C/C++ 来写的。
再往上走像 Java 是虚拟机语言,依赖虚拟机来运行,python 是解释器语言,依赖解释器来运行,还有其他各种我不了解的语言,但是如果要我去上手,我也可以很有信心的说,只要给我两周时间,我应该就能搞了。
那一门语言值不值得我们花时间呢?再比如现在要我新开负责一个项目应该怎么选型呢?本质上其实还只要我们足够了解就能做好了,需要了解团队成员技术栈,需要了解语言本身的底层原理,需要了解未来大的方向趋势。我在 17 年那会就说了要大家学 kotlin 但是网上很多同学抨击我,但我不是拍脑袋,所以我坚信,所以我学了,虽然后来我也没讲,现在也没怎么用,但两周时间也不多。

编译器
以前写简历我都是写精通 X 语言,但是到现在我都只敢写了解 X 语言,为什么这么说呢?

语言的强大,来自于编译器的强大,来自于操作系统的强大,来自于虚拟机的强大,来自于引擎的强大,来自于系统库的强大,来自于第三方库的强大。所以我只能说了解 X 语言。
C 语言的编译流程是将人类可读的 C 源代码转换为计算机可执行的二进制程序的过程,主要分为 预处理 → 编译 → 汇编 → 链接 四个核心阶段。以下是每个阶段的详细说明,结合输入输出、关键操作和常见问题展开:
一、预处理(Preprocessing)
目标:处理源代码中的预编译指令(以 #开头的指令),生成无预编译指令的中间文件(.i或 .ii)。 输入:C 源文件(如 main.c)。 输出:预处理后的文本文件(如 main.i,C 语言为 .i,C++ 为 .ii)。
关键操作:
1.头文件展开(#include) 将 #include "xxx.h" 或 #include <xxx.h> 替换为对应头文件的实际内容(递归展开所有嵌套头文件)。
2.宏替换(#define) 将代码中的宏(如 #define PI 3.14)替换为对应的值(文本替换)。
3.条件编译(#ifdef/#ifndef/#endif) 根据预定义宏(如 #ifdef DEBUG)决定是否保留某段代码。
4.删除注释 删除代码中的单行注释(//)和多行注释(/* ... */)。
5.处理特殊指令 如 #pragma once(防止头文件重复包含)、#line(修改编译器报告的行号)等。
二、编译(Compilation)
目标:将预处理后的文本文件(.i)转换为汇编语言代码(.s),并完成语法检查、语义分析和代码优化。 输入:预处理后的文件(.i)。 输出:汇编语言源文件(.s,如 main.s)。
关键步骤:
1.词法分析(Lexical Analysis) 将预处理后的文本拆分为有意义的"词法单元"(Token),如关键字(int、if)、标识符(变量名、函数名)、运算符(+、=)、常量(123、"abc")等。
2.语法分析(Syntax Analysis) 根据词法单元构建 抽象语法树(AST),检查代码是否符合 C 语言语法规则(如括号是否匹配、语句是否完整)。
3.语义分析(Semantic Analysis) 检查代码的语义正确性(如类型是否匹配、变量是否声明)。
4.中间代码生成与优化 将 AST 转换为与机器无关的中间代码(如三地址码、静态单赋值形式),并进行优化(如常量折叠、死代码消除、循环展开等)。
三、汇编(Assembly)
目标:将汇编语言代码(.s)转换为机器语言的目标文件(.o或 .obj),生成二进制指令和数据。 输入:汇编语言文件(.s)。 输出:目标文件(.o,Linux/macOS;.obj,Windows)。
关键操作:
1.指令翻译 将汇编指令(如 mov eax, 5、call printf)转换为 CPU 可识别的机器码(二进制指令)。
2.符号表生成 目标文件中记录所有全局变量、函数的名称及其在内存中的相对地址(符号表),用于后续链接。
3.重定位信息记录 记录代码中引用的外部符号(如其他目标文件或库中的函数)的位置,以便链接器调整地址(重定位)。
四、链接(Linking)
目标:将多个目标文件(.o)和库文件(静态库 .a/.lib、动态库 .so/.dll)合并为一个可执行程序(如 a.out或 main.exe),解决符号引用问题。 输入:多个目标文件(.o)、库文件。 输出:可执行文件(如 main、main.exe)。
关键步骤:
1.符号解析(Symbol Resolution) 链接器遍历所有目标文件和库,解析代码中对函数、变量的引用(如 printf),找到其实际定义的位置(可能在当前目标文件或其他库中)。 2.重定位(Relocation) 将所有目标文件的代码段、数据段合并到可执行文件的对应段中,并调整符号的相对地址为实际内存地址(因为每个目标文件的地址默认从 0开始,需映射到统一内存空间)。
3.静态链接 vs 动态链接 静态链接:将库的代码直接复制到可执行文件中(如 .a库),生成的可执行文件独立运行(无需依赖外部库)。 动态链接:仅记录库的路径和符号信息(如 .so、.dll),运行时加载库(节省内存,多个程序共享同一库)。
完整编译流程示例(GCC)
以 main.c为例,完整流程命令如下:
css
# 1. 预处理:生成 main.i
gcc -E main.c -o main.i
# 2. 编译:生成 main.s(汇编代码)
gcc -S main.i -o main.s
# 3. 汇编:生成 main.o(目标文件)
gcc -c main.s -o main.o
# 4. 链接:生成可执行文件 main(链接标准库)
gcc main.o -o main
main.c(源代码) → [预处理] → main.i(无指令文本) → [编译] → main.s(汇编代码) → [汇编] → main.o(目标文件) → [链接] → main(可执行文件)