Linux 0.01系统分析

                                                                      Frank Wonga / Freos

1.先了解makefile文件,系统主要是生成build可执行文件,解释如下:

首先,把system(二进制文件),拷贝到kernel中;

然后,把boot(二进制文件)连接成一个image映像文件(除掉注释和评论)。使用自带连接工具build

其中,system(二进制文件)由head.omain.omm.ofs.okernel.olib.a编译生成。而mm.ofs.okernel.olib.a是通过编译各自目录下的文件生成。

 

2Boot概览:

首先,系统启动后,设置各段寄存器地址,如下:

寄存器

寄存器

名称

内容

名称

内容

CS

*

GS

*

DS

07C0

DI

*

ES

9000

SI

*

FS

*

BP

*

SS

*

SP

*

把启动的512字节移动到0x9000:0000处,然后执行jmpi     go,  INITSEG

注意:jmpl把偏移地址go送往IP,把段地址INITSEG送往CS

也即执行go标签下的代码,但是CS内容改变了。

更新各寄存器地址:

寄存器

寄存器

名称

内容

名称

内容

CS

9000

GS

*

DS

9000

DI

*

ES

9000

SI

*

FS

*

BP

*

SS

9000

SP

0400

输出信息:Loading system ...

重新设置寄存器

寄存器

寄存器

名称

内容

名称

内容

CS

9000

GS

*

DS

9000

DI

*

ES

1000

SI

*

FS

*

BP

*

SS

9000

SP

0400

调用Read_it:把系统代码放在0x1000:0000注意,起始的64KB先保留

调用Kill_motor:关闭软盘驱动器

保存当前光标位置,保存的地址0x90510,注意后面需要引用该地址。

关闭中断:因为在进入保护模式的过程中,是不能有中断产生(NMI例外)。

 

 

 

又一次需要改动段寄存器内容:

寄存器

寄存器

名称

内容

名称

内容

CS

9000

GS

*

DS

9000变化中

DI

*

ES

0000

SI

*

FS

*

BP

*

SS

9000

SP

0400

把系统代码再一次移动到0x0000:0000处,每次移动64KB

移动完毕,再一次更新寄存器内容:

寄存器

寄存器

名称

内容

名称

内容

CS

9000

GS

*

DS

9000

DI

*

ES

0000

SI

*

FS

*

BP

*

SS

9000

SP

0400

装载IDTGDT这里装载的IDTGDT供临时使用。

设置A20

INOUT命令形式:IN  数据寄存器, 端口地址; OUT 端口地址,数据寄存器

首先介绍一下可编程中断控制器82598259是一个有多个中断输入,5个外部中断、DMA中断和3个定时器中断,其中主中断控制器地址为:0x0020。可编程中断控制器有俩,一个端口地址为20~3F,一个为A0~BF。其中端口20为中断命令寄存器,端口21为中断屏蔽寄存器。两个寄存器内容如下:

端口

打印机

软盘

硬盘

串口1

串口2

保留

键盘

定时器

21

7

6

5

4

3

2

1

0

端口

R

SL

EOI

0

0

L2

L1

L0

20

7

6

5

4

3

2

1

0

说明:L0~L2指定最低优先级的中断请求(IR0~IR7);SLSet Level)和RRotate)设置中断优先级的顺序。EOIEnd Of Interrupt)中断结束位,在中断处理完毕后,EOI应该置1

对于中断屏蔽寄存器,置0,允许;置1,禁止。

对于这段代码,我的理解是:

设置主从中断控制器,然后禁止所有中断。

设置PE位(进入保护模式)

跳转指令jmpi  0,  8(偏移量0传递给EIP,段选择子8传递给CS81000,全局描述符表,特权级别0,第1个段)。

 

观察此时GDTIDT以及GDTRIDTR

GDTRIDTR如下:

 

基地址(32Bit

界限

IDTR

0x00000000

0x0000

GDTR

0x00090000 + gdt

0x0800

说明

中断向量表先不考虑,临时全局描述符表在0x00090000 + gdt处。

 

GDT如下(IDT不存在):

IDT

描述符名称

实际物理地址

属性描述

高字节

低字节

0

0

NULL描述符

0x00090000 + gdt

 

0

0

0x00090000 + gdt+2

 

0

0

0x00090000 + gdt+4

 

0

0

0x00090000 + gdt+6

 

 

07

FF

代码段描述符

0x00090000 + gdt+8

基地址:0x00000000

00

00

0x00090000 + gdt+10

界限:0x007FF*4K

9A

00

0x00090000 + gdt+12

9A:代码段,可被读等

00

C0

0x00090000 + gdt+14

CG=14Kb分页;D=132位;段有效

 

07

FF

数据段描述符

0x00090000 + gdt+16

基地址:0x00000000

00

00

0x00090000 + gdt+18

界限:0x007FF*4K

92

00

0x00090000 + gdt+20

92:数据段,可被写等

00

C0

0x00090000 + gdt+22

CG=14Kb分页;D=132位;段有效

 

到这里,Boot启动完毕,程序跳转到0x000000000继续执行,让我们去0x00000000那里看看吧!

 

                                                 ——2003624日夜

                                                 ——Freos项目小组:Frank  Wonga

 

3System代码(也就是操作系统代码)

从启动分析可知,boot完毕后,代码应该跳转到0x00000000绝对物理地址处,也就是head.s代码编译后在内存中存放的地址。因此要先分析head.s

Head.s分析

首先设置各寄存器:

寄存器

寄存器

名称

内容

名称

内容

CS

0008

GS

0010

DS

0010

DI

*

ES

0010

SI

*

FS

0010

BP

*

SS

*9000?)

