《MIT 6.828 Lab 1 Exercise 3》实验报告
本实验的网站链接:mit 6.828 lab1 Exercise 3。
题目
Exercise 3. Take a look at the lab tools guide, especially the section on GDB commands. Even if you're familiar with GDB, this includes some esoteric GDB commands that are useful for OS work.
Set a breakpoint at address 0x7c00, which is where the boot sector will be loaded. Continue execution until that breakpoint. Trace through the code in boot/boot.S, using the source code and the disassembly file obj/boot/boot.asm to keep track of where you are. Also use the x/i command in GDB to disassemble sequences of instructions in the boot loader, and compare the original boot loader source code with both the disassembly in obj/boot/boot.asm and GDB.
Trace into bootmain() in boot/main.c, and then into readsect(). Identify the exact assembly instructions that correspond to each of the statements in readsect(). Trace through the rest of readsect() and back out into bootmain(), and identify the begin and end of the for loop that reads the remaining sectors of the kernel from the disk. Find out what code will run when the loop is finished, set a breakpoint there, and continue to that breakpoint. Then step through the remainder of the boot loader.
Be able to answer the following questions:
- At what point does the processor start executing 32-bit code? What exactly causes the switch from 16- to 32-bit mode?
- What is the last instruction of the boot loader executed, and what is the first instruction of the kernel it just loaded?
- Where is the first instruction of the kernel?
- How does the boot loader decide how many sectors it must read in order to fetch the entire kernel from disk? Where does it find this information?
解答
Exercise 3包含两部分:其一是使用GDB分析代码,其二是回答4个问题。
一、使用GDB分析代码
阅读lab Tools指导材料
阅读完成,并输出学习笔记。
分析boot/boot.S
的代码
00007c00 <start>:
7c00: fa cli
7c01: fc cld
7c02: 31 c0 xor %eax,%eax
7c04: 8e d8 mov %eax,%ds
7c06: 8e c0 mov %eax,%es
7c08: 8e d0 mov %eax,%ss
00007c0a <seta20.1>:
7c0a: e4 64 in $0x64,%al
7c0c: a8 02 test $0x2,%al
7c0e: 75 fa jne 7c0a <seta20.1>
7c10: b0 d1 mov $0xd1,%al
7c12: e6 64 out %al,$0x64
00007c14 <seta20.2>:
7c14: e4 64 in $0x64,%al
7c16: a8 02 test $0x2,%al
7c18: 75 fa jne 7c14 <seta20.2>
7c1a: b0 df mov $0xdf,%al
7c1c: e6 60 out %al,$0x60
7c1e: 0f 01 16 lgdtl (%esi)
7c21: 64 7c 0f fs jl 7c33 <protcseg+0x1>
7c24: 20 c0 and %al,%al
7c26: 66 83 c8 01 or $0x1,%ax
7c2a: 0f 22 c0 mov %eax,%cr0
7c2d: ea .byte 0xea
7c2e: 32 7c 08 00 xor 0x0(%eax,%ecx,1),%bh
00007c32 <protcseg>:
7c32: 66 b8 10 00 mov $0x10,%ax
7c36: 8e d8 mov %eax,%ds
7c38: 8e c0 mov %eax,%es
7c3a: 8e e0 mov %eax,%fs
7c3c: 8e e8 mov %eax,%gs
7c3e: 8e d0 mov %eax,%ss
7c40: bc 00 7c 00 00 mov $0x7c00,%esp
7c45: e8 cb 00 00 00 call 7d15 <bootmain>
在地址0x7c00处设置断点,这是boot loader第一条指令的位置。
使用si命令跟踪代码,可见
boot.S
文件中主要做了以下事情:初始化段寄存器、打开A20门、从实模式跳到虚模式(需要设置GDT和cr0寄存器),最后调用bootmain函数。- seta20.1和seta20.2两段代码实现打开A20门的功能,其中seta20.1是向键盘控制器的0x64端口发送0x61命令,这个命令的意思是要向键盘控制器的 P2 写入数据;seta20.2是向键盘控制器的 P2 端口写数据了。写数据的方法是把数据通过键盘控制器的 0x60 端口写进去。写入的数据是 0xdf,因为 A20 gate 就包含在键盘控制器的 P2 端口中,随着 0xdf 的写入,A20 gate 就被打开了。
- test对两个参数(目标,源)执行AND逻辑操作,并根据结果设置标志寄存器,结果本身不会保存。
- GDT是全局描述符表,GDTR是全局描述符表寄存器。想要在“保护模式”下对内存进行寻址就先要有 GDT,GDT表里每一项叫做“段描述符”,用来记录每个内存分段的一些属性信息,每个段描述符占8字节。CPU使用GDTR寄存器来保存我们GDT在内存中的位置和GDT的长度。
lgdt gdtdesc
将源操作数的值(存储在gdtdesc地址中)加载到全局描述符表寄存器中。 - x86一共有4个控制寄存器,分别为CR0~CR3,而控制进入“保护模式”的开关在CR0上,CR0上和保护模式有关的位是PE(标识是否开启保护模式)和PG(标识是否启用分页式)。
- 关于A20门、GDT和cr0寄存器的详细介绍可以参考【学习xv6】从实模式到保护模式。。
.byte
在当前位置插入一个字节;.word
在当前位置插入一个字。
题目中还要求我们比较
boot.S
,boot.asm
与GDB的代码差异。我观察到的差异有:boot.S
的指令含有表示长度的b,w,l等后缀,而boot.asm
和GDB没有;同样一条指令,boot.S
和GDB是操作ax寄存器,而boot.asm
却是操作%eax。
xorw %ax, %ax // boot.S
xor %eax, %eax // boot.asm
xor %ax, %ax // GDB
- 一个操作系统在计算机启动后到底应该做些什么:(摘自参考文献1《【学习xv6】从实模式到保护模式》)
- 计算机开机,运行环境为 1MB 寻址限制带“卷绕”机制
- 打开 A20 gate 让计算机突破 1MB 寻址限制
- 在内存中建立 GDT 全局描述符表,并将建立好的 GDT 表的位置和大小告诉 CPU
- 设置控制寄存器,进入保护模式
- 按照保护模式的内存寻址方式继续执行
分析boot/main.c
的代码
- 在bootmain函数的起始地址(0x7d15)处设置断点。bootmain函数开头定义了两个局部变量ph和eph,从汇编代码发现gcc分别用%ebx和%esi这两个寄存器来保存它们的值,而不是从栈中开辟空间来保存。从下面0x7d4c处的代码还可以发现ph指针加1对应地址偏移32个字节(Proghdr结构体占32个字节)。
// C codes:
// ph = (struct Proghdr *) ((uint8_t *) ELFHDR + ELFHDR->e_phoff);
// eph = ph + ELFHDR->e_phnum;
7d3a: a1 1c 00 01 00 mov 0x1001c,%eax
7d3f: 0f b7 35 2c 00 01 00 movzwl 0x1002c,%esi
7d46: 8d 98 00 00 01 00 lea 0x10000(%eax),%ebx
7d4c: c1 e6 05 shl $0x5,%esi
7d4f: 01 de add %ebx,%esi
- 接下来分析readsect函数。这个函数主要做了三件事情:等待磁盘(waitdisk)、输出扇区数目及地址信息到端口(out)、读取扇区数据(insl)。
- 等待磁盘。waitdisk的函数实现如下所示。它其实就做一件事情:不断地读端口0x1fc的bit_7和bit_6的值,直到bit_7=0和bit_6=1.结合参考文献1可知,端口1F7在被读的时候是作为状态寄存器使用,其中bit_7=0表示控制器空闲,bit_6=1表示驱动器就绪。因此,waitdisk在控制器空闲和驱动器就绪同时成立时才会结束等待。`
// waitdisk:
7c6a: 55 push %ebp
7c6b: ba f7 01 00 00 mov $0x1f7,%edx
7c70: 89 e5 mov %esp,%ebp
7c72: ec in (%dx),%al
7c73: 83 e0 c0 and $0xffffffc0,%eax
7c76: 3c 40 cmp $0x40,%al
7c78: 75 f8 jne 7c72 <waitdisk+0x8>
* 输出数据到端口。根据参考文献1的介绍,IDE定义了8个寄存器来操作硬盘。PC 体系结构将第一个硬盘控制器映射到端口 1F0-1F7 处,而第二个硬盘控制器则被映射到端口 170-177 处。out函数主要是是把扇区计数、扇区LBA地址等信息输出到端口1F2-1F6,然后将0x20命令写到1F7,表示要进行读扇区的操作。
// out:
7c7c: 55 push %ebp
7c7d: 89 e5 mov %esp,%ebp
7c7f: 57 push %edi
7c80: 8b 4d 0c mov 0xc(%ebp),%ecx
7c83: e8 e2 ff ff ff call 7c6a <waitdisk>
7c88: ba f2 01 00 00 mov $0x1f2,%edx
7c8d: b0 01 mov $0x1,%al
7c8f: ee out %al,(%dx)
7c90: ba f3 01 00 00 mov $0x1f3,%edx
7c95: 88 c8 mov %cl,%al
7c97: ee out %al,(%dx)
7c98: 89 c8 mov %ecx,%eax
7c9a: ba f4 01 00 00 mov $0x1f4,%edx
7c9f: c1 e8 08 shr $0x8,%eax
7ca2: ee out %al,(%dx)
7ca3: 89 c8 mov %ecx,%eax
7ca5: ba f5 01 00 00 mov $0x1f5,%edx
7caa: c1 e8 10 shr $0x10,%eax
7cad: ee out %al,(%dx)
7cae: 89 c8 mov %ecx,%eax
7cb0: ba f6 01 00 00 mov $0x1f6,%edx
7cb5: c1 e8 18 shr $0x18,%eax
7cb8: 83 c8 e0 or $0xffffffe0,%eax
7cbb: ee out %al,(%dx)
7cbc: ba f7 01 00 00 mov $0x1f7,%edx
7cc1: b0 20 mov $0x20,%al
7cc3: ee out %al,(%dx)
7cc4: e8 a1 ff ff ff call 7c6a <waitdisk>
* 读取扇区数据。主要用到insl函数,其实现是一个内联汇编语句。这个[stackflow网站](https://stackoverflow.com/questions/38410829/why-cant-find-the-insl-instruction-in-x86-document)解释了insl函数的作用:“That function will read cnt dwords from the input port specified by port into the supplied output array addr.”。关于内联汇编的介绍见[Brennan's Guide to Inline Assembly](http://www.delorie.com/djgpp/doc/brennan/brennan_att_inline_djgpp.html)和[GCC内联汇编基础](https://www.jianshu.com/p/1782e14a0766)。insl函数实质上就是从0x1F0端口连续读128个dword(即512个字节,也就是一个扇区的字节数)到目的地址。其中,0x1F0是数据寄存器,读写硬盘数据都必须通过这个寄存器。
// insl:
7cc9: 8b 7d 08 mov 0x8(%ebp),%edi
7ccc: b9 80 00 00 00 mov $0x80,%ecx
7cd1: ba f0 01 00 00 mov $0x1f0,%edx
7cd6: fc cld
7cd7: f2 6d repnz insl (%dx),%es:(%edi)
7cd9: 5f pop %edi
7cda: 5d pop %ebp
7cdb: c3 ret
- 跟踪for循环
题目要求我们找出for循环的起始语句和结束语句。这个简单。首先看起始语句:esi寄存器装着eph的值,ebx寄存器装着ph的值,可见起始语句的用处是判断ph是否小于eph,若不小于则跳到循环结束处。
// C Code:
// for (; ph < eph; ph++)
// readseg(ph->p_pa, ph->p_memsz, ph->p_offset);
7d51: 39 f3 cmp %esi,%ebx
7d53: 73 16 jae 7d6b <bootmain+0x56>
接着看结束语句:ebx寄存器装着ph的值,三个pushl语句将调用readseg所需的三个参数从右到左依次压栈,注意第三句将ebx寄存器自增32,对应ph指针加1.调用完readseg函数后,将esp寄存器自增12,相当于清除栈中那3个输入参数。最后跳回到循环起始处,判断是否继续下一轮循环。
// C Code:
// for (; ph < eph; ph++)
// readseg(ph->p_pa, ph->p_memsz, ph->p_offset);
7d55: ff 73 04 pushl 0x4(%ebx)
7d58: ff 73 14 pushl 0x14(%ebx)
7d5b: 83 c3 20 add $0x20,%ebx
7d5e: ff 73 ec pushl -0x14(%ebx)
7d61: e8 76 ff ff ff call 7cdc <readseg>
7d66: 83 c4 0c add $0xc,%esp
7d69: eb e6 jmp 7d51 <bootmain+0x3c>
循环结束后,执行以下语句,即调用ELF文件中的入口函数。
// C code:
// ((void (*)(void)) (ELFHDR->e_entry))();
7d6b: ff 15 18 00 01 00 call *0x10018
使用gdb继续跟踪,发现会进入kern目录下的entry.S
和init.c
文件:
=> 0x10000c: movw $0x1234,0x472
=> 0x100015: mov $0x110000,%eax
=> 0x10001a: mov %eax,%cr3
=> 0x10001d: mov %cr0,%eax
=> 0x100020: or $0x80010001,%eax
=> 0x100025: mov %eax,%cr0
=> 0x100028: mov $0xf010002f,%eax
=> 0x10002d: jmp *%eax
=> 0xf010002f <relocated>: mov $0x0,%ebp
relocated () at kern/entry.S:74
74 movl $0x0,%ebp # nuke frame pointer
=> 0xf0100034 <relocated+5>: mov $0xf0110000,%esp
relocated () at kern/entry.S:77
77 movl $(bootstacktop),%esp
=> 0xf0100039 <relocated+10>: call 0xf0100094 <i386_init>
80 call i386_init
=> 0xf0100094 <i386_init>: push %ebp
i386_init () at kern/init.c:24
二、回答问题
问:处理器从哪里开始执行32位代码?是什么导致了16位代码到32位代码的切换? 答:
- 处理器应该是从
boot.S
文件中的.code32
伪指令开始执行32位代码。补充:ljmp语句使得处理器从real mode切换到protected mode,地址长度从16位变为32位。
ljmp $PROT_MODE_CSEG, $protcseg
.code32 # Assemble for 32-bit mode
protcseg:
movw $PROT_MODE_DSEG, %ax # Our data segment selector
- 处理器由16位代码到32位代码的切换,主要是通过设置cr0寄存器的PE位(是否开启保护模式)和PG位(启用分段式还是分页式)来触发的。
- 处理器应该是从
问:boot loader执行的最后一条指令是什么?boot loader加载内核后,内核的第一条指令是什么? 答:
- boot loader的最后一条指令是
7d6b: ff 15 18 00 01 00 call *0x10018
- 内核的第一条指令是
0x10000c: movw $0x1234,0x472
- boot loader的最后一条指令是
问:内核的第一条指令的地址在哪里?
答: 根据gdb调试结果,内核的第一条指令的地址为0x10000c.问:boot loader怎么知道为了从磁盘中读取整个内核的内容需要加载多少扇区?它从哪里获得这个信息?
答:ELF文件头中包含有段数目、每个段的偏移和字节数。根据这些信息,boot loader可以知道加载多少扇区。
ph = (struct Proghdr *) ((uint8_t *) ELFHDR + ELFHDR->e_phoff);
eph = ph + ELFHDR->e_phnum;
for (; ph < eph; ph++)
readseg(ph->p_pa, ph->p_memsz, ph->p_offset);
备注
以下是阅读代码过程中查阅网上资料而整理的笔记。
- x86 EFLAGS寄存器各状态标志的含义:
- CF(bit 0) [Carry flag]: 若算术操作产生的结果在最高有效位(most-significant bit)发生进位或借位则将其置1,反之清零。这个标志指示无符号整型运算的溢出状态,这个标志同样在多倍精度运算(multiple-precision arithmetic)中使用。
- PF(bit 2) [Parity flag]: 如果结果的最低有效字节(least-significant byte)包含偶数个1位则该位置1,否则清零。
- AF(bit 4) [Adjust flag]: 如果算术操作在结果的第3位发生进位或借位则将该标志置1,否则清零。这个标志在BCD(binary-code decimal)算术运算中被使用。
- ZF(bit 6) [Zero flag]: 若结果为0则将其置1,反之清零。
- SF(bit 7) [Sign flag]: 该标志被设置为有符号整型的最高有效位。(0指示结果为正,反之则为负)
- OF(bit 11) [Overflow flag]: 如果整型结果是较大的正数或较小的负数,并且无法匹配目的操作数时将该位置1,反之清零。这个标志为带符号整型运算指示溢出状态。
疑问
- 如何查看某个地址对应的符号(函数名或变量名)?网上说使用
info symbol addr
命令,但我使用时提示"No symbol matches 0x7d15."
参考资料
《MIT 6.828 Lab 1 Exercise 3》实验报告的更多相关文章
- [操作系统实验lab3]实验报告
[感受] 这次操作系统实验感觉还是比较难的,除了因为助教老师笔误引发的2个错误外,还有一些关键性的理解的地方感觉还没有很到位,这些天一直在不断地消化.理解Lab3里的内容,到现在感觉比Lab2里面所蕴 ...
- Ucore lab1实验报告
练习一 Makefile 1.1 OS镜像文件ucore.img 是如何一步步生成的? + cc kern/init/init.c + cc kern/libs/readline.c + cc ker ...
- ucore操作系统学习(三) ucore lab3虚拟内存管理分析
1. ucore lab3介绍 虚拟内存介绍 在目前的硬件体系结构中,程序要想在计算机中运行,必须先加载至物理主存中.在支持多道程序运行的系统上,我们想要让包括操作系统内核在内的各种程序能并发的执行, ...
- 《ucore lab3》实验报告
资源 ucore在线实验指导书 我的ucore实验代码 练习1:给未被映射的地址映射上物理页 题目 完成do_pgfault(mm/vmm.c)函数,给未被映射的地址映射上物理页.设置访问权限的时候需 ...
- 《ucore lab1 exercise5》实验报告
资源 ucore在线实验指导书 我的ucore实验代码 题目:实现函数调用堆栈跟踪函数 我们需要在lab1中完成kdebug.c中函数print_stackframe的实现,可以通过函数print_s ...
- 《ucore lab8》实验报告
资源 ucore在线实验指导书 我的ucore实验代码 练习1: 完成读文件操作的实现(需要编码) 题目 首先了解打开文件的处理流程,然后参考本实验后续的文件读写操作的过程分析,编写在sfs_inod ...
- 《ucore lab7》实验报告
资源 ucore在线实验指导书 我的ucore实验代码 练习1: 理解内核级信号量的实现和基于内核级信号量的哲学家就餐问题(不需要编码) 题目 完成练习0后,建议大家比较一下(可用meld等文件dif ...
- 《ucore lab6》实验报告
资源 ucore在线实验指导书 我的ucore实验代码 练习1: 使用 Round Robin 调度算法(不需要编码) 题目 完成练习0后,建议大家比较一下(可用kdiff3等文件比较软件) 个人完成 ...
- 《ucore lab5》实验报告
资源 ucore在线实验指导书 我的ucore实验代码 练习1: 加载应用程序并执行(需要编码) 题目 do_execv函数调用load_icode(位于kern/process/proc.c中) 来 ...
- 《ucore lab4》实验报告
资源 ucore在线实验指导书 我的ucore实验代码 练习1:分配并初始化一个进程控制块 题目 alloc_proc函数(位于kern/process/proc.c中) 负责分配并返回一个新的str ...
随机推荐
- python 赋值魔法
序列解包: >>> x,y,z = 1, 2, 3>>> print(x, y, z)1 2 3 >>> a,b, *reset = [1,2,3 ...
- 「CF484E」Sign on Fence「整体二分」「线段树」
题意 给定一个长度为\(n\)的正整数序列,第\(i\)个数为\(h_i\),\(m\)个询问,每次询问\((l, r, w)\),为\([l, r]\)所有长度为\(w\)的子区间最小值的最大值.( ...
- 数据结构实验之栈与队列四:括号匹配(SDUT 2134)
#include <bits/stdc++.h> using namespace std; typedef long long ll; char s[100]; char a[100]; ...
- springboot工程打成war包
1.将pom.xml中默认的jar修改为war. <packaging>war</packaging> 2.排除SpringBoot内置的Tomcat容器. <depen ...
- 基于OVS命令的VLAN实现
利用mininet创建如下拓扑,要求支持OpenFlow 1.3协议,主机名.交换机名以及端口对应正确 直接在Open vSwitch下发流表,实现如下连通性要求 h1 -- h4互通 h2 -- h ...
- Js中Array常用方法小结
说起Array的方法,不免让人皱一下眉头,下面我们从增删改查角度依次来总结. 1.增 push: 将传入的参数 ,插入数组的尾部,并返回新数组的长度.不管传入参数为一个值还是一个数组,都作为插入数组的 ...
- Java并发指南11:解读 Java 阻塞队列 BlockingQueue
解读 Java 并发队列 BlockingQueue 转自:https://javadoop.com/post/java-concurrent-queue 最近得空,想写篇文章好好说说 java 线程 ...
- debian、ubuntu安装metasploit通用方法
网上有很多方法让去github上下载安装,这方法的确可以但是特别慢,更新也特别慢,这里写下比较快的方法 1.添加kali源 vim /etc/apt/sources.list 在原有源的基础上添加国内 ...
- Qt:使用Model-View,动态的加载显示数据
共有 main.cpp, Widget.h, Widget.cpp, Widget.ui, MyModel.h, MyModel.cpp 六个文件. 可从此下载整个工程文件: /Files/biao/ ...
- OpenCV学习笔记(2)——如何用OpenCV处理视频
如何用OpenCV处理视频 读取视频文件,显示视频,保存视频文件 从摄像头获取并显示视频 1.用摄像头捕获视频 为了获取视频,需要创建一个VideoCapature对象.其参数可以是设备的索引号,也可 ...