閱讀本文章需要的基礎:
計算機組成原理:針對8086,80386CPU架構的計算機硬件體系要有清楚的認知,我們都知道操作系統是用來管理硬件的,那我們就要對本版本的操作系統所依賴的硬件體系有系統的了解,有了系統的了解後才能全面的管理它,我們對8086,80386CPU架構的計算機硬件體系如果有非常深刻的認識,我們看源代碼內核的時候,就可以更可能的以一種開發者的角度去思考代碼的作用,先從全局的角度去思考問題,而不是采用一種眾人摸象的思維從頭看到末尾。回顧之前bootsect.s和setup.s建立起來的系統環境:
setup.s設置了局部全局描述符表(GDT):setup.s程序結束後內存中程序示意圖:
setup.s最後一句轉移指令解析:
jmpi 0,8 #將0h加載到EIP段內偏移地址寄存器,將8h加載到CS段選擇子中。#對照上表,我們來分析一下現在存在CS段寄存器的描述符高速緩存寄存器的那一段二進制數據代表了什麼:
(0C00 9A00 0000 07FF)h=(0000 1100 0000 0000 1001 1010 0000 0000 0000 0000 0000 0000 0000 0111 1111 1111)2
我們在linus寫的GDT表項中知道,當CS段選擇子選擇08h編號的全局描述符表項的時候,系統自動完成一系列操作。
此過程對程序員不可見,程序員可以在內存中通過設置GDT類型的數據結構,然後讓GDTR指向這個數據結構。
就可以實現系統保護模式下的段選擇操作。本次08h表示:
段限長為:8M,段基址為:00000000h,該段的特權級為:00(內核級),本段為代碼段:可以執行。
至於為什麼段限長為8M(0h—90000h),我覺得是要保護地址區為90000h以上的區域。
因為在90000h以上的區域,在setup.s代碼段的時候在那裡構造了一系列的數據結構。
系統硬件在CS代碼段寄存器中的描述符高速緩存寄存器中去出段基址和EIP中的0h相加,得到真正的線性地址。
# 如果系統開啟了分頁機制的話,線性地址在通過分頁機制映射出真正的內存物理地址。
head.s執行完之後,內存的分配情況,注意看head.s被數據結構填充完畢後的內存分配,最終head.s執行完就是這樣的:
head.s開始執行,進入正文:
/******************************************head.s代碼段的總體執行步驟***********************************************/
/***********************************startup_32:初始化各個擴展數據段寄存器******************************************/
/******************************************_stack_start:設置系統堆棧**************************************************/
/************************************setup_idt:重新設置中斷描述符表IDT*********************************************/
/************************************setup_gdt:重新設置全局描述符表GDT *******************************************/
/***************************************1:檢測a20地址線是否真的開啟*************************************************/
/*****************************************設置管理內存分頁的處理機制 ***********************************************/
/***********************check_x87:檢測現在的計算機硬件體系是否含有數學協處理器*******************************/
/***********************after_page_tables:將頁目錄表和頁表放置在內存地址0處占位********************************/
/********************************ignore_int:這就是那個統一的中斷服務程序*******************************************/
/*****************************Setup_paging:開始設置頁目錄表和頁表的值********************************************/
/****************利用ret指令彈出預先壓入的/init/main.c程序的入口地址,去運行main.c程序************************/
/******************************************頁目錄表和頁表的占位位置************************************************/
/*************************************************head.s END*********************************************************/
/*
* linux/boot/head.s
*
* (C) 1991 Linus Torvalds
*/
/*
*linux內核源代碼的結構是樹形的,本段head.s匯編程序代碼存放在 linux/boot目錄中
*
* 原著Linus Torvalds於1991年發表
*/
/*
* head.s contains the 32-bit startup code.
*
* NOTE!!! Startup happens at absolute address 0x00000000, which is also where
* the page directory will exist. The startup code will be overwritten by
* the page directory.
*/
/*
* head.s 含有32 位啟動代碼。(前面說到setup.s開啟了A20地址線後,系統地址線擴展到了32根,分段機制開啟後,進入32位保護模式,使用32位GNU as匯編。)
* 注意!!! 32 位啟動代碼是從絕對地址0x00000000 開始的,這裡也同樣是頁目錄將存在的地方,
* 因此這裡的啟動代碼將被頁目錄覆蓋掉。(這也是head,s代碼段的難點,head.s代碼段在執行的時候,邊執行,邊創建數據結構覆蓋掉自己的代碼段。)
*/
.text #偽代碼,告訴編譯器,後續編譯出來的內容放在代碼段(程序從這裡執行)。
.globl _idt,_gdt,_pg_dir,_tmp_floppy_area //偽代碼,告訴編譯器後續跟的是一個全局可見的標記(可能是變量名,也可以是過程段名)。
_pg_dir: #用來標識內核分頁機制完成後的內核起始位置,也就是物理內存的起始位置0X00000000h
/***********************************startup_32:初始化各個擴展數據段寄存器******************************************/
startup_32: #32 位啟動代碼從這裡正式開始
movl $0x10,%eax #將setup.s程序設置的全局描述符表GDT裡面的第10h描述符加載到EAX寄存器
#我們把第10h全局描述符從高位到低位拿出來,然後去對照存儲器的段描述符格式(圖上),解析出其中的描述和基地址。.
#(00C0 9200 0000 07FF)h=(0000 0000 1100 0000 1001 0010 0000 0000 0000 0000 0000 0000 0000 0111 1111 1111)2
#對照完上圖後發現:段限長為:8M,段基址為:00000000h,該段的特權級為:00(內核級),本段為數據段:可讀。
mov %ax,%ds #將EAX的值加載到EDS段選擇子中(EAX的低位為AX)
mov %ax,%es #將EAX的值加載到EES段選擇子中(EAX的低位為AX)
mov %ax,%fs #將EAX的值加載到EFS段選擇子中(EAX的低位為AX)
mov %ax,%gs #將EAX的值加載到EGS段選擇子中(EAX的低位為AX)
#我們來看下80386CPU體系架構的計算機硬件體系裡面,寄存器是上面樣的布局,我們先看下通用寄存器,再來看下段寄存器。
#注意實模式寄存器的位數和保護模式寄存器的位數差別,擴展的位數有多少,之前實模式寄存器在擴展寄存器中的位置。
/******************************************_stack_start:設置系統堆棧**************************************************/
lss _stack_start,%esp #將_stack_start結構體對象裡面的屬性在物理內存的高字加載到ESP寄存器中,將低字加載到SS段寄存器中。
# stack_start 定義在kernel/sched.c,69 行。
/*kernel/sched.c設置堆棧ss:esp代碼段
*long user_stack[PAGE_SIZE >> 2]; #定義系統堆棧,物理長度4K,一共1024項。
* #我們有必要說一下這個系統堆棧在內存的位置,它的位置緊跟操作系統內核SYSTEM代碼後面。
*struct #該結構用於設置堆棧ss:esp
*{
* long *a; #長整型指針,在32位的保護模式下,它就是32位數據大小。
*short b; #短整型
*}
*stack_start =
*&user_stack[PAGE_SIZE >> 2], 0x10}; #定義系統堆棧結構體對象,第一個屬性值是user_stack系統堆棧數組的最後一項的地址,第二個屬性自己想。
*/
call setup_idt # 調用設置中斷描述符表子過程
call setup_gdt # 調用設置全局描述符表子過程
movl $0x10,%eax #因為上一步更新了全局描述符表,全新的全局描述符每個項的段限長都發生了變化
#段限長從8MB(90000h,之前使用段機制的時候怕把90000h處的臨時數據結構沖掉)
#變成了16MB,所以我們要重新的加載數據段寄存器的值。
mov %ax,%ds #將AX通用寄存器的值加載到DS段選擇子中(EAX的低位為AX)
mov %ax,%es #將AX通用寄存器的值加載到ES段選擇子中(EAX的低位為AX)
mov %ax,%fs #將AX通用寄存器的值加載到FS段選擇子中(EAX的低位為AX)
mov %ax,%gs #將AX通用寄存器的值加載到GS段選擇子中(EAX的低位為AX)
lss _stack_start,%esp #將_stack_start結構體對象裡面的屬性在物理內存的高字加載到ESP寄存器中,將低字加載到SS段寄存器中。
/***************************************1:檢測a20地址線是否真的開啟*************************************************/
# 用於測試A20 地址線是否已經開啟。采用的方法是向內存地址0x000000 處寫入任意
# 一個數值,然後看內存地址0x100000(1M)處是否也是這個數值。如果一直相同的話,就一直
# 比較下去,也即死循環、死機。表示地址A20 線沒有選通,結果內核就不能使用1M(FFFFFh) 以上內存。
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
# '1b'表示向後(backward)跳轉到標號1 去(33 行)。
# 若是'5f'則表示向前(forward)跳轉到標號5 去。
/***************************************1:檢測a20地址線是否真的開啟*************************************************/
/*****************************************設置管理內存分頁的處理機制 ***********************************************//*
* 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.
*/
/*
* 注意! 在下面這段程序中,486 應該將位16 置位,以檢查在超級用戶模式下的寫保護,
* 此後"verify_area()"調用中就不需要了。486 的用戶通常也會想將NE(#5)置位,以便
* 對數學協處理器的出錯使用int 16。
*/
# 下面這段程序(43-65)用於檢查數學協處理器芯片是否存在。方法是修改控制寄存器CR0,在
# 假設存在協處理器的情況下執行一個協處理器指令,如果出錯的話則說明協處理器芯片不存在,
# 需要設置CR0 中的協處理器仿真位EM(位2),並復位協處理器存在標志MP(位1)。
movl %cr0,%eax #將CR0控制寄存器的內容加載到EAX寄存器中
andl $0x80000011,%eax #源操作碼:0X80000011=(1000 0000 0000 0000 0000 0000 0001 0001)2
#andl指令的作用主要是對目標操作數相應位置零
#linux0.11操作系統是架構在80386中
#本段代碼的作用主要是對CR0控制寄存器的0-4位進行控制
#我來解釋一下源操作碼為什麼要這樣設置
#首先,我們看到源操作碼對應的二進制數控制碼的第4位(從0開始計)
#第4位為ET擴展類型位,我們要將之置為1,意義為我們先假設80387協處理器的存在
#在其假設存在的情況下執行一個協處理其指令,如果出錯則說明80387協處理器不存在。
#CR0控制寄存器中的第1-3位是指令類型控制位
#CR0控制寄存器的1-3位從低位到高位分別為MP監控協處理器標志,EM仿真標志位,TS任務已切換標志位
#3個位組合可以形成指令類型控制,現在先對其初始化,置為0,方便下面代碼的使用。
orl $2,%eax #2=(0000 0000 0000 0000 0000 0000 0000 0010)2
#orl指令的作用是對目標操作數相應位置一
#這個操作是針對CR0控制寄存器的1-3位(控制指令類型位),將MP監控協處理器標志位置為1,其他2位為0.
#我們對應看看下面的圖表,看看產生了什麼效果
#它的作用是可以執行浮點運算的指令和執行WAIT/FWAIT指令類型
#好的,下面我們就來開始測試,我們看看到底計算機硬件體系裡面到底有沒有包含80387協處理器
movl %eax,%cr0 #將剛才EAX寄存器裡面設置好的值,加載到CR0,此時CR0開始執行上面我們預設的效果
call check_x87
jmp after_page_tables
CR0控制寄存器圖:
/***********************check_x87:檢測現在的計算機硬件體系是否含有數學協處理器*******************************/
#80387協處理器小科普:為了彌補X86系列架構的計算機在浮點運算上的不足,Intel在1980年推出了X87系列協處理器
#那時候還是一個外置的,可選的芯片,(linus當時他就沒有安裝那種外部的協處理器)1989年,Intel發布了487協處理器
#至此之後CPU都內置了協處理器,這樣對於486以前的計算機而言,操作系統檢驗X87協處理器的存在就非常有必要了。
check_x87:
fninit #finit 向協處理器發出初始化命令,如果系統中存在協處理器的話,那麼在執行了fninit指令後其狀態字低字節(8位)肯定為0。
fstsw %ax #fstsw(store status register)這個指令的功能是把協處理器的狀態寄存器中的值取出並存入內存變量裡.
cmpb $0,%al #我們的AX已經接收到了協處理器的狀態寄存器中的值,如果這個值的低字節位(8位)是0的話,也就是在AL中的值
#如果AL是0的話,那麼由這個操作系統管理的系統中存在協處理器
#我們很容易想到用CMP這種匯編指令去處理這種問題,
#CMP匯編指令的特點是,通過比較(compare)目標操作數於源操作數的差,不影響兩個操作數的值,只影響FLAGS標志寄存器的值。
#我們來看看,目標操作數和源操作數是不是相等的,也就是立即數0和AL寄存器中的值。
#如果是相等的,兩操作數相減為0,這個為0的結果就會影響FLAGS標志寄存器中的ZF零標志位置零。
je 1f #我們要把這一瞬間的比較結果保存下來。je匯編指令執行的時候看FLAGS標志寄存器裡面的ZF零標志,
#如果其ZF標志位置零了,我們跳轉到後面(forward)的1標號
#EFLAGS標志寄存器圖:
movl %cr0,%eax # 如果存在的則向前跳轉到標號1 處,否則改寫cr0。
#將CR0擴展寄存器中的值加載到EAX中保存,我們把EAX在本過程的作用看作一個中間作用的寄存器。
xorl $6,%eax #xorl匯編指令的作用是將目標操作數的相應位,對應源操作數的相應位。
#源操作數位1的位對應到目標操作數的位將會置反。
#我們來說說這句匯編指令干了什麼
#6=(0000 0000 0000 0000 0000 0000 0000 0110)2
#作用是把CR0寄存器的1-2位(從0開始計數)置反。
#也就是CR0中的MP監控協處理器標志位置反,由1變成0.
#EM仿真標志位置為1,EM標志位置為1後,其中的意思是表明處理器內部或外部沒有協處理器。
#我們在來看看這樣的話,由TS,EM,MP控制的協處理器指令類型置成了什麼效果。
#協處理器類型被置為,當計算機執行浮點運算的指令類型的時候,機器發出DNA異常。
#WAIT/FWAIT被置為可執行的。
#CR0控制寄存器中的TS,EM,MP位控制的協處理器指令類型圖:
movl %eax,%cr0 #將預設的一串控制CR0控制寄存器的值從EAX寄存器加載到CR0控制寄存器中。
ret #程序返回到調用本check_x87代碼段的指令的下一句指令,並執行它。
.align 2 # 這裡".align 2"的含義是指存儲邊界對齊調整。"2"表示調整到地址最後2 位為零。
# 即按4 字節方式對齊內存地址。
#對齊偽指令ALIGN
#對齊偽指令格式:
#ALIGN Num
#其中:Num必須是2的冪,如:2、4、8和16等。
#偽指令的作用是:告訴匯編程序,本偽指令下面的內存變量必須從下一個能被Num整除的地址開始分配。
1: .byte 0xDB,0xE4 # 如果存在協處理器,程序就跳轉到了這裡,再把協處理器的處理碼保存。
ret #程序返回到調用本check_x87代碼段的指令的下一句指令,並執行它。
/************************************setup_idt:重新設置中斷描述符表IDT*********************************************/
/*
* 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
*
* 將中斷描述符表idt 設置成具有256 個項,並都指向ignore_int 中斷門。然後加載中斷
* 描述符表寄存器(用lidt 指令)。真正實用的中斷門以後再安裝。當我們在其它地方認為一切
* 都正常時再開啟中斷。該子程序將會被頁表覆蓋掉。
*/
#head.s在一系列的檢查所依賴的系統硬件環境裡面,設備能否正常使用之後。
#head.s繼續建立新的真正的數據結構提供main這個SYSTEM模塊下的主代碼段的執行。
#head.s的存在就是為了main這個主代碼塊可以正常運行。
#程序運行到了這裡開始建立新的真正的數據結構。
#現在我們一起來建立IDT中斷描述符表這個數據結構。
#我們現在想想我們要把IDT這個中斷描述符表做成什麼樣子呢?
#首先,IDT中斷描述符表在內存中整體分布是這樣的。
#中斷描述符表在整個內存中的分布位置如下圖,可以結合這篇文章的第一張圖構造一個整體的印象,我們先看下圖:
#IDT中斷描述符表中的中斷描述符項的格式如下圖:
#IDT中斷描述符表在線性內存中的位置:在ignore_int匯編程序段的上面,在GDT全局描述符表的下面。
#由於808386CPU只能識別256種中斷,並我看中斷描述符的格式,我們知道,一個中斷門描述符的大小是8個BYTE(64BIT)。
#也就是說明IDT中斷描述符表跨越了256*8B的內存大小,也就是2KB的物理內存大小。
#也就是說跨越了2K的線性尋址大小。
#本程序先讓所有的中斷描述符默認指向ignore_int這個位置,ignore_int這個代碼段的作用是調用/kernel/printk.c 中的printk函數,打印“Unknown interrupt”。
#將來main函數裡面還要讓中斷描述符對應具體的中斷服務程序。
#我們會設置中斷描述符表寄存器的值(IDTR),讓中斷描述符表寄存器指向中斷描述符表的最低位。
#我們這樣做的目的是先將中斷機制的整體架構搭建起來,給中斷描述符表的每個項都賦予一個固定的值(值向ignore_int過程),避免野指針。
setup_idt:
lea ignore_int,%edx #取ignore_int標號的基址加載到EDX擴展寄存器中,EDX要將其低位的中斷服務程序基址傳給EAX的低位。
movl $0x00080000,%eax #下面我來講一下怎麼去設置中斷描述符項
#我們先看一個中斷描述符項裡面有什麼,我們把中斷描述符設置完後,要和我們預想的一樣指向ignore_int程序段。
#所以我們要把ignore_int程序段的信息按照中斷描述符項的格式加載進去。
#中斷描述符64位,包含了其對應的中斷服務程序的段內偏移地址(OFFSET),所在段段選擇符(SELECTOR),
#段特權級(DPL),段存在標志(P),段描述符類型(TYPE)等信息。
#中斷描述符的長度為64位,分高32位和低32位。我們用EDX保存低32位的描述格式,用EDX保存高32位的描述格式。
#然後在將EAX的值加載到中斷描述符的低位,將EDX的值加載到中斷描述符的高位。
#循環這個加載過程,把剩下的中斷描述符表項全部設置完畢。
#用ECX控制循環次數,當然我們的循環次數為256次。
#我們來看看IDT表項要被設置成什麼樣的格式。
#首先ignore_int中斷服務程序的偏移地址(OFFSET)就可以使用lea指令取出ignore_int標號的基地址
#這個基地址要被拆分成第0-15位和第48-63位,分別放在EAX和EDX寄存器中
#段選擇符,我們要選用代碼段的段選擇符,所以我們使用0008h號GDT段選擇符。
#中斷描述符的高位0-15位是中斷服務程序的描述信息,我們設為(8E00)h=(1000 1110 0000 0000)2
#第47位為段存在標志(P),此位被置位,說明此中斷服務程序存在於內存中。
#第45-46位為特權級標志(DPL),表示本中斷服務程序是內核特權級
#第40-43位為段描述符類型標志(TYPE),我們設置的是1110.即將此段描述符標記為“386中斷門”。
#本句匯編指令的作用是將立即數00080000h加載到EAX中,EAX保存其加載來的高位0008,那是本中斷服務程序的段選擇。
movw %dx,%ax #將EDX的低位,也就是DX(裡面保存了ignore_inr程序的基址)加載到EAX的低位(AX)。
movw $0x8E00,%dx #將8E00中斷服務程序描述序列號加載到EDX的低位(DX)中。
#開始將EAX,EDX寄存器的值加載到IDT中斷描述符表的位置,從IDT中斷描述符表的低位開始加載,循環完畢後,IDT中斷描述符表就設置好了。
lea _idt,%edi #將IDT服務程序的基地址加載到EDI目標索引寄存器中。
#解釋一下_idt這個標號,如圖:
#這個標號是指向head.s代碼段為IDT中斷描述符表預留的空間的
#這個標號在head,s代碼段的後部分。
#如上圖,就在標號_idt位置處,linus在寫head.s程序的時候預留了256*8BYTE的位置。
#linus在預留的時候將標號_idt以後的256*8BYTE的內存空間都置為了零。
#所以我們現在要做的就是將這段以_idt開頭的空間裡的描述符全部指向ignore_int中斷服務程序。
mov $256,%ecx #設置循環次數256次。
rp_sidt: #開始進入循環體
movl %eax,(%edi) #將EAX中的內容加載到EDI所指向的內存空間中
movl %edx,4(%edi) #將EDX中的內容加載到EDI+4所指向的內存空間中
addl $8,%edi #將EDI目標引索寄存器中的值加8,其效果就是將EDI指向下一個IDT中斷描述符項
dec %ecx #ECX自減(C語言語法的自減效果一樣)
jne rp_sidt #如果上面的自減運算為零,觸動了ZF(EFLAGS標志寄存器零標志位)復位,就跳出循環,要不然,繼續循環。
lidt idt_descr #解釋一下idt_descr這個標號,如圖:
# 這個標號指向的是給IDTR中斷描述符表基址寄存器賦值的內容。
#也可以說它指向的是IDT中斷描述符表的限長和IDT中斷描述符表的入口地址。
#通過這段內容給IDTR賦值
#下面是IDTR寄存器的格式,你想想為什麼這段idt_descr後的內容要這樣寫?
ret #函數返回
/************************************setup_gdt:重新設置全局描述符表GDT *******************************************/
/*
* 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
* 這個子程序設置一個新的全局描述符表gdt,並加載。此時僅創建了兩個表項,與前
* 面的一樣。該子程序只有兩行,“非常的”復雜,所以當然需要這麼長的注釋了?。
setup_gdt:
lgdt gdt_descr #根據gdt_descr標號後的內容,加載全局描述符表寄存器。
#gdt_descr標號在head.s代碼段的後面部分。
#GDT全局描述符表也設置在head.s代碼段的後面。
#GDT全局描述符表在head.s代碼段中占位的時候就已經直接初始化了,內容如下:
ret #函數返回
/***********************after_page_tables:將頁目錄表和頁表放置在內存地址0處占位********************************/
#計算機尋址從段機制到頁機制都是由計算機硬件系統完成的。
#首先,計算機的尋址是通過段寄存器中的描述符高速緩存寄存器中獲取段地址後,在用段地址和存在通用寄存器中的偏移地址相加獲取完全的線性地址。
#采用分頁機制。此時32位的線性地址分為3個部分,(10位)頁目錄索引 (10位)頁表索引 (12位)
#偏移地址首先高10位的頁目錄索引部分和CR3寄存器(它存放著頁目錄地址)結合,找到相應的頁表的地址
#然後根據中間的10位的頁表索引,找到相應的頁的起始位置
#然後,根據低12位的偏移地址,在這個頁中就可以找到對應的地址了——而這就是物理地址!
#如圖:
#頁目錄表項和頁表項的格式:
#頁目錄表和頁表在內存中的位置:
#CR3頁目錄表寄存器的格式:
#我們從頁目錄項和頁表項的圖中看到,頁目錄項和頁表項都是4BYTE長度。
#頁目錄表有1024(1K)個頁目錄項,每個頁目錄項4BYTE,所以頁目錄表的長度是4KB,地址跨度是4K。
#每個頁表有1024(1K)個頁表項,每個頁表項有4BYTE,所以每個頁表項的長度是4KB,地址跨度是4K。
/*
* 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.
*/
/* Linus 將內核的內存頁表直接放在頁目錄表之後,使用了4 個頁目錄表項來尋址16 Mb 的物理內存。
* 如果你有多於16 Mb 的內存,就需要在頁目錄表這裡進行擴充修改。
*/
#所以頁目錄表和頁表在內存中的位置和長度是這樣的:
#頁目錄和頁表是計算機系統尋址的索引,就像我們書的目錄一樣,我們都習慣把它放在開頭。
#CPU只會根據CR3頁目錄表基地址寄存器找到頁目錄的起始地址,然後開始頁目錄尋址機制,根據這個規則你也可以把頁目錄表放在內存的其他地方。
#.org 起始地址,是匯編偽指令,表示後面的代碼從.org指定的起始地址開始存放。
.org 0x1000 //從0X1000開始存放內存頁表pg0,內存頁表pg0長度4KB,尋址跨度4K(FFFh)
pg0: //頁表pg0開始出添加一個標號pg0,方便後面的賦值操作。
.org 0x2000 //從0X2000開始存放內存頁表pg1,內存頁表pg1長度4KB,尋址跨度4K(FFFh)
pg1: //頁表pg0開始出添加一個標號pg1,方便後面的賦值操作。
.org 0x3000 //從0X3000開始存放內存頁表pg2,內存頁表pg2長度4KB,尋址跨度4K(FFFh)
pg2: //頁表pg2開始出添加一個標號pg2,方便後面的賦值操作。
.org 0x4000 //從0X4000開始存放內存頁表pg3,內存頁表pg3長度4KB,尋址跨度4K(FFFh)
pg3: //頁表pg3開始出添加一個標號pg3,方便後面的賦值操作。
.org 0x5000 # 定義下面的內存數據塊從偏移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 isnt
* on a 64kB border.
*/
/* 當DMA(直接存儲器訪問)不能訪問緩沖塊時,下面的tmp_floppy_area 內存塊
* 就可供軟盤驅動程序使用。其地址需要對齊調整,這樣就不會跨越64kB 邊界。
*/
_tmp_floppy_area:
.fill 1024,1,0 # 共保留1024 項,每項1 字節,填充數值0。
#下面的幾個入棧操作(pushl)將數據壓入系統堆棧user_stack中,在前面_stack_start:設置系統堆棧那一章中我們說過。
#壓入的數據是為了調用/init/main.c 程序和返回作准備的,前面3 個入棧指令不知道作什麼用的,也許是Linus 用於在調試時能看清機器碼用的。
#程序跳轉到setup_paging後,setup_paging程序段執行到最後一條語句ret,從系統堆棧中取出_main(main.c程序的入口地址),設置完CS:IP.
#程序就開始跳轉到main函數中執行,當然我們的操作系統肯定是是設置為無限循環的,操作系統不可能停止的,它只要不關機,都是無限執行下去的。
#如果main.c 程序真的退出時,就會返回到這裡的標號L6 處繼續執行下去,也即死循環。
# 下面這幾個入棧操作(pushl)用於為調用/init/main.c 程序和返回作准備。
after_page_tables:
pushl $0
pushl $0
pushl $0
pushl $L6
pushl $_main
#如圖,這張圖是在剛進入main.c函數中系統堆棧的情況。
jmp setup_paging
L6:
jmp L6 //無條件跳轉指令,這裡做了一個無條件的無限循環
// 如果main.c 程序真的退出時, 通過系統堆棧的L6,我們跳轉到這裡無限循環。。。。。。。
/* This is the default interrupt "handler" */
/* 下面是默認的中斷“向量句柄” */
int_msg:
.asciz "Unknown interrupt\n\r" # 定義字符串“未知中斷(回車換行)”。
/********************************ignore_int:這就是那個統一的中斷服務程序*******************************************/
#我們程序員在編寫中斷服務程序的時候,我們要注意一些什麼事情呢?
#首先保護現場是我們匯編程序員應該要有的素質,我們要將中斷匯編程序中會改變的寄存器的值壓入系統堆棧中。
#其次在我們寫完中斷服務程序的核心代碼段的時候,我們要將現場恢復。
#將中斷服務程序開頭的那些被壓入系統堆棧的寄存器的值反入棧順序加載到相應寄存器中。
ignore_int:
#開始保存現場
pushl %eax
pushl %ecx
pushl %edx
push %ds # 這裡請注意!!ds,es,fs,gs 等雖然是16 位的寄存器,但入棧後
# 仍然會以32 位的形式入棧,也即需要占用4 個字節的堆棧空間。
push %es
push %fs
#保存現場完畢,下面開始對寄存器操作。
movl $0x10,%eax # 置段選擇符(使ds,es,fs 指向gdt 表中的數據段)。
mov %ax,%ds
mov %ax,%es
mov %ax,%fs
pushl $int_msg # 把調用printk 函數的參數指針(地址)入棧。
call _printk # 該函數在/kernel/printk.c 中。調用printk.c打印出"Unknown interrupt\n\r" 向量句柄。
# '_printk'是printk 編譯後模塊中的內部表示法。
//開始恢復現場。
popl %eax
pop %fs
pop %es
pop %ds
popl %edx
popl %ecx
popl %eax
//恢復現場完畢。
iret # 中斷返回(把中斷調用時壓入棧的CPU 標志寄存器(32 位)值也彈出)。
/*****************************Setup_paging:開始設置頁目錄表和頁表的值********************************************/
/*
* 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 :-( )
*/
/*
* 這個子程序通過設置控制寄存器cr0 的標志(PG 位31)來啟動對內存的分頁處理功能,
* 並設置各個頁表項的內容,以恆等映射前16 MB 的物理內存。分頁器假定不會產生非法的
* 地址映射(也即在只有4Mb 的機器上設置出大於4Mb 的內存地址)。
* 注意!盡管所有的物理地址都應該由這個子程序進行恆等映射,但只有內核頁面管理函數能
* 直接使用>1Mb 的地址。所有“一般”函數僅使用低於1Mb 的地址空間,或者是使用局部數據
* 空間,地址空間將被映射到其它一些地方去 -- mm(內存管理程序)會管理這些事的。
* 對於那些有多於16Mb 內存的家伙 - 太幸運了,我還沒有,為什麼你會有?。代碼就在這裡,
* 對它進行修改吧。(實際上,這並不太困難的。通常只需修改一些常數等。我把它設置為
* 16Mb,因為我的機器再怎麼擴充甚至不能超過這個界限(當然,我的機器很便宜的?)。
* 我已經通過設置某類標志來給出需要改動的地方(搜索“16Mb”),但我不能保證作這些
* 改動就行了??)。
*/
.align 2 # 這裡".align 2"的含義後面的代碼是指存儲邊界對齊調整。"2"表示調整到地址最後2 位為零。
# 即按4 字節方式對齊內存地址。
setup_paging: #開始設置剛才占位的在0X0000地址開始的頁目錄表和頁表。
movl $1024*5,%ecx #我們做一個循環先把從0X0000地址開始的頁目錄表和頁表初始化為零。
#再做一個開始真正的設置尋址映射的數據,這個setup_paging代碼段是這樣分兩段來設置頁目錄和頁表的值的。
#現在我們來做第一步,我們做一個循環先把從0X0000地址開始的頁目錄表和頁表初始化為零。
#現在我們要想清楚兩個東西,一是循環體要做什麼,二是循環次數要多少次。
#我們現在是將頁目錄和頁表這一打段的值全部設置成為0。
#我們的循環體裡面就把一個固定長度的0賦值給目標地址,就這樣一直循環下去,這個問題就解決了。
#我們現在保護模式的通用寄存器的長度是4BYTE的,我們把一個通用寄存器的值全部置為零。
#再在循環體裡面把通用寄存器裡面的4個字節的0加載到目標地址中。循環下去就搞定了。
#那我們的循環次數設置為多少呢?我們知道我們的那塊頁目錄表和頁表的地址是連續的,大小是20KB的。
#所以,循環次數是20KB除以4B,是5K(1024*5)。
xorl %eax,%eax #將通用寄存器裡面所有的BIT都置為零。
xorl %edi,%edi #頁目錄表和頁表的偏移地址是從0XOOOO開始的。所以我們把目標偏移地址設置為零。
cld;rep;stosl #CLD指令的解釋:與cld相對應的指令是std,二者均是用來操作方向標志位DF(Direction Flag)。
#cld使DF 復位,即是讓DF=0,std使DF置位,即DF=1.這兩個指令用於串操作指令中。
#通過執行cld或std指令可以控制方向標志DF,決定內存地址是增大(DF=0,向高地址增加)還是減小(DF=1,向地地址減小)。
#CLD指令即告訴程序si,di向內存地址增大的方向走。
#rep指令表示緊跟著下面的一條指令重復執行,直到ECX的值是零。
#STOSL指令相當於將EAX中的值保存到ES:EDI指向的地址中。
#頁目錄表和頁表這段內存的值都是零後,我們來真正設置頁目錄和頁表的內容。
# 下面4 句設置頁目錄中的項,我們共有4 個頁表所以只需設置4 項。
#_pg_dir標記是在head.s代碼段的開頭就設置好的地址標記,head.s開頭地址=內存的開始地址=目錄表的開頭地址=_pg_dir標記的地址
# 頁目錄項的結構與頁表中項的結構一樣,4 個字節為1 項。
# "$pg0+7"這個立即數是要作為0號頁表的地址加載到_pg_dir處的(也就是頁目錄表的第0項)。
#pg0這個標號是頁表0的地址,放到頁目錄表的第0項很正常,這個7是什麼呢?
#7不用說都是0號頁表的描述符,你還記得嗎?
#則第1 個頁表所在的地址 = 0x00001007 & 0xfffff000 = 0x1000;
# 第1 個頁表的描述標志 = 0x00001007 & 0x00000fff = 0x07,表示該頁存在、用戶可讀寫。
#後面的以此類推
movl $pg0+7,_pg_dir
movl $pg1+7,_pg_dir+4
movl $pg2+7,_pg_dir+8
movl $pg3+7,_pg_dir+12
#下面我們就要填寫頁表的內容了,我們再來回顧下頁表的格式,如圖:
#物理尋址內存是這樣分配的,在保護模式下,最大尋址為4GB=4BYTE(一個頁表項的大小)*G=1024(頁目錄項數量)*1024(頁表項數量)*4BYTE(頁表項大小)
#我們linux0.11只能做到管理16MB大小的物理內存。我們是16MB= 4(頁目錄項數量)*1024(頁表項數量)*4BYTE(頁表項大小)
#下面6 行填寫4 個頁表中所有項的內容,共有:4(頁表)*1024(項/頁表)=4096 項(0 - 0xfff),
# 頁表的每項可以尋址4KB的大小,也即能映射物理內存 4096*4Kb = 16Mb。# 位置是1023*4 = 4092。因此最後一頁的最後一項的位置就是$pg3+4092。
movl $pg3+4092,%edi # 3號頁表的最後一項的偏移基地址。jge 1b # 如果小於0 則說明全添寫好了。
#我們現在設置好了頁目錄表和頁表的值,我們要開啟頁表尋址機制了。
#打開方式:先設置CR3頁目錄表基地址寄存器的值,將裡面的頁目錄表基地址寄存器指向內存的頁目錄。
#再開啟CR0控制寄存器中的第32位,用來開啟分頁處理。
#CR0和CR3控制寄存器的圖如下:
# 設置頁目錄基址寄存器cr3 的值,指向頁目錄表。/******************************************頁目錄表和頁表的占位位置************************************************/
.align 2 # 按4 字節方式對齊內存地址邊界。