启动之后从实模式到保护模式再到main函数的简单分析。

首先,应该了解磁盘里面存放数据的结构:

bootsect.s    : 1个扇区

setup.s       : 4个扇区

system模块: 约240个扇区

启动之后会读取bootsect.s到0x7c00:0x0000地址内,并从当前地址开始执行bootsect.s的代码。

bootsect.s所完成的任务大概如下:

1.把自身复制到0x9000:0x0000处,之后jmp go, INITSEG,则CS:0x9000 ip:(offset go) 处继续执行。

2.设置ss:sp为0x9000:0xff00  -> 0x9ff00处。(之后从磁盘读取setup.s需要用int 0x13的软中断,需设置堆栈,并且堆栈的值是远大于0x90000,保证不会覆盖掉bootsect.s和setup.s的代码)

3.加载setup.s(磁盘的第2个扇区之后的4个扇区)到0x90200处

4.读取system模块到0x10000地址处。(大小为192KB,SYSSIZE = 0x3000 节,一节16字节)

   那么占用的地址为0x10000 ~ 0x40000 

5. 保存设备号到root_dev地址(508)处,供之后程序所用。

    (  249   .org 508

          250   root_dev:

                                   .word   ROOT_DEV

          252   boot_flag:

                                   .word  0x55AA

     )

    最后 jmpi 0, 0x9020    地址出0x90200执行,为setup.s的代码。

此时内存分布如下图:

setup.s所完成的工作:

1.询问BIOS有关内存、磁盘、其他参数,并保存到0x90000 ~ 0x901ff 地址出(之前的bootsect.s代码位置)

2.进入保护模式

   2.1 cli禁止中断

   2.2 把system模块移动到0x00000地址处(复制的区间是0x10000 - 0x90000)

         注:之所以bootsect.s不直接复制到0x00000的地址处是因为,还需要利用BIOS去得到一些参数。

              就像第一步所做的一样,就是利用BIOS去得到参数。

   2.3 lidt idt_48

   2.4 lgdt gdt_48

   2.5 开始A20地址线,并重新对中断编写(0x20 - 0x2f)

   2.6 CR0 的bit0 = 1,CPU切换到保护模式

   2.7 jmpi 0, 8 真正进入保护模式!!!

uploading.4e448015.gif正在上传…重新上传取消

uploading.4e448015.gif正在上传…重新上传取消uploading.4e448015.gif正在上传…重新上传取消提供参数所对应的内存地址和描述:

现在大概说说gdt和ldt

205 gdt:

206           .word 0, 0, 0, 0

207

208           .word 0x07ff              ! 段长 8Mb -limit 2047 (2047 * 4096 = 8Mb)

209           .word 0x0000            ! 基址 0

210           .word 0x9A00            ! 代码段,只读、可执行

211           .word 0x00c0            !颗粒度4096, 32位模式

212

213           .word 0x7F00           !段长 8Mb -limit 2047 (2047 * 4096 = 8Mb)

214           .word 0x0000           !基址 0

215           .word 0x9200           !数据段、可读、可写

216           .word 0x00c0           !颗粒度4096, 32位模式

第一个描述符空描述符、第二个为代码段描述符、第三个为数据段描述符。

218  idt_48: 

219            .word 0

210            .word 0, 0

CPU要求进入保护模式时候设置IDT寄存器,这里暂且设为空。之后会在进行重新设置。

222  gdt_48:

223                   .word 0x800

224                   .word 512 + gdt, 0x9

解释:基地址0x90200 + gdt , 段长0x8000

          gdt就是上面的标号,0x90200为setup.s代码的基地址,所以0x90200 + gdt 就是gdt所在的绝对地址。

          0x800/ 8 = 0x100 为256个项。

