操作系统学习:内存分页与中断

本文参考书籍

1.操作系统真相还原
2.Linux内核完全剖析:基于0.12内核
3.x86汇编语言  从实模式到保护模式
ps:基于x86硬件的pc系统

内存分页机制

内存信息的获取

在进行内存分页之前,需要先知道机器的物理内存有多少。当前Linux获取机器内存的方法是,在进入保护模式之前,通过bios中断来获取机器的物理内存大小,通过调用bios中断0x15来实现,通过该中断设置子功能号存放到寄存器EAX或AX中,可以有三个方法获取物理内存大小,分别为:1、EAX=0xE820,遍历主机上全部内存;2、AX=0xE801,分别检测低15MB和16MB~4GB的内存,最大支持4GB;3、AH=0x88,最多检测出64MB内存,实际内存超过此容量也按照64MB返回。

内存分页的原理

内存分页的主要是为了解决内存空间碎片化,加载程序大小受限等问题。在cpu不打开分页机制的情况下,是按照默认的分段方式进行的,段基址和段内偏移地址经过段部件处理后所输出的线性地址,cpu就认为是物理地址,如果打开了分页机制,段部件输出的线性地址就不在等同于物理地址,此时称为虚拟地址,虚拟地址不应该被送上地址总线上,cpu的执行必须是访问物理地址,此时虚拟地址对应的物理地址需要在页表中查找,这项查找工作是由页部件自动完成,如图所示:
段基址不分页和段基址分页访问内存
分页机制的主要原理是:通过映射,可以使连续的线性地址与任意物理内存地址相关联,逻辑上连续的线性地址其对应的物理地址可以不连续。分页机制的作用主要是将线性地址转换成物理地址,用大小相等的页代替大小不等的段。如图所示:
分段分页机制的布局图
分页机制建立在分段机制之上,每加载一个进程,操作系统按照进程中各段的起始范围,在进程中的4GB虚拟地址空间中寻找可用空间分配内存段,代码段和数据段在逻辑上被拆分成以页为单位的小内存块,接着操作系统开始为这些虚拟内存页分配真实的物理内存页,查找到物理内存中可用的页,然后在页表中登记这些物理页地址,这样就完成了虚拟页到物理页的映射,每个进程都以为自己独享4GB地址空间。
内存块的大小,在cpu中采用的就是4KB的大小,4GB地址空间被划分成了1M个页,如图所示:
页表和页表项
在只有一级页表的情况下,线性地址的宽度也是32位,可以考虑将虚拟地址的高20位作为索引定位具体的物理页,剩余12位可以作为页内偏移,此时就可以寻找到物理地址,然后就可以访问。此时地址转换过程大致过程如下:一个页表项对应一个页,用线性地址的高20位作为页表项的索引,每个页表项要占用4字节大小,高20位的索引乘以4才是该页表项相对于页物理地址的字节偏移量,用寄存器中的页表物理地址加上此偏移量便是该页表项的物理地址,从寄存器中得到映射的物理页地址,然后用线性的低12位与该物理页地址相加,所得到的地址之和表示最终要访问的物理地址。此时就使用硬件完成地址的转换,使用页部件转换,页部件的工作:用线性地址的高20位在页表中索引页表项,用过线性地址的低12位与页表项中的物理地址相加,所求的和便是最终线性地址对应的物理地址,如图所示:
一级页表的转换过程
由于一级页表如果全部占满的话,会占用4MB的内存大小,一级页表需要提前建好,每个进程都有自己的页表,进程太多的话占用的页表空间也会很大,所以需要不一次性将全部页表项建好,需要时动态创建页表项,解决方法便是二级页表。
二级页表用虚拟地址的高10位在页目录中定位一个页表,然后通过通过21~12位定位页表中的具体物理页,然后低12位就表示页内偏移量,二级页表的流程如图所示:
二级页表内存示意图
二级页表的地址转换过程如下:
1、用虚拟地址的高10位乘以4,作为页目录表内的偏移地址,加上页目录表的物理地址,所得的和,便是页目录项的物理地址,读取该页目录项,从中获取到页表的物理地址;
2、用虚拟地址的中间10位乘以4,作为页表内的偏移地址,加上在第1步中得到的页表物理地址,所得的和,便是页表项的物理地址,读取该页表项,从中获取到分配的物理页地址;
3、虚拟地址的高10位和中间10位分别是PDE(页目录项)和PTE(二级目录索引)的索引值,所以需要乘以4,低12位作为页内偏移,所以虚拟地址的低12位加上第2步中得到的物理地址,便是最终的物理地址。
二级页表的转换过程如图所示:
二级页表虚拟地址

