操作系统实验1

 

HUNAN  UNIVERSITY

 

 

 

 

操作系统

实验报告

 

 

 

 

 

 

 

  目:

实验1

 

学生姓名:

周思宇

 

学生学号:

201608030201

 

专业班级:

计科1601

 

完成日期:

2018.11.22

目  录

一、内容

二、目的

三、实验设计思想和流程

四、主要数据结构及符号说明

五、实验环境以及实验过程与结果分析

六、实验体会和思考题

附录(源代码及注释)

一、内容

lab1中包含一个bootloader和一个OS。这个bootloader可以切换到X86保护模式,能够读磁盘并加载ELF执行文件格式,并显示字符。而这lab1中的OS只是一个可以处理时钟中断和显示字符的幼儿园级别OS。

 

二、目的

操作系统是一个软件,也需要通过某种机制加载并运行它。在这里我们将通过另外一个更加简单的软件-bootloader来完成这些工作。为此,我们需要完成一个能够切换到x86的保护模式并显示字符的bootloader,为启动操作系统ucore做准备。lab1提供了一个非常小的bootloader和ucore OS,整个bootloader执行代码小于512个字节,这样才能放到硬盘的主引导扇区中。通过分析和实现这个bootloader和ucore OS,读者可以了解到:

 

计算机原理

  • CPU的编址与寻址: 基于分段机制的内存管理
  • CPU的中断机制
  • 外设:串口/并口/CGA,时钟,硬盘

 

Bootloader软件

  • 编译运行bootloader的过程
  • 调试bootloader的方法
  • PC启动bootloader的过程
  • ELF执行文件的格式和加载
  • 外设访问:读硬盘,在CGA上显示字符串

 

ucore OS软件

  • 编译运行ucore OS的过程
  • ucore OS的启动过程
  • 调试ucore OS的方法
  • 函数调用关系:在汇编级了解函数调用栈的结构和处理过程
  • 中断管理:与软件相关的中断处理
  • 外设管理:时钟

 

三、实验设计思想和流程(需要编程的题)

第五题:

代码设计步骤:

  1. 调用read_ebp()以获得ebp.类型为(uint32_t);
  2. 调用read_eip()以获得eip的值。类型为(uint32_t);
  3. 从0.。。。到stackdepth

ebp、eip的值打印出来

uint32_t)calling arguments [0..4] = the contents in address (unit32_t)ebp +2 [0..4]

输出("n");

调用print_debuginfo(eip-1)来打印c语言调用函数名和行号等。

弹出调用堆栈帧

注意:

调用函数的返回加法器eip=ss:[ebp+4]

调用函数的ebp=ss:[ebp]

 

第六题:

代码设计步骤:

首先确定每个中断服务例程的entry addrs在哪里?

存储在vectors中。

uintptr_t_vectors[]在哪里?

_vectors[]位于kern/trap/vector.S中,它是由tools/vector.c生成的。

可以使用“extern uintptr_t __vectors[]”定义这个extern变量。

然后,在中断描述表(IDT)中设置ISR的条目。

然后使用“lidt”指令让CPU知道IDT在哪里。

Google libs/x86.h看一下具体是啥意思

题目中也给过,lidt的参数是idt_pd。

 

然后是设置100tick的输出,更简单:

在定时器中断之后,使用全局变量记录

每个TICK_NUM周期,都可以使用函数打印print_ticks()。

 

第七题:

代码设计步骤:

指针得弄懂,堆栈也得彻底弄懂才能看懂

在从内核态,通过中断,切换为用户态时:

首先要执行sub 0x8,esp 这个语句

然后执行int T_SWITCH_TOU表示发生这个中断,按照之前的叙述,此时的执行过程是:中断向量--查找中断向量表--查找入口地址:发现此时CPL并没有发生切换

所以并不把当前的ss和esp入栈,直接把eflags,cs,eip入栈,然后进入vector规定的地址后,继续把errorno和trapno入栈,然后进入__alltraps,把ds es gs ss 入栈,pushal,当前esp入栈,执行trap,执行trapdispatch,执行相应中断向量号case处的代码:

当前不是用户态,要不要切换呢?此时需要对堆栈进行一些操作。现在是内核栈,我们原来从用户到内核态转换时,是通过TSS查到内核态的ss和esp的,但是这里似乎并没有从TSS查用户态的ss和esp?我们需要自己建立一个堆栈给他使用,这个就是这里的switchk2u变量所对应的地址。这个变量是全局变量,所以它具体在哪存着?

所以至少有一个意识:用户态的堆栈和内核态的堆栈不在一个地方

看具体代码实现

这里首先把tf所指的内容复制过来到switchk2u所对应的地址上,然后设置switchk2u这个变量的cs段,ds,es,ss等为用户数据段选择。

为啥设置esp呢?

原来的trapframe结构的esp保存的是如果发生了权限切换,那么保存原来那个特权级的esp,便于之后恢复。

他的意思是tf结构体esp所在的位置。然而真正给esp赋值的地方,是在中断结束返回后,手动把当前的ebp的值给esp。

之后设置eflags,因为用户态要实现IO,需要把eflags寄存器中的IOPL标志位设置为3,这样CPL<=IOPL是恒成立的,用户态也可以实现IO了

switchk2u是与内核栈不同的一个地址,我们要把它作为新的用户栈,并且还要保证在iret恢复寄存器时,要从switchl2u所规定的这个栈中恢复。

