C 语言初识:从 Hello World 走进 C 的世界
本章不是 C 语法速成,而是从一段最简单的
Hello, World!程序出发,
顺着代码 → 工具 → 编译 → 链接 → 运行这条链路,把 C 语言开发涉及的
所有环节都过一遍。读完后你应该能在 Linux 终端独立完成:
编辑代码、编译、运行、调试。
一、示例代码
新建一个文件 hello.c:
/* hello.c - 第一个 C 程序 */
#include <stdio.h>
int main(void)
{
printf("Hello, World!\n");
return 0;
}
只有 5 行业务代码 + 1 行头文件引用,但它已经是一个完整可执行的程序。
二、逐行解释(注释版代码)
把所有解释直接嵌入到代码注释里,看代码 = 看文档。
/*==============================================================================
* hello.c —— 第一个 C 程序
*
* C 语言有两种注释(都不会被编译,仅供阅读):
* 1. /* ... * / 块注释,可跨行(本注释就是)
* 2. // ... 行注释(C99 起支持)
*============================================================================*/
// #include 是「预处理指令」,不是普通语句
// 编译之前,预处理器会把 <stdio.h> 的全部内容
// 原封不动地复制粘贴到这一行
// <> 表示去系统头文件目录找(通常 /usr/include)
// 双引号 "" 则先在当前目录找
#include <stdio.h> // stdio.h = Standard I/O
// 声明了 printf / scanf / fopen 等函数
// int main(void) —— 程序的入口函数
// - 操作系统加载完程序后,第一个执行的就是 main
// - void 表示「这个函数不接受任何参数」
// - int 表示「这个函数返回一个整数」
// - 约定:return 0 表示正常退出;非 0 表示异常退出(父进程 Shell 能拿到)
int main(void)
{ // 花括号 { } 划定 main 的作用域
printf("Hello, World!\n"); // printf = print formatted
// 把字符串打印到「标准输出 stdout」(默认是终端)
// \n 是转义字符 = 换行
// "..." 内是字符串字面量
return 0; // 把 0 作为 main 的返回值,交给操作系统
} // main 函数结束
执行流程对照:
程序启动
↓
操作系统调用 main()
↓
printf("Hello, World!\n") → 屏幕显示 "Hello, World!" 并换行
↓
return 0 → 操作系统收到退出码 0
↓
进程结束
三、工具链介绍
写一个 C 程序至少需要两类工具:编辑器 + 编译器。
1. 编辑器(写代码)
| 编辑器 | 特点 | 适合场景 |
|---|---|---|
| VS Code | 现代、插件丰富、跨平台 | 入门首选 |
| Vim / Neovim | 终端内运行、纯键盘、极度高效 | 嵌入式开发(远程服务器) |
| Emacs | 可扩展性极强 | 老牌硬核玩家 |
| Sublime Text | 轻量、启动快 | 编辑单个文件 |
| gedit / Notepad++ | 极简 | 临时改改 |
嵌入式 Linux 推荐:VS Code + Remote SSH 插件,或者直接用 Vim/nano。
2. 编译器(翻译代码)
主流 C 编译器:
| 编译器 | 来源 | 默认系统 |
|---|---|---|
| GCC(GNU Compiler Collection) | GNU | Linux、MinGW |
| Clang | LLVM | macOS、FreeBSD |
| MSVC(Microsoft Visual C++) | 微软 | Windows |
| Arm Compiler / arm-none-eabi-gcc | ARM | 嵌入式 MCU |
嵌入式 Linux 交叉编译:
aarch64-linux-gnu-gcc、arm-linux-gnueabihf-gcc等。
3. 其他常用工具
| 工具 | 用途 |
|---|---|
| make / cmake | 自动化构建(多文件项目) |
| GDB | 调试器(断点、单步) |
| ldd | 查看可执行文件的动态库依赖 |
| objdump / readelf | 反汇编 / 查看 ELF 结构 |
| strace | 跟踪进程的系统调用 |
| nm / ldd / file | 符号、依赖、类型查看 |
四、编译环境
不同操作系统有不同的「标准」编译环境:
| 系统 | 编译器 | C 标准库 | 典型工具链 |
|---|---|---|---|
| Linux | GCC 或 Clang | glibc(或 musl) | gcc, make, gdb |
| macOS | Clang(Apple 改版) | Apple libc | clang, lldb |
| Windows | MSVC 或 MinGW | MSVCRT / ucrt | cl.exe / gcc |
嵌入式 Linux 通常用交叉编译工具链:
# 主机(x86)→ 目标(ARM)
apt install gcc-aarch64-linux-gnu g++-aarch64-linux-gnu
# 编译时指定
aarch64-linux-gnu-gcc -o hello hello.c
五、编译命令
打开终端,进入 hello.c 所在目录:
gcc -o hello hello.c
这行命令做了什么?
- gcc:调用 GCC 编译器
- -o hello:指定输出文件名为 hello(不指定则默认为 a.out)
- hello.c:源文件
常用参数
gcc -Wall -Wextra -O2 -g -o hello hello.c
| 参数 | 作用 |
|---|---|
-Wall |
开启常用警告 |
-Wextra |
开启更多警告 |
-O2 |
优化级别(生成更快代码) |
-O0 |
不优化(调试用) |
-g |
生成调试符号(GDB 需要) |
-std=c11 |
指定 C 标准(C89/C99/C11/C17) |
-I<dir> |
添加头文件搜索目录 |
-L<dir> |
添加库文件搜索目录 |
-l<name> |
链接库(如 -lm 链接数学库) |
-D<name> |
宏定义(如 -DDEBUG) |
-E |
只预处理(生成 .i 文件) |
-S |
只编译到汇编(生成 .s 文件) |
-c |
只编译不链接(生成 .o 文件) |
六、编译链接过程
C 程序从源码到可执行文件经历四个阶段:
hello.c ──预处理──> hello.i ──编译──> hello.s ──汇编──> hello.o ──链接──> hello
│ │ │ │ │
源码 展开宏 汇编代码 目标文件 可执行文件
包含头文件 (人可读) (机器码)
删注释
阶段 1:预处理(Preprocessing)
gcc -E hello.c -o hello.i
预处理器做的事:
- #include 的文件原样插入
- #define 的宏被替换
- 条件编译 #if/#ifdef 被求值
- 注释被删除
- 行号标记 # lineno "file" 加入(编译报错时定位用)
输出 hello.i 通常有几万行(因为 stdio.h 展开后很大)。
阶段 2:编译(Compilation)
gcc -S hello.i -o hello.s
编译器(真正的 cc1)把预处理后的 C 代码翻译成汇编语言。
- 检查语法、类型
- 做优化(-O2)
- 输出汇编文件 hello.s
人可以读懂 hello.s:
.file "hello.c"
.section .rodata
.LC0:
.string "Hello, World!"
.text
.globl main
.type main, @function
main:
pushq %rbp
movq %rsp, %rbp
leaq .LC0(%rip), %rax
movl $0, %esi
movq %rax, %rdi
call puts@PLT
movl $0, %eax
popq %rbp
ret
阶段 3:汇编(Assembly)
gcc -c hello.s -o hello.o
汇编器(as)把汇编代码翻译成机器码,生成目标文件 hello.o。
- 已经是二进制,但还不能执行(缺库函数 printf 的实现)
阶段 4:链接(Linking)
gcc hello.o -o hello
链接器(ld)做的事:
- 把多个 .o 合并
- 解析未定义符号(如 printf)
- 从标准库(glibc)找到 printf 的实现
- 把所有代码段、数据段排好地址
- 生成最终的可执行文件
可以用 ldd hello 查看依赖:
$ ldd hello
linux-vdso.so.1 (0x00007ffd12345000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 ← glibc
/lib64/ld-linux-x86-64.so.2
静态链接 vs 动态链接:
- 默认是动态链接(hello运行时需要libc.so.6)
- 加-static可静态链接(文件大很多,但可独立运行)
bash gcc -static -o hello hello.c
一行命令完成四阶段
gcc -o hello hello.c
GCC 默认一次性完成全部四步。如果想看中间产物:
gcc -save-temps -o hello hello.c
ls
# hello hello.c hello.i hello.o hello.s
七、编译结果
$ ls -lh hello
-rwxr-xr-x 1 user user 16K 7月 21 10:00 hello
文件类型:
$ file hello
hello: ELF 64-bit LSB pie executable, x86-64, ...
是 ELF(Executable and Linkable Format) 格式(Linux 上)。
ELF 文件结构(粗略):
┌──────────────┐
│ ELF 头部 │ → 元信息(架构、入口点等)
├──────────────┤
│ 程序头表 │ → 告诉系统怎么加载
├──────────────┤
│ .text │ → 代码(main 等)
│ .rodata │ → 只读数据("Hello, World!" 字符串)
│ .data │ → 已初始化全局变量
│ .bss │ → 未初始化全局变量(运行时清零)
├──────────────┤
│ .symtab │ → 符号表(调试用)
│ .strtab │ → 字符串表
│ .debug_* │ → 调试信息(-g 时生成)
└──────────────┘
可以用 readelf 或 objdump 查看详情:
readelf -h hello # 头部
objdump -d hello # 反汇编
nm hello # 符号表
八、运行环境
要让 hello 真正运行起来,需要:
- 操作系统:Linux 内核负责加载、调度进程
- C 标准库:
/lib/x86_64-linux-gnu/libc.so.6(即 glibc) - 动态链接器:
/lib64/ld-linux-x86-64.so.2 - 终端:标准输出要有个目的地(通常是 tty)
实际运行命令:
$ ./hello
Hello, World!
$
$ 是 Shell 提示符,意思是「程序已退出」。
九、运行结果
成功运行时:
- 屏幕上输出 Hello, World! 后跟一个换行
- 程序退出码为 0(正常)
可以用 $? 查看退出码:
$ ./hello
Hello, World!
$ echo $?
0
如果 main 返回非 0:
return 1; // 出错了
$ ./hello
$ echo $?
1
Shell 脚本会据此判断成功/失败。
十、运行时系统执行过程(深入)
图例:⭐ = 基础(嵌入式工程师必须知道) | 🔥 = 进阶(内核/系统程序员才需要深究)
本节按完整时间线列出所有阶段。标 ⭐ 的部分先掌握,标 🔥 的部分可在需要时再深入。
阶段 A:Shell 解析命令行 ⭐
1. Shell 读取键盘输入 ⭐
- 你按回车,Shell 从 tty 读到一行字符串 ./hello
- Shell 用自己的语法解析器拆分成命令 + 参数
2. Shell 在 PATH 中查找 hello ⭐
- 顺序遍历 /usr/local/bin、/usr/bin、/bin...
- 找到可执行文件,拿到完整路径
3. Shell 检查文件权限 ⭐
- 验证 ./hello 有可执行位(-rwxr-xr-x)
- 当前用户对它有 x 权限
- 不是目录(否则报错 "Is a directory")
阶段 B:Shell 创建子进程 ⭐
4. Shell 调用 fork() ⭐
- Shell 进程调用 fork(),把自己完整复制一份
- 现在有两个几乎一样的进程:父(Shell)和子(待运行 hello)
- 复制的内容:代码、数据、堆、栈、文件描述符表、信号处理...
- 写时复制(COW):实际内存页暂不复制,子进程改时才复制
5. Shell 在父进程中调用 waitpid() ⭐
- Shell 阻塞自己,等待子进程结束
- 这就是为什么 Shell 提示符要等命令执行完才出现
阶段 C:子进程加载可执行文件(execve) ⭐
6. 子进程调用 execve("./hello", argv, envp) ⭐
- 替换:子进程的代码段、数据段、堆、栈全部丢弃
- 保留:进程 ID(PID)、打开的文件描述符、信号掩码
7. 内核的 ELF 加载器(fs/binfmt_elf.c)开始工作 ⭐
8. 内核读取 ELF 头部 🔥
- 验证魔数 0x7f 'E' 'L' 'F'
- 确认架构(x86_64 / aarch64 / arm)
- 读取程序头表(Program Headers)数量和位置
- 找到入口点(Entry Point)地址
9. 内核创建新的虚拟地址空间 🔥
- 分配新的页表(mm_struct)
- 清空旧的虚拟内存映射
10. 内核按段(Segment)映射 ⭐
- PT_LOAD 段被映射到虚拟地址:
- .text(代码)→ 只读 + 可执行
- .rodata(字符串字面量)→ 只读
- .data(已初始化全局变量)→ 可读写
- .bss(未初始化全局变量)→ 可读写,初始全零
- 使用 mmap() 系统调用把文件段映射进虚拟内存
11. 内核处理动态链接器 ⭐
- 如果是动态链接 ELF,找到 .interp 段指定的 ld-linux.so
- 把 ld-linux.so 也 mmap 进进程地址空间
12. 内核设置用户栈 🔥
- 在地址空间高端分配一页作为初始栈
- 把 argv、envp、auxv(辅助向量)压栈
- ELF auxv 包含:可执行文件路径、PHDR 地址、随机化种子等
13. 内核关闭 O_CLOEXEC 之外的文件描述符 ⭐
- 标记 CLOEXEC 的 fd 会被关闭
- 其余保留(stdin/stdout/stderr 通常保留)
14. 内核将进程状态设为"就绪" ⭐
- 修改进程 task_struct
- 把控制权交还给进程调度器
阶段 D:动态链接器运行 🔥
这一段标 🔥 是因为大多数应用开发者一辈子不用碰。但嵌入式系统工程师需要知道。
15. CPU 从入口点开始执行 ⭐
- 入口点不是 main,而是 _start(crt0 提供)
- 或动态链接情况下,先跳到 ld-linux.so 的入口
16. 动态链接器(ld-linux)解析依赖 🔥
- 读取 hello 的 .dynamic 段
- 解析 DT_NEEDED 字段:libc.so.6 等
- 用 mmap 把每个依赖库映射进进程
17. 动态链接器做符号解析 🔥
- 遍历 hello 的重定位表(.rel.plt、.rel.dyn)
- 对每个未定义符号(如 printf),在已加载库中查找
- 写入 GOT(Global Offset Table)
18. 动态链接器执行初始化函数 🔥
- 调用每个库的 __attribute__((constructor)) 函数
- C++ 全局对象的构造也在这里执行
19. 动态链接器跳转到程序入口 _start ⭐
- 把控制权交给 hello
阶段 E:C 运行时初始化(crt0) 🔥
glibc 的
crt0(crt = C RunTime)。嵌入式裸机工程师会自己写一个简化版。
20. _start 设置栈指针 ⭐
- 从栈顶读取 argc、argv、envp(由内核压栈)
- 调用 __libc_start_main
21. __libc_start_main 初始化 libc 内部 🔥
- 设置 errno
- 初始化 stdin/stdout/stderr 的 FILE 结构
- 初始化 malloc 内存池
- 初始化线程局部存储(TLS)
- 初始化 locale
- 注册 atexit 清理函数
22. __libc_start_main 调用 main(argc, argv, envp) ⭐
- 此时用户代码才真正开始执行
阶段 F:执行 main() ⭐
23. main 执行函数序言 ⭐
asm
pushq %rbp
movq %rsp, %rbp
- 保存调用者栈帧
- 建立自己的栈帧
24. 调用 printf(实际是 puts,GCC 自动优化) ⭐
asm
leaq .LC0(%rip), %rax ; rax = "Hello, World!\n" 字符串地址
movl $0, %esi ; 变参占位
movq %rax, %rdi ; 第一参数 = 字符串
call puts@PLT
- GCC 看到只有一个字符串 + \n,自动优化为 puts
- PLT(Procedure Linkage Table)是动态链接跳板
25. 进入 libc 的 puts() ⭐
- 拿到字符串指针
- 加锁 stdout(防止多线程竞争)
- 拷贝字符串到 stdout 缓冲区
- 调用底层的 _IO_file_xsputn
26. printf/puts 触发 write() 系统调用 ⭐
- 把用户态缓冲区数据传给内核
- 系统调用号:x86_64 用 syscall 指令,ARM 用 svc #0
阶段 G:内核处理 write() 系统调用 🔥
27. CPU 切换到内核态 ⭐
- syscall 指令触发:
- 保存用户态寄存器到内核栈
- 切换到内核页表
- 跳到 entry_SYSCALL_64(x86)/vector_swi(ARM)
28. 内核系统调用分派 ⭐
- 读取系统调用号 → 这里是 write
- 查 sys_call_table → 找到 sys_write 函数
- 传入参数:fd, buf, count
29. sys_write 处理 ⭐
- 检查 fd 合法性(这里是 1 = stdout)
- 找到对应的 struct file(文件描述符指向的文件对象)
- 调用 file->f_op->write()(VFS 抽象层)
30. VFS 层 🔥
- 对于 stdout 这种字符设备,绕过 page cache
- 调用 tty 驱动的 write 方法
31. tty 驱动层 ⭐
- 把数据写入 tty 的发送缓冲区
- 触发硬件中断
- 串口 / 终端硬件把数据发出
32. 终端接收端 ⭐
- 终端模拟器(xterm 等)收到数据
- 解析 ANSI 转义序列(\n = 光标移到下一行)
- 通过 X11 / Wayland 协议绘制到屏幕
33. 屏幕上显示 Hello, World! ⭐
34. 系统调用返回 ⭐
- write 返回写入字节数
- CPU 切换回用户态
- 恢复用户态寄存器
- 回到 puts 中继续执行
阶段 H:main 返回与清理 ⭐
35. puts 解锁 stdout ⭐
36. puts 返回到 main ⭐
37. main 执行 return 0 ⭐
asm
movl $0, %eax ; 返回值 = 0
popq %rbp ; 恢复栈帧
ret ; 跳回调用者(__libc_start_main)
38. __libc_start_main 接住 main 返回值 ⭐
- 保存为 exit_code = 0
- 调用 exit(exit_code)
39. exit 触发 atexit 清理链 🔥
- 按 LIFO 顺序调用所有 atexit() 注册的函数
- 刷新所有 stdio 缓冲区
- 关闭所有 C 库管理的 FILE*
40. exit 调用 __run_exit_handlers 🔥
- 处理 atexit 注册的函数
- 关闭 on_exit 等注册的清理器
41. 关闭文件描述符 0/1/2(如果 CLOEXEC) ⭐
42. 调用 _exit()(或 exit_group() 系统调用) ⭐
阶段 I:进程退出 ⭐
43. 内核处理 exit_group 系统调用 ⭐
- 标记进程为 EXIT_ZOMBIE(僵尸态)
- 释放用户空间内存(mmap 区域)
- 关闭进程打开的所有文件描述符
- 释放 PID 等待父进程回收
- 进程调度器不再调度该进程
44. 父进程 Shell 从 waitpid 返回 ⭐
- Shell 之前阻塞在 waitpid(pid, &status, 0)
- 系统调用被唤醒,返回子进程 PID 和退出状态
- Shell 读取 status 中的退出码(这里是 0)
45. Shell 打印下一个提示符 $ ⭐
- 准备读下一条命令
阶段 J:完整的进程地址空间(最终态) 🔥
执行期间,进程地址空间大致如下:
高地址
0xFFFF_FFFF_FFFF_FFFF ┐
│ 内核空间(所有进程共享)
... │
0x00007FFF_FFFF_FFFF │ ← 用户栈边界
│
0x00007FFE_xxxx_xxxx │ ← 命令行参数、环境变量
│ ← (由内核压栈)
│
... │ 栈(向下增长)
│
0x00007Fxx_xxxx_xxxx │ 共享库映射区
(libc.so, ld-linux.so)
│
│ 堆(向上增长)
0x000055xx_xxxx_xxxx │ ← hello 主程序 .text .data .bss
│
0x00000000_00000000 ┘ NULL(不可访问)
低地址
阶段汇总表
| 阶段 | 主要参与者 | 步骤范围 | 重要程度 |
|---|---|---|---|
| A. Shell 解析 | Shell | 1-3 | ⭐ 基础 |
| B. Shell fork | Shell | 4-5 | ⭐ 基础 |
| C. 内核加载 | 内核 ELF 加载器 | 6-14 | ⭐ + 🔥 |
| D. 动态链接 | ld-linux | 15-19 | 🔥 进阶 |
| E. C 运行时 | crt0 / __libc_start_main | 20-22 | 🔥 |
| F. main 执行 | 用户代码 | 23-26 | ⭐ 基础 |
| G. 系统调用 | 内核 VFS / tty | 27-34 | ⭐ + 🔥 |
| H. main 返回 | libc / 用户代码 | 35-42 | ⭐ |
| I. 进程退出 | 内核 / Shell | 43-45 | ⭐ 基础 |
| J. 地址空间 | 全程 | 整体视图 | 🔥 |
时间线总图
你按回车
│
├─ [A] Shell 解析命令行 ⭐
│
├─ [B] Shell: fork() + 子进程 waitpid() ⭐
│
├─ [C] 子进程: execve()
│ │
│ ├─ 内核读取 ELF 头部 ⭐
│ ├─ 内核创建地址空间 + 页表 🔥
│ ├─ mmap 各段(.text/.data/.bss/.rodata)⭐
│ ├─ 映射 ld-linux.so ⭐
│ ├─ 压栈 argv/envp/auxv 🔥
│ └─ 进程设为就绪 ⭐
│
├─ [D] ld-linux 动态链接
│ ├─ 解析 DT_NEEDED 🔥
│ ├─ mmap libc.so 等 🔥
│ ├─ 符号解析 + 重定位 🔥
│ └─ 调用 _start ⭐
│
├─ [E] C 运行时初始化
│ ├─ _start 设置栈 🔥
│ ├─ __libc_start_main 初始化 libc 🔥
│ └─ 调用 main() ⭐
│
├─ [F] main 执行
│ ├─ 函数序言 ⭐
│ ├─ printf/puts → write() 系统调用 ⭐
│ │
│ │ [G] 内核处理 write()
│ │ ├─ syscall 切内核态 ⭐
│ │ ├─ sys_write → VFS ⭐
│ │ ├─ tty 驱动 ⭐
│ │ └─ 终端显示 Hello, World! ⭐
│ │
│ ├─ puts 返回 ⭐
│ └─ return 0 ⭐
│
├─ [H] 清理
│ ├─ exit() ⭐
│ ├─ atexit 处理 🔥
│ ├─ 刷新 stdio 缓冲区 ⭐
│ └─ _exit() 系统调用 ⭐
│
├─ [I] 内核回收
│ ├─ 进程变 ZOMBIE ⭐
│ ├─ 释放内存、关闭 fd ⭐
│ └─ Shell: waitpid() 返回 ⭐
│
└─ Shell: 打印下一个 $ ⭐
总耗时:几毫秒(在现代 PC 上)
嵌入式场景的差异
嵌入式 Linux 与桌面 Linux 在这条链路上几乎完全一样,区别只在:
| 环节 | 桌面 Linux | 嵌入式 Linux |
|---|---|---|
| 终端 | xterm / SSH | 串口 / fbterm |
| 显示 | X11 / Wayland | framebuffer / LCD |
| Shell | bash / zsh | busybox sh / msh |
| 库 | glibc | musl / uClibc / newlib |
| 文件系统 | ext4 / xfs | squashfs / jffs2 / ubifs |
| 启动 | systemd | BusyBox init / systemd |
如果跑裸机(无 OS),步骤 A-I 全部不存在,直接是:
1. CPU 上电 → 从 Flash 读第一条指令
2. 跳到 main
3. 直接操作硬件寄存器(无 libc、无 syscall)
这就是为什么嵌入式工程师要学「启动流程」——自己手写没有 OS 时的 _start 和 main。
十一、动手试试
把上面的 hello.c 抄下来,自己跑一遍:
# 1. 编辑
vim hello.c # 或 vscode hello.c
# 2. 编译
gcc -Wall -Wextra -o hello hello.c
# 3. 查看产物
ls -lh hello
file hello
# 4. 运行
./hello
# 5. 看看中间产物
gcc -save-temps -o hello hello.c
cat hello.i | head -20 # 预处理后
cat hello.s # 汇编
objdump -d hello.o # 目标文件反汇编
# 6. 用 GDB 调试
gdb ./hello
(gdb) break main
(gdb) run
(gdb) next
(gdb) step
(gdb) info registers
(gdb) quit
跑完这些命令,C 语言的"Hello World"才算真正掌握。
小结
| 步骤 | 工具 | 输入 | 输出 |
|---|---|---|---|
| 编辑 | vim / VS Code | 键盘 | hello.c |
| 预处理 | cpp | hello.c |
hello.i |
| 编译 | cc1 | hello.i |
hello.s |
| 汇编 | as | hello.s |
hello.o |
| 链接 | ld | hello.o + libc |
hello(可执行) |
| 加载 | 内核 | hello |
进程 |
| 执行 | CPU + 库 | 进程 | Hello, World! |
C 语言的魅力:简单到极致,复杂到无限。从这一行
printf出发,
你可以一路学到操作系统、编译器、链接器、ELF 格式、系统调用……
这是嵌入式 Linux 工程师的必修基本功。