实验目的:

操作系统是一个软件,也需要通过某种机制加载并运行它。在这里我们将通过另外一个更加简单的软件-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的方法
函数调用关系:在汇编级了解函数调用栈的结构和处理过程
中断管理:与软件相关的中断处理
外设管理:时钟

实验内容:

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

练习

练习1:理解通过make生成执行文件的过程

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

  1. 操作系统镜像文件ucore.img是如何一步一步生成的?(需要比较详细地解释Makefile中每一条相关命令和命令参数的含义,以及说明命令导致的结果)
  2. 一个被系统认为是符合规范的硬盘主引导扇区的特征是什么?

练习1.1

输入命令:

make V=

获得结果


从上面并没有看到:根据sign规范生成bootblock的命令
查看makefile文件找到:

@$(call totarget,sign) $(call outfile,bootblock)     $(bootblock)

所以从上面可以看出ucore.img的生成过程:

  1. 编译所有生成bin/kernel所需的文件
  2. 链接生成bin/kernel
  3. 编译bootasm.S bootmain.c sign.c
  4. 根据sign规范生成obj/bootblock.o
  5. 生成ucore.img

练习1.2

截取sign.c文件中的部分源码

主引导扇区的规则如下:

  1. 大小为512字节
  2. 多余的空间填0
  3. 第510个(倒数第二个)字节是0x55,
  4. 第511个(倒数第一个)字节是0xAA。

练习2:使用qemu执行并调试lab1中的软件

  1. 从CPU加电后执行的第一条指令开始,单步跟踪BIOS的执行。
  2. 在初始化位置0x7c00设置实地址断点,测试断点正常。
  3. 从0x7c00开始跟踪代码运行,将单步跟踪反汇编得到的代码与bootasm.S和 bootblock.asm进行比较。
  4. 自己找一个bootloader或内核中的代码位置,设置断点并进行测试。

练习2.1

  1. tools/gdbinit的内容如下。可见,这里是对内核代码进行调试,并且将断点设置在内核代码的入口地址,即kern_init函数
file bin/kernel
target remote :1234
break kern_init
continue
  1. 为了从CPU加电后执行的第一条指令开始调试,需要修改tools/gdbinit的内容为:
set architecture i8086
file bin/bootblock
target remote :1234
break start
continue
  1. 执行make debug,这时会弹出一个QEMU窗口和一个Terminal窗口,这是正常的,因为我们在makefile中定义了debug的操作正是启动QEMU、启动Terminal并在其中运行gdb。
debug: $(UCOREIMG)$(V)$(QEMU) -S -s -parallel stdio -hda $< -serial null &$(V)sleep 2$(V)$(TERMINAL) -e "gdb -q -tui -x tools/gdbinit"
  1. Terminal窗口此时停在0x0000fff0的位置,这是eip寄存器的值,而cs寄存器的值为0xf000. (遇到一个问题:此时无法正确反汇编出代码,使用x来查询内存0xfff0处的值时显示全0,不知道什么原因)
The target architecture is assumed to be i8086
0x0000fff0 in ?? ()
Breakpoint 1 at 0x7c00: file boot/bootasm.S, line 16.
  1. 输入si,执行1步,程序会跳转到0xe05b的地方。查看寄存器也可以发现eip的值变为0xe05b,而cs的值不变,仍然是0xf000.
  2. 反复输入si,以单步执行。(由于BIOS中全是汇编代码,看不懂其功能)。

练习2.2

  1. 我直接在tools/gdbinit中设置了断点break start,由于boot loader的入口为start,其地址为0x7c00,因此这和break *0x7c00效果是相同的。
  2. 设置断点后,输入continue或c,可以看到程序在0x7c00处停了下来,说明断点设置成功。

练习2.3

  1. 反汇编的代码与bootblock.asm基本相同,而与bootasm.S的差别在于:
    (1)反汇编的代码中的指令不带指示长度的后缀,而bootasm.S的指令则有。比如,反汇编 的代码是xor %eax, %eax,而bootasm.S的代码为xorw %ax, %ax
    (2)反汇编的代码中的通用寄存器是32位(带有e前缀),而bootasm.S的代码中的通用寄存器是16位(不带e前缀)。

练习2.4

修改gdbinit文件,在0x7c4a处设置断点 (调用bootmain函数处)

set architecture i8086
target remote :1234
break *0x7c4a

输入 make debug ,得到结果:

断点设置正常

练习3:分析bootloader进入保护模式的过程

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