启动分页机制的流程大致分为三个步骤:1、准备好页目录表及页表;2、将页表地址写入控制寄存器cr3;3、寄存器cr0的PG位置1。

内存分页的实现
操作系统页表与用户进程关系

在保护模式下,为了计算机安全,用户进程必须运行在低特权级,当用户进程需要访问硬件相关资源时,需要向操作系统申请,有操作系统去完成,之后将完成结果返回给用户进程,进程可以有很多个,但是操作系统就一个,所以操作系统必须共享给所有用户进程。我们可以将4GB虚拟地址空间分为两部分,一部分专门划给操作系统,另一部分给用户进程使用,像Linux为了实现共享操作系统,让所有用户进程3GB~4GB的虚拟地址空间指向同一个操作系统,也就是所有进程的虚拟地址3GB~4GB本质上都是指向同一片物理地址,虚拟地址空间的0~3GB的是用户进程。

相关的页表的内存布局如图所示:
页目录表与页表的关系

页目录表的位置和页表都存在于物理内存之中,页目录表的位置,可以放置在物理地址0x100000处,可以让页目录表紧挨在页目录表,页目录本身占4KB,所以第一个页表的物理地址是0x101000,物理布局如图:
页目录表与页表内存布局图

相关代码(代码出自操作系统真想还原):


   ; 创建页目录及页表并初始化页内存位图
   call setup_page

   ;要将描述符表地址及偏移量写入内存gdt_ptr,一会用新地址重新加载
   sgdt [gdt_ptr]         ; 存储到原来gdt所有的位置

   ;将gdt描述符中视频段描述符中的段基址+0xc0000000
   mov ebx, [gdt_ptr + 2]  
   or dword [ebx + 0x18 + 4], 0xc0000000      ;视频段是第3个段描述符,每个描述符是8字节,故0x18。
                          ;段描述符的高4字节的最高位是段基址的31~24位

   ;将gdt的基址加上0xc0000000使其成为内核所在的高地址
   add dword [gdt_ptr + 2], 0xc0000000

   add esp, 0xc0000000        ; 将栈指针同样映射到内核地址

   ; 把页目录地址赋给cr3
   mov eax, PAGE_DIR_TABLE_POS
   mov cr3, eax

   ; 打开cr0的pg位(第31位)
   mov eax, cr0
   or eax, 0x80000000
   mov cr0, eax

   ;在开启分页后,用gdt新的地址重新加载
   lgdt [gdt_ptr]             ; 重新加载

   mov byte [gs:160], 'V'     ;视频段段基址已经被更新,用字符v表示virtual addr

   jmp $

;-------------   创建页目录及页表   ---------------
setup_page:
;先把页目录占用的空间逐字节清0
   mov ecx, 4096
   mov esi, 0
.clear_page_dir:
   mov byte [PAGE_DIR_TABLE_POS + esi], 0
   inc esi
   loop .clear_page_dir

;开始创建页目录项(PDE)
.create_pde:                     ; 创建Page Directory Entry
   mov eax, PAGE_DIR_TABLE_POS
   add eax, 0x1000               ; 此时eax为第一个页表的位置及属性
   mov ebx, eax                  ; 此处为ebx赋值,是为.create_pte做准备,ebx为基址。

;   下面将页目录项0和0xc00都存为第一个页表的地址,
;   一个页表可表示4MB内存,这样0xc03fffff以下的地址和0x003fffff以下的地址都指向相同的页表,
;   这是为将地址映射为内核地址做准备
   or eax, PG_US_U | PG_RW_W | PG_P      ; 页目录项的属性RW和P位为1,US为1,表示用户属性,所有特权级别都可以访问.
   mov [PAGE_DIR_TABLE_POS + 0x0], eax       ; 第1个目录项,在页目录表中的第1个目录项写入第一个页表的位置(0x101000)及属性(7)
   mov [PAGE_DIR_TABLE_POS + 0xc00], eax     ; 一个页表项占用4字节,0xc00表示第768个页表占用的目录项,0xc00以上的目录项用于内核空间,
                         ; 也就是页表的0xc0000000~0xffffffff共计1G属于内核,0x0~0xbfffffff共计3G属于用户进程.
   sub eax, 0x1000
   mov [PAGE_DIR_TABLE_POS + 4092], eax      ; 使最后一个目录项指向页目录表自己的地址

