文章目录
1.总体过程概述2.bootsect.s3.setup.s4.head.s5.main.c
1.总体过程概述
我们都知道,计算机是一个取指执行的机器,那么打开电源后,计算机所执行的第一句指令是什么呢?
计算机进行取值的时候,要根据CS:IP这两个寄存器的内容来寻找。所以我们要关注这两个寄存器所指向的内容。而这是由硬件设计者所决定的。
我们所讨论的Linux0.11是基于x86的,刚开机时,CPU处于实模式,其中CS=0xFFFF,IP=0x0000。此时它寻找的地址就是0xFFFF0(ROM BIOS的映射区),就是基本的BIOS程序,这段程序会检查我们的RAM,键盘,显示器等设备,之后将磁盘0磁道0扇区的内容读入到0x7c00处。并设置CS=0x07c0,IP=0x0000。而磁盘0扇区0中存放着512字节的信息,它就是操作系统所要执行的第一个程序。(即bootsect.s)
引导扇区代码bootsect.s的作用:把自己移动到内存绝对地址0x90000(576KB)处,并把启动设备中后2KB字节代码(setup.s)读入到内存0x90200处,而内核的其它部分(system模块)则被读入到内存地址0x10000(64KB)处。setup.s:当bootsect执行完成后,会跳转到setup.s的模块开始执行,它是一个操作系统的加载程序,主要作用是利用ROM BIOS中断读取机器系统数据,并将这些数据保存到0x90000开始的位置(覆盖掉了bootsect程序所在的地方)。然后setup将system模块整体向下移动到内存绝对地址0x0000处,并加载了中断描述符表寄存器(idtr)和全局描述符表寄存器(gdtr),进入保护模式,并跳转到system模块最前面部分的head.shead.s:它是system模块最前面的部分,主要功能是加载各个数据段寄存器,重新设置中断描述符表idt,并使各个表均指向ignore_int。并设置了内存的分页处理机制,然后利用返回指令将预先放置在堆栈中的/init/main.c程序的入口地址弹出,去运行main()程序。
整个过程可以用下面这张图来展示: 下面我们详细的了解下这几个程序的内容。
2.bootsect.s
这是我们所要执行的第一段程序,我们通过讨论它的一些重要源代码来深入的里了解这段程序到底做了什么事情。这是第一部分代码:
从start开始看起,它首先设置了ds寄存器的值为0x07c0,es寄存器的值为0x9000。这两个都是段寄存器。cx通用寄存器的值为256,si和di寄存器的值都为0.si是源变址寄存器,di是目的变址寄存器,它们一般与数据段寄存器DS联用,但是在串处理指令中,si和di作为隐藏的源变址和目的变址寄存器,此时si和ds联用,di和es联用。rep:将重复执行下面的语句,直到寄存器cx为0movw:将ds:si的内容送至es:di,是复制过去,原来的代码还在。
所以,这段代码的作用已经很清楚了,就是将自己给移动到0x90000处。
从load_setup开始看起,首先给这些dx,cx,bx,ax这些寄存器进行赋值,而0x13中断的作用就是读磁盘,其中ah是ax寄存器的高八位,即ah=02,al是ax寄存器的低八位,al=4,同理ch=0,cl=2,dh=0,dl=0,而es:bx代表的是要将setup读入的开始内存地址(这里我也不明白为什么是es:bx),即0x90200。所以此中断的作用就是从第二个扇区开始读入四个扇区到0x90200处。
在读入完成setup模块后,会进行一些信息的打印,具体的代码如下:
这段代码的核心是0x10中断,它的作用是读光标的位置,显示信息到屏幕上。这里我也不是很明白,cx寄存器中存放的是要显示的字符的个数,打印完信息后,call read_it又开始读system模块的代码。
最后通过jmpi 0,SETUPSET这句代码跳转到setup模块开始执行。
3.setup.s
setup.s主要完成OS系统启动前的一些设置
SYSSEG
= 0x1000
start
: mov ax
, #INITSEG mov ds
,ax mov ah
,#
0x03
xor bh
,bh
int 0x10
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 # 将system模块移到
0地址
movsw
jmp do_move
操作系统是用来管理各种硬件的,所以它必须知道这些硬件是什么,是什么样的型号,用什么样的数据结构来管理。上面这部分代码就是来做这件事情的。它获取计算机的硬件参数,然后将这些信息放到0x90000处,即将bootsect部分给覆盖了。然后将system模块的代码给移动到0地址处,这就是为什么要将bootsect移动到0x90000处的原因,防止被覆盖掉。
在完成上面的操作后,开始加载段描述符,进入32位保护模式的操作,在进入保护模式运行中之前,需要首先设置好要使用的段描述符表,这里需要设置全局描述符表(GDT)和中断描述符表(IDT)。关于保护模式部分,不明白的可以去百度。
首先通过加载IDT寄存器和GDT寄存器,来完成对GDT表和IDT表的初始化设置,之后进行一些进入保护模式前的设置。
这段代码可以看到是将cr0这个寄存器的最后一位设置成了1,这代表着此时进入了保护模式。然后通过一个跳转指令进行跳转,但此时的跳转规则已经不是csx16+ip这样的方式了,而是保护模式下,根据gdt表的内容来查表。
下面我们说下cr0这个寄存器。
cr0是一个32位的寄存器,如果它的最后一位是1,那么就进入保护模式,如果第一位是1,那么就启动分页。
而进入保护模式后,就要通过查询gdt表来进行指令的跳转,那么gdt表是如何查询的呢
.word
0x07FF, 0x0000, 0x9A00, 0x00C0
可以看到一个gdt表的构建,每个word都是一个表项,后面是4个值,16位一个,共64位。gdt表的单位是字节。cs为8。
即07FF放入0-15位,0000放入16-31位,9A00放入32-47位,00C0放入48-63位
到此我们gdt表也有了,可以执行跳转指令了,即此时cs=8,ip=0,根据保护模式下的寻址方式,我们得到基地址是0x00000000,偏移也为0。所以会跳转到0地址处执行,即system模块。
4.head.s
system模块有很多模块组成,那么该如何保证到system模块后,有正确的执行顺序呢?这里就用到了makefile来控制它执行的顺序,这样就保证了代码执行的顺序,所以它执行的第一个部分就是head.s。head.s的主要功能就是首先加载各个段寄存器,重新设置中断描述符表idt,共256项,并使各个表项均指向一个只报错误的哑中断子程序ignore_int,并重新加载了全局段描述符表gdt。实际上新的gdt与原来的除了在段限场上有些区别外(原为8MB,现为16MB),其它内容完全一样。简单概括来说它所作的就是保护模式之后初始化工作。
stratup_32
: movl $
0x10,%eax mov
%ax
,%ds mov
%ax
,%es
mov
%as
,%fs mov
%as
,%gs
lss _stack_start
,%esp
call setup_idt
call setup_gdt
xorl
%eax
,%eax
1:incl
%eax
movl
%eax
,0x000000 cmpl
%eax
,0x100000
je
1b
jmp after_page_tables
setup_idt
: lea ignore_int
,%edx
movl $
0x00080000,%eax movw
%dx
,%ax
lea _idt
,%edi movl
%eax
,(%edi
)
上面是进行一些设置的代码,并开启了A20地址线,寻址能力从1M变为了4G
after_page_tables
:
pushl $
0 pushl $
0 pushl $
0 pushl $L6
pushl $_main jmp setup_paging
L6
: jmp L6
setup_paging
: 设置页表 ret
前面开启20号地址线之后就jmp到after_page_tables这个标号,在setup_paging执行完后,ret到哪里呢?到main()函数。在after_page_tables里面将main函数三个参数、L6、main函数的入口地址都压入栈中,在setup_paging的ret直接跳_main,如果main函数再返回的话就跳到L6处。
接下来就跳转到了main函数的部分进行执行
5.main.c
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();}
}
可以看到main函数的功能就是对内存、中断、设备、CPU、时钟等内容进行初始化。
到这里操作系统已经树立了起来,事实上这段引导代码只做了两件事:
1.读入操作系统的代码,并移动到合适的位置。2.初始化。