练习3.1

在i8086时代,CPU的数据总线是16bit,地址总线是20bit,寄存器是16bit,因此CPU只能访问1MB以内的空间。因为数据总线和寄存器只有16bit,如果需要获取20bit的数据, 我们需要做一些额外的操作,比如移位。实际上,CPU是通过对segment(每个segment大小恒定为64K) 进行移位后和offset一起组成了一个20bit的地址,这个地址就是实模式下访问内存的地址:

address = segment << 4 | offset

理论上,20bit的地址可以访问1MB的内存空间(0x00000 - (2^20 - 1 = 0xFFFFF))。但在实模式下, 这20bit的地址理论上能访问从0x00000 - (0xFFFF0 + 0xFFFF = 0x10FFEF)的内存空间。也就是说,理论上我们可以访问超过1MB的内存空间,但越过0xFFFFF后,地址又会回到0x00000。上面这个特征在i8086中是没有任何问题的(因为它最多只能访问1MB的内存空间),但到了i80286/i80386后,CPU有了更宽的地址总线,数据总线和寄存器后,这就会出现一个问题: 在实模式下, 我们可以访问超过1MB的空间,但我们只希望访问1MB以内的内存空间。为了解决这个问题, CPU中添加了一个可控制A20地址线的模块,通过这个模块,我们在实模式下将第20bit的地址线限制为0,这样CPU就不能访问超过1MB的空间了。
进入保护模式后,我们再通过这个模块解除对A20地址线的限制,这样我们就能访问超过1MB的内存空间了。默认情况下,A20地址线是关闭的(20bit以上的地址线限制为0),因此在进入保护模式(需要访问超过1MB的内存空间)前,我们需要开启A20地址线(20bit以上的地址线可为0或者1)。具体代码如下:

seta20.1:inb $0x64, %al                                  # Wait for not busy(8042 input buffer empty).testb $0x2, %aljnz seta20.1movb $0xd1, %al                                 # 0xd1 -> port 0x64outb %al, $0x64                                 # 0xd1 means: write data to 8042's P2 portseta20.2:inb $0x64, %al                                  # Wait for not busy(8042 input buffer empty).testb $0x2, %aljnz seta20.2movb $0xdf, %al                                 # 0xdf -> port 0x60outb %al, $0x60                                 # 0xdf = 11011111, means set P2's A20 bit(the 1 bit) to 1

练习3.2

可以看出这里所有GDT表项(除了空段)初始化为全段,此时段偏移量EIP等于物理地址

...
#define SEG_NULLASM                                             \.word 0, 0;                                                 \.byte 0, 0, 0, 0#define SEG_ASM(type,base,lim)                                  \.word (((lim) >> 12) & 0xffff), ((base) & 0xffff);          \.byte (((base) >> 16) & 0xff), (0x90 | (type)),             \(0xC0 | (((lim) >> 28) & 0xf)), (((base) >> 24) & 0xff)
...
lgdt gdtdesc
...
gdt:SEG_NULLASM                                     # null segSEG_ASM(STA_X|STA_R, 0x0, 0xffffffff)           # code seg for bootloader and kernelSEG_ASM(STA_W, 0x0, 0xffffffff)                 # data seg for bootloader and kernelgdtdesc:.word 0x17                                      # sizeof(gdt) - 1.long gdt                                       # address gdt

段选择子
在实模式下, 逻辑地址由段选择子和段选择子偏移量组成. 其中, 段选择子16bit, 段选择子偏移量是32bit. 下面是段选择子的示意图:

  1. image.png 在段选择子中,其中的INDEX[15:3]是GDT的索引。
  2. TI[2:2]用于选择表格的类型,1是LDT,0是GDT。
  3. RPL[1:0]用于选择请求者的特权级,00最高,11最低。
    GDT的访问
    有了上面这些知识,我们可以来看看到底应该怎样通过GDT来获取需要访问的地址了。我们通过这个示意图来讲解:
  4. 根据CPU给的逻辑地址分离出段选择子。
  5. 利用段选择子查找到对应的段描述符。
  6. 将段描述符里的Base Address和EIP相加而得到线性地址。

练习3.3

开启A20,初始化gdt后,将控制寄存器CR0的PE(bit0)置为1即可

movl %cr0, %eax
orl 0x1, %eax
movl %eax, %cr0

练习4:分析bootloader加载ELF格式的OS的过程

  1. bootloader如何读取硬盘扇区的?
  2. bootloader是如何加载ELF格式的OS?