最后在解释一下jmpi 0, 8

                 二进制8: 1000        

                             bit1 - bit0 : RPL = 0

                                   bit2    : TI = 0 (GDT表中找段描述符)

                             bit15-bit3 : 1         (GDT表中的第一个描述符)

                  由该描述符可以知道所在段的基地址为0,由jmpi 0, 8指令知道偏移地址0,所以现在要跳到地址为0的地方去执行代码,也就是system模块(head.s)。    

现在在看一下此时的内存分布:

现在会跳转到system模块的最开始的head.s代码执行。

明天再继续分析下去到最后到main函数。

最后在总结一下,前面这3个汇编程序到底给操作系统提供了些什么信息。

执行head.s的代码已经是在保护模式下,那么先大概说一些它所完成的功能:

1.重新设置IDT表

2.重新设置GDT表、所以setup.s所设置的2个描述符仅仅为临时的,之后就无效了。

3.设置页目录和页表

4.开启分页机制,然后跳转到main函数。

注意head.s中开头的

.globl _idt, _gdt, _pg_dir, _tmp_floppy_area

把它们设置为全局的,之后的C里面的函数都会调用这几个变量的。

比如在head.h这个头文件:

typedef struct desc_struct

{

             unsigned long a, b

}desc_table[256];

extern desc_table idt, gdt;

所设置的外部变量就是这里的_idt和_gdt。

lss _stack_start, %esp 这条指令

在sched.h中有这么定义:

//在其他地方定义了 PAGE_SIZE   4096

long user_stack [ PAGE_SIZE>>2 ] ; 

struct 

{
long * a;
short b;
} stack_start = { & user_stack [PAGE_SIZE>>2] , 0x10 };

解释:

long 型数据长度为4个字节、  long user_stack[4096 >> 2]    =>  long user_stack[1024] 

                                               1024个long型变量,长度则为4096 一页的长度。

&user_stack[PAGE_SIZE>>2] => &user_stack[1024]  最后一个数据的下标为1023。但是在堆栈操作时候是先递减ESP的值,然后再存放值。

所以当堆栈操作时候是这样的:  esp = esp - 4  

                                                  [esp] = 变量

等价于数据放入到user_stack[1023]。所以说,esp指向一页的末端。

那么该指令执行后,ss = 0x10  esp = &user_stack[1024]....

call setup_idt        设置IDT表

call setup_gdt      设置GDT表

先了解一下IDT描述符的结构:

中断门:

[31                            16]    [15]   [14 13]   [12  8]   [7  5]   [4   0]

过程入口偏移地址31-16      P        DPL      01110   000    0000

[31                    16]     [15                            0]

     段选择符                过程入口偏移地址15-0

setup_idt:

             lea ignore_int, %edx

             movl $0x00080000, %eax

             movw %dx, %ax

             movw $0x8E00, %dx

             lea _idt, %edi

             mov $256, %ecx

rp_sidt:

            movl %eax, (%edi)

            movl %edx, 4($edi)

            addl $8, %edi

            jne rp_sdi

            lidt idt_descr

            ret

该段代码 ignore_init为过程偏移地址

               0x00080000    主要的就是前面0008  作为选择符

               0x8E000000    主要为8E00 :1000 1110 0000 0000  存在、DPL = 0、01110 中断门

               最后用edx来存放高4字节、eax存放低4字节。 作为一个中断描述符

               其实它们最后表示为:

               x x x x      8E00

               0 0 0 8    x x x x

 lea _idt, %edi   

_idt标号在head.s 的232行:

_idt:

        .fill 256, 8, 0   

所以表示为256项、每项为8个字节。

rp_sidt:

            。。。

            。。。 

            。。。

            jne rp_sidt

该段作用是对这个255项进行赋值,之前的edx和eax。这样之后就是所有终端描述符最终都指向

ignore_int这个过程。至于这个过程做什么事、仅仅是打印一段信息,然后返回,没有实际用处。

到main函数时候,会在继续具体的赋值各个中段描述符。

lidt idt_descr   //设置IDTR寄存器

