OS操作系统学习笔记【操作系统启动】

前言

通过os这门课的一段时间的学习,我发现自己还是需要补充大量的知识。单单是一个简单的操作系统启动就带来了很多的知识点,硬嚼慢咽,总算是啃了下来。不过哈工大李治军老师讲的课真的很不错,同时还配套了8个小实验。上他的课只需要记住这句话:不coding操作系统,就学不会OS。

操作系统简介与引导扇区

该课程只学比较基础的操作系统:

img

即单核CPU,以及内存管理文件系统等最基本的知识,而暂不涉及网络管理等更高级的知识。

img

课程目标:能够设计,修改操作系统相关模块,掌控操作系统,保证完成八个大作业。(因为要自己动手实践,所以,比较难。老师原话:很难。)

img

看了斯坦福的操作系统目标,都不好意思说自己学过操作系统了。世界计算机最牛逼的学府卡内基梅隆的要求是给一个板子,自己通过数据手册来给它上操作系统。

有些小激动啊。虽然感觉凭借多年的编程技术,这肯定难不倒我,但肯定还是有一定挑战性的。Come on!

从通用图灵机到计算机

通用图灵机,简单来说,就是给你一系列的操作指令,然后你去执行相关的动作。比如给你一个菜谱,你去把菜做了。

根据冯诺依曼提出来的结构,我们知道,计算机会把程序存在内存里,然后用一个指针指向它,然后解析执行它,然后指针后移,再去指向下一条指令,再去解析执行。(取指->执行)

img

img

img

(1)和(2)CS和IP是8086CPU中两个重要的寄存器,指示了CPU当前要读取指令的地址。CS为代码段寄存器,IP为指令指针寄存器(偏移),两个结合起来的位置就是当前指向的指令。和保护模式对应,实模式的寻址CS:IP(CS左移4位+IP), 和保护模式不一样。8086CPU地址总线为20位,所以它的寻址能力是2 ^ 20 = 1MB。

(3)该地址正好是前面CS和IP结合后指向的位置,该程序是固化在ROM里的,叫做BIOS,即Basic input output system基本输入输出系统。(4)检查外围设备是否可用。

(5)读入1个扇区,即512字节的内容,0磁道0扇区叫操作系统的引导扇区。把该扇区里的内容读到7c00处,然后设置cs和ip指向7c00。注意0x07c0<<4+0x0000=0x7c00。

引导扇区的作用:

引导扇区就是从磁盘引导扇区读入的那512个字节。

引导扇区就是启动设备的第一个扇区,启动设备信息被设置在CMOS中(CMOS: 互补金属氧化物半导体(64B-128B),用来存储实 时钟和硬件配置信息)开机时按住del键可进入启动设备设置界面,可以设置为光盘启动。 因此,硬盘的第一个扇区上存放着开机 后执行的第一段我们可以控制的程序

引导扇区的代码是汇编语言。(为什么不用C?因为,比如你声明一个变量int a,你不知道编译后a会存在哪个位置,这是不行的,我们需要用汇编来进行绝对控制)

引导扇区代码: bootsect.s

BOOTSEG = 0x07c0



INITSEG = 0x9000



SETUPSEG = 0x9020
.globl begtext,begdata,begbss,endtext,enddata,endbss



.text //文本段



begtext:



.data //数据段



begdata:



.bss //未初始化数据段



begbss:



entry start //关键字entry告诉链接器“程序入口”



start:



mov ax, #BOOTSEG mov ds, ax



mov ax, #INITSEG mov es, ax



mov cx, #256



sub si, si sub di,di



rep movw



jmpi go, INITSEG



go:........

text等是伪操作符,告诉编译器产生 文本段,.text用于标识文本段的开始位置。 因为begtext:相当于跳转,也就是说.text跳到.data,再跳到.bss,所以此处的.text、.data、.bss这3个段重叠,不分段。

ax,bx,cx,dx这四个寄存器是通用寄存器,又叫数据寄存器,用于存放数据。