那该如何实现iret恢复寄存器时,是从switchk2u这里恢复而不是从之前的tf这里恢复呢????

trapentry.S在call trap后第一句执行的语句是什么?

popl esp

这个popl esp就是我们修改用户栈指针的地方。如果把popl esp这个语句原来要弹出的内容,换成switchk2u的地址,那么就可以把esp指针设置为switchk2u了。要时后来根据esp恢复寄存器,就会从switchk2u这块恢复。

具体实现过程在后面。这一部分只说思路。

 

四、主要数据结构及符号说明(需要编程的题)

第五题:

数据结构:函数堆栈

一个函数调用动作可分解为:零到多个PUSH指令(用于参数入栈),一个CALL指令。CALL指令内部其实还暗含了一个将返回地址(即CALL指令下一条指令的地址)压栈的动作(由硬件完成)。几乎所有本地编译器都会在每个函数体之前插入类似如下的汇编指令:

这样在程序执行到一个函数的实际指令前,已经有以下数据顺序入栈:参数、返回地址、ebp寄存器。由此得到类似如下的栈结构

这两条汇编指令的含义是:首先将ebp寄存器入栈,然后将栈顶指针esp赋值给ebp。“mov ebp esp”这条指令表面上看是用esp覆盖ebp原来的值,其实不然。因为给ebp赋值之前,原ebp值已经被压栈(位于栈顶),而新的ebp又恰恰指向栈顶。此时ebp寄存器就已经处于一个非常重要的地位,该寄存器中存储着栈中的一个地址(原ebp入栈后的栈顶),从该地址为基准,向上(栈底方向)能获取返回地址、参数值,向下(栈顶方向)能获取函数局部变量值,而该地址处又存储着上一层函数调用时的ebp值。

ss:[ebp+4]处为返回地址

ss:[ebp+8]处为第一个参数值(最后一个入栈的参数值,此处假设其占用4字节内存)

ss:[ebp-4]处为第一个局部变量

ss:[ebp]处为上一层ebp值

由于ebp中的地址处总是“上一层函数调用时的ebp值”,而在每一层函数调用中,都能通过当时的ebp值“向上(栈底方向)”能获取返回地址、参数值,“向下(栈顶方向)”能获取函数局部变量值。如此形成递归,直至到达栈底。这就是函数调用栈。

内联函数Read Type()可以告诉我们当前EBP的值。以及

非内联函数Read Type()是有用的,它可以读取当前EIP的值,

因为在调用这个函数时,read_eip()可以读

容易堆叠。

函数说明:

内联函数read_ebp()可以告诉我们当前的ebp。

非内联函数read_eip()可以读取当前eip的值,因为在调用这个函数时,read_eip()可以轻松地从堆栈中读取调用方的eip。

在print_debuginfo()中,函数debuginfo_eip()可以获得关于调用链的足够信息。

print_stackframe()将跟踪并打印它们以进行调试。

在boot/bootasm.S中,在跳转到内核条目之前,ebp的值被设置为零,这就是边界。

 

第六题:

数据结构的解释:

__vectors[i]

这个数组是干什么的?

保存了每个中断向量的入口地址

而这些入口地址,就是当中断发生时,中断描述符中所对应的那个offset,所以一旦中断发生,中断处理程序首先是会跳到vector[i]所对应的代码

idt_init就是初始化中断向量表:vector.S规定了每个中断处理例程的代码偏移,然后idt_init通过这些偏移,设置好idt表,然后再通过lidt,把idt表的初始地址保存到idtr寄存器中,这样中断相关的数据结构初始化完毕了

 

第三小问的数据结构:

vector.S规定了中断的入口地址

vector.S文件,有两部分,第一部分是代码段,定义了vector0到vector255这256个标号所对应的代码段的起始位置,每个标号后的代码无非是两种:要么是压入0和中断向量,要么就不压入0,只压入中断向量。然后是jmp __alltraps

TICK_NUM之前都设置过了

每轮100次就调用print函数即可,没什么特别复杂的数据结构。

 

第七题:

   lab1_print_cur_status();当前状态:是内核态还是用户态?

   lab1_switch_to_user(); 转换去用户态

   lab1_switch_to_kernel();转换去内核态

五、实验环境以及实验过程与结果分析

练习1:理解通过make生成执行文件的过程。(要求在报告中写出对下述问题的回答)

列出本实验各练习中对应的OS原理的知识点,并说明本实验中的实现部分如何对应和体现了原理中的基本概念和关键知识点。

在此练习中,大家需要通过静态分析代码来了解:

操作系统镜像文件ucore.img是如何一步一步生成的?(需要比较详细地解释Makefile中每一条相关命令和命令参数的含义,以及说明命令导致的结果)

一个被系统认为是符合规范的硬盘主引导扇区的特征是什么?

       

实验分析:

lab1的整体目录结构如下所示

.

├── boot

│   ├── asm.h

│   ├── bootasm.S

│   └── bootmain.c

├── kern

│   ├── debug

│   │  ├── assert.h

│   │  ├── kdebug.c

│   │  ├── kdebug.h

│   │  ├── kmonitor.c

│   │   ├── kmonitor.h

│   │  ├── panic.c

│   │  └── stab.h

│   ├── driver

│   │  ├── clock.c

│   │  ├── clock.h

│   │  ├── console.c

│   │  ├── console.h

