Files
obsidian/操作系统/03_C语言编程基础/03_C语言编程基础.md

632 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 第03讲C语言编程基础 -- Linux环境下的编译、调试与程序结构
> **本节目标**掌握Linux环境下C语言程序的编译链接过程、可执行程序的内存结构、gdb调试方法和Makefile编写为后续操作系统编程打下基础
## 前置知识
- [[02_Linux基础]] -- Linux基本命令和文件操作
- C语言基础语法
---
## 一、Linux环境下C语言程序编译链接过程
### 1.1 编译的四个阶段
在Linux下一个C源文件要经过四个阶段才能变成可执行程序
```mermaid
graph LR
A["hello.c
源代码"] -->|预处理
gcc -E| B["hello.i
预处理后的源代码"]
B -->|编译
gcc -S| C["hello.s
汇编代码"]
C -->|汇编
gcc -c| D["hello.o
目标代码(机器码)"]
D -->|链接
gcc| E["hello
可执行程序"]
style A fill:#e1f5fe
style B fill:#fff3e0
style C fill:#f3e5f5
style D fill:#ffcdd2
style E fill:#e8f5e9
```
| 阶段 | 输入 | 输出 | gcc选项 | 主要工作 |
|------|------|------|---------|---------|
| **预处理** | `.c` | `.i` | `-E` | 展开宏 `#define`、包含头文件 `#include`、条件编译 |
| **编译** | `.i` | `.s` | `-S` | 将C代码翻译为汇编代码进行语法分析和优化 |
| **汇编** | `.s` | `.o` | `-c` | 将汇编代码翻译为机器码(目标文件) |
| **链接** | `.o` | 可执行文件 | (默认) | 合并库函数、解析符号引用、生成最终可执行文件 |
### 1.2 实际操作演示
```bash
# 分步编译 hello.c
gcc -E hello.c -o hello.i # 第1步预处理
gcc -S hello.i -o hello.s # 第2步编译生成汇编
gcc -c hello.s -o hello.o # 第3步汇编生成目标文件
gcc hello.o -o hello # 第4步链接生成可执行文件
# 一步完成(等价于上面四步)
gcc hello.c -o hello
```
### 1.3 各阶段产物的特点
```bash
# 查看预处理结果(宏被展开,头文件被包含)
gcc -E hello.c -o hello.i
wc -l hello.c hello.i # hello.i 会比 hello.c 长很多
# 查看汇编代码
gcc -S hello.c -o hello.s
cat hello.s # 可以看到汇编指令
# 查看目标文件内容
gcc -c hello.c -o hello.o
file hello.o # 显示文件类型ELF 64-bit relocatable
nm hello.o # 查看符号表
```
### 1.4 链接的作用
链接器主要完成以下工作:
1. **符号解析**:将函数调用和变量引用与它们的定义关联起来
2. **地址重定位**:为代码和数据分配最终的内存地址
3. **库合并**:将程序使用的库函数(如 `printf`)合并进来
```mermaid
graph TB
subgraph "链接前"
A["main.o
调用 printf()"]
B["sum.o
定义 sum()"]
C["libc.a
定义 printf()"]
end
subgraph "链接后"
D["可执行文件
所有代码和数据合并"]
end
A --> D
B --> D
C --> D
style A fill:#e1f5fe
style B fill:#fff3e0
style C fill:#f3e5f5
style D fill:#e8f5e9
```
---
## 二、Linux可执行程序结构
### 2.1 ELF文件格式
Linux下的可执行文件采用 **ELFExecutable and Linkable Format** 格式。一个可执行文件在内存中由多个段section/segment组成
```mermaid
graph TB
subgraph "高地址"
STACK["栈 (Stack)
局部变量、函数调用帧
向下增长 ↓"]
end
subgraph ""
FREE["空闲区域
(向中间增长)"]
end
subgraph ""
HEAP["堆 (Heap)
malloc/free 动态分配
向上增长 ↑"]
end
subgraph ""
BSS[".bss 段
未初始化的全局变量
运行时初始化为0"]
end
subgraph ""
DATA[".data 段
已初始化的全局变量"]
end
subgraph ""
RODATA[".rodata 段
只读数据(如字符串常量)"]
end
subgraph "低地址"
TEXT[".text 段
程序代码(机器指令)"]
end
STACK --- FREE
FREE --- HEAP
HEAP --- BSS
BSS --- DATA
DATA --- RODATA
RODATA --- TEXT
style TEXT fill:#e1f5fe
style RODATA fill:#fff3e0
style DATA fill:#e8f5e9
style BSS fill:#f3e5f5
style HEAP fill:#ffcdd2
style STACK fill:#c8e6c9
```
### 2.2 各段详解
| 段名 | 内容 | 特点 |
|------|------|------|
| **.text**(代码段) | 程序的机器指令 | 只读、可执行 |
| **.rodata**(只读数据段) | 字符串常量、`const` 修饰的全局变量 | 只读 |
| **.data**(数据段) | 已初始化的全局变量和静态变量 | 可读可写 |
| **.bss** | 未初始化的全局变量和静态变量 | 运行时由系统初始化为0不占磁盘空间 |
| **堆Heap** | `malloc()` / `calloc()` 动态分配的内存 | 由程序员手动管理,向上增长 |
| **栈Stack** | 局部变量、函数参数、返回地址 | 由编译器自动管理,向下增长 |
### 2.3 用代码验证
```c
// memory_layout.c - 验证程序内存布局
#include <stdio.h>
#include <stdlib.h>
int global_init = 100; // .data 段(已初始化全局变量)
int global_uninit; // .bss 段(未初始化全局变量)
const int READONLY = 42; // .rodata 段(只读数据)
int main() {
int local_var = 10; // 栈(局部变量)
int *heap_var = malloc(sizeof(int)); // 堆(动态分配)
printf("代码段地址 (main): %p\n", main);
printf("只读数据段地址: %p\n", &READONLY);
printf("数据段地址 (global): %p\n", &global_init);
printf("BSS段地址 (uninit): %p\n", &global_uninit);
printf("堆地址 (malloc): %p\n", heap_var);
printf("栈地址 (local): %p\n", &local_var);
free(heap_var);
return 0;
}
```
```bash
gcc memory_layout.c -o memory_layout
./memory_layout
```
预期输出中,地址从低到高排列为:`.text` < `.rodata` < `.data` < `.bss` < 堆 < 栈。
### 2.4 用 readelf 和 objdump 查看
```bash
# 查看ELF文件的段信息
readelf -S hello
# 反汇编 .text 段
objdump -d hello
# 查看符号表
nm hello
```
---
## 三、gdb调试
### 3.1 编译时添加调试信息
使用 `-g` 选项编译gdb才能将机器指令与源代码行号对应
```bash
gcc -g gdbuse.c -o gdbuse
gdb ./gdbuse
```
### 3.2 gdb常用命令
| 命令 | 缩写 | 作用 | 示例 |
|------|------|------|------|
| `run` | `r` | 运行程序 | `run` |
| `break` | `b` | 设置断点 | `b main``b gdbuse.c:10` |
| `next` | `n` | 单步执行(不进入函数) | `n` |
| `step` | `s` | 单步执行(进入函数) | `s` |
| `continue` | `c` | 继续运行到下一个断点 | `c` |
| `print` | `p` | 打印变量值 | `p count` |
| `backtrace` | `bt` | 查看调用栈 | `bt` |
| `list` | `l` | 查看源代码 | `l` |
| `info locals` | -- | 查看所有局部变量 | `info locals` |
| `watch` | -- | 监视变量变化 | `watch count` |
| `quit` | `q` | 退出gdb | `q` |
### 3.3 调试实例gdbuse.c 中的bug
课程提供的 `gdbuse.c` 包含一个典型的bug适合用来练习gdb调试
```c
// gdbuse.c - 包含bug的程序
#include <stdio.h>
#include <string.h>
int main() {
char c = 't';
char s[100];
int i;
int count = 0;
strcpy(s, "abcdefghijklmopqrstuvstuxyz0123456789");
for (i = 0; i < strlen(s); i++)
if (s[i] = c) // BUG: 应该是 == 而不是 =
count++;
printf("count = %d\n", count);
}
```
**调试步骤**
```bash
# 1. 编译(加 -g 选项)
gcc -g gdbuse.c -o gdbuse
# 2. 启动gdb
gdb ./gdbuse
# 3. 在 main 函数设置断点
(gdb) b main
# 4. 运行程序
(gdb) run
# 5. 单步执行,观察变量
(gdb) n # 执行到 for 循环
(gdb) p s[i] # 打印当前字符
(gdb) p c # 打印目标字符
(gdb) p count # 打印计数器
# 6. 发现问题s[i] = c 是赋值,不是比较
# 应该写成 s[i] == c
```
**bug分析**`if (s[i] = c)` 中使用了赋值运算符 `=` 而非比较运算符 `==`,导致每次循环都把 `c` 赋值给 `s[i]`,且条件永远为真(`c` 的值 `'t'` 非零),最终 `count` 等于字符串长度。
---
## 四、Makefile
### 4.1 为什么需要Makefile
当项目包含多个源文件时手动逐个编译非常麻烦。Makefile 可以:
- 自动判断哪些文件需要重新编译
- 并行编译,加快速度
- 统一管理编译选项
### 4.2 基本规则
Makefile 的每条规则由三部分组成:
```
目标: 依赖
命令必须以Tab开头
```
```mermaid
graph LR
A["target
目标"] --> B["dependencies
依赖"]
B --> C["recipe
命令"]
style A fill:#ffcdd2
style B fill:#fff3e0
style C fill:#e8f5e9
```
### 4.3 Makefile实例
以课程中的 `sum.c` 多文件项目为例(`sum.c` + `main.c` + `calc.h`
```makefile
# Makefile - 多文件编译示例
CC = gcc
CFLAGS = -Wall -g
LDFLAGS = -L. -lwrapper
# 目标文件
TARGET = myprogram
OBJS = main.o sum.o
# 默认目标
all: $(TARGET)
# 链接规则
$(TARGET): $(OBJS)
$(CC) $(OBJS) -o $(TARGET) $(LDFLAGS)
# 通用编译规则
%.o: %.c calc.h
$(CC) $(CFLAGS) -c $< -o $@
# 清理
clean:
rm -f $(OBJS) $(TARGET)
```
**自动变量说明**
| 变量 | 含义 |
|------|------|
| `$@` | 当前目标文件名 |
| `$<` | 第一个依赖文件名 |
| `$^` | 所有依赖文件名 |
```bash
# 使用Makefile
make # 默认编译
make clean # 清理编译产物
make -j4 # 4个线程并行编译
```
### 4.4 Makefile的执行逻辑
```mermaid
graph TD
A["make 命令"] --> B{检查 all 目标}
B --> C{myprogram 存在且最新?}
C -->|是| D["无需编译"]
C -->|否| E{main.o 需要更新?}
E -->|是| F["gcc -c main.c -o main.o"]
E -->|否| G{sum.o 需要更新?}
F --> G
G -->|是| H["gcc -c sum.c -o sum.o"]
G -->|否| I["gcc main.o sum.o -o myprogram"]
H --> I
style A fill:#ffcdd2
style D fill:#e8f5e9
style I fill:#e8f5e9
```
---
## 五、Wrapper库
### 5.1 什么是Wrapper库
课程提供的 **Wrapper库**`libwrapper.a`)封装了常用的系统调用,在原始系统调用的基础上增加了 **错误检查** 功能。如果系统调用失败Wrapper函数会自动打印错误信息并终止程序。
### 5.2 设计哲学
```mermaid
graph LR
subgraph "不使用Wrapper"
A[程序员] -->|"手动检查返回值"| B[系统调用]
B -->|"可能忘记检查"| C[隐患:错误被忽略]
end
subgraph "使用Wrapper"
D[程序员] -->|"直接调用"| E[Wrapper函数]
E -->|"自动检查"| F[系统调用]
F -->|"失败时"| G[打印错误并退出]
end
style C fill:#ffcdd2
style G fill:#e8f5e9
```
### 5.3 使用示例
```c
// 不使用 Wrapper容易忘记检查
int fd = open("file.txt", O_RDONLY);
if (fd < 0) {
perror("open");
exit(1);
}
// 使用 Wrapper自动检查代码更简洁
int fd = Open("file.txt", O_RDONLY, 0); // 注意:大写开头
```
### 5.4 常用Wrapper函数对照
| 系统调用 | Wrapper函数 | 包含头文件 |
|----------|-------------|-----------|
| `fork()` | `Fork()` | `wrapper.h` |
| `execve()` | `Execve()` | `wrapper.h` |
| `wait()` | `Wait()` | `wrapper.h` |
| `open()` | `Open()` | `wrapper.h` |
| `read()` | `Read()` | `wrapper.h` |
| `write()` | `Write()` | `wrapper.h` |
| `close()` | `Close()` | `wrapper.h` |
| `malloc()` | `Malloc()` | `wrapper.h` |
| `free()` | `Free()` | `wrapper.h` |
| `pthread_create()` | `Pthread_create()` | `wrapper.h` |
### 5.5 编译时链接Wrapper库
```bash
# 编译时链接 libwrapper.a
gcc -o myprogram myprogram.c -L. -lwrapper
# 或者在 Makefile 中
LDFLAGS = -L. -lwrapper
```
> **注意**Wrapper函数名是对应系统调用的首字母大写形式。详见 [[附录A_Wrapper库参考]]。
---
## 六、命令行参数
### 6.1 argc 和 argv
C程序的 `main` 函数可以接收命令行参数:
```c
int main(int argc, char *argv[])
```
- **argc**argument count参数个数包括程序名本身
- **argv**argument vector参数字符串数组`argv[0]` 是程序名
### 6.2 实例cmdpar.c
```c
// cmdpar.c - 命令行参数处理
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
int i;
printf("参数个数(含程序名): %d\n", argc);
for (i = 0; i < argc; i++)
printf("argv[%d] = %s\n", i, argv[i]);
return 0;
}
```
```bash
gcc cmdpar.c -o cmdpar
./cmdpar hello world 123
```
**预期输出**
```
参数个数(含程序名): 4
argv[0] = ./cmdpar
argv[1] = hello
argv[2] = world
argv[3] = 123
```
### 6.3 参数传递原理
```mermaid
graph TB
subgraph "命令行"
CMD["./cmdpar hello world 123"]
end
subgraph "操作系统解析"
ARG0["argv[0] = \"./cmdpar\""]
ARG1["argv[1] = \"hello\""]
ARG2["argv[2] = \"world\""]
ARG3["argv[3] = \"123\""]
ARGC["argc = 4"]
end
CMD --> ARG0
CMD --> ARG1
CMD --> ARG2
CMD --> ARG3
CMD --> ARGC
style CMD fill:#fff3e0
style ARGC fill:#e1f5fe
```
---
## 七、基本C编程
### 7.1 hello.c
```c
// hello.c - 最简单的C程序
#include <stdio.h>
void main() {
printf("hello World\n");
}
```
```bash
gcc hello.c -o hello
./hello
# 输出hello World
```
### 7.2 sum.c -- 多文件项目
**calc.h**(头文件):
```c
// calc.h - 函数声明
#ifndef CALC_H
#define CALC_H
double aver(double, double);
double sum(double, double);
#endif
```
**sum.c**(实现文件):
```c
// sum.c - 求和函数实现
#include "calc.h"
double sum(double num1, double num2) {
return (num1 + num2);
}
```
**main.c**(主文件):
```c
// main.c - 主函数
#include <stdio.h>
#include "calc.h"
int main() {
double a = 10.0, b = 20.0;
printf("Sum = %.2f\n", sum(a, b));
printf("Average = %.2f\n", aver(a, b));
return 0;
}
```
```bash
# 分步编译
gcc -c sum.c -o sum.o
gcc -c main.c -o main.o
gcc sum.o main.o -o calc
./calc
```
---
## 八、知识关联
- 编译链接过程在 [[01_系统运行机制]] 中理解程序如何被加载到内存
- 可执行程序的内存布局(堆、栈)在 [[05_进程控制]] 中与进程地址空间对应
- gdb调试在实验中会大量使用
- Makefile在 [[09_并发网络服务器]] 的多文件项目中会用到
- Wrapper库贯穿整个课程详见 [[附录A_Wrapper库参考]]
- 文件I/O编程是 [[04_文件IO编程]] 的核心内容
---
## 九、思考题
1. **预处理阶段做了什么?** `#include <stdio.h>` 在预处理后变成了什么?
2. **为什么需要链接?** 如果没有链接器,编程会变成什么样?
3. **.bss段为什么不在文件中占空间** 系统如何保证未初始化全局变量为0
4. **栈和堆的增长方向为什么相反?** 这种设计有什么好处?
5. **Wrapper库的价值是什么** 为什么课程不直接使用系统调用?
6. **`if (a = 1)``if (a == 1)` 的区别?** 如何用gdb发现这类bug
---
## 十、扩展阅读
- 《深入理解计算机系统》第7章链接
- 《UNIX环境高级编程》第1章UNIX基础知识
- GCC官方文档https://gcc.gnu.org/onlinedocs/
- GDB官方教程https://www.sourceware.org/gdb/documentation/