Skip to content

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): 错误的类型转换可能导致数据损坏或安全问题。