│   │  ├── intr.c

│   │  ├── intr.h

│   │  ├── kbdreg.h

│   │  ├── picirq.c

│   │  └── picirq.h

│   ├── init

│   │  └── init.c

│   ├── libs

│   │  ├── readline.c

│   │  └── stdio.c

│   ├── mm

│   │  ├── memlayout.h

│   │  ├── mmu.h

│   │  ├── pmm.c

│   │  └── pmm.h

│   └── trap

│       ├── trap.c

│       ├── trapentry.S

│       ├── trap.h

│       └── vectors.S

├── libs

│   ├── defs.h

│   ├── elf.h

│   ├── error.h

│   ├── printfmt.c

│   ├── stdarg.h

│   ├── stdio.h

│   ├── string.c

│   ├── string.h

│   └── x86.h

├── Makefile

└── tools

    ├── function.mk

    ├── gdbinit

    ├── grade.sh

    ├── kernel.ld

    ├── sign.c

    └── vector.c

bootloader部分

  • boot/bootasm.S :定义并实现了bootloader最先执行的函数start,此函数进行了一定的初始化,完成了从实模式到保护模式的转换,并调用bootmain.c中的bootmain函数。
  • boot/bootmain.c:定义并实现了bootmain函数实现了通过屏幕、串口和并口显示字符串。bootmain函数加载ucore操作系统到内存,然后跳转到ucore的入口处执行。
  • boot/asm.h:是bootasm.S汇编文件所需要的头文件,主要是一些与X86保护模式的段访问方式相关的宏定义。

ucore操作系统部分

系统初始化部分:

  • kern/init/init.c:ucore操作系统的初始化启动代码

内存管理部分:

  • kern/mm/memlayout.h:ucore操作系统有关段管理(段描述符编号、段号等)的一些宏定义
  • kern/mm/mmu.h:ucore操作系统有关X86 MMU等硬件相关的定义,包括EFLAGS寄存器中各位的含义,应用/系统段类型,中断门描述符定义,段描述符定义,任务状态段定义,NULL段声明的宏SEG_NULL, 特定段声明的宏SEG,设置中断门描述符的宏SETGATE(在练习6中会用到)
  • kern/mm/pmm.[ch]:设定了ucore操作系统在段机制中要用到的全局变量:任务状态段ts,全局描述符表gdt[],加载全局描述符表寄存器的函数lgdt,临时的内核栈stack0;以及对全局描述符表和任务状态段的初始化函数gdt_init

外设驱动部分:

  • kern/driver/intr.[ch]:实现了通过设置CPU的eflags来屏蔽和使能中断的函数;
  • kern/driver/picirq.[ch]:实现了对中断控制器8259A的初始化和使能操作;
  • kern/driver/clock.[ch]:实现了对时钟控制器8253的初始化操作;- kern/driver/console.[ch]:实现了对串口和键盘的中断方式的处理操作;

中断处理部分:

  • kern/trap/vectors.S:包括256个中断服务例程的入口地址和第一步初步处理实现。注意,此文件是由tools/vector.c在编译ucore期间动态生成的;
  • kern/trap/trapentry.S:紧接着第一步初步处理后,进一步完成第二步初步处理;并且有恢复中断上下文的处理,即中断处理完毕后的返回准备工作;
  • kern/trap/trap.[ch]:紧接着第二步初步处理后,继续完成具体的各种中断处理操作;

内核调试部分:

  • kern/debug/kdebug.[ch]:提供源码和二进制对应关系的查询功能,用于显示调用栈关系。其中补全print_stackframe函数是需要完成的练习。其他实现部分不必深究。
  • kern/debug/kmonitor.[ch]:实现提供动态分析命令的kernel monitor,便于在ucore出现bug或问题后,能够进入kernel monitor中,查看当前调用关系。实现部分不必深究。
  • kern/debug/panic.c | assert.h:提供了panic函数和assert宏,便于在发现错误后,调用kernel monitor。大家可在编程实验中充分利用assert宏和panic函数,提高查找错误的效率。

公共库部分

  • libs/defs.h:包含一些无符号整型的缩写定义。
  • Libs/x86.h:一些用GNU C嵌入式汇编实现的C函数(由于使用了inline关键字,所以可以理解为宏)。

工具部分

  • Makefile和function.mk:指导make完成整个软件项目的编译,清除等工作。
  • sign.c:一个C语言小程序,是辅助工具,用于生成一个符合规范的硬盘主引导扇区。
  • tools/vector.c:生成vectors.S,此文件包含了中断向量处理的统一实现。

 

这一部分写的贼长,助教你前面的不想看可以从P21的简化开始看!有画图也有简单描述文件之间的关系

Make之后:

 

在make ”V=”之后,输出:

+ cc kern/init/init.c

i386-elf-gcc -Ikern/init/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/init/init.c -o obj/kern/init/init.o

编译init.c

 

+ cc kern/libs/readline.c

i386-elf-gcc -Ikern/libs/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/libs/readline.c -o obj/kern/libs/readline.o

编译readline.c

 

+ cc kern/libs/stdio.c

i386-elf-gcc -Ikern/libs/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/libs/stdio.c -o obj/kern/libs/stdio.o

编译stdio.c

 

+ cc kern/debug/kdebug.c

i386-elf-gcc -Ikern/debug/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/debug/kdebug.c -o obj/kern/debug/kdebug.o

编译

 

