vault backup: 2026-06-13 23:46:22
This commit is contained in:
631
操作系统/03_C语言编程基础/03_C语言编程基础.md
Normal file
631
操作系统/03_C语言编程基础/03_C语言编程基础.md
Normal file
@@ -0,0 +1,631 @@
|
||||
# 第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/
|
||||
Reference in New Issue
Block a user