BUAA_OS_2020_Lab1_Code_Review

最近一个月已经做过了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
Lab1文件树(已折叠)

 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)
./boot/start.S(已折叠)

 在这个文件中首先定义了三个全局变量、定义了数据段与栈空间,然后便是_start函数。_start函数设置了CP0状态、禁用了内核缓存、设置了栈空间后跳转到了C语言的main函数。具体的操作在代码的注释中已经标注出来了,其实都是通过写入寄存器完成的。这个文件还引入了三个与汇编有关的头文件,其中cp0regdef.hregdef.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:
./include/asm/asm.h(已折叠)

首先其定义了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] == '\0')) return;
17 
18     for (i = 0; i < l; i++) {
19         printcharc(s[i]);
20         if (s[i] == '\n') printcharc('\n');
21     }
22 }
23 
24 void printf(char *fmt, ...) {
25     va_list ap;
26     va_start(ap, fmt);
27     lp_Print(myoutput, 0, fmt, ap);
28     va_end(ap);
29 }
30 
31 void _panic(const char *file, int line, const char *fmt, ...) {
32     va_list ap;
33 
34     va_start(ap, fmt);
35     printf("panic at %s:%d: ", file, line);
36     lp_Print(myoutput, 0, (char *) fmt, ap);
37     printf("\n");
38     va_end(ap);
39 
40     for (;;);
41 }
./lib/printf.c(已折叠)

 熟悉的printf()就定义在了这里,不过可以看到,它并没有直接实现解析格式串和打印的功能,而是定义了一个可变参数列表,并将其与符号串以及myoutput()的函数指针一同传入了另一个lp_print()函数中。这个lp_print()函数也就是要我们来实现的函数。与va即variable arguments有关的定义在stdarg.h中(如下),va_list、va_start、va_end分别对应ap(指向变参的指针)的声明、初始化与析构,每次调用va_arg(ap, type),从参数列表中返回一个类型为type的参数,具体的用法可以自行查阅。

 1 //
 2 // stdarg.h
 3 //
 4 //      Copyright (c) Microsoft Corporation. All rights reserved.
 5 //
 6 // The C Standard Library <stdarg.h> header.
 7 //
 8 #pragma once
 9 #define _INC_STDARG
10 
11 #include <vcruntime.h>
12 
13 _CRT_BEGIN_C_HEADER
14 
15 #define va_start __crt_va_start
16 #define va_arg   __crt_va_arg
17 #define va_end   __crt_va_end
18 #define va_copy(destination, source) ((destination) = (source))
19 
20 _CRT_END_C_HEADER
stdarg.h(已折叠)

传的myoutput()函数指针是在./drivers/gxconsole/console.c中函数的指针,这个文件的内容如下:

 1 /*
 2  * ./drivers/gxconsole/console.c
 3  */
 4 
 5 #include "dev_cons.h"
 6 
 7 /*  Note: The ugly cast to a signed int (32-bit) causes the address to be
 8     sign-extended correctly on MIPS when compiled in 64-bit mode  */
 9 #define    PHYSADDR_OFFSET        ((signed int)0x80000000)
10 
11 #define    PUTCHAR_ADDRESS        (PHYSADDR_OFFSET +        \
12                 DEV_CONS_ADDRESS + DEV_CONS_PUTGETCHAR)
13 #define    HALT_ADDRESS        (PHYSADDR_OFFSET +        \
14                 DEV_CONS_ADDRESS + DEV_CONS_HALT)
15 
16 void printcharc(char ch) {
17     *((volatile unsigned char *) PUTCHAR_ADDRESS) = ch;
18 }
19 
20 void halt(void) {
21     *((volatile unsigned char *) HALT_ADDRESS) = 0;
22 }
23 
24 void printstr(char *s) {
25     while (*s) printcharc(*s++);
26 }

可以看出,这个打印字符的函数,其实现方式还是向某个特定的地址写入一个字符,具体的数值定义在./drivers/gxconsole/dev_cons.h中,不再介绍了。

除了printf()外,printf.c中还定义了一个_panic()函数,panic机制是Linux系统中当内核运行出现问题时,终止系统并向屏幕打印错误日志的一种机制,这里是一种简易的实现,即先打印错误信息再进入一个死循环来实现功能。值得注意的是,这个函数在头文件中被__attribute__((noreturn))修饰为可以没有返回值,并封装成了panic()函数宏以供使用。

__attribute__((noreturn))

作用:定义有返回值的函数时,而实际情况有可能没有返回值,此时编译器会报错。加上attribute((noreturn))则可以很好的处理类似这种问题。

用法:__attribute__((noreturn))

例子:

1 void __attribute__((noreturn)) onExit();
2 
3 int test(int state) {
4     if (state == 1) {
5         onExit();
6     } else {
7         return 0;
8     }
9 }

了解了以上知识后,便可以补全print.c中的相关函数。因为涉及剧透并且这个文件中没有过于复杂或难以理解的代码,因此不在此处展开print.c的相关分析了。值得注意的是,在其对应的头文件中,定义了buffer的最大长度为80,并且没有发现当buffer长度超过最大限度时会怎样处理,因此当使用printf()打印过长的参数时,可能会出现问题。

至此,Lab1部分的代码就已经梳理得差不多了。本文没有介绍过多有关操作系统的知识(理论课和指导书已经讲解得很透彻了),而是从语言层面上分析了vmlinux小操作系统的代码,分析后感觉思路清晰了不少,希望能够对以后的学习有所帮助(并防止返校之后的课上测试受害)

posted @ 2020-03-30 23:26  LittleNyima  阅读(1062)  评论(0编辑  收藏  举报