+ cc kern/debug/kmonitor.c

i386-elf-gcc -Ikern/debug/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/debug/kmonitor.c -o obj/kern/debug/kmonitor.o

编译

 

+ cc kern/debug/panic.c

i386-elf-gcc -Ikern/debug/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/debug/panic.c -o obj/kern/debug/panic.o

编译

 

+ cc kern/driver/clock.c

i386-elf-gcc -Ikern/driver/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/driver/clock.c -o obj/kern/driver/clock.o

编译

 

+ cc kern/driver/console.c

i386-elf-gcc -Ikern/driver/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/driver/console.c -o obj/kern/driver/console.o

编译

 

+ cc kern/driver/intr.c

i386-elf-gcc -Ikern/driver/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/driver/intr.c -o obj/kern/driver/intr.o

编译

 

+ cc kern/driver/picirq.c

i386-elf-gcc -Ikern/driver/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/driver/picirq.c -o obj/kern/driver/picirq.o

编译

 

+ cc kern/trap/trap.c

i386-elf-gcc -Ikern/trap/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/trap/trap.c -o obj/kern/trap/trap.o

编译

 

+ cc kern/trap/trapentry.S

i386-elf-gcc -Ikern/trap/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/trap/trapentry.S -o obj/kern/trap/trapentry.o

编译trapentry.S

 

+ cc kern/trap/vectors.S

i386-elf-gcc -Ikern/trap/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/trap/vectors.S -o obj/kern/trap/vectors.o

编译

 

+ cc kern/mm/pmm.c

i386-elf-gcc -Ikern/mm/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/mm/pmm.c -o obj/kern/mm/pmm.o

编译

 

+ cc libs/printfmt.c

i386-elf-gcc -Ilibs/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/  -c libs/printfmt.c -o obj/libs/printfmt.o

编译

 

+ cc libs/string.c

i386-elf-gcc -Ilibs/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/  -c libs/string.c -o obj/libs/string.o

编译

 

+ ld bin/kernel

i386-elf-ld -m    elf_i386 -nostdlib -T tools/kernel.ld -o bin/kernel obj/kern/init/init.o obj/kern/libs/readline.o obj/kern/libs/stdio.o obj/kern/debug/kdebug.o obj/kern/debug/kmonitor.o obj/kern/debug/panic.o obj/kern/driver/clock.o obj/kern/driver/console.o obj/kern/driver/intr.o obj/kern/driver/picirq.o obj/kern/trap/trap.o obj/kern/trap/trapentry.o obj/kern/trap/vectors.o obj/kern/mm/pmm.o obj/libs/printfmt.o obj/libs/string.o

链接为kernel

在makefile文件里面,有详细写

 

+ cc boot/bootasm.S

i386-elf-gcc -Iboot/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Os -nostdinc -c boot/bootasm.S -o obj/boot/bootasm.o

$(bootfiles)

+ cc boot/bootmain.c

i386-elf-gcc -Iboot/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Os -nostdinc -c boot/bootmain.c -o obj/boot/bootmain.o

$(bootfiles)

 

+ cc tools/sign.c

gcc -Itools/ -g -Wall -O2 -c tools/sign.c -o obj/sign/tools/sign.o

gcc -g -Wall -O2 obj/sign/tools/sign.o -o bin/sign

sign

 

以上都是编译

+ ld bin/bootblock

i386-elf-ld -m    elf_i386 -nostdlib -N -e start -Ttext 0x7C00 obj/boot/bootasm.o obj/boot/bootmain.o -o obj/bootblock.o'

根据sign规范生成bootblock

为了生成ucore.img,首先需要生成bootblock  √kernel √

需要kernel.ld init.o readline.o stdio.o kdebug.o kmonitor.o panic.o clock.o console.o intr.o picirq.o trap.o trapentry.o vectors.o pmm.o  printfmt.o string.o kernel.ld都存在

 

obj/bootblock.out' size: 500 bytes    

build 512 bytes boot sector: 'bin/bootblock' success!     每个块为512字节

dd if=/dev/zero of=bin/ucore.img count=10000   

创建10000个块

10000+0 records in

10000+0 records out

5120000 bytes transferred in 0.054623 secs (93733191 bytes/sec)

dd if=bin/bootblock of=bin/ucore.img conv=notrunc

1+0 records in

1+0 records out

512 bytes transferred in 0.000051 secs (10034970 bytes/sec)

dd if=bin/kernel of=bin/ucore.img seek=1 conv=notrunc    

从第二个块开始写kernel中的内容

154+1 records in

154+1 records out

78940 bytes transferred in 0.001103 secs (71573359 bytes/sec)

 

简化:

为了生成bootblock,首先需要生成bootasm.o、bootmain.o、sign

生成bootasm.o需要bootasm.S

gcc -Iboot/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Os -nostdinc -c boot/bootasm.S -o obj/boot/bootasm.o

 

生成bootmain.o需要bootmain.c

gcc -Iboot/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Os -nostdinc -c boot/bootmain.c -o obj/boot/bootmain.o

 

生成sign工具的makefile代码为

$(call add_files_host,tools/sign.c,sign,sign)

$(call create_target_host,sign,sign)

gcc -Itools/ -g -Wall -O2 -c tools/sign.c -o obj/sign/tools/sign.o

gcc -g -Wall -O2 obj/sign/tools/sign.o -o bin/sign

 

首先生成bootblock.o