练习4.1

读硬盘扇区的代码如下:

// bootmain.c
/* readsect - read a single sector at @secno into @dst */
static void
readsect(void *dst, uint32_t secno) {// wait for disk to be readywaitdisk();outb(0x1F2, 1);                         // count = 1outb(0x1F3, secno & 0xFF);outb(0x1F4, (secno >> 8) & 0xFF);outb(0x1F5, (secno >> 16) & 0xFF);outb(0x1F6, ((secno >> 24) & 0xF) | 0xE0);outb(0x1F7, 0x20);                      // cmd 0x20 - read sectors// wait for disk to be readywaitdisk();// read a sectorinsl(0x1F0, dst, SECTSIZE / 4);
}

从outb()可以看出这里是用LBA模式的PIO(Program IO)方式来访问硬盘的。从磁盘IO地址和对应功能表可以看出,该函数一次只读取一个扇区。
其中insl的实现如下:

// x86.h
static inline void
insl(uint32_t port, void *addr, int cnt) {asm volatile ("cld;""repne; insl;": "=D" (addr), "=c" (cnt): "d" (port), "0" (addr), "1" (cnt): "memory", "cc");
}

练习4.2

  1. 从硬盘读了8个扇区数据到内存0x10000处,并把这里强制转换成elfhdr使用;
  2. 校验e_magic字段;
  3. 根据偏移量分别把程序段的数据读取到内存中。

练习5:实现函数调用堆栈跟踪函数

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

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

这个task是为了让我们了解函数的调用和堆栈的关系。对于函数调用的细节,我在之前的文章中已经写过了,具体请参见C函数调用过程原理及函数栈帧分析。这里主要分析下代码,源代码在 kern/debug/kdebug.c文件中。

/*
栈底方向      高位地址
...
...
参数3
参数2
参数1
返回地址
上一层[ebp]   <-------- [esp/当前ebp]
局部变量      低位地址
*/
void
print_stackframe(void) {uint32_t cur_ebp, cur_eip; uint32_t args[4]; cur_ebp = read_ebp();cur_eip = read_eip();/* 假设最多有20层的函数调用 */for (int stack_level = 0; stack_level < STACKFRAME_DEPTH + 1; stack_level++) {cprintf("ebp: 0x%08x eip: 0x%08x ", cur_ebp, cur_eip);/* 假设函数最多有4个参数 */for (int arg_num = 0; arg_num < 4; arg_num++)args[arg_num] = *((uint32_t *)cur_ebp + (2 + arg_num));cprintf("args:0x%08x 0x%08x 0x%08x 0x%08x\n", args[0], args[1], args[2], args[3]);print_debuginfo(cur_eip);/* 获取上一层函数的返回地址和$ebp的值 */cur_eip = *((uint32_t *)cur_ebp + 1); cur_ebp = *((uint32_t *)cur_ebp);  }
}

练习6:完善中断初始化和处理

  1. 中断描述符表(也可简称为保护模式下的中断向量表)中一个表项占多少字节?其中哪几位代表中断处理代码的入口?
  2. 请编程完善kern/trap/trap.c中对中断向量表进行初始化的函数idt_init。在idt_init函数中,依次对所有中断入口进行初始化。使用mmu.h中的SETGATE宏,填充idt数组内容。每个中断的入口由tools/vectors.c生成,使用trap.c中声明的vectors数组即可。
  3. 请编程完善trap.c中的中断处理函数trap,在对时钟中断进行处理的部分填写trap函数中处理时钟中断的部分,使操作系统每遇到100次时钟中断后,调用print_ticks子程序,向屏幕上打印一行文字”100 ticks”。

练习6.1

一个表项的结构如下:

/*lab1/kern/mm/mmu.h*/
/* Gate descriptors for interrupts and traps */
struct gatedesc {unsigned gd_off_15_0 : 16;        // low 16 bits of offset in segmentunsigned gd_ss : 16;            // segment selectorunsigned gd_args : 5;            // # args, 0 for interrupt/trap gatesunsigned gd_rsv1 : 3;            // reserved(should be zero I guess)unsigned gd_type : 4;            // type(STS_{TG,IG32,TG32})unsigned gd_s : 1;                // must be 0 (system)unsigned gd_dpl : 2;            // descriptor(meaning new) privilege levelunsigned gd_p : 1;                // Presentunsigned gd_off_31_16 : 16;        // high bits of offset in segment
};


