OS操作系统学习笔记【操作系统启动】
前言
通过os这门课的一段时间的学习,我发现自己还是需要补充大量的知识。单单是一个简单的操作系统启动就带来了很多的知识点,硬嚼慢咽,总算是啃了下来。不过哈工大李治军老师讲的课真的很不错,同时还配套了8个小实验。上他的课只需要记住这句话:不coding操作系统,就学不会OS。
操作系统简介与引导扇区
该课程只学比较基础的操作系统:
即单核CPU,以及内存管理文件系统等最基本的知识,而暂不涉及网络管理等更高级的知识。
课程目标:能够设计,修改操作系统相关模块,掌控操作系统,保证完成八个大作业。(因为要自己动手实践,所以,比较难。老师原话:很难。)
看了斯坦福的操作系统目标,都不好意思说自己学过操作系统了。世界计算机最牛逼的学府卡内基梅隆的要求是给一个板子,自己通过数据手册来给它上操作系统。
有些小激动啊。虽然感觉凭借多年的编程技术,这肯定难不倒我,但肯定还是有一定挑战性的。Come on!
从通用图灵机到计算机:
通用图灵机,简单来说,就是给你一系列的操作指令,然后你去执行相关的动作。比如给你一个菜谱,你去把菜做了。
根据冯诺依曼提出来的结构,我们知道,计算机会把程序存在内存里,然后用一个指针指向它,然后解析执行它,然后指针后移,再去指向下一条指令,再去解析执行。(取指->执行)
(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的位置。
jmpi go, INITSEG : go是ip,代表偏移,其实是一个标号,表示从程序开始一直到该段的地址长度。执行完以后相当于我们跳到刚才移动到的位置继续往下执行(因为我们把这些扇区的东西往后搬了,是为了腾出地方给别的代码,以后会再说腾出地方给了谁)
引导扇区
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] 间接寻址到的地址里,
也就是放到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的方式不一样。
gdt是一个硬件,用来执行寻址方面的功能,之所以用硬件实现,是为了快速响应。
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表。同时还需要注意中断函数也是非常类似的,这里也提一下。
现在,我们找地址就是保护模式的寻址方式了,现在地址是多少呢?
gdt: .word 0,0,0,0
.word 0x07FF, 0x0000, 0x9A00, 0x00C0
.word 0x07FF, 0x0000, 0x9200, 0x00C0
0x00C09A00000007FF
所以就是0x00000000,然后加上ip仍然为0。跳到0x0这个地址上,开始执行system模块。
操作系统system模块执行
注意我们程序执行时就是按照首先读第一个扇区,然后第一个扇区的bootset继续读后面的扇区,所以代码的安排必须要合理:
需要用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表。因为之前的这个表是为了之前操作使用的,现在操作系统需要真正控制硬件了。
内嵌汇编就是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里面也是不断循环)。
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的地方 。
前面使用的区域是操作系统的内存,后面是未使用区。通过执行内存初始化函数,我们就分出了用过的和空白的内存区域。
本文内容转载:https://feimo.blog.csdn.net/article/details/105459478