;下面创建页表项(PTE)
   mov ecx, 256                  ; 1M低端内存 / 每页大小4k = 256
   mov esi, 0
   mov edx, PG_US_U | PG_RW_W | PG_P         ; 属性为7,US=1,RW=1,P=1
.create_pte:                     ; 创建Page Table Entry
   mov [ebx+esi*4],edx               ; 此时的ebx已经在上面通过eax赋值为0x101000,也就是第一个页表的地址 
   add edx,4096
   inc esi
   loop .create_pte

;创建内核其它页表的PDE
   mov eax, PAGE_DIR_TABLE_POS
   add eax, 0x2000           ; 此时eax为第二个页表的位置
   or eax, PG_US_U | PG_RW_W | PG_P  ; 页目录项的属性US,RW和P位都为1
   mov ebx, PAGE_DIR_TABLE_POS
   mov ecx, 254              ; 范围为第769~1022的所有目录项数量
   mov esi, 769
.create_kernel_pde:
   mov [ebx+esi*4], eax
   inc esi
   add eax, 0x1000
   loop .create_kernel_pde
   ret

至此,内存分页已经完成,并且开启了分页。

中断

中断的原理
中断概述

中断和异常是指明系统、处理器或当前执行程序或任务的某处出现一个事件,该事件需要处理器进行处理,这种事件会导致执行控制被强迫从当前运行程序转移到被称为中断处理程序或异常处理程序的特殊软件函数或任务中,处理器响应中断或异常锁采取的行动被称为中断或异常服务。
对应用程序和操作系统来说,80x86的中断和异常处理机制可以透明地处理发生的中断和异常事件。当收到一个中断或检测到一个异常时,处理器会自动把当前正在处理的程序或任务挂起,并开始运行中断或异常处理程序。当处理程序处理完毕,处理器就会恢复并继续执行被中断的程序或任务。被中断程序的恢复过程并不会失去程序执行的连贯性,除非从异常中恢复是不可能的或者中断导致当前运行程序被终止。
处理器可以从两种地方接受中断:外部(硬件产生)的中断和软件产生的中断。外部中断通过处理器芯片上两个引脚(INTR和NMI)接收。当引脚INTR接收到外部发生的中断信号时,处理器就会从系统总线上读取外部中断控制器(8259A)提供的中断向量号。当引脚NMI接收到信号时,就产生一个非屏蔽中断,它使用固定的中断向量号2。任何通过处理器INTR引脚接受的外部中断都被称为可屏蔽中断,标志寄存器EFLAGS中的IF标志可用来屏蔽所有这些硬件中断。通过在指令操作数中提供中断向量号,int n指令可用于从软件产生中断。处理器接受的异常来源页分为两个:处理器检测到的程序错误异常和软件产生的异常;异常按照导致异常的指令能否被重新执行,异常可分为故障、陷阱和终止,故障是一种通常可以被纠正的异常,并且一旦被纠正程序就可以继续运行,陷阱是指由trap引起陷阱的指令被执行后立刻回报告的异常,中止是一种不会总是报告导致异常的指令的精确位置的异常,并且不允许导致异常的程序重新继续执行。

中断描述符表

在操作系统的初始化过程中,会建立起中断描述符表,中断描述符表将每个异常或中断向量分别与它们的处理过程联系起来,IDT是由8字节描述符组成的一个数组,为了构建IDT表中的一个索引值,处理器把异常或中断的向量号乘以8,IDT最多包含256个中断或异常向量。IDT表可以驻留在线性地址空间的任何地方,处理器使用IDTR寄存器来定位IDT表的位置,这个寄存器中含IDT表32位的基地址和16位的长度值,如图所示:
中断描述符表IDT和寄存器IDTR
LIDT和SIDT指令分别用于加载和保存IDTR寄存器的内容,LIDT指令用于把内存中的限长值和基地址操作数加载到IDTR寄存器中,该指令仅能由当前特权级CPL是0的代码执行,通常被用于创建IDT时的操作系统初始化代码中,SIDT指令用于把IDTR中的基地址和限长内容复制到内存中。
IDT表中可用存放3中类型的门描述符:中断门描述符、陷阱门描述符、任务门描述符。中断门和陷阱门含有一个长指针(段选择符和偏移值),处理器使用这个长指针把程序执行权限转移到代码段中异常或中断的处理过程中,这两个段的主要区别在于处理器操作EFLAGS寄存器IF标志上,IDT中任务门描述符的格式与GDT和LDT中任务门的格式相同。任务门描述符中含有一个任务TSS段的选择符,该任务用于处理异常或中断。相关描述符格式如下:
中断门、陷阱门和任务门格式

