ucore-lab1
本文最后更新于:2023年8月29日 下午
RT
参考博客
准备工作:下载ucorelab在
实验环境:
准备工作:
安装gcc:
1
sudo apt-get install build-essential
安装qemu:
1
sudo apt-get install qemu-system
涉及到的知识点:
,建议参考Heeler-Deer,以及博客 - gcc,gdb等的基本使用
- shell的基本使用,诸如diff,ld,readelf等命令
详细请参考ucorelab的官方文档
强烈建议在学堂在线看thu的ucore课程
知识点
先定一个小目标,学一学doxygen以及binutils的相关工具
meld
先sudo apt-get install meld
,然后就可以用了
这是meld官网:usage
doxygen
参考博客
doxygen能生成latex,通过规范注释来解释项目结构。
常用命令:
1 |
|
1 |
|
注释规范:
1 |
|
或者是:
1 |
|
binutils
binutils包含很多命令,不详细介绍了,列举一下:
ld - the GNU linker.
as - the GNU assembler.
addr2line - Converts addresses into filenames and line numbers.
ar - A utility for creating, modifying and extracting from archives.
c++filt - Filter to demangle encoded C++ symbols.
dlltool - Creates files for building and using DLLs.
gold - A new, faster, ELF only linker, still in beta test.
gprof - Displays profiling information.
nlmconv - Converts object code into an NLM.
nm - Lists symbols from object files.
objcopy - Copies and translates object files.
objdump - Displays information from object files.
ranlib - Generates an index to the contents of an archive.
readelf - Displays information from any ELF format object file.
size - Lists the section sizes of an object or archive file.
strings - Lists printable strings from files.
strip - Discards symbols.
windmc - A Windows compatible message compiler.
windres - A compiler for Windows resource files.
系统启动及中断
bios提供了基本的i/o功能
cpu加电后执行ROM里的初始化代码:
从硬盘/网络启动是由bios决定,bios加载程序到磁盘的引导扇区,跳转到cs:ip=0000:7c00,读取加载程序,用加载程序识别文件系统,识别文件系统之后就可以读取os的内核代码,在把控制权转给os:
bios和uefi的区别:
中断(来自硬件)、异常(非法指令等)、系统调用(应用程序主动向os发的请求):
计算机对上述三种情况采用的中断处理机制:
cpu初始化时设置中断使能,设置中断标志,根据中断向量调用相关中断服务例程
软件处理:
中断是可以嵌套的,也就是异常里可以出现异常 🤔
系统调用采用int和iret指令,可以进行堆栈切换以及特权级的转换,函数调用采用call和ret指令,一般情况下没有堆栈切换
遗漏的知识点参考kiprey,仅记录本人不熟悉的知识点
bios/linux/dos中断的区别
bios/dos建立在实模式下,他们建立的中断调用都采用中断向量表,linux则是在进入保护模式后才建立中断例程,通过中断描述符表idt实现中断
文件
os通过文件系统的magic number得知文件系统类型,一般在分区的第二个扇区
实模式
实模式的主要特性是:程序用到的地址都是真实的物理地址。同时,实模式下的地址寻址空间只有1MB(20bit)
从intel 80386开始的CPU,只要进入实模式,地址寻址空间就限制在1MB。
实模式下的地址计算方式为16*段寄存器值+段内偏移地址,其CPU寻址方式为
- 寄存器寻址
- 立即数寻址
- 内存寻址
- 直接寻址。
- 基址寻址
- 变址寻址
- 基址变址寻址
在分段存储管理机制的保护模式下,每个段由如下三个参数进行定义:段基地址(Base Address)、段界限(Limit)和段属性(Attributes)
- 段基地址:规定线性地址空间中段的起始地址。任何一个段都可以从32位线性地址空间中的任何一个字节开始,不用像实模式下规定边界必须被16整除。
- 段界限:规定段的大小。可以以字节为单位或以4K字节为单位。
- 段属性:确定段的各种性质。
全局描述符表(GDT)是一个保存多个段描述符的“数组”,其起始地址保存在全局描述符表寄存器GDTR中。GDTR长48位,其中高32位为基地址,低16位为段界限
线性地址部分的选择子是用来选择哪个描述符表和在该表中索引哪个描述符的。选择子可以做为指针变量的一部分,从而对应用程序员是可见的,但是一般是由连接加载器来设置的。
段选择子结构
- 索引(Index):在描述符表中从8192个描述符中选择一个描述符。处理器自动将这个索引值乘以8(描述符的长度),再加上描述符表的基址来索引描述符表,从而选出一个合适的描述符。
- 表指示位(Table Indicator,TI):选择应该访问哪一个描述符表。0代表应该访问全局描述符表(GDT),1代表应该访问局部描述符表(LDT)。
- 请求特权级(Requested Privilege Level,RPL):保护机制
中断与异常
在操作系统中,有三种特殊的中断事件:
- 异步中断(asynchronous interrupt)。这是由CPU外部设备引起的外部事件中断,例如I/O中断、时钟中断、控制台中断等。
- 同步中断(synchronous interrupt)。这是CPU执行指令期间检测到不正常的或非法的条件(如除零错、地址访问越界)所引起的内部事件。
- 陷入中断(trap interrupt)。这是在程序中使用请求系统服务的系统调用而引发的事件。
IDT中包含了3种类型的Descriptor
- Task-gate descriptor
- Interrupt-gate descriptor (中断方式用到)
- Trap-gate descriptor(系统调用用到)
特权
特权级共分为四档,分别为0-3,其中Kernel
为第0特权级(ring
0),用户程序为第3特权级(ring
3),操作系统保护分别为第1和第2特权级。
DPL存储于段描述符中,规定访问该段的权限级别(Descriptor Privilege Level),每个段的DPL固定。 当进程访问一个段时,需要进程特权级检查。
CPL存在于CS寄存器的低两位,即CPL是CS段描述符的DPL,是当前代码的权限级别(Current Privilege Level)。
RPL存在于段选择子中,说明的是进程对段访问的请求权限(Request Privilege Level),是对于段选择子而言的,每个段选择子有自己的RPL。而且RPL对每个段来说不是固定的,两次访问同一段时的RPL可以不同。RPL可能会削弱CPL的作用,例如当前CPL=0的进程要访问一个数据段,它把段选择符中的RPL设为3,这样虽然它对该段仍然只有特权为3的访问权限。
IOPL(I/O Privilege Level)即I/O特权标志,位于eflag寄存器中,用两位二进制位来表示,也称为I/O特权级字段。该字段指定了要求执行I/O指令的特权级。如 果当前的特权级别在数值上小于等于IOPL的值,那么,该I/O指令可执行,否则将发生一个保护异常。
只有当CPL=0时,可以改变IOPL的值,当CPL<=IOPL时,可以改变IF标志位。
访问门时(中断、陷入、异常),要求DPL[段] <= CPL <= DPL[门]
访问门的代码权限比门的特权级要高,因为这样才能访问门。
但访问门的代码权限比被访问的段的权限要低,因为通过门的目的是访问特权级更高的段,这样就可以达到低权限应用程序使用高权限内核服务的目的。
访问段时,要求DPL[段] >= max {CPL, RPL}
只能使用最低的权限来访问段数据。
TSS(Task State Segment) 是操作系统在进行进程切换时保存进程现场信息的段
trapframe
结构是进入中断门所必须的结构,其结构如下
1 |
|
以上内容大部分复制自kiprey,不知道kiprey佬是在那里学的
ucore_lab1
练习1
- 理解通过make生成执行文件的过程。
在os_kernel_lab-master/labcodes/lab1/Makefile中,可以找到要分析的makefile。
makefile的结构如下:
1 |
|
我们通过:
1 |
|
来查看make到底执行了那些命令。
直接看
1 |
|
也就是说要生成ucore.img,会先生成bootblock,kernel这两个target,我们依次找寻生成bootblock、kernel所需要的文件,得到这样一个文件树:
graph LR
ucore-->bootblock
ucore-->kernel
bmo[(bootmain.o)]
bso[(bootasm.o)]
bootblock-->bso
bootblock-->bmo
bootblock-->sign
bsos[(bootasm.S)]
bso-->bsos
bmoc[(bootmain.c)]
bmo-->bmoc
sign-->sign.c
num[(kernel.ld等)]
kernel-->num
对于ucore:
1 |
|
其中dd命令和cp差不多,但dd更多的针对底层文件的复制.
if和ifeq差不多,仍是实现条件判断的语句,实际上是判断后面的文件是否存在,比如/dev/zero就存在于linux的/dev/目录下
of以及后面的语句则是对参数进行赋值,count指定是10000个块的字节,之后从第一个块写入bootblock的内容,seek则是指定从第二个块开始写入kernel的内容,有点类似于sg_read
生成bootasm.o时的实际命令为:
1 |
|
解释一下参数:
-fno-builtin指若函数前没有builtin则不进行内部函数的优化(如下图),-Wall就是生成警告信息,-ggdb生成gdb调试信息,-m32就是按照32位机器生成代码,-gstabs生成stabs格式的调试信息,-nostdinc不是用标准库,-fno-stack-protector关闭栈保护,-Os是优化选项,-Iboot/指在boot下生成文件。
生成sign的实际命令:
1 |
|
在有了sing,bootasm.o,bootmain.o后,makefile生成bootblock.o:
1 |
|
ld是连接器,详细用法我参考的是computerhope
上面的代码实际上是模拟了i386的连接器(-m elf_i386),-nostdlib即不准使用标准库,-N设置代码段以及数据段均可读写,-e指定了入口是start,-Text指定了代码段的开始位置。
最后就是使用sign处理bootblock.o,首先objcopy -S -O binary obj/bootblock.o obj/bootblock.out
,实际上-S移除了所有符号和重定位信息,-O指定输出格式,sign在处理bootblock.out生成bootblock
对于kernel,实际上没有用到多少新的参数。只需要注意ld的参数-T < script > 是让ld使用指定的script即可。
1 |
|
- 一个被系统认为是符合规范的硬盘主引导扇区的特征是什么?
我们可以看sign.c,一个磁盘主引导扇区只有512字节,且第510个是0x55,第511个是0xAA
练习2
参考真正の佬kiprey
修改tools/gdbinit:
1 |
|
其中define hook-step ... end告诉gdb在每一次断点时自动输出下一条指令。
之后make debug:
单步跟踪使用si即可,这是continue的结果:
设置断点:
修改gdbinit为:
1 |
|
之后make debug,输入x/5i (($cs <<4)+$eip)
得到:
修改为:
1 |
|
输入qemu-system-i386 -hda yourpath/os_kernel_lab-master/labcodes/lab1/bin/ucore.img -s -S -d in_asm -D q.log
打开ucore,打开gdb,输入target remote localhost:1234
,之后单步调试si得到:
可以看到0x7c00处的代码和bootasm.S处的代码一致
练习3
在bootblock.asm中,通过注释不难发现,disable interrupts禁用了中断,之后初始化了DS,ES,SS这三个寄存器的值,都先置0,之后enable A20,开启原因在注释里面也有:
physical address line 20 is tied low ,that addresses higher than 1MB.因此我们需要开启A20去兼容早期的回绕特征。kipery这样解释:
- Intel早期的8086 CPU提供了20根地址线,但寄存器只有16位,所以使用段寄存器值 << 4 + 段内偏移值的方法来访问到所有内存,但按这种方式来计算出的地址的最大值为1088KB,超过20根地址线所能表示的范围,会发生“回卷”(和整数溢出有点类似)。但下一代的基于Intel 80286 CPU的计算机系统提供了24根地址线,当CPU计算出的地址超过1MB时便不会发生回卷,而这就造成了向下不兼容。为了保持完全的向下兼容性,IBM在计算机系统上加个硬件逻辑来模仿早期的回绕特征,而这就是A20 Gate。
- A20 Gate的方法是把A20地址线控制和键盘控制器的一个输出进行AND操作,这样来控制A20地址线的打开(使能)和关闭(屏蔽)。一开始时A20地址线控制是被屏蔽的(总为0),直到系统软件通过一定的IO操作去打开它。当A20 地址线控制禁止时,则程序就像在8086中运行,1MB以上的地址不可访问;保护模式下A20地址线控制必须打开。A20控制打开后,内存寻址将不会发生回卷。
- 在当前环境中,所用到的键盘控制器8042的IO端口只有0x60和0x64两个端口。8042通过这些端口给键盘控制器或键盘发送命令或读取状态。输出端口P2用于特定目的。位0(P20引脚)用于实现CPU复位操作,位1(P21引脚)用于控制A20信号线的开启与否。
接下来就是加载gdt这个全局描述符表,最后重新使用32位模式,重新切回到保护模式。
在bootasm.S中,
初始化了gdt:
1 |
|
看注释即可理解。bootasm.s中的代码设置了CR0_PE_ON来返回保护模式:
1 |
|
练习4
ELF文件结构:
在bootmain.c中,bootloader为了读取扇区,采用了下面的代码:
1 |
|
即先waitdisk,等待磁盘,然后写入0x1f2~0x1f5,0x1f7,准备读取磁盘,磁盘准备好时再调用insl读取到内存。
bootloader先将os加载到:
1 |
|
在比对elf文件的magic number来判断elf文件的正确性,最后在加载elf的每一个段,
1 |
|
练习5
在kern/debug/kdebug.c的最后有我们要完成的代码,不难写出:
1 |
|
注意先切换eip,在切换ebp
结果:
练习6
直接上图和代码:
1 |
|
1 |
|
扩展练习
challenge 1
修改:
1 |
|
以及:
1 |
|
inti.c里有坑,第一个函数需要是void类型
chanllenge 2
1 |
|
本文作者: Heeler-Deer
本文链接: https://heeler-deer.top/posts/28603/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!