ld -m elf_i386 -nostdlib -N -e start -Ttext 0x7C00 obj/boot/bootasm.o obj/boot/bootmain.o -o obj/bootblock.o

 

拷贝二进制代码bootblock.o到bootblock.out

objcopy -S -O binary obj/bootblock.o obj/bootblock.out

 

使用sign工具处理bootblock.out,生成bootblock

bin/sign obj/bootblock.out bin/bootblock

$(kernel): tools/kernel.ld

$(kernel): $(KOBJS)

@echo + ld $@

$(V)$(LD) $(LDFLAGS) -T tools/kernel.ld -o $@ $(KOBJS)

@$(OBJDUMP) -S $@ > $(call asmfile,kernel)

@$(OBJDUMP) -t $@ | $(SED) '1,/SYMBOL TABLE/d; s/ .* / /; /^$$/d' > $(call symfile,kernel)

 

为了生成kernel,首先需要kernel.ld init.o readline.o stdio.o kdebug.o

kmonitor.o panic.o clock.o console.o intr.o picirq.o trap.o

trapentry.o vectors.o pmm.o printfmt.o string.o

 

生成一个有10000个块的文件,每个块默认512字节,用0填充

dd if=/dev/zero of=bin/ucore.img count=10000

 

把bootblock中的内容写到第一个块

dd if=bin/bootblock of=bin/ucore.img conv=notrunc

 

从第二个块开始写kernel中的内容

dd if=bin/kernel of=bin/ucore.img seek=1 conv=notrunc

 

 

[练习1.2] 一个被系统认为是符合规范的硬盘主引导扇区的特征是什么?

一个磁盘主引导扇区只有512字节。且

第510个(倒数第二个)字节是0x55,

第511个(倒数第一个)字节是0xAA。

1 大小为512字节

2 多余的空间填0

3 第510个(倒数第二个)字节是0x55,

4 第511个(倒数第一个)字节是0xAA。

 

 

练习2:使用qemu执行并调试lab1中的软件。(要求在报告中简要写出练习过程)

为了熟悉使用qemu和gdb进行的调试工作,我们进行如下的小练习:

从CPU加电后执行的第一条指令开始,单步跟踪BIOS的执行。

在初始化位置0x7c00设置实地址断点,测试断点正常。

从0x7c00开始跟踪代码运行,将单步跟踪反汇编得到的代码与bootasm.S和bootblock.asm进行比较。

自己找一个bootloader或内核中的代码位置,设置断点并进行测试。

 

实验分析:

[练习2.1] 从CPU 加电后执行的第一条指令开始,单步跟踪BIOS 的执行。

计算机启动后,CPU一开始会到一个特定的地址开始执行指令,这个特定的地址存放了系统初始化软件,负责完成计算机基本的IO初始化,这是系统加电后运行的第一段软件代码。

计算机加电后,CPU从物理地址0xFFFFFFF0开始执行。在0xFFFFFFF0这里只是存放了一条跳转指令,通过跳转指令跳到BIOS例行程序起始点。BIOS做完计算机硬件自检和初始化后,会选择一个启动设备并且读取该设备的第一扇区到内存一个特定的地址0x7c00处,然后CPU控制权会转移到那个地址继续执行。至此BIOS的初始化工作做完了,进一步的工作交给了ucore的bootloader。

file bin/kernel加载kernel的符号信息,我们可以看到符号值

target remote :1234 建立联系

set architecture i8086 实模式

b *0x7c00设置断点

continue继续执行

x /2i $pc 把地址内容以反汇编形式展现

加电之后可以设置断点

加电之后第一条指令是什么?在什么位置?

Bios

Bios执行完之后会把bootloader放到0x7c00

所以要设置断点看是不是放到了那个位置。

16位cpu执行模式

cli是屏蔽中断,cld让后续操作递增操作

发现,这个和之前看到的汇编内容是一样的

那么我可以确定,Bootloader确实被bios从硬盘加载到内存中执行了。

 

Bin里面的q.log

Bios第一条指令:看到有一个ljmp。

Bios的代码(汇编级的)

与bootblock.asm相同

 

 

练习3:分析bootloader进入保护模式的过程。(要求在报告中写出分析)

BIOS将通过读取硬盘主引导扇区到内存,并转跳到对应内存中的位置执行bootloader。请分析bootloader是如何完成从实模式进入保护模式的。

 

实验分析:

BIOS将通过读取硬盘主引导扇区到内存,并转跳到对应内存中的位置执行bootloader。bootloader完成的工作包括:

  • 切换到保护模式,启用分段机制
  • 读磁盘中ELF执行文件格式的ucore操作系统到内存
  • 显示字符串信息
  • 把控制权交给ucore操作系统
  • 在bootloader接手BIOS的工作后,当前的PC系统处于实模式(16位模式)运行状态,在这种状态下软件可访问的物理内存空间不能超过1MB
  • 只有在保护模式下,80386的全部32根地址线有效,可寻址高达4G字节的线性地址空间和物理地址空间

在保护模式下,特权级总共有4个,CPU只用到其中的2个特权级:0(内核态)和3(用户态)

 

依次把问题解决:

  • 为何开启A20,以及如何开启A20
  • 如何初始化GDT表
  • 如何使能和进入保护模式

 

开启A20:通过将键盘控制器上的A20线置于高电位,全部32条地址线可用,可以访问4G的内存空间。

