# 第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下的可执行文件采用 **ELF（Executable 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/
