Linux
0.01系统分析
1.先了解makefile文件,系统主要是生成build可执行文件,解释如下:
首先,把system(二进制文件),拷贝到kernel中;
然后,把boot(二进制文件)连接成一个image映像文件(除掉注释和评论)。使用自带连接工具build。
其中,system(二进制文件)由head.o,main.o,mm.o,fs.o,kernel.o与lib.a编译生成。而mm.o,fs.o,kernel.o与lib.a是通过编译各自目录下的文件生成。
2.Boot概览:
首先,系统启动后,设置各段寄存器地址,如下:
寄存器 |
寄存器 |
||
名称 |
内容 |
名称 |
内容 |
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 |
装载IDT与GDT:这里装载的IDT和GDT供临时使用。
设置A20
(IN与OUT命令形式:IN 数据寄存器, 端口地址;
OUT 端口地址,数据寄存器)
首先介绍一下可编程中断控制器8259:8259是一个有多个中断输入,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);SL(Set Level)和R(Rotate)设置中断优先级的顺序。EOI(End Of Interrupt)中断结束位,在中断处理完毕后,EOI应该置1。
对于中断屏蔽寄存器,置0,允许;置1,禁止。
对于这段代码,我的理解是:
设置主从中断控制器,然后禁止所有中断。
设置PE位(进入保护模式)
跳转指令jmpi 0, 8(偏移量0传递给EIP,段选择子8传递给CS;8:1000,全局描述符表,特权级别0,第1个段)。
观察此时GDT、IDT以及GDTR、IDTR
GDTR和IDTR如下:
|
基地址(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 |
C:G=1,4Kb分页;D=1,32位;段有效 |
|
|
||||
07 |
FF |
数据段描述符 |
0x00090000 + gdt+16 |
基地址:0x00000000 |
00 |
00 |
0x00090000 + gdt+18 |
界限:0x007FF*4K |
|
92 |
00 |
0x00090000 + gdt+20 |
92:数据段,可被写等 |
|
00 |
C0 |
0x00090000 + gdt+22 |
C:G=1,4Kb分页;D=1,32位;段有效 |
到这里,Boot启动完毕,程序跳转到0x000000000继续执行,让我们去0x00000000那里看看吧!
——2003年6月24日夜
——Freos项目小组:Frank Wonga
3.System代码(也就是操作系统代码)
从启动分析可知,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 |
建立IDT和GDT:这里建立的才是最后系统真正采用的。
重新设置各寄存器:因为IDT和GDT表更新了,其实各寄存器内容不变。这叫脱裤子放屁。
寄存器 |
寄存器 |
||
名称 |
内容 |
名称 |
内容 |
CS |
0008 |
GS |
0010 |
DS |
0010 |
DI |
* |
ES |
0010 |
SI |
* |
FS |
0010 |
BP |
* |
SS |
*(9000?) |
ESP |
stack_start |
检查A20是否设置了。
检查PG,ET,PE,看是否有387(浮点运算CPU?)
GDT、IDT表设置好了,应该设置分页技术了,跳转到after_page_tables。
现在最主要是关注SS和ESP:
寄存器 |
寄存器 |
||
名称 |
内容 |
名称 |
内容 |
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:建立页。
建立页表,设置CR3、CR0中与页相关的位,然后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上有三个内部独立的计数器,分别对应端口地址为:40H、41H、42H,它们有一个公用的控制寄存器端口为43H,控制寄存器如下:
位 |
7 |
6 |
5 |
4 |
3 |
2 |
1 |
0 |
用途 |
SC1 |
SC0 |
RL1 |
RL0 |
M2 |
M1 |
M0 |
BCD |
其中,SC0、SC1用来选择计数器,确定控制字是对那一个计数器初始化。
RL1、RL0,读写指示位:1、寄存器锁存操作,锁存当前计数值,以便读出。2、只读/写高位字节。3、只读/写低位字节。4、先读/写低字节,紧接着读写高字节。
M0、M1、M2、M3,选择操作模式(000~010)共有6种操作模式,决定输出脉冲的形状。
BCD位:选择计数器值的格式,0,计数值是二进制格式;1,计数值是BCD码。这个计数值在编程时作为与CLK输入频率相除的除数,最小为0001,最大为10000H(二进制时)或4000(十进制时)。(还是搞不明白?)
因此,计数器0,先读低字节再读高字节,模式3,二进制计数值,计数值0的初始值为0x2E9B,设置中断门20H为定时器中断,然后打开定时器中断,设置系统调用80。
(该调用的作用是:如果调用成功,保存当前DS、ES、FS、EDX、ECX、EBX,然后把DS、ES指向内核数据段,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的地址,CS为0FH,FLAGS恢复原值*/
"1:\tmovl $0x17,%%eax\n\t" \ /*设置,DS、ES、FS、GS均指向第2个段*/
"movw %%ax,%%ds\n\t" \
"movw %%ax,%%es\n\t" \
"movw %%ax,%%fs\n\t" \
"movw %%ax,%%gs" \
:::"ax")
系统进入用户级别模式,然后继续下面的程序,先Fork一个内核出来?完毕,就循环等待,任务0的等待意味着系统空闲。到此,系统启动完成,然后操作权就交给了用户。