执行完前两行以后,ds = 7c0,es = 9000。这两个寄存器是段寄存器,只有段寄存器不行,还需要加偏移,才能构成地址。偏移这时就放在si和di寄存器里,执行完第四行以后,si减自己当然为0,所以si = 0,同理di = 0。ds与si构成地址7c00,es与di构成地址90000。

cx里放着移动计数值:移动计数值=256 字;

rep

movw

这两条指令代表重复执行,直到cx内计数器为0才停下。注意一个字是两个字节,即把这个引导扇区的512个字节全都移动到es与di构成的90000的位置。

img

jmpi go, INITSEG : go是ip,代表偏移,其实是一个标号,表示从程序开始一直到该段的地址长度。执行完以后相当于我们跳到刚才移动到的位置继续往下执行(因为我们把这些扇区的东西往后搬了,是为了腾出地方给别的代码,以后会再说腾出地方给了谁)

img

引导扇区

img

go: mov ax,cs //cs=0x9000



mov ds,ax mov es,ax mov ss,ax mov sp,#0xff00



load_setup: //载入setup模块



mov dx,#0x0000 mov cx,#0x0002 mov bx,#0x0200



mov ax,#0x0200+SETUPLEN int 0x13 //BIOS中断



jnc ok_load_setup



mov dx,#0x0000



mov ax,#0x0000 //复位



int 0x13



j load_setup //重读

0x13是BIOS读磁盘扇区的中断: ah=0x02-读磁盘,al= 扇区数量(SETUPLEN=4), ch=柱面号,cl=开始扇区(即cx的低八位,这里是2,因为第一个扇区是引导扇区,要从第2个扇区开始读), dh=磁头号,dl=驱动器号, es:bx=内存地址

读磁盘读到哪里?肯定是bootset的上面,bootset从90000开始,偏移512字节,为90200。es:bx从上面的程序确实可以看出,是90200。

读完四个扇区还需要读进去操作系统。

Ok_load_setup: //载入setup模块



mov dl,#0x00 mov ax,#0x0800 //ah=8获得磁盘参数



int 0x13 mov ch,#0x00 mov sectors,cx



mov ah,#0x03 xor bh,bh int 0x10 //读光标



mov cx,#24 mov bx,#0x0007



mov bp,#msg1 mov ax,#1301 int 0x10 //显示字符



mov ax,#SYSSEG //SYSSEG=0x1000



mov es,ax



call read_it //读入system模块



jmpi 0,SETUPSEG

int 0x10 是显示字符,bp告诉你要显示的东西在内存中什么区域,例如:

bootsect.s中的数据 //在文件末尾



sectors: .word 0 //磁道扇区数



msg1:.byte 13,10



     .ascii “Loading system...”



     .byte 13,10,13,10

肯定是要把字符打印到光标位置,所以前面有一个读光标位置的操作。

如果我们想替换一下字符串,还需要改一些配置,例如如果mov cx,#24这里的24表示要输出的字符个数,则把前面的mov cx,#24修改一下。

boot的工作:读setup, 读system。注意,为什么读入 要跨越磁道! system模块还需要定义一个函数?

因为system模块可能很大,读入要跨越磁道。

read_it: mov ax,es cmp ax,#ENDSEG jb ok1_read



ret



ok1_read:



mov ax,sectors



sub ax,sread //sread是当前磁道已读扇区数,ax未读扇区数



call read_track //读磁道...

ENDSEG=SYSSEG+SYSSIZE SYSSIZE=0x8000 //该变量可根据 Image大小设定(编译操作系统时)

引导扇区的末尾 //BIOS用以识别引导扇区:

.org 510



    .word 0xAA55 //扇区的最后两个字节

.org 510 指定一个地址,后面的程序或数据从这个地址值开始分配,即以后语句从地址510(0x1FE)开始存放

.word 0xAA55 表示有效引导扇区标志,供BIOS加载引导扇区使用

可以转入setup执行了:

jmpi 0, SETUPSEG

注意该跳转语句,上一节里代码中显示SETUPSEG=9020,作为cs,再与ip=0组合成90200,即跳转后执行的地址。