中断的响应过程

处理器对异常和中断处理过程的调用操作方法与使用CALL指令调用程序过程和任务的方法类似。当响应一个异常或中断时,处理器使用异常或中断向量作为IDT表中的索引,如果索引值指向中断门或陷阱门,则处理器使用与CALL指令操作调用门类似的方法调用异常或中断处理过程。如果索引值指向任务门,则处理器使用与CALL指令操作任务门类似的方法进行任务切换,执行异常或中断的处理任务。如图所示:
中断过程调用

中断的实现过程

在使用80x86组成的微机系统中采用了8259A可编程中断控制器芯片,每个8259A可以管理8个中断源,通过多片级联方式,8259A能构成最多管理64个中断向量的系统,在x86PC机中,使用了两片8259A芯片,共可管理15级中断向量。级联如图所示:
PC级联8259控制系统
8259A详细的编程过程请参考操作系统真想还原相关代码。

Linux分页与中断的初始化过程

位于setup.S汇编文件中,执行了中断的程序的编写,head.S的文件移动,在执行完成相关初始化后,跳转到head.S代码开头处执行代码。
相关setup.S的文件执行流程如下(去除了有关显卡的操作):

INITSEG  = DEF_INITSEG  ! we move boot here - out of the way
SYSSEG   = DEF_SYSSEG   ! system loaded at 0x10000 (65536).
SETUPSEG = DEF_SETUPSEG ! this is the current segment

.globl begtext, begdata, begbss, endtext, enddata, endbss
.text
begtext:
.data
begdata:
.bss
begbss:
.text

entry start
start:

! ok, the read went well so we get current cursor position and save it for
! posterity.

    mov ax,#INITSEG ! this is done in bootsect already, but...  重新设置ds地址
    mov ds,ax

! Get memory size (extended mem, kB)

    mov ah,#0x88            ! 利用bios中断0x15功能号ah=0x88获取系统所含内存大小
    int 0x15
    mov [2],ax              ! 将扩展内存数值存在0x90002处

! check for EGA/VGA and some config parameters

    mov ah,#0x12            ! 检查获取显卡相关信息 
    mov bl,#0x10
    int 0x10
    mov [8],ax
    mov [10],bx
    mov [12],cx
    mov ax,#0x5019
    cmp bl,#0x10
    je  novga
    call    chsvga
novga:  mov [14],ax
    mov ah,#0x03    ! read cursor pos
    xor bh,bh
    int 0x10        ! save it in known place, con_init fetches
    mov [0],dx      ! it from 0x90000.

! Get video-card data:   获取显卡当前显示模式

    mov ah,#0x0f
    int 0x10
    mov [4],bx      ! bh = display page
    mov [6],ax      ! al = video mode, ah = window width

! Get hd0 data
! 获取第一个硬盘信息(赋值硬盘参数表)
    mov ax,#0x0000
    mov ds,ax
    lds si,[4*0x41]   ! 取中断向量0x41的值
    mov ax,#INITSEG
    mov es,ax
    mov di,#0x0080    !传输的目的地址:0x9000:0x0080处
    mov cx,#0x10      !共传输16个字节
    rep
    movsb

! Get hd1 data
! 获取第二个硬盘信息(赋值硬盘参数表)
    mov ax,#0x0000
    mov ds,ax
    lds si,[4*0x46]   !取中断向量0x46的值,
    mov ax,#INITSEG
    mov es,ax
    mov di,#0x0090    !传输的目的地址:0x9000:0x0090
    mov cx,#0x10
    rep
    movsb