Lgdt:描述gdt的信息加载(建立这个段表)

那么从哪里加载呢?

这里:

一个段描述符占用8个字节

  1. 位置
  2. 大小

第一个空

第二个:代码段+数据段

0x0基地址—对等映射,大小是4g

注意:

代码段可执行可读不可写

数据段可读可写

 

那么如何enable保护模式?

把c20寄存器P1位置一

orl $CR0_PE_ON, %eax

段表的0x8处取段描述符

长跳:

然后就进入32位保护模式----16位访问方式到32位保护模式的转变,可以访问4g的空间了。

这里设置地址空间

设置堆栈ebp最开始,0,esp大小

堆栈是向上增长,做减操作

然后调用了bootmain!

 

 

练习4:分析bootloader加载ELF格式的OS的过程。(要求在报告中写出分析)

通过阅读bootmain.c,了解bootloader如何加载ELF文件。通过分析源代码和通过qemu来运行并调试bootloader&OS,

bootloader如何读取硬盘扇区的?

bootloader是如何加载ELF格式的OS?

 

实验分析:

它的作用是加载ucore

那我们来解析elf格式:

  1. 要读一个扇区(bootloader是第0个扇区ucoreos是第一个扇区
  2. 是否是个valid的ELF?—存着ELF的头(对于段的纪录))

readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);

Call哪里去了?

我们可以知道,它放到eax里面了

可执行文件的程序头部是一个program header结构的数组,每个结构描述了一个段或者系统准备程序执行所必需的其它信息。目标文件的“段” 包含一个或者多个“节区”(section),也就是“段内容(Segment Contents)” 。程序头部仅对于可执行文件和共享目标文件有意义。

              // 上面四条指令联合制定了扇区号

              // 在这4个字节线联合构成的32位参数中

              //   29-31位强制设为1

              //   28位(=0)表示访问"Disk 0"

              //   0-27位是28位的偏移量

就是Kernel起始位置

Kernel的汇编代码入口地址也是这个

Bootloader已经把内核加载过来,解析,把控制权从bootloader转到了ucore

别忘了返回

 

 

练习5:实现函数调用堆栈跟踪函数(需要编程)

我们需要在lab1中完成kdebug.c中函数print_stackframe的实现,可以通过函数print_stackframe来跟踪函数调用堆栈中记录的返回地址。在如果能够正确实现此函数,可在lab1中执行“make qemu”后,在qemu模拟器中得到类似如下的输出:

请完成实验,看看输出是否与上述显示大致一致,并解释最后一行各个数值的含义。

 

实验分析:

              

他被什么调用呢?

往上拉

是这个

那么该如何实现?

首先uint32_t ebp = read_ebp(), eip = read_eip();

我们需要把ebp eip读出来

用的是内连汇编:

Eip同理读返回地址

 

Ebp有调用关系链,循环打印出来就可以

Kdebug:

读进ebp

打出ebp eip

把ebp当指针,跳回上一个caller的地方,就是打印下一个ebp

Ebp不等于0且小于20的时候循环

20指的就是:

这个深度,是设置的。

Ebp=0的时候;

到头了,当然就停了。

 

 

练习6:完善中断初始化和处理(需要编程)

请完成编码工作和回答如下问题:

中断描述符表(也可简称为保护模式下的中断向量表)中一个表项占多少字节?其中哪几位代表中断处理代码的入口?

请编程完善kern/trap/trap.c中对中断向量表进行初始化的函数idt_init。在idt_init函数中,依次对所有中断入口进行初始化。使用mmu.h中的SETGATE宏,填充idt数组内容。每个中断的入口由tools/vectors.c生成,使用trap.c中声明的vectors数组即可。

请编程完善trap.c中的中断处理函数trap,在对时钟中断进行处理的部分填写trap函数中处理时钟中断的部分,使操作系统每遇到100次时钟中断后,调用print_ticks子程序,向屏幕上打印一行文字”100 ticks”。

 

实验分析:

中断向量表一个表项占用8字节,其中2-3字节是段选择子,0-1字节和6-7字节拼成位移,两者联合便是中断处理程序的入口地址。

IDT的第一项可以包含一个描述符。CPU把中断(异常)号乘以8做为IDT的索引。IDT可以位于内存的任意位置,CPU通过IDT寄存器(IDTR)的内容来寻址IDT的起始地址。指令LIDT和SIDT用来操作IDTR。

设置好中断向量表

需要区分外设还是软件中断还是异常,需要设置区分细节

要求是时钟中断,如何写处理历程?

1:中断向量表

在mmu.H里面

用来定义段描述符的结构体

Sel选择值

段内偏移

中断号0-255分别对应的历程

覆盖了中断异常系统调用 属性上会有一些差别

Trap.c里面:init

Vector.s

向量表

这里有个vectors.S 是vector.c生成的

vector.S文件通过vectors.c 自动生成,其中定义了每个中断的入口程序和入口地址(保存在vectors 数组中)。其中,中断可以分成两类:一类是压入错误编码的(error code),另一类不压入错误编码。对于第二类,vector.S 自动压入一个0。此外,还会压入相应中断的中断号。在压入两个必要的参数之后,中断处理函数跳转到统一的入口alltraps 处。

最后跳到:

软件的保存现场与恢复

 有call trap:

 

初始化在前面trap c

建立中断向量表

还需要enable

使能产生中断

在哪屏蔽掉的呢?