操作系统启动

上一节说到把setup和操作系统弄到内存里了,现在开始执行setup程序。

start: mov ax,#INITSEG mov ds,ax mov ah,#0x03



xor bh,bh int 0x10//取光标位置dx mov [0],dx //取出光标位置(包括其他硬件参数)到0x90000处



mov ah,#0x88 int 0x15 mov [2],ax ...



cli ///不允许中断



mov ax,#0x0000 cld



do_move: mov es,ax add ax,#0x1000



cmp ax,#0x9000 jz end_move



mov ds,ax sub di,di



sub si,si



mov cx,#0x8000



rep



movsw



jmp do_move

int 0x15 就是获得实际物理内存的大小,然后放到ax寄存器里。然后把这个值放到 [2] 间接寻址到的地址里,

img

也就是放到90002这个地址里。读出来大小就是为了以后管理。

现在理解一下这个步骤为什么叫setup:即操作系统要开始管理接管内存了。

然后通过上面的代码我们可以看到重新设置了 ds:si 和 es:di,然后就是把操作系统移到0x0000的位置,我们可以看到,移动的长度是0x8000个字,这就解释了当时为什么要把0x7c00的东西搬到0x90000去,因为如果操作系统太大,就会把之前的bootset和setup都给覆盖了,但是我们现在正在执行setup,所以无论如何都不能覆盖了setup,所以要上移。从此操作系统就一直呆在这个区域里,永远不被改变了。其他一些细节,可以去《Linux内核剖析1.11》上看。

call empty_8042 mov al,#0xD1 out #0x64,al



//8042是键盘控制器, 其输出端口P2用来控制A20地址线 D1表示写数据到8042的P2端口



call empty_8042 mov al,#0xDF out #0x60,al



//选通A20地址线 call empty_8042



初始化8259(中断控制) //一段非常机械化的程序



mov ax,#0x0001 mov cr0,ax



jmpi 0,8

操作系统在最后执行了jmpi 0 8,该指令发挥了重要作用:

执行这句话,cs=8,ip=0,加起来以后是80。如果真的跳到80地址,那就会死机。跳到80一定是一个非法指令,所以这里不是跳到80,应该跳到另外一个地方:0地址处,这是system模块。从这个时候,寻址方式开始改变。在这之前,计算机能访问的地址只有1M(20位),要从1M变到4G内存,不能再使用这个寻址方式。所以要从16位模式变到32位模式:保护模式

寻址方式不同的根本区别:CPU的解释程序不一样,解释cs和ip的方式不一样。

img

img

gdt是一个硬件,用来执行寻址方面的功能,之所以用硬件实现,是为了快速响应。

img

