操做系统ucore实验 lab1
实验目的:
操作系统是一个软件,也需要通过某种机制加载并运行它。在这里我们将通过另外一个更加简单的软件-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生成执行文件的过程
在此练习中,大家需要通过静态分析代码来了解:
- 操作系统镜像文件ucore.img是如何一步一步生成的?(需要比较详细地解释Makefile中每一条相关命令和命令参数的含义,以及说明命令导致的结果)
- 一个被系统认为是符合规范的硬盘主引导扇区的特征是什么?
练习1.1
输入命令:
make V=
获得结果
从上面并没有看到:根据sign规范生成bootblock的命令
查看makefile文件找到:
@$(call totarget,sign) $(call outfile,bootblock) $(bootblock)
所以从上面可以看出ucore.img的生成过程:
- 编译所有生成bin/kernel所需的文件
- 链接生成bin/kernel
- 编译bootasm.S bootmain.c sign.c
- 根据sign规范生成obj/bootblock.o
- 生成ucore.img
练习1.2
截取sign.c文件中的部分源码
主引导扇区的规则如下:
- 大小为512字节
- 多余的空间填0
- 第510个(倒数第二个)字节是0x55,
- 第511个(倒数第一个)字节是0xAA。
练习2:使用qemu执行并调试lab1中的软件
- 从CPU加电后执行的第一条指令开始,单步跟踪BIOS的执行。
- 在初始化位置0x7c00设置实地址断点,测试断点正常。
- 从0x7c00开始跟踪代码运行,将单步跟踪反汇编得到的代码与bootasm.S和 bootblock.asm进行比较。
- 自己找一个bootloader或内核中的代码位置,设置断点并进行测试。
练习2.1
- tools/gdbinit的内容如下。可见,这里是对内核代码进行调试,并且将断点设置在内核代码的入口地址,即kern_init函数
file bin/kernel
target remote :1234
break kern_init
continue
- 为了从CPU加电后执行的第一条指令开始调试,需要修改tools/gdbinit的内容为:
set architecture i8086
file bin/bootblock
target remote :1234
break start
continue
- 执行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"
- 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.
- 输入si,执行1步,程序会跳转到0xe05b的地方。查看寄存器也可以发现eip的值变为0xe05b,而cs的值不变,仍然是0xf000.
- 反复输入si,以单步执行。(由于BIOS中全是汇编代码,看不懂其功能)。
练习2.2
- 我直接在tools/gdbinit中设置了断点break start,由于boot loader的入口为start,其地址为0x7c00,因此这和break *0x7c00效果是相同的。
- 设置断点后,输入continue或c,可以看到程序在0x7c00处停了下来,说明断点设置成功。
练习2.3
- 反汇编的代码与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进入保护模式的过程
- 为何开启A20,以及如何开启A20
- 如何初始化GDT表
- 如何使能和进入保护模式
练习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. 下面是段选择子的示意图:
- image.png 在段选择子中,其中的INDEX[15:3]是GDT的索引。
- TI[2:2]用于选择表格的类型,1是LDT,0是GDT。
- RPL[1:0]用于选择请求者的特权级,00最高,11最低。
GDT的访问
有了上面这些知识,我们可以来看看到底应该怎样通过GDT来获取需要访问的地址了。我们通过这个示意图来讲解:
- 根据CPU给的逻辑地址分离出段选择子。
- 利用段选择子查找到对应的段描述符。
- 将段描述符里的Base Address和EIP相加而得到线性地址。
练习3.3
开启A20,初始化gdt后,将控制寄存器CR0的PE(bit0)置为1即可
movl %cr0, %eax
orl 0x1, %eax
movl %eax, %cr0
练习4:分析bootloader加载ELF格式的OS的过程
- bootloader如何读取硬盘扇区的?
- 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
- 从硬盘读了8个扇区数据到内存0x10000处,并把这里强制转换成elfhdr使用;
- 校验e_magic字段;
- 根据偏移量分别把程序段的数据读取到内存中。
练习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:完善中断初始化和处理
- 中断描述符表(也可简称为保护模式下的中断向量表)中一个表项占多少字节?其中哪几位代表中断处理代码的入口?
- 请编程完善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”。
练习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相关推荐
- 操作系统ucore实验——lab1
** ## 操作系统ucore实验--lab1 ** 紧急更新实验用的源代码在lab0中的有误改为: 链接:https://pan.baidu.com/s/1RLCG57xDSydH8oQD-JwgP ...
- 计算机操做系统(十二):进程同步和互斥
计算机操做系统(十二):进程同步和互斥 来源王道考研视频: https://www.bilibili.com/video/BV1YE411D7nH?p=18 基本概念 异步性:各并发执行的进程以各自独 ...
- 闪讯客户端 linux,Linux操做系统下链接闪讯的方法(支持有线与无线)
1.前言 用过电信闪讯的同窗都知道,闪讯没有开发Linux的客户端程序,因此这让不少玩Linux操做系统同时又是闪讯用户的同窗很头疼,今天我就来介绍一下如何在Linux下链接闪讯网络,而且支持有线链接 ...
- 操作系统课程ucore实验 lab1
Ucore实验lab1 练习一:理解通过make生成执行文件的过程. 在Makefile中生成ucore.img的代码是: $(UCOREIMG): $(kernel) $(bootblock) $( ...
- linux实时还是分时,linux是实时系统仍是分时操做系统
实时操做系统 实时操做系统 英文称Real Time Operating System,简称RTOS. 1.实时操做系统定义 实时操做系统(RTOS)是指当外界事件或数据产生时,可以接受并以足够快的速 ...
- win10找不到oracle11g客户端,win10操做系统下oracle11g客户端/服务端的下载安装配置卸载总结...
注意:如今有两种安装的方式sql 1. oracle11g服务端(64位)+oracle客户端(32位)+plsql(32位)数据库 2. oracle11g服务端(32位)+plsql(32位)wi ...
- win10操做系统恢复操做
流程步骤 1.点击开始菜单,打开设置,在设置里打开更新和安全. 2.点击恢复,点击重置此电脑处的"开始". 3.选择保留或删除文件,同样有两个选项,可以选择"删除文件并清 ...
- 测试需要的的linux命令,(面试必备)软件测试人员必备Linux命令操做(初级基础)...
1 目录与文件操做 1.1 ls(初级) 使用权限:全部人 功能 : 显示指定工做目录下以内容(列出目前工做目录所含之档案及子目录). 参数 : -a 显示全部档案及目录 (ls内定将档案名或目录名称 ...
- ucore实验报告lab1
练习1 1.生成操作系统镜像文件ucore.img 生成ucore.imge的代码如下: $(UCOREIMG): $(kernel) $(bootblock)$(V)dd if=/dev/zero ...
- 基于Domoticz智能家居系统(十四)用ESP8266做MQTT客户端实验
基于Domoticz智能家居系统(十四)用ESP8266做MQTT客户端实验 用ESP8266做MQTT客户端 一些前期的准备 第一步 设置ESP8266开发板的BSP的搜索引擎链接 第二步 下载安装 ...
最新文章
- ES6中的异步对象Promise
- Detective Book
- C语言二叉树的lowest common ancestor最低公共祖先(附完整源码)
- sqlserver安装显示句柄无效_Sqlserver 2016 R Service环境安装的各种错误(坑)解决办法...
- 计算机涉及数学知识点,初二数学知识点归纳
- 变形监测期末复习_材料力学复习题
- Spring4.x()--Spring的Jdbc事务-零配置
- LeetCode 109. Convert Sorted List to Binary Search Tree
- hadoop的作业提交过程之yarn
- HDU2999 Stone Game, Why are you always there?【SG函数】
- php 单位食堂订餐,单位饭堂订餐系统(手机订餐)
- 多线程与NSTimer
- SAP 物料编码更改标准解决方案
- 浏览器上模拟qq的消息提示声/网页播放声音
- Abaqus动力学分析基础
- 利用Udacity模拟器实现自己的自动驾驶小车
- PPT制作技巧汇总之动画设置与播放(office 2007)
- 12月31日起涉线上支付的微信小程序需设置订单中心页
- 蓝奏云批量下载修复版 v0.3
- 小强升职记-一本好书
热门文章
- 物业管理系统c语言,物业管理系统C语言程序实习.doc
- Java语句详解(图解java语句概念、快速掌握java基础知识点)——Java基础系列
- Python遗传算法初学者教程
- 【python数据处理】替代Excel三维地图依据经纬度坐标的绘制热力地图的方式
- 短语wipe the slate clean
- SpringSecurity之权限管理
- 台达A2/B2伺服电机编码器改功率软件 台达A2/B2伺服电机编码修改, 用于更换编码器写匹配电机参数
- 台达b3伺服modbus通讯_A2伺服modbus通讯难题-专业自动化论坛-中国工控网论坛
- PLC编程软件等工具打包下载1.0【好用绿色三菱plc编程软件】
- 深入浅出数据分析 Head First Data Analysis Code 一书中的文档下载