Appearance
6.1810 2024 第2讲:用C语言为xv6编程
为什么内核开发偏爱C语言?
- 适合底层编程:
- C语言与RISC-V指令之间有简单的映射关系。
- C语言类型能直接对应硬件结构,例如,可以直接操作设备硬件寄存器的位标志。
- 最小的运行时 (Minimal runtime):
- 易于移植到其他硬件平台。
- 允许对硬件的直接访问。
- 显式的内存管理:
- 没有垃圾回收器(GC),内核完全控制内存管理。
- 高效:
- C是编译型语言,直接编译成机器码,没有解释器开销。
- 流行度:
- 是构建内核、系统软件等事实上的标准,在几乎所有平台上都有良好的支持。
- 缺点:
- 容易写出不正确的代码。
- 容易写出有安全漏洞的代码。
本讲重点:xv6中的C语言实践
- 内存布局
- 指针
- 数组
- 字符串
- 链表
- 位运算符
C程序的内存布局
一个C程序在xv6中的典型内存布局(参见教材图3.4):
- Text段: 存放代码和只读数据。
- Data段: 存放已初始化的全局变量。
- BSS段: 存放未初始化或初始化为零的全局变量。
- Stack(栈): 存放函数的局部变量,向下增长。
- Heap(堆): 用于动态内存分配(通过
sbrk,malloc/free),向上增长。
C 指针 (Pointers)
- 指针是一个内存地址。
- 每个变量都有一个内存地址 (
p = &i)。 - 可以通过指针解引用来访问变量 (
*p)。 - 指针本身也是一个变量,它也有自己的内存地址 (
pp = &p)。 - 指针算术:
p + 1增加的地址量取决于指针p的类型大小。例如,int *p; p+1;地址会增加4个字节(在64位系统上)。
C 数组 (Arrays)
- 数组是相同数据类型的连续内存区域。
- C语言不进行数组边界检查,也不会自动增长。
- 访问方式:
- 索引:
buf[0],buf[1] - 指针:
*buf,*(buf+1)
- 索引:
C 字符串 (Strings)
- 字符串是以空字符
\0结尾的字符数组。 ulib.c中有几个字符串处理函数,如strlen()和strcmp()。main函数的argv参数是一个字符串数组(char *argv[]),其中每个元素都是一个指向命令行参数字符串的指针。
C 链表 (Lists)
- 单向链表:
kernel/kalloc.c使用单向链表来管理空闲内存页。kfree()将一个页添加到链表头,kalloc()从链表头取出一个页。 - 双向链表:
kernel/bio.c使用双向链表来实现一个LRU(最近最少使用)的缓冲区缓存。当一个缓冲区被释放时(brelse()),它需要被移动到链表的头部。
位运算符
- C语言提供了
|(或),&(与),~(非),^(异或) 等运算符来直接操作变量的二进制位。 - 这在底层编程中非常有用,例如,用于设置或清除硬件寄存器中的特定标志位。
常见C语言Bug
- 释放后使用 (Use after free): 访问已经释放的内存。
- 重复释放 (Double free): 多次释放同一块内存。
- 使用未初始化的内存: 栈上或
malloc返回的内存内容是随机的,不是零。 - 缓冲区溢出 (Buffer overflow): 写入超出数组边界。
- 内存泄漏 (Memory leak): 分配了内存但忘记释放。
- 类型混淆 (Type confusion): 错误的类型转换可能导致数据损坏或安全问题。