Bootloader中cli就把中断disable掉了!之前有在实验报告里写噢!

等idt_init之后,才开始enabke intr

之后就可以产生中断了。

 

在要写的代码部分,能看到提示:

After setup the contents of IDT, you will let CPU know where is the IDT by using 'lidt' instruction.

You don't know the meaning of this instruction? just google it! and check the libs/x86.h to know more. Notice: the argument of lidt is idt_pd. try to find it!

可知应要写成lidt(&ldt_pd)

使操作系统每遇到 100 次时钟中断后,调用 print_ticks 子程序,向屏幕上打印一行文字”100 ticks”。

那我来查看函数trap,发现它调用中断处理函数trap_dispatch来处理。

它又调用:

具体的中断处理:

首先建立了表中所有的入口地址

比如case那一段:

产生时钟中断打印tricks

若中断号是IRQ_OFFSET + IRQ_COM1 为串口中断,则显示收到的字符。 
若中断号是IRQ_OFFSET + IRQ_KBD 为键盘中断,则显示收到的字符。 
若为其他中断且产生在内核状态,则挂起系统;

查看函数trap_dispatch,发现在 “case IRQ_OFFSET + IRQ_TIMER:”下方有一段注释,指导我们写这题。

他说使用变量ticks计数,每达到TICK_NUM次,就调用print_ticks来输出点东西。

操作系统每遇到100次时钟中断后,调用print_ticks子程序,向屏幕上打印一行文字”100 ticks”。

结束√

 

 

扩展练习Challenge 1(需要编程)

扩展proj4,增加syscall功能,即增加一用户态函数(可执行一特定系统调用:获得时钟计数值),当内核初始完毕后,可从内核态返回到用户态的函数,而用户态的函数又通过系统调用得到内核态的服务(通过网络查询所需信息,可找老师咨询。如果完成,且有兴趣做代替考试的实验,可找老师商量)。需写出详细的设计和分析报告。完成出色的可获得适当加分。

扩展练习Challenge 2(需要编程)

用键盘实现用户模式内核模式切换。具体目标是:“键盘输入3时切换到用户模式,键盘输入0时切换到内核模式”。基本思路是借鉴软中断(syscall功能)的代码,并且把trap.c中软中断处理的设置语句拿过来。

注意:

 1.关于调试工具,不建议用lab1_print_cur_status()来显示,要注意到寄存器的值要在中断完成后tranentry.S里面iret结束的时候才写回,所以再trap.c里面不好观察,建议用print_trapframe(tf)

 2.关于内联汇编,最开始调试的时候,参数容易出现错误,可能的错误代码如下

要去掉参数int %0 \n这一行

3.软中断是利用了临时栈来处理的,所以有压栈和出栈的汇编语句。硬件中断本身就在内核态了,直接处理就可以了

 

实验分析:

拿到这个题,先分析:

要做啥?主要的变化是啥:

Trap

初始化针对switch to kernel

那这个函数都有什么呢?

从内核台切换到用户态:打印当前状态

切换回去:打印当前状态

来回切换。

 

我们来看switch:

粉字:跨越集的转换,要多存ess esp

为啥?

为了恢复的时候正常工作

Int中断号/系统调用号完成硬件中断跳到case T

 

看init c:

要从用户态执行int产生软中断trap.c

Init里面先把权限打开

然后

设成了use_ds,也有esp站的指针,返回地址也没变

然后会执行trep里面的iret

从内核态到了用户态了

Print cur status了

(打寄存器)

重点:reg1与3----区分用户态

低两位0:内核

低两位是11:用户态

通过这个来观察当前处于哪个态

如果可以正常执行——

Print会在03之间来回切换。

从低优先级到高优先级的跳变

设置允许-通过特定的中断号

其中:

tapfream:中断帧

trap.h里面定义结构信息:产生中断之后要保存哪些,回复之后要恢复哪些

比如时钟中断处理

比如用户到内核/内核到用户——软中断

 

六、实验体会和思考题

一开始配置环境的时候,我想要用mac来配置实验环境的,困难重重。

收获是我加上了两个12级学长的微信,都是大大大大大佬!

膜拜。

然而mit的实验和ucore还是有很大差别,在后面坑了我很久。

首先,在mac上配置环境的时候,光按照readme操作是远远不够的,他只是用brew来下载了必备资源

那么,我剩下还要做的:

    • 把mac编译默认的clang换成gcc
    • Macos虽然有gcc,但是是很低版本的,更新,等待,找网络好的地方(唉)
    • 除了gcc,把gdb啊cgdb啊,g++啊等等一系列下载

    • 光下载不够,一定要定义到路径上去,由于我一开始接触这个一脸懵逼,路径定义的很乱,而且是强行写的只读文件。

    • 第一件让我开心的事情发生了,make成功!练习一总算做出来了。
    • 短暂的快乐之后,开始面对debug哭泣。

报错gnome-terminal command not find *n

仔细看makefile里面debug这里,

用到了$(TERMINAL)

Terminal设置的是ubuntu里面的shell gnome-terminal

Mac里面没有啊咋办啊啊啊

第一步,我的想法是,找到mac里面的gnome-terminal是啥,改过去不就可以了

然而:

似乎是mac上没有可以replace gnome的,不过底下回答给了一个方案,用iTerm2.

我下载了,试了,仍然不ok

说明gnome-terminal根本不是应用程序的名称,是一条指令啊!