cs这个时候是一个选择子,选择表中的一个项,然后产生地址。gdt表:全局描述符表(global descripitor batle。

但是需要这个表中有内容,这样才能去选择,所以setup程序前面还有一段内容,是关于表的:

end_move: mov ax,#SETUPSEG mov ds,ax



lidt idt_48 lgdt gdt_48//设置保护模式下的中断和寻址



进入保护模式的命令...



idt_48:.word 0 .word 0,0 //保护模式中断函数表



gdt_48:.word 0x800 .word 512+gdt,0x9



gdt: .word 0,0,0,0



.word 0x07FF, 0x0000, 0x9A00, 0x00C0



.word 0x07FF, 0x0000, 0x9200, 0x00C0

这里的gdt就是生成gdt表。同时还需要注意中断函数也是非常类似的,这里也提一下。

img

现在,我们找地址就是保护模式的寻址方式了,现在地址是多少呢?

img

gdt: .word 0,0,0,0



    .word 0x07FF, 0x0000, 0x9A00, 0x00C0



    .word 0x07FF, 0x0000, 0x9200, 0x00C0

img

0x00C09A00000007FF
所以就是0x00000000,然后加上ip仍然为0。跳到0x0这个地址上,开始执行system模块。

操作系统system模块执行

注意我们程序执行时就是按照首先读第一个扇区,然后第一个扇区的bootset继续读后面的扇区,所以代码的安排必须要合理:

img

需要用makefile来自己设计编译细节。设计哪个放在前面,哪个放在后面。编译出来以后,操作系统叫image,一定要符合上图的样子。我们需要写到0磁道0扇区。

makefile如下:

disk: Image



dd bs=8192 if=Image of=/dev/PS0



Image: boot/bootsect boot/setup tools/system tools/build



tools/build boot/bootsect boot/setup tools/system > Image



tools/system: boot/head.o init/main.o $(DRIVERS) …



$(LD) boot/head.o init/main.o $(DRIVERS) … -o tools/system

生成Image需要很多依赖,依赖有了以后,就把这些东西合成到Image镜像里。同时这些依赖也分别要依赖其他项。注意上面的$(DRIVERS)表示驱动程序。

system的第一个部分就是head.s。

注意head.s是32位保护模式,所以要执行32位的汇编(和原来的汇编不一样了):

stratup_32: movl $0x10,%eax mov %ax,%ds mov %ax,%es



mov %as,%fs mov %as,%gs //指向gdt的0x10项(数据段)



lss _stack_start,%esp //设置栈(系统栈)



call setup_idt



call setup_gdt



xorl %eax,%eax



1:incl %eax



movl %eax,0x000000 cmpl %eax,0x100000



je 1b //0地址处和1M地址处相同(A20没开启), 就死循环



jmp after_page_tables //页表, 什么东东?



setup_idt: lea ignore_int,%edx



movl $0x00080000,%eax movw %dx,%ax



lea _idt,%edi movl %eax,(%edi)

主要工作就是重新设置idt和gdt表。因为之前的这个表是为了之前操作使用的,现在操作系统需要真正控制硬件了。

img

内嵌汇编就是C语言文件中,有些指令必须使用汇编来严格控制执行。

head.s文件执行完以后,要跳转到main.c。它的做法是:

after_page_tables:



pushl $0 pushl $0 pushl $0 pushl $L6



pushl $_main jmp set_paging



L6: jmp L6



setup_paging: 设置页表 ret

setup_paging执行ret后,会执行函数main()

进入main()后的栈为0, 0, 0, L6

main()函数的三个参数是0, 0, 0

main()函数返回时进入L6, L6是个死循环,即死机了。所以main永远不会返回(main里面也是不断循环)。

img

jmp set_paging以后,就跳到 set_paging ,然后设置页表,并执行ret,弹栈,main出栈,执行main函数。

main函数:

void main(void)



{ 



    mem_init();



    trap_init();



    blk_dev_init();



    chr_dev_init();



    tty_init();



    time_init();



    sched_init();



    buffer_init();



    hd_init();



    floppy_init();



    sti();



    move_to_user_mode();



    if(!fork()){init();}



}



 

三个参数分别是envp,argv,argc,但此处main并没使用,此处的main只保留传统main的形式和命名,main表示C语言函数的入口。main的工作就是xx_init: 内存、 中断、 设备、时钟、 CPU等内容的初始化。

我们取出内存初始化程序来查看一下:(在linux/mm/memory.c中)

void mem_init(long start_mem,long end_mem)



{



int i;



for(i=0; i<PAGING_PAGES; i++)



mem_map[i] = USED;



i = MAP_NR(start_mem);



end_mem -= start_mem;



end_mem >>= 12;



while(end_mem -- > 0)



mem_map[i++] = 0; }

end_mem >>= 12; 左移12表示除以2的12次方,即4K,也就是每4K分为1页。

注意start_mem和end_mem,回想之前学的内容,扩展内存的大小保存在0x90002的地方 。

img

img

前面使用的区域是操作系统的内存,后面是未使用区。通过执行内存初始化函数,我们就分出了用过的和空白的内存区域。

本文内容转载:https://feimo.blog.csdn.net/article/details/105459478

posted @ 2020-09-13 15:23  Shoo1er  阅读(489)  评论(1编辑  收藏  举报