学习参考:B站哈工大李治军老师的网课
前置知识:计组,和x86有亿点点区别的RISC-V指令集。
其他章节笔记:
(一)操作系统的启动
(二)系统调用
(三)操作系统历史
(四)多进程管理图像
(五)进程,用户级线程与内核级线程
(六)进程同步与信号量
(七)内存管理
汇编基础
参考:
王爽汇编笔记
x86寄存器
x86汇编指令集大全
-
x86泛指一系列基于Intel8086且向后兼容的中央处理器指令集架构。最早的8086处理器于1978年由Intel推出,为16位微处理器。
-
8086CPU给出物理地址的方法:采用一种在内部用两个16位地址合成的方法来形成一个20位的物理地址。
#两个16位的地址,一个称为段地址,另一个称为偏移地址。地址加法器将两个16位地址合成为一个20位的物理地址。
#物理地址 = 段地址×16+偏移地址。
#段地址:将一段内存定义为一个段,用一个段地址指示段,用偏移地址访问段内单元。 -
CS:IP
#CS为代码段寄存器,IP为指令指针寄存器。
#CS为指令的段地址,IP为指令的偏移地址,用它们合成指令的物理地址。将CS:IP指向的内容当作指令执行(PC)。
01 什么是操作系统
操作系统是计算机硬件和应用之间的一层软件。
02 操作系统启动过程
- BIOS模块读入引导扇区代码bootsect.s 。
- 引导扇区代码的工作是读入setup代码,显示系统标识,读入system模块。
- setup部分准备初始化参数,启动保护模式,进入system模块。
- system模块的第一段代码head.s的任务是对操作系统管理资源的关键数据结构进行初始化,主要包括设置中断表,设置GDT表,设置页表 ,跳转至main()函数。
- 进入main()函数,进行其他重要数据结构的初始化。初始化完毕后,操作系统正式启动。
读入引导扇区代码
1. x86 PC刚开机时CPU处于实模式。
2. CS = 0xFFFF;IP = 0x0000。
3. 寻址0xFFFF0:ROM BIOS映射区,存有对基本硬件的测试代码,如对主板,内存等硬件的测试,同时还提供一些让用户调用硬件基本输入输出功能的子程序。
4. 检查RAM,键盘,显示器,软硬磁盘。
5. 将磁盘0磁道0扇区1(引导扇区,存储操作系统第一段代码)读入0x7c00处。
6. 设置CS = 0x07c0;IP = 0x0000。
执行引导扇区代码
这段代码的意思是将内存中开始地址为DS:SI的256个字移动到开始地址为ES:DI的地方,为读入操作系统核心代码腾出空间。
以上最后一个指令“jmpi go INITSEG”是为了在移动代码存放位置后依然可以顺序执行。
将0号驱动器中从0号柱面,0号磁头,2号扇区开始的4个扇区读入到内存中,放到bootsect.s后。
其中“int 0x13”即调用0x13号中断,这是一个BIOS读写磁盘的中断调用,使用这段代码来从磁盘上读入setup.s
具体调用0x13号中断时:
寄存器AH = 0x02表示要读磁盘内容到内存
寄存器AL = 0x04表示要读入4个扇区
寄存器CL = 0x02表示要读的磁盘扇区从2号开始
寄存器CH = 00表示要读的磁盘扇区所在的柱面号为0
寄存器DH = 0x00表示要读的磁盘扇区所在的磁头号为0
寄存器DL = 0x00表示要读扇区所在的驱动器号为0
寄存器ES:BX合在一起形成的内存地址表示从磁盘读入的内容要放到的内存起始地址,此处是0x90200,正好是移动后的bootsect.s的后面。
这段代码调用了BIOS中断int 0x10,该中断的作用是在屏幕上输出信息。
AH中的值通常被称为BIOS调用功能号。当用寄存器AH = 0x03来调用BIOS中断int 0x10时,会取出当前光标的位置,其中DH寄存器存放光标所在行,DL存放光标所在列。
紧接着用寄存器AH = 0x13再次调用BIOS中断int 0x10时,就会在屏幕上输出信息。其中ES:BP说明输出字符串所在内存地址,DH,DL给出屏幕输出的光标位置。
“mov bp,#msg”告诉BIOS要输出的信息放置在标号为msg的地方。
"mov cx,#24"表示要显示24个字符。
寄存器BL = 0x07用来设置显示字符的属性。
读入真正的系统模块,用循环实现一个磁道一个磁道地读入。
磁道的读入地址:ES:BX
循环条件:判断AX是否大于ENDSEG = SYSSEG + SYSSIZE
CX:表示当前磁道剩下要读取的字节个数。被赋值为sectors - sread。其中sectors存放一个磁道的扇区总数,sread存放当前磁道中已读入的扇区数量。
主体逻辑:每次循环,CX的值会被累加到BX上,BX表示段内偏移,最多可以表示64KB。如果BX溢出,即当前磁道剩下要读取的字节个数超过64k,所以不能读入当前磁道剩余的全部内容,只能读到正好满64KB的内容。用64KB减去当前偏移量BX计算剩余字节数,这个值右移9位,即除于512,计算剩余多少扇区,然后进入ok_read部分读磁盘。读完一个磁盘后,ES会往后走,BX清零。如果BX不溢出,直接跳转到ok_read处读磁盘,其中read_trace函数是读写磁盘具体操作函数。
执行setup代码
setup要做的第一件事就是获取硬件参数,使用的方法是BIOS中断(因为此时能使用的设备驱动只是BIOS)。
以功能号0x88调用0x15号BIOS中断就能获知扩展内存的大小,返回值放在寄存器AX中。
用指令“mov [0],ax”将内存尺寸存放在地址0x90000处。
用0x41号BIOS中断可以取出硬盘信息。
代码“rep”与“movsb”可以从DS:SI内存地址连续复制CX个字节到内存地址ES:DI处。即把system模块起始移到内存0地址。
setup的第二个核心任务是启动保护模式,即从原来的20位寻址变为可以32位寻址。因为原来的20位寻址最多可以包含1M的地址空间,对于计算机运行来说太少了。
开启32位寻址方式后,需要切换到保护模式电路。启动保护模式电路需要将寄存器CR0(一个很酷的寄存器,设计硬件的许多重要控制都是它来完成的)最后一位设置为1。
取出的第一条指令是“jmpi 0,8”,即设置CS = 8,EIP (extension IP,32位)= 0。此时不能按照实模式的CS << 4 +IP来寻址,而要变为保护模式。
保护模式下的寻址:用段寄存器作为索引在一个地址表里找到32位的基址,再和偏移寄存器中存放的32位数值相加(CS相当于指针的指针 ?)。则需要两个信息,一是地址表(GDT,global descriptor table),二是如何让计算机找到这个地址表(寻址过程由硬件自动完成,因为硬件更快)。
setup.s完成GDT表的构建及其初始化工作。其中gdt是初始化内容,gdtaddr指示GDT表的基址。
GDT表的格式:
则指令“jmpi 0,8”表示跳转到零地址。
执行system模块
-
操作系统镜像的生成——Makefile:
操作系统体系庞杂,会有一系列源码文件,如何知道先执行哪一个文件呢?因此需要Makefile来组织。
Makefile文件的基本格式是:
目标:该目标依赖的其他目标。 -
执行head.s代码片段:
对操作系统管理资源的关键数据结构进行初始化。
初始化之前的准备工作是:
- 设置中断表(IDT):因为操作系统不再使用BIOS中断,要建立起自己的中断表。
- 设置GDT表:setup中建立的GDT表是为了执行“jmpi 0,8”临时建立的,现在需要重新建立。
- 设置页表:进入32位保护模式以后,GDT[CS]+EIP计算出来的地址不能直接输出到地址总线上,通常还需要用这个地址再去查一次页表才得到真正的物理地址并输出到地址总线上。
完成从汇编到c的跳转。
一般进行函数跳转的时候,CS:EIP跳转到该函数起始点,同时把调用该函数指令的下一条指令地址压入栈中。执行完函数内容后,运行ret指令,ret解释为从栈中弹出一个内容赋给EIP,这样就可以保证程序顺序执行。而在此的main函数不需要返回,因为操作系统一开启就没必要关闭,所以在跳转到main函数时,把L6压入栈中,“L6:jmp L6”表示死循环。即一旦main函数返回,系统就进入死机状态。
- 执行main()函数:
初始化各种管理软硬件资源的数据结构
其中mem_init()执行内存初始化。memory_start表示内存起始地址,为4MB,因为0-1MB分配给了system,1MB-4MB将分配给cache,4MB之后才是用户应用程序可用的。结束地址由0x90000处内容决定,因为不是每一台电脑的内存都是一样大小的。管理时需要以4KB为一块管理,因为操作系统按照页来管理内存。
main()中的最后四条语句让操作系统开始运转,至此操作系统正式启动,可喜可贺,关于它的故事刚刚开始~
问题:
- 为什么一开始不用保护模式而用实模式。
- 为什么把第一段代码读到7c000h,直接读到90000h不是更方便。
- 果然还是要好好学x86 T T…
分享:李治军老师讲的是linux0.11的代码,在GitHub捞了一个很有趣的项目:像读小说一样品读linux0.11的源代码