中断处理过程:
可以看到,中断向量表一个表项占用8字节,其中2-3字节是段选择子,0-1字节和6-7字节拼成偏移量,通过段选择子去GDT中找到对应的基地址,然后基地址加上偏移量就是中断处理程序的地址。

练习6.2

SETGATE函数的实现:

#define SETGATE(gate, istrap, sel, off, dpl) {            \(gate).gd_off_15_0 = (uint32_t)(off) & 0xffff;        \(gate).gd_ss = (sel);                                \(gate).gd_args = 0;                                    \(gate).gd_rsv1 = 0;                                    \(gate).gd_type = (istrap) ? STS_TG32 : STS_IG32;    \(gate).gd_s = 0;                                    \(gate).gd_dpl = (dpl);                                \(gate).gd_p = 1;                                    \(gate).gd_off_31_16 = (uint32_t)(off) >> 16;        \
}

宏定义和数组说明:

#define GD_KTEXT    ((SEG_KTEXT) << 3)        // kernel text
#define DPL_KERNEL    (0)
#define DPL_USER    (3)
#define T_SWITCH_TOK                121    // user/kernel switch
static struct gatedesc idt[256] = {{0}};

idt_init函数的实现:

void
idt_init(void) {extern uintptr_t __vectors[];  //保存在vectors.S中的256个中断处理例程的入口地址数组int i;//使用SETGATE宏,对中断描述符表中的每一个表项进行设置for (i = 0; i < sizeof(idt) / sizeof(struct gatedesc); i ++) { //IDT表项的个数//在中断门描述符表中通过建立中断门描述符,其中存储了中断处理例程的代码段GD_KTEXT和偏移量__vectors[i],特权级为DPL_KERNEL。这样通过查询idt[i]就可定位到中断服务例程的起始地址。SETGATE(idt[i], 0, GD_KTEXT, __vectors[i], DPL_KERNEL);}SETGATE(idt[T_SWITCH_TOK], 0, GD_KTEXT,     __vectors[T_SWITCH_TOK], DPL_USER);//建立好中断门描述符表后,通过指令lidt把中断门描述符表的起始地址装入IDTR寄存器中,从而完成中段描述符表的初始化工作。lidt(&idt_pd);
}

练习6.3

首先加入 string.h头文件,为了使用memmove函数

void *memmove(void *dst, const void *src, size_t n);

定义变量:

struct trapframe switchk2u, *switchu2k;

结构体 trapframe

struct trapframe {struct pushregs tf_regs;uint16_t tf_gs;uint16_t tf_padding0;uint16_t tf_fs;uint16_t tf_padding1;uint16_t tf_es;uint16_t tf_padding2;uint16_t tf_ds;uint16_t tf_padding3;uint32_t tf_trapno;/* below here defined by x86 hardware */uint32_t tf_err;uintptr_t tf_eip;uint16_t tf_cs;uint16_t tf_padding4;uint32_t tf_eflags;/* below here only when crossing rings, such as from user to kernel */uintptr_t tf_esp;uint16_t tf_ss;uint16_t tf_padding5;
} __attribute__((packed));

宏定义:

#define IRQ_OFFSET                32
#define IRQ_TIMER                 0
#define IRQ_KBD                   1
#define IRQ_COM1                  4
#define T_SWITCH_TOU              120
#define USER_CS        ((GD_UTEXT) | DPL_USER)
#define USER_DS        ((GD_UDATA) | DPL_USER)
#define KERNEL_DS    ((GD_KDATA) | DPL_KERNEL)
#define TICK_NUM 100

print_ticks函数

static void print_ticks() {cprintf("%d ticks\n",TICK_NUM);
#ifdef DEBUG_GRADEcprintf("End of Test.\n");panic("EOT: kernel seems ok.");
#endif
}

运行结果:

操做系统ucore实验 lab1相关推荐

  1. 操作系统ucore实验——lab1

    ** ## 操作系统ucore实验--lab1 ** 紧急更新实验用的源代码在lab0中的有误改为: 链接:https://pan.baidu.com/s/1RLCG57xDSydH8oQD-JwgP ...

  2. 计算机操做系统(十二):进程同步和互斥

    计算机操做系统(十二):进程同步和互斥 来源王道考研视频: https://www.bilibili.com/video/BV1YE411D7nH?p=18 基本概念 异步性:各并发执行的进程以各自独 ...

  3. 闪讯客户端 linux,Linux操做系统下链接闪讯的方法(支持有线与无线)

    1.前言 用过电信闪讯的同窗都知道,闪讯没有开发Linux的客户端程序,因此这让不少玩Linux操做系统同时又是闪讯用户的同窗很头疼,今天我就来介绍一下如何在Linux下链接闪讯网络,而且支持有线链接 ...

  4. 操作系统课程ucore实验 lab1

    Ucore实验lab1 练习一:理解通过make生成执行文件的过程. 在Makefile中生成ucore.img的代码是: $(UCOREIMG): $(kernel) $(bootblock) $( ...

  5. linux实时还是分时,linux是实时系统仍是分时操做系统

    实时操做系统 实时操做系统 英文称Real Time Operating System,简称RTOS. 1.实时操做系统定义 实时操做系统(RTOS)是指当外界事件或数据产生时,可以接受并以足够快的速 ...

  6. win10找不到oracle11g客户端,win10操做系统下oracle11g客户端/服务端的下载安装配置卸载总结...

    注意:如今有两种安装的方式sql 1. oracle11g服务端(64位)+oracle客户端(32位)+plsql(32位)数据库 2. oracle11g服务端(32位)+plsql(32位)wi ...

  7. win10操做系统恢复操做

    流程步骤 1.点击开始菜单,打开设置,在设置里打开更新和安全. 2.点击恢复,点击重置此电脑处的"开始". 3.选择保留或删除文件,同样有两个选项,可以选择"删除文件并清 ...

  8. 测试需要的的linux命令,(面试必备)软件测试人员必备Linux命令操做(初级基础)...

    1 目录与文件操做 1.1 ls(初级) 使用权限:全部人 功能 : 显示指定工做目录下以内容(列出目前工做目录所含之档案及子目录). 参数 : -a 显示全部档案及目录 (ls内定将档案名或目录名称 ...

  9. ucore实验报告lab1

    练习1 1.生成操作系统镜像文件ucore.img 生成ucore.imge的代码如下: $(UCOREIMG): $(kernel) $(bootblock)$(V)dd if=/dev/zero ...

  10. 基于Domoticz智能家居系统(十四)用ESP8266做MQTT客户端实验

    基于Domoticz智能家居系统(十四)用ESP8266做MQTT客户端实验 用ESP8266做MQTT客户端 一些前期的准备 第一步 设置ESP8266开发板的BSP的搜索引擎链接 第二步 下载安装 ...

最新文章

  1. ES6中的异步对象Promise
  2. Detective Book
  3. C语言二叉树的lowest common ancestor最低公共祖先(附完整源码)
  4. sqlserver安装显示句柄无效_Sqlserver 2016 R Service环境安装的各种错误(坑)解决办法...
  5. 计算机涉及数学知识点,初二数学知识点归纳
  6. 变形监测期末复习_材料力学复习题
  7. Spring4.x()--Spring的Jdbc事务-零配置
  8. LeetCode 109. Convert Sorted List to Binary Search Tree
  9. hadoop的作业提交过程之yarn
  10. HDU2999 Stone Game, Why are you always there?【SG函数】
  11. php 单位食堂订餐,单位饭堂订餐系统(手机订餐)
  12. 多线程与NSTimer
  13. SAP 物料编码更改标准解决方案
  14. 浏览器上模拟qq的消息提示声/网页播放声音
  15. Abaqus动力学分析基础
  16. 利用Udacity模拟器实现自己的自动驾驶小车
  17. PPT制作技巧汇总之动画设置与播放(office 2007)
  18. 12月31日起涉线上支付的微信小程序需设置订单中心页
  19. 蓝奏云批量下载修复版 v0.3
  20. 小强升职记-一本好书

热门文章

  1. 物业管理系统c语言,物业管理系统C语言程序实习.doc
  2. Java语句详解(图解java语句概念、快速掌握java基础知识点)——Java基础系列
  3. Python遗传算法初学者教程
  4. 【python数据处理】替代Excel三维地图依据经纬度坐标的绘制热力地图的方式
  5. 短语wipe the slate clean
  6. SpringSecurity之权限管理
  7. 台达A2/B2伺服电机编码器改功率软件 台达A2/B2伺服电机编码修改, 用于更换编码器写匹配电机参数
  8. 台达b3伺服modbus通讯_A2伺服modbus通讯难题-专业自动化论坛-中国工控网论坛
  9. PLC编程软件等工具打包下载1.0【好用绿色三菱plc编程软件】
  10. 深入浅出数据分析 Head First Data Analysis Code 一书中的文档下载