再去疯狂google:

这位老哥确实能够轻松解决我的问题,但是写在makefile里面的,还用$(TERMINAL)代替就是为了简化代码,用那一大串肯定不方便,而且我刚接触这块,肯定还会出一坨错,不用这个方法。

最后我选择了

接触到这个之后才发现这个真的好神奇!

当然,先把后面的代码改一下:

就像是直接用英语和机器对话,tell terminal去开一个新的bash去执行gdb。

 

那为啥我最后还是用了虚拟机呢

因为用osascript相当于要新打开一个在usr地址里面的新脚本,等于说我还要设置地址!

这么麻烦!

不干了。

 

附录(源代码及注释)

Kdebug.c:第五题的

void

print_stackframe(void) {

     /* LAB1 YOUR CODE : STEP 1 */

     /* (1) call read_ebp() to get the value of ebp. the type is (uint32_t);

      * (2) call read_eip() to get the value of eip. the type is (uint32_t);

      * (3) from 0 .. STACKFRAME_DEPTH

      *   (3.1) printf value of ebp, eip

      *   (3.2) (uint32_t)calling arguments [0..4] = the contents in address (unit32_t)ebp +2 [0..4]

      *   (3.3) cprintf("\n");

      *   (3.4) call print_debuginfo(eip-1) to print the C calling function name and line number, etc.

      *   (3.5) popup a calling stackframe

      *           NOTICE: the calling funciton's return addr eip  = ss:[ebp+4]

      *                   the calling funciton's ebp = ss:[ebp]

      */

    uint32_t ebp = read_ebp(), eip = read_eip();

 

    inti, j;

    for(i = 0; ebp != 0 && i < STACKFRAME_DEPTH; i ++) {

        cprintf("ebp:0x%08x eip:0x%08x args:", ebp, eip);

        uint32_t *args = (uint32_t *)ebp + 2;

        for(j = 0; j < 4; j ++) {

            cprintf("0x%08x ", args[j]);

        }

        cprintf("\n");

        print_debuginfo(eip - 1);

        eip = ((uint32_t *)ebp)[1];

        ebp = ((uint32_t *)ebp)[0];

    }

}

 

Trap.c:第六题的

idt_init(void) {

     /* LAB1 YOUR CODE : STEP 2 */

     /* (1) Where are the entry addrs of each Interrupt Service Routine (ISR)?

      *    All ISR's entry addrs are stored in __vectors. where is uintptr_t __vectors[] ?

      *    __vectors[] is in kern/trap/vector.S which is produced by tools/vector.c

      *    (try "make" command in lab1, then you will find vector.S in kern/trap DIR)

      *    You can use  "extern uintptr_t __vectors[];" to define this extern variable which will be used later.

      * (2) Now you should setup the entries of ISR in Interrupt Description Table (IDT).

      *    Can you see idt[256] in this file? Yes, it's IDT! you can use SETGATE macro to setup each item of IDT

      * (3) After setup the contents of IDT, you will let CPU know where is the IDT by using 'lidt' instruction.

      *    You don't know the meaning of this instruction? just google it! and check the libs/x86.h to know more.

      *    Notice: the argument of lidt is idt_pd. try to find it!

      */

    externuintptr_t __vectors[];

    inti;

    for(i = 0; i < sizeof(idt) / sizeof(structgatedesc); i ++) {

        SETGATE(idt[i], 0, GD_KTEXT, __vectors[i], DPL_KERNEL);

    }

    // set for switch from user to kernel

    SETGATE(idt[T_SWITCH_TOK], 0, GD_KTEXT, __vectors[T_SWITCH_TOK], DPL_USER);

    // load the IDT

    lidt(&idt_pd);

}

 

void

trap(structtrapframe *tf) {

    // dispatch based on what type of trap occurred

    trap_dispatch(tf);

}

 

caseIRQ_OFFSET + IRQ_TIMER:

        /* LAB1 YOUR CODE : STEP 3 */

        /* handle the timer interrupt */

        /* (1) After a timer interrupt, you should record this event using a global variable (increase it), such as ticks in kern/driver/clock.c

         * (2) Every TICK_NUM cycle, you can print some info using a funciton, such as print_ticks().

         * (3) Too Simple? Yes, I think so!

         */

        ticks ++;

        if(ticks % TICK_NUM == 0) {

            print_ticks();

        }

        break;

 

init.c:第七题

staticvoid

lab1_switch_to_user(void) {

    //LAB1 CHALLENGE 1 : TODO

    asmvolatile(

        "sub $0x8, %%esp \n"

        "int %0 \n"

        "movl %%ebp, %%esp"

        : 

        : "i"(T_SWITCH_TOU)

    );

}

 

staticvoid

lab1_switch_to_kernel(void) {

    //LAB1 CHALLENGE 1 :  TODO

    asmvolatile(

        "int %0 \n"

        "movl %%ebp, %%esp \n"

        : 

        : "i"(T_SWITCH_TOK)

    );

}

 

staticvoid

lab1_switch_test(void) {

    lab1_print_cur_status();

    cprintf("+++ switch to  user  mode +++\n");

    lab1_switch_to_user();

    lab1_print_cur_status();

    cprintf("+++ switch to kernel mode +++\n");

    lab1_switch_to_kernel();

    lab1_print_cur_status();

}

 

 

你可能感兴趣的:(操作系统)