idt_descr:

              .word 256 * 8 - 1  段限长

              .long _idt              idt表的线性地址

setup_gdt:

              lgdt gdt_descr  //设置GDTR寄存器

              ret

gdt_descr:    

             .word 256 * 8 - 1 

             .long  _gdt    

_gdt:

        .quad 0x0000000000000000

        .quad 0x00c09a0000000f f f

        .quad 0x00c0920000000f f f

        .quad 0x0000000000000000

        .fill 252, 8, 0

所以_gdt和_idt是差不多的。都是设置了256项的8字节长度。那么_gdt存放的就是GDT表项。

第一个描述符为空、第二个为代码段、第三个为数据段、第四个为系统段(不用)

剩下的252个是供进程的IDT和TSS段的描述符。

这里的代码段和数据段的限长为16M,就是整个内存的长度(默认内存大小为16M)。所以说内核可以访问整个内存。

最后就是设置页目录和页表接着开启分页,然后跳转到main

head.s:

16:   pg_dir:

114: .org 0x1000

         pg0:

117: .org 0x2000

        pg1:

120: .org 0x3000

        pg2:

123: .org 0x4000        

        pg3:

126: .org 0x5000

132: _tmp_floop_area:

        .fill 1024, 1, 0

pg_dir: [0 - 0xfff]              4096

pg0    : [0x1000 - 0x1fff]  4096

pg1    : [0x2000 - 0x2fff]  4096

pg2    : [0x3000 - 0x3fff]  4096

pg2    : [0x4000 - 0x4fff]  4096

设置页目录和页表代码:

setup_paging:
movl $1024*5,%ecx       //4个页表1个页目录 5 页内存清零
xorl %eax,%eax
xorl %edi,%edi
cld;rep;stosl
movl $pg0+7,pg_dir         pg_dir 第一个页目录项 pg0 为第一个页表的基地址 7 为页存在、 用户可读可写、
movl $pg1+7,pg_dir+4      pg_dit  + 4第二个页目录项 pg0 为第二个页表的基地址 7 为页存在、 用户可读可写、
movl $pg2+7,pg_dir+8            。。。 
movl $pg3+7,pg_dir+12   。。。


movl $pg3+4092,%edi           edi指向第3页表的最后一项 (4092指向最后一项、因为一项长度为4)
movl $0xfff007,%eax /*  16Mb - 4096 + 7 (r/w user,p) */
std
1: stosl                  
subl $0x1000,%eax               //设置3个页表内容 
jge 1b                                    //一个页表可以存放1024个表项,一个表项指向一个页(4096字节)

                                                      //16M = 4096 * 4096  所以需要4096个表项、4096 / 1024 = 4 ,4个页表来映射

 
xorl %eax,%eax      //pg_dir地址在0x00000000
movl %eax,%cr3     //页目录地址存放到CR3
movl %cr0,%eax                 //置位PG 、开启分页机制
orl $0x80000000,%eax
movl %eax,%cr0 /* set paging (PG) bit */
ret /* this also flushes prefetch-queue */

after_page_tables:
pushl $0 # These are the parameters to main :-)
pushl $0
pushl $0
pushl $L6 # return address for main, if it decides to.
pushl $main
jmp setup_paging
L6:
jmp L6 # main should never return here, but

# just in case, we know what happens.

pushl $main 则为main函数的入口地址。

jmp setup_paging  就是前面的设置页目录和页表。

最后的ret  则返回到main函数去执行了。

内存布局:

之后,给后面提供了

1. gdt  GDT表

2. idt    IDT表 (还需要在为各个中断进行赋值)

3.pg_dir  页目录   

4.pg0 pg1 pg2 pg3页表 (4个页表)

5.0x90000 - 0x901ff的参数

发布了21 篇原创文章 · 获赞 1 · 访问量 6100

猜你喜欢

转载自blog.csdn.net/darling54454/article/details/37838779