C 贴近硬件
C 是系统编程的"通用语"——直接映射指令、无运行时、完全控制内存。
MIT 6.1810 · 第 2 课
操作系统内核几乎都用 C 编写。本课聚焦于 xv6 内核中实际使用的 C 语言特性:内存布局、指针、数组、字符串、链表、位运算,以及最常见的 C 语言陷阱。
学习识别这些陷阱,是成为系统程序员的基本功。
参考 xv6 教材图 3.4,一个用户程序的地址空间由以下几段组成:
| 段 | 内容 | 属性 |
|---|---|---|
| text | 机器码指令、只读数据 | R+X, 无 W |
| data | 已初始化的全局/静态变量 | R+W |
| stack | 函数局部变量、返回地址 | R+W, 向下增长 |
| heap | malloc/sbrk 动态分配 | R+W, 向上增长 |
$ riscv64-linux-gnu-objdump -htr user/cat.o # 目标文件 + 重定位记录
$ riscv64-linux-gnu-objdump -fp user/_cat # 可执行文件头 + 段
$ riscv64-linux-gnu-objdump -S user/_cat # 反汇编
$ riscv64-linux-gnu-nm -n user/_cat # 符号表 + 地址
可执行文件使用 a.out 格式,包含 text、data、符号表、调试信息等段。ulib.o 是 xv6 的最小 C 库,提供 main() 入口和系统调用封装。
// ulib.c 中的 _start
void _start() {
exit(main()); // 调用 main,返回后 exit
}
_start 是 ELF 入口点(user.ld 定义),由它调用用户的 main()。
指针是内存地址。每个变量都有地址,可以通过指针间接访问:
int i = 10;
int *p = &i; // p 保存 i 的地址
*p = 20; // 通过指针修改 i
int a = *p; // 通过指针读取 i
char *c;
int *i;
// c+1 的地址 = c 的地址 + 1
// i+1 的地址 = i 的地址 + 4(RISC-V 上 int 为 4 字节)
指针加 1 的偏移量取决于指向类型的大小——这是 C 数组访问的基础。
struct point { int x; int y; };
struct point p = {1, 2};
struct point *pp = &p;
pp->x = 10; // 等价于 (*pp).x = 10
char buf[64];
buf[0] = 'a';
*(buf + 1) = 'b'; // 等价于 buf[1] = 'b'
C 数组是连续的内存,不检查边界,不能自动增长。数组访问 buf[i] 和指针访问 *(buf+i) 完全等价。
C 字符串是以 \0(字节 0)结尾的字符数组:
char *s = "hello"; // h e l l o \0
char buf[] = "world"; // 栈上数组
ulib.c 提供常用字符串函数:
// strlen 用数组索引
int strlen(char *s) {
int n = 0;
while (s[n]) n++;
return n;
}
// strcmp 用指针
int strcmp(char *p, char *q) {
while (*p && *p == *q)
p++, q++;
return (uchar)*p - (uchar)*q;
}
xv6 内核大量使用链表。
// kernel/kalloc.c
struct run {
struct run *next; // 单链表指针
};
struct {
struct run *head; // 链表头
} kmem;
void kfree(void *pa) {
struct run *r = (struct run*)pa;
r->next = kmem.head;
kmem.head = r; // 头插法
}
void *kalloc(void) {
struct run *r = kmem.head;
if (r) kmem.head = r->next;
return (void*)r;
}
// kernel/buf.h
struct buf {
struct buf *prev;
struct buf *next;
// ...
};
void brelse(struct buf *b) {
// 把 b 移到链表头部(LRU 策略)
b->next->prev = b->prev;
b->prev->next = b->next;
b->next = bcache.head.next;
b->prev = &bcache.head;
bcache.head.next->prev = b;
bcache.head.next = b;
}
C 提供四种基本位运算,直接映射到 RISC-V 指令:
10001 & 10000 = 10000 // 按位与:提取位
10001 | 10000 = 10001 // 按位或:设置位
10001 ^ 10000 = 00001 // 按位异或:翻转位
~1000 = 0111 // 按位取反
在系统编程中,位运算常用于操作硬件寄存器:
// kernel/fcntl.h
#define O_RDONLY 0x000
#define O_WRONLY 0x001
#define O_RDWR 0x002
#define O_CREATE 0x200
#define O_TRUNC 0x400
// 组合标志位:fd = open("f", O_WRONLY | O_CREATE | O_TRUNC);
| 陷阱 | 描述 |
|---|---|
| use-after-free | 访问已释放的内存 |
| double-free | 同一块内存被 free 两次 |
| 未初始化内存 | 栈变量和 malloc 返回的内存不自动清零 |
| buffer overflow | 写入超出数组边界 |
| memory leak | 分配后不释放,最终耗尽内存 |
| type confusion | 错误的类型转换导致解释错误 |
void f() {
int x; // x 的值未定义!
char buf[64]; // buf 内容未定义
if (x == 0) ... // 行为不确定
}
内核中尤其危险——未初始化的内存可能泄露敏感数据(如密钥),这就是著名的"内核栈信息泄露"漏洞。
static:限制变量/函数的可见性为文件内部,但在文件内是"全局的"(整个程序生命周期)。void:表示"无类型"、"无返回值"、"无参数"。void * 是通用指针类型。Q1:为什么操作系统内核通常用 C 而不是 Java/Go/Python?
Q2:设有 int arr[10]; int *p = arr;,p+3 的地址与 p 的地址相差多少字节?
Q3:以下哪项不是 xv6 中 C 链表的典型用途?
Q4:表达式 0x0F & 0x3C 的结果是?
Q5:以下哪项是典型的 C 安全漏洞?
C 是系统编程的"通用语"——直接映射指令、无运行时、完全控制内存。
指针、数组、字符串、链表本质上都是内存地址的操作。
与、或、异或、取反——操作硬件寄存器、标志位的基础。
use-after-free、buffer overflow、未初始化内存——这些是内核漏洞的温床。