zoukankan      html  css  js  c++  java
  • 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] == '')) return;
    17 
    18     for (i = 0; i < l; i++) {
    19         printcharc(s[i]);
    20         if (s[i] == '
    ') printcharc('
    ');
    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("
    ");
    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小操作系统的代码,分析后感觉思路清晰了不少,希望能够对以后的学习有所帮助(并防止返校之后的课上测试受害)

  • 相关阅读:
    python:利用asyncio进行快速抓取
    os.path.exists(path) 和 os.path.lexists(path) 的区别
    isdigit()判断是不是数字
    switf资源
    51cto培训课程
    51cto运维培训课程
    Python: 在Unicode和普通字符串之间转换
    VC++ CopyFile函数使用方法
    Eclipse断点调试
    AFNetworking2.0后 进行Post请求
  • 原文地址:https://www.cnblogs.com/littlenyima/p/12601639.html
Copyright © 2011-2022 走看看