! Check that there IS a hd1 :-)   ! 检查系统是否有第2个硬盘,如果没有则把第2个表清零

    mov ax,#0x01500               ! ah功能号为ah=0x15
    mov dl,#0x81                  !  输入驱动器号 0x81代表第二个硬盘
    int 0x13                      ! bios系统调用0x13
    jc  no_disk1                  ! 如果cf为有进位的情况下则是没有该盘
    cmp ah,#3                     !如果是硬盘3
    je  is_disk1    
no_disk1:
    mov ax,#INITSEG               !如果没有第2个硬盘则对第二个硬盘参数清零
    mov es,ax
    mov di,#0x0090
    mov cx,#0x10
    mov ax,#0x00
    rep
    stosb
is_disk1:

! now we want to move to protected mode ...  进入保护模式

    cli         ! no interrupts allowed !   关闭中断

! first we move the system to it's rightful place
!把system模块移动到0x00000位置,即把从0x100000x8ffff的内存块整块地移动了0x10000
    mov ax,#0x0000
    cld         ! 'direction'=0, movs moves forward
do_move:
    mov es,ax       ! destination segment  目的的起始位置 0x0:0x0
    add ax,#0x1000   ! 加上当前的位置
    cmp ax,#0x9000   !是否把最后一段程序移动完成,
    jz  end_move     !如果移动完成则跳转
    mov ds,ax       ! source segment  
    sub di,di
    sub si,si
    mov     cx,#0x8000   ! 移动0x8000字节
    rep
    movsw
    jmp do_move

! then we load the segment descriptors

end_move:
    mov ax,#SETUPSEG    ! right, forgot this at first. didn't work :-)
    mov ds,ax
    lidt    idt_48      ! load idt with 0,0   加载中断
    lgdt    gdt_48      ! load gdt with whatever appropriate  加载全局描述符

! that was painless, now we enable A20

    call    empty_8042   ! 测试8042状态寄存器,等待输入缓冲器空
    mov al,#0xD1        ! command write
    out #0x64,al          ! 8042的P2端口,用于A20线的选通
    call    empty_8042    !等待缓存区为空
    mov al,#0xDF        ! A20 on  打开A20地址线的参数
    out #0x60,al          ! 数据写到0x60口
    call    empty_8042    ! 若此时缓冲器为空,则A20线已经打开

