<ul id="qxxfc"><fieldset id="qxxfc"><tr id="qxxfc"></tr></fieldset></ul>


      前言

      上一篇其實已經(jīng)說完了boot的大致工作,但是Linux在最后進入操作系統(tǒng)之前還有一些操作,比如進入保護模式。在我自己的FragileOS
      <https://github.com/dejavudwh/FragileOS>里進入保護模式是在引導(dǎo)程序結(jié)束后完成的。

      實模式到保護模式屬于操作系統(tǒng)的一個大坎,所以需要先提一下

      從實模式到保護模式

      實模式和保護模式都是CPU的工作模式,它們的主要區(qū)別就是尋址方式


      實模式出現(xiàn)于早期8088CPU時期。當(dāng)時由于CPU的性能有限,一共只有20位地址線(所以地址空間只有1MB),以及8個16位的通用寄存器,以及4個16位的段寄存器。所以為了能夠通過這些16位的寄存器去構(gòu)成20位的主存地址,必須采取一種特殊的方式。訪問內(nèi)存的就變成了:

        物理地址 = 段基址 << 4 + 段內(nèi)偏移


      隨著CPU的發(fā)展,可以訪問的內(nèi)存空間也從1MB變?yōu)楝F(xiàn)在4GB,寄存器的位數(shù)也變?yōu)?2位。并且在實模式下,用戶程序?qū)?nèi)存的訪問非常自由,沒有任何限制,隨隨便便就可以修改任何一個內(nèi)存單元。所以實模式已經(jīng)不能滿足時代的要求了,保護模式就應(yīng)運而生了

      保護模式的偏移值變成了32位,尋址方式仍然需要段寄存器,但是這些段寄存器存放的不再是段基址了,而是類似一個數(shù)組的索引

      而這個數(shù)組就是一個就做全局描述符表 (GDT)的東西,GDT中含有一個個表項,每一個表項稱為段描述符。

      而我們通過段寄存器里的的這個索引,可以找到對應(yīng)的表項。段描述符存放了段基址、段界限、內(nèi)存段類型屬性

      處理器內(nèi)部有一個 48 位的寄存器,稱為全局描述符表寄存器(GDTR)。也就是為了來記錄GDT的



      段描述符

      FragileOS里進入保護模式

      * 根據(jù)上面的描述,在進入保護模式時就先需要構(gòu)造一個GDT
      * 當(dāng)然中間還需要一些其它的初始化,在后面詳細提
      * 然后再根據(jù)特定操作來讓CPU識別該進入保護模式了
      一部分代碼
      [SECTION .gdt] ; 利用宏定義定義gdt ; 段基址 段界限 屬性 LABEL_GDT: Descriptor 0, 0, 0
      LABEL_DESC_CODE32: Descriptor 0, 0fffffh, DA_C | DA_32 | DA_LIMIT_4K
      LABEL_DESC_VIDEO: Descriptor 0B8000h, 0fffffh, DA_DRW LABEL_DESC_VRAM:
      Descriptor 0, 0fffffh, DA_DRW | DA_LIMIT_4K in al, 92h ; 切換到保護模式 or al,
      00000010b out 92h, al mov eax, cr0 or eax , 1 mov cr0, eax
      Linux啟動前的最后準(zhǔn)備

      現(xiàn)在來看看Linux在啟動前最后還做了什么

      獲得系統(tǒng)數(shù)據(jù)和進入保護模式

      setup.s主要的任務(wù)就是從BIOS拿到系統(tǒng)數(shù)據(jù)然后存放到一個內(nèi)存位置

      獲取當(dāng)前光標(biāo)的位置
      mov ax,#INITSEG ! this is done in bootsect already, but... mov ds,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.
      獲取內(nèi)存大小
      mov ah,#0x88 int 0x15 mov [2],ax
      檢查現(xiàn)在的顯示方式
      mov ah,#0x0f int 0x10 mov [4],bx ! bh = display page mov [6],ax ! al = video
      mode, ah = window width
      進入保護模式

      進入保護模式的代碼也在setup中

      首先先把內(nèi)核SYSTEM部分移動到0位置,在之前它是被讀入在0x10000位置
      mov ax,#0x0000 cld ! 'direction'=0, movs moves forward do_move: mov es,ax !
      destination segment add ax,#0x1000 cmp ax,#0x9000 jz end_move mov ds,ax !
      source segment sub di,di sub si,si mov cx,#0x8000 rep movsw jmp do_move
      然后就是加載上面說的全局描述符表和中斷向量表

      中斷向量表前面沒有提過,但是比較簡單,有點類似GDT,就是 操作系統(tǒng)必須維護一份中斷向量表,每一個表項紀(jì)錄一個中斷處理程序(ISR,Interrupt
      Service Routine)的地址
      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
      再接著就是打開A20地址線,如果不打開A20地址線,即使在保護模式下最大尋址還是1M
      call empty_8042 mov al,#0xD1 ! command write out #0x64,al call empty_8042 mov
      al,#0xDF ! A20 on out #0x60,al call empty_8042
      初始化8259A芯片,
      8259A是專門為了對8085A和8086/8088進行中斷控制而設(shè)計的芯片,它是可以用程序控制的中斷控制器。單個的8259A能管理8級向量優(yōu)先級中斷。
      對于對硬件的初始化其實就是依照CPU的固定套路

      部分代碼
      mov al,#0x11 ! initialization sequence out #0x20,al ! send it to 8259A-1 .word
      0x00eb,0x00eb ! jmp $+2, jmp $+2 out #0xA0,al ! and to 8259A-2
      最后的最后,終于可以正式進入保護模式,可以看到這里進入保護模式的方法和我上面的move cr0
      ax不太一樣,Linux之所以使用這種方法是為了兼容286之前的CPU,另外需要注意的是在進入保護模式之后需要立馬執(zhí)行一條段間跳轉(zhuǎn)來讓CPU刷新指令隊列
      ,這里跳轉(zhuǎn)的描述就已經(jīng)是用段值來描述了,段指的第三位到第十五位用來指向GDT里的索引(1000),也就是跳到第2個段描述符里記錄的地址
      mov ax,#0x0001 ! protected mode (PE) bit lmsw ax ! This is it! jmpi 0,8 ! jmp
      offset 0 of segment 8 (cs)
      第二個GTD段描述符,所以上面也就是跳轉(zhuǎn)到內(nèi)存0處
      .word 0x07FF ! 8Mb - limit=2047 (2048*4096=8Mb) .word 0x0000 ! base address=0
      .word 0x9A00 ! code read/exec .word 0x00C0 ! granularity=4096, 386
      IDT和分頁管理機制

      再往下就是正式進入到了內(nèi)核部分,在此之前需要再提一下IDT和分頁管理機制

      IDT


      中斷描述符表把每個中斷或異常編號和一個指向中斷處理事件服務(wù)程序的描述符聯(lián)系起來。同GDT和LDT一樣,IDT是一個8-字節(jié)的描述符數(shù)組。和GDT、LDT不同的是,IDT的第一項可以包含一個描述符。為了形成一個在IDT內(nèi)的索引,處理器把中斷、異常標(biāo)識號乘以8以后來做為IDT的索引。因為只有256個編號,IDT不必包含超過256個描述符。它可以包含比256更少的項,只是那些需要使用的中斷、異常的項。

      IDT可以在內(nèi)存的任意位置。處理器通過IDT寄存器(IDTR)來定位IDT。指令LIDT和SIDT用來操作IDTR。



      分頁機制

      將用戶程序(進程)的邏輯地址空間分成若干個頁(4KB)并編號,同時將內(nèi)存的物理地址也分成若干個塊或頁框
      4KB)并編號,這樣也就是為了讓所有的應(yīng)用程序看都像是獨占一片內(nèi)存,起始地址都是為0,最后再建立一個頁表存儲著頁到頁框也就是真實內(nèi)存地址的映射



      在內(nèi)存里有一個寄存器(PTR)來存儲頁表

      映射的完成

      * 進程訪問某個邏輯地址
      * 由線性地址的頁號,以及頁表寄存器中的始址,找到頁表并找到對應(yīng)的頁表項
      * 由頁表項上的塊號,找到物理內(nèi)存中的塊號
      * 根據(jù)塊號,和線性地址的頁內(nèi)地址,找到物理地址
      我們通過設(shè)置CR0寄存器的PG位來開啟分頁功能,而其它操作就都由CPU來完成,當(dāng)然前提是我們有一張頁表

      兩級頁表結(jié)構(gòu)

      為了減少內(nèi)存的占用量,80X86采用了分級頁表

      頁目錄有2的十次方個4字節(jié)的表項,這些表項指向?qū)?yīng)的二級表,線性地址的最高10位作為頁目錄用來尋找二級表的索引

      二級頁表里的表項含有相關(guān)頁面的20位物理基地址,二級頁表使用線性地址中間10位來作為尋找表項的索引

      * 進程訪問某個邏輯地址
      * 由線性地址中的頁號,以及外層頁表寄存器(CR3)中的外層頁表始址,找到二級頁表的始址
      * 由二級頁表的始址,加上線性地址中的外層頁內(nèi)地址,找到對應(yīng)的二級頁表中的頁表項
      * 由頁表項中的物理塊號,加上線性地址中的頁內(nèi)地址,找到對物理地址


      所以說CPU尋址一共需要進行兩步:

      * 首先將給定一個邏輯地址 (其實是段內(nèi)偏移量)
      * CPU利用段式內(nèi)存管理單元,先將為個邏輯地址轉(zhuǎn)換成一個線程地址 (也就是前面說的GDT)
      * 再利用其頁式內(nèi)存管理單元,轉(zhuǎn)換為最終物理地址。(二級頁表)
      進入到了內(nèi)核部分


      head.s這部分其實已經(jīng)是進入了內(nèi)核部分了,但是在Linux0.12里還是把它歸為Boot部分。這一部分的主要工作是重新設(shè)置GDT和IDT,然后在設(shè)置管理內(nèi)存的分頁處理機制
      (在進入保護模式后,Linux用的就是AT&T的匯編語法了,最顯著的差別就是源操作數(shù)和目的數(shù)的位置對調(diào)了)

      * 設(shè)置IDT setup_idt: lea ignore_int,%edx movl $0x00080000,%eax movw %dx,%ax /*
      selector = 0x0008 = cs */ movw $0x8E00,%dx /* interrupt gate - dpl=0, present
      */ lea idt,%edi mov $256,%ecx rp_sidt: movl %eax,(%edi) movl %edx,4(%edi) addl
      $8,%edi dec %ecx jne rp_sidt lidt idt_descr ret
      * 設(shè)置GDT setup_gdt: lgdt gdt_descr ret gdt_descr: .word 256*8-1 # so does gdt
      (not that that's any .long gdt # magic number, but it works for me :^) .align 8
      * 這里就是已經(jīng)準(zhǔn)備跳入C語言的main部分了,也就是匯編里的函數(shù)調(diào)用,先把main的地址壓入棧中,當(dāng)下一個函數(shù)執(zhí)行完ret的時候,就會去執(zhí)行main了
      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.
      * 最后就是設(shè)置分頁機制了
      STOS指令:將AL/AX/EAX的值存儲到[EDI]指定的內(nèi)存單元

      CLD清除方向標(biāo)志和STD設(shè)置方向標(biāo)志,當(dāng)方向標(biāo)志是0,該指令通過遞增的指針數(shù)據(jù)每一次迭代之后(直到ECX是零或一些其它條件,這取決于REP前綴的香味)工作,而如果該標(biāo)志是1,指針遞減。
      setup_paging: movl $1024*5,%ecx /* 5 pages - pg_dir+4 page tables */ xorl
      %eax,%eax xorl %edi,%edi /* pg_dir is at 0x000 */ cld;rep;stosl 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 movl
      $0xfff007,%eax /* 16Mb - 4096 + 7 (r/w user,p) */ std 1: stosl /* fill pages
      backwards - more efficient :-) */ subl $0x1000,%eax jge 1b xorl %eax,%eax /*
      pg_dir is at 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 */
      小結(jié)

      這一節(jié)主要是描述了保護模式和一些CPU需要的數(shù)據(jù)結(jié)構(gòu)。這幾篇文章相當(dāng)于講述了一臺計算機啟動的時候都發(fā)生了什么。

      * 通過引導(dǎo)程序boot來加載真正的內(nèi)核代碼
      * 獲得一些硬件上的系統(tǒng)參數(shù)保存在一些內(nèi)存里供后面使用
      * 最后是初始化像GDT、IDT等,然后設(shè)置分頁等等

      友情鏈接
      ioDraw流程圖
      API參考文檔
      OK工具箱
      云服務(wù)器優(yōu)惠
      阿里云優(yōu)惠券
      騰訊云優(yōu)惠券
      京東云優(yōu)惠券
      站點信息
      問題反饋
      郵箱:[email protected]
      QQ群:637538335
      關(guān)注微信

        <ul id="qxxfc"><fieldset id="qxxfc"><tr id="qxxfc"></tr></fieldset></ul>
          色爱激情网 | 精品国产AⅤ一区二区三区四川人 | 蜜桃在线无码 | 高清无码免费视频 | 免费插逼视频 | 18禁黄| 夫妻生活一级片 | 国产三级电影在线观看 | www.国产视频在线观看 | 18禁成人无套内射无码免费 |