ESP

stack_start

建立IDTGDT这里建立的才是最后系统真正采用的。

 

 

 

 

 

重新设置各寄存器:因为IDTGDT表更新了,其实各寄存器内容不变。这叫脱裤子放屁。

寄存器

寄存器

名称

内容

名称

内容

CS

0008

GS

0010

DS

0010

DI

*

ES

0010

SI

*

FS

0010

BP

*

SS

*9000?)

ESP

stack_start

检查A20是否设置了。

检查PGETPE,看是否有387浮点运算CPU

GDTIDT表设置好了,应该设置分页技术了,跳转到after_page_tables

现在最主要是关注SSESP

寄存器

寄存器

名称

内容

名称

内容

SS

*9000?)

ESP

stack_start

个人认为,为什么不把SS也设置为0010?)

SS内容(每个单元4字节)

ESP所指地址

 

堆栈顶端

stack_start

 

0

stack_start-4

Main函数的入口参数,实际上没有用

0

stack_start-8

0

stack_start-12

L6

stack_start-16

Main函数跳出后进入死循环

main

stack_start-20

Main函数入口地址

跳转到setup_paging:建立页。

建立页表,设置CR3CR0中与页相关的位,然后Ret

 

我们来看看Ret的用法:在32位状态下,(EIP<- Pop(),也就是把main函数地址传递给EIP,因此,下一步是执行main()函数啦!系统开始进入C语言编码时代,我们熟悉的C语言来了。

 

Main.c文件

main()函数中,

第一步:time_init()函数,读取CMOS中时间寄存器的(年月日时分秒)时间,并转换为从1970年起的秒时间,然后保存在全局变量startup_time中。

第二步:tty_init()函数,好象是串口通信?

第三步:trap_init()函数,设置自定义中断服务,(设置中断门,覆盖了原有中断?不妥吧?为什么不把原有中断移植过来呢?

第四步:sched_init()函数,初始化系统调度进程,(把系统调度进程的任务状态段(TSS)和局部描述符表加入到全局描述符表中,并初始化这些描述符,然后装载任务寄存器和局部描述符表)

顺便浅析可编程定时器,在PC/AT上有三个内部独立的计数器,分别对应端口地址为:40H41H42H,它们有一个公用的控制寄存器端口为43H,控制寄存器如下:

7

6

5

4

3

2

1

0

用途

SC1

SC0

RL1

RL0

M2

M1

M0

BCD

其中,SC0SC1用来选择计数器,确定控制字是对那一个计数器初始化。

RL1RL0,读写指示位:1、寄存器锁存操作,锁存当前计数值,以便读出。2、只读/写高位字节。3、只读/写低位字节。4、先读/写低字节,紧接着读写高字节。

M0M1M2M3,选择操作模式(000~010)共有6种操作模式,决定输出脉冲的形状。

BCD位:选择计数器值的格式,0,计数值是二进制格式;1,计数值是BCD码。这个计数值在编程时作为与CLK输入频率相除的除数,最小为0001,最大为10000H(二进制时)或4000(十进制时)。(还是搞不明白?

因此,计数器0,先读低字节再读高字节,模式3,二进制计数值,计数值0的初始值为0x2E9B,设置中断门20H为定时器中断,然后打开定时器中断,设置系统调用80

(该调用的作用是:如果调用成功,保存当前DSESFSEDXECXEBX,然后把DSES指向内核数据段,FS指向局部数据段(第三个段?)),然后查找系统调用表,启动对应的系统调用?调用完毕,保存EAX(也就是系统函数返回值?),把当前任务地址放到EAX中,若当前任务状态不是运行状态,需要重新调整任务调度,然后判断当前任务的剩余的时间片(COUNTER)是否为0,如果为0,说明当前任务即将结束,需要重新调度,无论如何,获取新的当前任务地址,如果是任务0(核心任务),恢复现场,继续执行;然后判断是否是超级模式的任务?如果是,恢复现场,继续执行;然后判断堆栈段是否是第2个数据段,如果不是,恢复现场,继续执行;否则,就需要判断信号量,(哇,好复杂,头晕了,先跳过),最后仍然是恢复现场,继续执行。

初始化完schdule之后,还要初始化Buffer。(还是好复杂,跳过去),然后初始化硬盘。

打开中断,进入保护模式,好好看看怎么进入保护模式的:

#define move_to_user_mode() \

__asm__ ("movl %%esp,%%eax\n\t" \               /*ESP保存在EAX*/

       "pushl $0x17\n\t" \                                     /*把第2个段压栈*/

       "pushl %%eax\n\t" \                                   /*把第EAX压栈*/

       "pushfl\n\t" \                                             /*FLAG压栈*//

       "pushl $0x0f\n\t" \                                     /*0X0F压栈*/

       "pushl $1f\n\t" \                                         /*把下面的标签1地址压栈*/

       "iret\n" \                /*EIP为标签1的地址,CS0FHFLAGS恢复原值*/

       "1:\tmovl $0x17,%%eax\n\t" \                     /*设置,DSESFSGS均指向第2个段*/

       "movw %%ax,%%ds\n\t" \

       "movw %%ax,%%es\n\t" \

       "movw %%ax,%%fs\n\t" \

       "movw %%ax,%%gs" \

       :::"ax")

 

系统进入用户级别模式,然后继续下面的程序,先Fork一个内核出来?完毕,就循环等待,任务0的等待意味着系统空闲。到此,系统启动完成,然后操作权就交给了用户。