! well, that went ok, I hope. Now we have to reprogram the interrupts :-(
! we put them right after the intel-reserved hardware interrupts, at
! int 0x20-0x2F. There they won't mess up anything. Sadly IBM really
! messed this up with the original PC, and they haven't been able to
! rectify it afterwards. Thus the bios puts interrupts at 0x08-0x0f,
! which is used for the internal hardware interrupts as well. We just
! have to reprogram the 8259's, and it isn't fun.
! 8259芯片主片端口是0x20-0x21,从片端口是0xA0_0xA1,输出值0x11表示初始化命令开始
    mov al,#0x11        ! initialization sequence
    out #0x20,al        ! send it to 8259A-1  !发送到8259A主片
    .word   0x00eb,0x00eb       ! jmp $+2, jmp $+2  
    out #0xA0,al        ! and to 8259A-2      ! 再发送到8259A从片
    .word   0x00eb,0x00eb
    mov al,#0x20        ! start of hardware int's (0x20)
    out #0x21,al        ! 送主芯片ICW2命令字,设置起始中断号,
    .word   0x00eb,0x00eb
    mov al,#0x28        ! start of hardware int's 2 (0x28)
    out #0xA1,al        !送从芯片ICW2命令字,从芯片的其实中断号
    .word   0x00eb,0x00eb
    mov al,#0x04        ! 8259-1 is master
    out #0x21,al        !送主芯片ICW3命令字,主芯片的IR2连从芯片INT
    .word   0x00eb,0x00eb
    mov al,#0x02        ! 8259-2 is slave
    out #0xA1,al        ! 送从芯片ICW3命令字,表示从芯片的INT连到芯片的IR2引脚上
    .word   0x00eb,0x00eb
    mov al,#0x01        ! 8086 mode for both
    out #0x21,al        ! 送主芯片ICW4命令字,8086模式,普通EOI、非缓存方式
    .word   0x00eb,0x00eb
    out #0xA1,al        ! 送从芯片ICW4命令字
    .word   0x00eb,0x00eb
    mov al,#0xFF        ! mask off all interrupts for now
    out #0x21,al        ! 屏蔽主芯片所有中断请求
    .word   0x00eb,0x00eb
    out #0xA1,al        ! 屏蔽从芯片所有中断请求

! well, that certainly wasn't fun :-(. Hopefully it works, and we don't
! need no steenking BIOS anyway (except for the initial loading :-).
! The BIOS-routine wants lots of unnecessary data, and it's less
! "interesting" anyway. This is how REAL programmers do it.
!
! Well, now's the time to actually move into protected mode. To make
! things as simple as possible, we do no register set-up or anything,
! we let the gnu-compiled 32-bit programs do that. We just jump to
! absolute address 0x00000, in 32-bit protected mode.

    mov ax,#0x0001  ! protected mode (PE) bit  进入保护模式
    lmsw    ax      ! This is it!   
    jmpi    0,8     ! jmp offset 0 of segment 8 (cs)  跳转到cs段偏移0

此时执行head.S文件时,首先加载各个数据段寄存器,重新设置中断描述符表idt,并使各个表项均指向一个只报错误的中断子程序ignore_int,相关代码如下:

.text
.globl _idt,_gdt,_pg_dir,_tmp_floppy_area
_pg_dir:
startup_32:
    movl $0x10,%eax
    mov %ax,%ds
    mov %ax,%es
    mov %ax,%fs
    mov %ax,%gs
    lss _stack_start,%esp   # 设置系统堆栈
    call setup_idt          # 调用设置中断描述符的子程序
    call setup_gdt          # 调用设置全局描述符的子程序
    movl $0x10,%eax        # reload all the segment registers
    mov %ax,%ds     # after changing gdt. CS was already
    mov %ax,%es     # reloaded in 'setup_gdt'
    mov %ax,%fs             # 因为修改了gdt,所有需要重新装载段寄存器
    mov %ax,%gs
    lss _stack_start,%esp   #加载所有段寄存器
    xorl %eax,%eax
1:  incl %eax       # check that A20 really IS enabled
    movl %eax,0x000000  # loop forever if it isn't
    cmpl %eax,0x100000
    je 1b
/*
 * NOTE! 486 should set bit 16, to check for write-protect in supervisor
 * mode. Then it would be unnecessary with the "verify_area()"-calls.
 * 486 users probably want to set the NE (#5) bit also, so as to use
 * int 16 for math errors.
 */
    movl %cr0,%eax      # check math chip
    andl $0x80000011,%eax  # Save PG,PE,ET
/* "orl $0x10020,%eax" here for 486 might be good */
    orl $2,%eax        # set MP
    movl %eax,%cr0
    call check_x87
    jmp after_page_tables     # 跳转建立页表

/*
 * We depend on ET to be correct. This checks for 287/387.
 */
check_x87:
    fninit              # 初始化协处理器
    fstsw %ax           # 取协处理器状态字到ax寄存器
    cmpb $0,%al
    je 1f           /* no coprocessor: have to set bits */
    movl %cr0,%eax
    xorl $6,%eax       /* reset MP, set EM */
    movl %eax,%cr0
    ret
.align 2
1:  .byte 0xDB,0xE4     /* fsetpm for 287, ignored by 387 */
    ret

/*
 *  setup_idt
 *
 *  sets up a idt with 256 entries pointing to
 *  ignore_int, interrupt gates. It then loads
 *  idt. Everything that wants to install itself
 *  in the idt-table may do so themselves. Interrupts
 *  are enabled elsewhere, when we can be relatively
 *  sure everything is ok. This routine will be over-
 *  written by the page tables.
 */
setup_idt:
    lea ignore_int,%edx         # 将ignore_int的有效地址存入edx
    movl $0x00080000,%eax      # 将选择符0x0008植入eax的高16位
    movw %dx,%ax        /* selector = 0x0008 = cs */
    movw $0x8E00,%dx   /* interrupt gate - dpl=0, present */

    lea _idt,%edi               # _idt是中断描述符表的地址
    mov $256,%ecx              # 重复次数256次
rp_sidt: 
    movl %eax,(%edi)            # 将中断门描述符存入表中
    movl %edx,4(%edi)           # eax内容放到edi+4所指内存处
    addl $8,%edi               # edi指向表中下一项
    dec %ecx                    # 重复执行256次
    jne rp_sidt
    lidt idt_descr              # 加载中断描述符表寄存器值
    ret

/*
 *  setup_gdt
 *
 *  This routines sets up a new gdt and loads it.
 *  Only two entries are currently built, the same
 *  ones that were built in init.s. The routine
 *  is VERY complicated at two whole lines, so this
 *  rather long comment is certainly needed :-).
 *  This routine will beoverwritten by the page tables.
 */
setup_gdt:
    lgdt gdt_descr          # 加载全局描述符表寄存器(内容已经设置好)
    ret

/*
 * I put the kernel page tables right after the page directory,
 * using 4 of them to span 16 Mb of physical memory. People with
 * more than 16MB will have to expand this.
 */
.org 0x1000
pg0:

.org 0x2000
pg1:

.org 0x3000
pg2:

.org 0x4000
pg3:

.org 0x5000
/*
 * tmp_floppy_area is used by the floppy-driver when DMA cannot
 * reach to a buffer-block. It needs to be aligned, so that it isn't
 * on a 64kB border.
 */
_tmp_floppy_area:
    .fill 1024,1,0

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   # 压入_main函数
    jmp setup_paging   # 跳转加载页表
L6:
    jmp L6          # main should never return here, but
                # just in case, we know what happens.

/* This is the default interrupt "handler" :-) */
int_msg:
    .asciz "Unknown interrupt\n\r"
.align 2
ignore_int:   # 现在设置的默认中断处理函数
    pushl %eax
    pushl %ecx
    pushl %edx
    push %ds
    push %es
    push %fs
    movl $0x10,%eax
    mov %ax,%ds
    mov %ax,%es
    mov %ax,%fs
    pushl $int_msg
    call _printk
    popl %eax
    pop %fs
    pop %es
    pop %ds
    popl %edx
    popl %ecx
    popl %eax
    iret


/*
 * Setup_paging
 *
 * This routine sets up paging by setting the page bit
 * in cr0. The page tables are set up, identity-mapping
 * the first 16MB. The pager assumes that no illegal
 * addresses are produced (ie >4Mb on a 4Mb machine).
 *
 * NOTE! Although all physical memory should be identity
 * mapped by this routine, only the kernel page functions
 * use the >1Mb addresses directly. All "normal" functions
 * use just the lower 1Mb, or the local data space, which
 * will be mapped to some other place - mm keeps track of
 * that.
 *
 * For those with more memory than 16 Mb - tough luck. I've
 * not got it, why should you :-) The source is here. Change
 * it. (Seriously - it shouldn't be too difficult. Mostly
 * change some constants etc. I left it at 16Mb, as my machine
 * even cannot be extended past that (ok, but it was cheap :-)
 * I've tried to show which constants to change by having
 * some kind of marker at them (search for "16Mb"), but I
 * won't guarantee that's all :-( )
 */
.align 2                    # 按4字节对齐
setup_paging:
    movl $1024*5,%ecx      /* 5 pages - pg_dir+4 page tables */ #对1页目录和4页页表清零
    xorl %eax,%eax
    xorl %edi,%edi          /* pg_dir is at 0x000 */  #页目录从0x000处开始
    cld;rep;stosl                                    # eax内容存到es:edi所指内存位置处
    movl $pg0+7,_pg_dir        /* set present bit/user r/w */
    movl $pg1+7,_pg_dir+4      /*  --------- " " --------- */
    movl $pg2+7,_pg_dir+8      /*  --------- " " --------- */
    movl $pg3+7,_pg_dir+12     /*  --------- " " --------- */
    movl $pg3+4092,%edi        # edi最后一页的最后一项
    movl $0xfff007,%eax        /*  16Mb - 4096 + 7 (r/w user,p) */
    std
1:  stosl           /* fill pages backwards - more efficient :-) */
    subl $0x1000,%eax          # 每填好一项物理地址减0x1000
    jge 1b                      # 如果小于0则全部填好了
    xorl %eax,%eax      /* pg_dir is at 0x0000 */  # 页目录表在0x0000
    movl %eax,%cr3      /* cr3 - page directory start */  #设置启动分页机制
    movl %cr0,%eax
    orl $0x80000000,%eax
    movl %eax,%cr0      /* set paging (PG) bit */
    ret         /* this also flushes prefetch-queue */

当重新设置好中断描述符合全局描述符后,启动了cpu的分页机制,当分页设置完成后就跳转到main函数处执行了调用位于init/main.c中的main函数。

猜你喜欢

转载自blog.csdn.net/qq_33339479/article/details/80423291
今日推荐