最近一个月已经做过了OS实验的内核启动和内存管理两个lab,也学习了OS理论课的相关知识。然而在面对实验课给出的操作系统代码时仍然感到比较茫然,对于课下测试的要求也仍有些不知所措。因此决定在此梳理一下操作系统实验的核心代码,顺便整理一下相关的知识点,以期对操作系统有一个更清晰的了解。
首先,Lab1的文件树如下:
1 . 2 ├── boot 3 │ ├── Makefile 4 │ └── start.S 5 ├── drivers 6 │ ├── gxconsole 7 │ │ ├── console.c 8 │ │ ├── dev_cons.h 9 │ │ └── Makefile 10 │ └── Makefile 11 ├── gxemul 12 │ ├── elfinfo 13 │ ├── r3000 14 │ ├── r3000_test 15 │ └── test 16 ├── include 17 │ ├── asm 18 │ │ ├── asm.h 19 │ │ ├── cp0regdef.h 20 │ │ └── regdef.h 21 │ ├── asm-mips3k 22 │ │ ├── asm.h 23 │ │ ├── cp0regdef.h 24 │ │ └── regdef.h 25 │ ├── env.h 26 │ ├── error.h 27 │ ├── kclock.h 28 │ ├── mmu.h 29 │ ├── pmap.h 30 │ ├── printf.h 31 │ ├── print.h 32 │ ├── queue.h 33 │ ├── sched.h 34 │ ├── stackframe.h 35 │ ├── trap.h 36 │ └── types.h 37 ├── include.mk 38 ├── init 39 │ ├── init.c 40 │ ├── main.c 41 │ └── Makefile 42 ├── lib 43 │ ├── Makefile 44 │ ├── printBackUp 45 │ ├── print.c 46 │ └── printf.c 47 ├── Makefile 48 ├── readelf 49 │ ├── kerelf.h 50 │ ├── main.c 51 │ ├── Makefile 52 │ ├── readelf.c 53 │ ├── testELF 54 │ └── types.h 55 └── tools 56 └── scse0_3.lds
Makefile与Linker Script
顶层就是一个Makefile和它引用的include文件,其定义了vmlinux虚拟机的编译选项和一些其他的命令,例如clean等。比较值得注意的是其中定义的linker script:
1 /* 2 * ./tools/scse0_3.lds 3 */ 4 5 OUTPUT_ARCH(mips) 6 7 ENTRY(_start) 8 9 SECTIONS 10 { 11 . = 0x80010000; 12 .text : {*(.text)} 13 .data : {*(.data)} 14 .bss : {*(.bss)} 15 16 end = . ; 17 }
linker script是指导链接器在链接时控制可执行文件地址空间布局的脚本。其中的OUTPUT_ARCH(mips)
指定了输出的程序在MIPS架构的CPU上运行,ENTRY(_start)
指定了虚拟机的入口函数为_start
(这是一个汇编函数,定义在./boot/start.S
中),而SECTIONS
将程序的各个段定位到指定的位置(.text
对应代码段,.data
对应数据段,.bss
即Block Standard by Symbol对应未初始化的全局和静态变量段)。最后的end = .
是一个普通赋值语句,其中的.
是定位器,每定位一段之后其数值自增段的长度,因此此处是将end
赋值为了0x80010000+text、data、bss三段的长度之和,在内存管理的时候这个end还有用,这里不再赘述了。除了以上的功能之外,貌似linker script中还可以进行一些更复杂的操作,不过这里没有遇到就不再过多展开了。
关于Makefile的其他细节应该没什么了,不过可以利用Makefile定义一些方便的操作,比如将运行虚拟机的那一段指令定义为make run
,可以节约些许时间。
boot的汇编部分
在vmlinux的系统启动时,首先进入_start
函数,进行设备状态初始化、创建堆栈等操作。这个汇编函数定义在./boot/start.S
中:
1 # ./boot/start.S 2 3 #include <asm/regdef.h> 4 #include <asm/cp0regdef.h> 5 #include <asm/asm.h> 6 7 .data 8 .globl mCONTEXT 9 mCONTEXT: 10 .word 0 11 .globl delay 12 delay: 13 .word 0 14 .globl tlbra 15 tlbra: 16 .word 0 17 .section .data.stk 18 KERNEL_STACK: 19 .space 0x8000 20 21 .text 22 LEAF(_start) 23 24 .set mips2 25 .set reorder 26 27 /* Disable interrupts */ 28 mtc0 zero, CP0_STATUS 29 30 /* Disable watch exception. */ 31 mtc0 zero, CP0_WATCHLO 32 mtc0 zero, CP0_WATCHHI 33 34 /* disable kernel mode cache */ 35 mfc0 t0, CP0_CONFIG 36 and t0, ~0x7 37 ori t0, 0x2 38 mtc0 t0, CP0_CONFIG 39 40 /* set up stack */ 41 li sp, 0x80400000 42 li t0,0x80400000 43 sw t0,mCONTEXT 44 45 /* jump to main */ 46 jal main 47 nop 48 49 loop: 50 j loop 51 nop 52 END(_start)
在这个文件中首先定义了三个全局变量、定义了数据段与栈空间,然后便是_start
函数。_start
函数设置了CP0状态、禁用了内核缓存、设置了栈空间后跳转到了C语言的main
函数。具体的操作在代码的注释中已经标注出来了,其实都是通过写入寄存器完成的。这个文件还引入了三个与汇编有关的头文件,其中cp0regdef.h
与regdef.h
分别定义了协处理器和处理器的寄存器名称与用到的常量,可以略过。比较有趣的是asm.h
这个头文件,它定义了几个与汇编相关的函数宏:
1 /* 2 * asm.h: Assembler macros to make things easier to read. 3 */ 4 5 #include "regdef.h" 6 #include "cp0regdef.h" 7 8 /* 9 * LEAF - declare leaf routine 10 */ 11 #define LEAF(symbol) 12 .globl symbol; 13 .align 2; 14 .type symbol,@function; 15 .ent symbol,0; 16 symbol: .frame sp,0,ra 17 18 /* 19 * NESTED - declare nested routine entry point 20 */ 21 #define NESTED(symbol, framesize, rpc) 22 .globl symbol; 23 .align 2; 24 .type symbol,@function; 25 .ent symbol,0; 26 symbol: .frame sp, framesize, rpc 27 28 29 /* 30 * END - mark end of function 31 */ 32 #define END(function) 33 .end function; 34 .size function,.-function 35 36 #define EXPORT(symbol) 37 .globl symbol; 38 symbol: 39 40 #define FEXPORT(symbol) 41 .globl symbol; 42 .type symbol,@function; 43 symbol:
首先其定义了leaf routine与nested routine,后者是嵌套过程,前者直译是“叶过程”,叶过程不调用其他过程,而嵌套过程会调用其他过程。这两者的区别就在于是否用到堆栈,叶过程因为不嵌套调用所以用不到堆栈。之所以这样命名,也许是在类比数据结构中的叶节点,叶节点没有子树对应于叶过程没有子过程。由于其中用到了数个手册中没有的directives,实现原理暂时不明。除了这两个之外还定义了函数结束标记、全局变量与全局函数标记,这三者的实现原理在代码中的体现比较清晰。
汇编部分执行结束后跳转到的main()
定义在./init/main.c
,不过由于lab1的main()
只是打印了几句话,所以略过这一部分。在main()
中调用mips_init()
后,boot的过程就算结束了。
printf()相关部分
除了boot相关的代码,lab1的另一个比较重要的部分便是与printf()
相关的代码,并且补全printf()
相关函数也是lab1课下的任务之一,可以说这一部分是之后所有评测的基础(如果print写错的话后边就gg了)。(其实lab1还有一个readelf
子程序,这个部分的头文件注释已经很清晰了,而且与整个系统的其他部分无关,所以也略过这一部分。)
printf()函数的定义位于./lib/printf.c中,其依赖关系如下图所示:
上图中的stdarg.h
为C标准库,用于支持可变参数的接收,其他文件均包含在lab1的项目源代码中。printf.c代码如下:
1 /* 2 * ./lib/printf.c 3 */ 4 5 #include <printf.h> 6 #include <print.h> 7 #include <drivers/gxconsole/dev_cons.h> 8 9 void printcharc(char ch); 10 11 void halt(void); 12 13 static void myoutput(void *arg, char *s, int l) { 14 int i; 15 // special termination call 16 if ((l == 1) && (s[0] == '