XV6操作系统入门系列-03-栈与函数调用

353 阅读6分钟

XV6操作系统入门系列

03-栈与函数调用

前情提要

1. RiscV引导程序

RiscV在引导程序时,初始化了几个寄存器:a0,a1,a2,t0,pc,整个过程发生在硬件电路中。我汇总一下这个寄存器的状态。在引导程序的最后,程序计数器调整到 0x8000_0000 的内核代码继续执行。

2. XV6代码在硬件中的布局

RiscV上电后的第一行代码在ROM只读寄存器中,对应起始地址是 0x1000,由硬件控制。操作系统被装载在内存中,是一系列精心设计的代码,它们背后包含着很多巧妙的抽象概念,我们这个系列就是搞懂这些抽象概念。

第一个概念是:抽象代码占据着硬件内存的一段实体空间。 抽象的代码依次罗列在内存中,按照代码文件顺序,依次是:entry.S,start.c 等等。

第二个概念是:每个文件包含一系列符号(symbol),它们是对自己内存空间的某些位置起的名字。名字仅仅是为了人类方便阅读,硬件Risc-V并不知晓这些名字。

entry.S详解

entry.S 文件只做了两件事情:

  1. 初始化每个CPU计算核心的栈空间;
  2. 跳转到 start函数继续执行。

第一节事情:栈的初始化

在xv6启动了8个计算核心。在 entry.S 中,申请了一段连续的内存空间,并且将这段连续的内存空间平均分成8份,分配给这8个核心。

由于这段内存空间在后续有特别的用途,我们将它起名叫做栈。每个核心的分配到一个4096字节(byte)的栈,换算下来是4096x8=32,768位(bit)。

栈是一种抽象的约束规则,RiscV不会限制它的访问,这种约束规则是由软件代码来实现的。

第二件事情:函数调用指令

第二件事情对应 entry.S 的最后执行的代码:

call start

表面看起来很简单,就是调用函数 start。但实际上,这不仅仅是一行简单的代码。

调用一个函数存在一个的约定惯例,这个约定惯例是由RiscV指令集C语言共同规定的。为了实现这个约定惯例,RiscV硬件电路和编译器默默做了一些事情,并且对于操作系统而言是透明的。也正是这个函数调用惯例需要用到栈,所以在entry.S中需要初始化栈。

第一视角,从纯软件层面看,entry.S 调用 call 命令跳转到 start.c

第二视角,从编译器编译的汇编层面看,start.c开头几行代码并不是原来的C语言代码,由编译器根据被调用者的惯例添加。我这里简单解释一下:

  • add sp,sp,-16:栈指针减去16。因为栈由高位地址向低位地址,等价于向栈申请16个字节。
  • sd ra,8(sp) :存入返回地址寄存器 ra,因为是64位处理器,所以需要8个字节。
  • sd s0,0(sp):存入调用前的函数所用的栈顶地址s0,s0也叫做帧指针寄存器
  • add s0,sp,16:将当前函数的栈顶地址存入帧指针寄存器s0。

第三视角,从硬件电路层面看,跳转指令call编译后的jal指令做了两件事情。将当前jal的下一条指令存入返回地址寄存器 ra。由于jal被放在0x8000_0016,所以返回地址 ra=0x8000_0016+4=0x8000_001a

RiscV通用寄存器表

目前,我们接触到了很多CPU内部的寄存器:程序计数器pc、栈指针sp、帧指针s0,返回地址寄存器ra等等。我们干脆汇总一下RiscV里的常规寄存器。

除了程序计数器pc是特殊寄存器外,其他寄存器都是通用寄存器。习惯上,我们选择将一些通用寄存器用作专门用途。虽然纯粹硬件电路上看,这些寄存器和其他通用寄存器没有特殊的地方,但是在编程的时候,我们会根据它们的用途,起一个别名,方便记忆。

RiscV一共有32个通用寄存器x0x31,按照使用惯例,它们被赋予对应的别名。 比如前面的栈指针sp实际上也是x2寄存器,帧指针s0实际上是x8寄存器,返回地址寄存器ra实际上是x1寄存器。

寄存器 别名描述
x0zero 硬连线为0
x1 ra返回地址 
x2sp 栈指针
x3gp全局指针 
x4 tp线程指针 
x5t0 临时/备用链接寄存器 
x6-x7t1-t2临时寄存器 
x8 s0/fp保存寄存器/帧指针 
x9 s1保存寄存器
x10-x11a0-a1函数参数/返回值
x12-x17a2-a7函数参数 
x18-x27s2-s11保存寄存器 
x28-x31t3-t6临时寄存器 

RiscV除了一些通用寄存器,还有一系列专门用途的寄存器,硬件电路上有专门的设计,和通用寄存器也不一样。在操作系统中会碰到的任务调度内存管理内核态等等操作系统的概念,底层依赖都这些寄存器。在学习到相关概念时,我们同时也需要了解硬件层面的概念。

网上很多文章都说过,他们在做到内存管理部分的实验时,感觉非常困难。我认为,本质的原因就是他们硬件概念的不熟悉,概念只停留在软件层面,在靠直觉和猜测做题目。在应试教育中,我们经常会用到直觉和猜测,效果还会不错。

但是,硬件中很多东西都是人为的规定,我们当然无法凭空猜测多年以前千里之外的人做出了如何的设计了RiscV处理器和C编程语言,所以直觉在这里是失效的,从而导致了我们难以理解软件层面的代码。

总结

我们通过分析entry.S文件具体作用,理解了操作系统中就是一段特殊用途的内存空间。同时通过对entry.S中函数调用功能的解析,我们引入了一个新的概念,函数调用惯例。函数调用惯例涉及到RiscV处理器、C语言编译器以及操作系统共同的配合。在调用一个函数的过程中,C语言程序隐式地用到了栈。因此,在调用C语言代码前,需要初始化栈