head.s 剖析——Linux-0.11 剖析笔记(五)
文章目錄
- 一、加載段寄存器
- LSS指令
- 二、設置中斷描述符表(IDT)
- 中斷處理過程 `ignore_int`
- 三、設置全局描述符表(GDT),加載 GDTR
- 四、重新加載段寄存器
- 五、檢測A20是否開啟
- 六、檢測 `x87` 協處理器
- 關于異或
- 按位異或的3個特點
- 按位異或的幾個常見用途
- 1. 使某些特定的位翻轉
- 2. 不使用臨時變量就可以實現兩個值的交換
- 3. 在匯編語言中經常用于將變量置零
- 4. 快速判斷兩個值是否相等
- 七、開啟分頁,跳轉到 main()
- 為調用 `main()`函數做準備
- 設置頁目錄和頁表
- 八、總結
head.s 程序在被編譯生成目標文件后會與內核其他程序一起被鏈接成 system 模塊,它位于 system 模塊的最開始部分,這也就是為什么稱其為“頭部(head)”程序的原因。
從這里開始,內核完全是在保護模式下運行了。head.s 匯編程序與前面的語法格式不同,它采用的是AT&T匯編語言格式,并且需要使用 GNU 的 as 和 ld 進行編譯和連接。因此請注意代碼中賦值的方向是從左到右。
這段程序實際上處于內存地址 0 處,在理解代碼的時候,請務必記住。
一、加載段寄存器
.text .globl idt,gdt,pg_dir,tmp_floppy_area pg_dir: # 頁目錄將會存放在這里,把這里的代碼覆蓋掉 .globl startup_32 startup_32:movl $0x10,%eaxmov %ax,%dsmov %ax,%esmov %ax,%fsmov %ax,%gslss stack_start,%esp第6行:0x10 是數據段的選擇子,在 setup.s 文件的末尾處定義,基地址是 0,段界限是 0x7FF,粒度 4KB,可讀可寫,向上擴展。如果讀者忘了,可以參考我的博文:setup.s 分析
7~10行:令ds,es,fs,gs指向數據段。
第11行:stack_start 的定義在 kernel/sched.c(以后會分析)中。
為了閱讀方便,截取部分代碼在這里。
// kernel/sched.c long user_stack [ PAGE_SIZE>>2 ] ;struct {long * a;short b;} stack_start = { & user_stack [PAGE_SIZE>>2] , 0x10 };LSS指令
lss 指令的格式是
LSS r32,m16:32
注意,這是 Intel 匯編語法,賦值方向是從右到左。含義是用內存中的長指針加載 SS:r32
Load SS : r32 with far pointer from memory
m16:32表示一個內存操作數,這個操作數是一個長指針,由 2 部分組成:16 位的段選擇子和 32 位的偏移。
A memory operand containing a far pointer composed of two numbers. The number to the left of the colon corresponds to the pointer’s segment selector. The number to the right corresponds to its offset.
注意,長指針在內存中的布局如下:低4字節是偏移,高2字節是段選擇子。
stack_start 處的 6 字節是 long * a 和 short b.
a 被賦值為user_stack[]數組最末端的地址,b 被賦值為 0x10.
所以,第 11 行代碼表示用 a 的值加載 ESP,用 b 的值加載 SS,即棧的初始化。
二、設置中斷描述符表(IDT)
call setup_idt setup_idt:lea ignore_int,%edxmovl $0x00080000,%eaxmovw %dx,%ax /* selector = 0x0008 = cs */movw $0x8E00,%dx /* interrupt gate:dpl=0, present */# 以上,在 edx、eax 中組合設置出 8 字節默認的中斷描述符值lea idt,%edi # 取idt的偏移給edimov $256,%ecx # 循環256次,和后面的 dec %ecx 聯合使用 rp_sidt:movl %eax,(%edi) # eax -> [edi]movl %edx,4(%edi) # edx -> [edi+4]addl $8,%edi # edi + 8 -> edidec %ecxjne rp_sidtlidt idt_descr # 加載IDTRret...idt_descr:.word 256*8-1 # idt contains 256 entries.long idt # IDT 的線性基地址...idt: .fill 256,8,0 # idt is uninitializedIDT 共 256 項,作者使各個表項均指向一個只報錯誤的啞中斷子程序ignore_int。
lea 指令:Load Effective Address,用來加載有效地址到寄存器,有效地址,就是偏移地址。
第 2 行:取標號 ignore_int 的偏移地址到 edx
2~5行:組裝中斷門,示意圖如下,藍色圓圈是行號。
2~5行:在 edx、eax 中組合設置出 8 字節默認的中斷描述符值。eax 含有描述符低 4 字節(對應中斷門描述符格式的第二行),edx 含有高 4 字節(對應中斷門描述符格式的第一行)。
8E00 是因為:P = 1;DPL = 0;
9~14 行:在 idt 表每一項中都放置該描述符,共 256 項。內核在隨后的初始化過程中會替換那些真正使用的中斷描述符項。
中斷處理過程 ignore_int
/* This is the default interrupt "handler" :-) */ int_msg:.asciz "Unknown interrupt\n\r" .align 2 ; 4字節對齊 ignore_int:pushl %eaxpushl %ecxpushl %edxpush %ds # 這里請注意ds,es,fs,gs等雖然是16位的寄存器,# 但仍然會以32位的形式入棧,即需要占用4個字節的??臻g。 push %espush %fs # 以上用于保存寄存器movl $0x10,%eax # 0x10是數據段選擇子mov %ax,%dsmov %ax,%esmov %ax,%fs # ds,es,fs均指向數據段pushl $int_msg call printk # 該函數在 kernel/printk.c 中popl %eax # 清理參數pop %fspop %espop %dspopl %edxpopl %ecxpopl %eaxiret第 3 行,偽指令 .asciz
為了說明清楚,先要介紹 .ascii
.ascii
從位置計數器所計當前位置為字符串分配空間并存儲字符串??蓪懚鄠€字符串,并用逗號分開。例如:
.asciz
該偽指令與 .ascii類似,但是每個字符串后面會自動添加 NULL 字符。
第 4 行,.align 的含義是指存儲邊界對齊調整?!?” 表示把隨后的代碼或數據的偏移位置調整到地址值最后 2 比特位為零的位置,即按 4 字節方式對齊。
需要注意的是,“2” 在不同的語境下有不同的涵義:
對于現在使用 ELF 目標格式的 Intel 80X86 CPU ,2 表示以 2 字節對齊。再比如, .align 8 表示調整位置計數器,讓它在 8 的倍數邊界上。
但對于 Linux 0.11 中使用 a.out 目標格式的系統來說,2 表示對齊到 2 的 2 次方。再比如,.align 3 表示位置計數器需要位于 8 的倍數邊界上。
gas(GNU as) 對 ELF 和 a.out 這兩個目標格式的處理方法不同是由于 gas 為了模仿各種體系結構系統上自帶的匯編器的行為而形成的。
想了解 .align 的完整格式和含義,可以參考我的博文:偽指令 .align 的含義
第 17 行:把 printk 函數的參數(即 int_msg 代表的偏移地址)入棧。注意:若符號 int_msg 前不加 $,則表示把 int_msg 符號處的雙字“Unkn”入棧。
第18行:調用 printk 函數,該函數在 kernel/printk.c 中,以后再具體分析。
第19行:清理參數 $int_msg.
說明:匯編程序調用 C 函數時,函數的入口參數使用棧來傳送,參數的傳遞順序是從右到左。調用者負責清除參數占用的棧空間。C 函數的返回值如果是 32 位整數,則保存在 eax 寄存器;如果是 64 位整數,則保存在 edx:eax 寄存器。
具體可以參考我的博文: 在匯編程序中調用C函數
三、設置全局描述符表(GDT),加載 GDTR
call setup_gdt setup_gdt:lgdt gdt_descr # 加載GDTRretLGDT 指令的格式是:
LGDT m16&32
該指令的操作數是一個 48 位(6字節)的內存區域。在這 6 字節的內存區域中,要求前(低)16位是 GDT 的界限值,后(高)32 位是 GDT 的基地址 。該指令在實模式和保護模式下都可以執行。
gdt_descr:.word 256*8-1 .long gdt gdt: .quad 0x0000000000000000 /* NULL descriptor */.quad 0x00c09a0000000fff /* 16Mb */.quad 0x00c0920000000fff /* 16Mb */.quad 0x0000000000000000 /* TEMPORARY - don't use */.fill 252,8,0 /* space for LDT's and TSS's etc */GDT 定義的描述符如下:
| 0 | - | - | - | - | - | - | - | - |
| 1 | 0x08 | 代碼段 | 0 | 0xFFF | 1(表示 4KB) | 0 | 1 | 非一致,可讀 |
| 2 | 0x10 | 數據段 | 0 | 0xFFF | 1(表示 4KB) | 0 | 1 | 向上擴展,可寫 |
段長度可以這樣算:(Limit + 1)* 4KB = (0xFFF + 1) * 4KB = 0x1000 * 4KB = 16MB
有人問,在 setup.s 中不是已經定義且加載 GDT 了嗎?這里為何要再做一遍?
答案是之前的那個 GDT 只是臨時的,這里才是真正的。
四、重新加載段寄存器
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 mov %ax,%gs ; 以上加載數據段到 DS、ES、FS、GS lss stack_start,%esp ; 本文開頭已經解釋了由于段描述符中的段限長(Limit)從 setup.s 中的 8MB 改成了本程序設置的 16MB,因此這里再次對所有段寄存器執行加載操作是必須的。另外,如果不對 CS 再次加載,那么在執行到第1行時,CS段寄存器的“描述符高速緩存器”中的段限長還是 8MB。這樣看來應該重新加載 CS。
但是由于 setup.s 中的代碼段描述符與本程序中重新設置的代碼段描述符除了段限長以外其余部分完全一樣,8MB 的限長在內核初始化階段不會有問題,而且在以后內核執行段間跳轉時會重新加載 CS,因此這里沒有加載它并沒有讓程序出錯。
針對該問題,目前內核中就在movl $0x10,%eax之前添加了一條長跳轉指令ljmp $(_KERNEL_CS), $1f,大概的代碼如下:
call setup_idtcall setup_gdt # reload all the segment registersljmp $(_KERNEL_CS), $1f 1:movl $0x10,%eax mov %ax,%ds mov %ax,%es mov %ax,%fsmov %ax,%gslss stack_start,%esp注意:以上的代碼只是為了說明問題,并非源碼。
ljmp $(_KERNEL_CS), $1f
$1f 中的 1 是標號,緊跟在其后的 f 表示向前(forwards);_KERNEL_CS 表示內核代碼段的選擇子,這條指令會跳轉到第 6 行來確保 CS 被重新加載。
五、檢測A20是否開啟
xorl %eax,%eax 1: incl %eax # check that A20 really is enabledmovl %eax,0x000000 # loop forever if it isn'tcmpl %eax,0x100000je 1b用于測試 A20 地址線是否已開啟。
1981 年,IBM 公司最初推出的個人計算機所使用的 CPU 是 Intel 8088。在該微機中地址線只有 20 根(A0~A19)。當時,計算機的 RAM 只有幾百 KB 或不到 1MB 時,20 根地址線已足夠用來尋址。對于超出 0x100000(1MB) 的尋址地址, 將默認回卷到 0x00000。
所以,如果 A20 沒有開啟,那就存在回卷的現象,訪問 0x100000,就是訪問 0 地址。
檢查方法是向內存地址 0 處寫入一個數值(比如 1),然后訪問內存地址 Ox100000(1M),如果讀出來的也是 1,那就說明沒有開啟 A20。
第 4 行,讀出 Ox100000 處的內容,和 1 比較,如果相同,就給 0 地址寫 2,再讀出 Ox100000 處的內容,和 2 比較……
六、檢測 x87 協處理器
為了彌補 x86 系列在進行浮點運算時的不足,Intel 于 1980 年推出了 x87 系列數學協處理器,那時 x87 是一個外置的、可選的芯片。1989 年,Intel 發布了 486 處理器。從 486 開始,以后的 CPU 一般都內置了協處理器。這樣,對于 486 以前的計算機而言,操作系統檢測 x87 協處理器是否存在就非常必要了。
注:1991 年,一名 21 歲的就讀于芬蘭赫爾辛基大學的計算機科學專業學生—— Linus Torvalds 基于 gcc、bash 開發了針對 386 機器的 Liniux 內核。
下面這段程序用于檢查數學協處理器芯片是否存在。方法是修改控制寄存器 CR0,在假設協處理器存在的情況下執行一個協處理器指令,如果出錯的話則說明協處理器芯片不存在。
movl %cr0,%eax # check math chipandl $0x80000011,%eax # Save PE ET PGorl $2,%eax # set MP=1movl %eax,%cr0call check_x87jmp after_page_tables第2行:保留 PE、ET、PG 位,其他位都清零(包含 EM)。
PE 指示是否開啟保護模式。PG 指示是否開啟分頁。關于 ET,Intel 手冊如是說:
The ET (extension type) flag (bit 4 of the CR0 register) is used in the Intel386? processor to
indicate whether the math coprocessor in the system is an Intel 287 math coprocessor (flag is
clear) or an Intel 387 DX math coprocessor (flag is set).
也就是說,在 386 中,ET 指示數學協處理器是 287 還是 387
第 3 行,設置 MP=1.
這塊我不是很明白,根據下面的表格,在數學協處理器存在的時候,推薦(見最后一行)設置 EM=0,MP=1.
既然作者的意圖是假設數學協處理器存在,那么就設置 EM=0,MP=1 吧。
關于 NE 位:
The NE flag determines whether unmasked floating-point exceptions are handled by generating
a floating-point error exception internally (NE is set, native mode) or through an external inter-
rupt (NE is cleared). In systems where an external interrupt controller is used to invoke numeric
exception handlers (such as MS-DOS-based systems), the NE bit should be cleared.
看來,NE 是控制浮點異常的處理模式的,以后用到再研究。
check_x87:fninit # 向協處理器發出初始化命令fstsw %ax # 把FPU的狀態字保存到AX中# 初始化后狀態字應該為0,否則說明協處理器不存在cmpb $0,%alje 1f # 存在則跳轉到標號1處movl %cr0,%eax xorl $6,%eax # 把 eax 的值和 0110b 異或,翻轉 MP、EMmovl %eax,%cr0ret.align 2 # 4字節對齊 1: .byte 0xDB,0xE4 /* fsetpm for 287, ignored by 387 */ret第 2 行,fninit 是一條指令,手冊上這樣解釋:Initialize FPU without checking for pending unmasked
floating-point exceptions. 此指令執行后,FPUStatusWord ← 0;
第 3 行,fstsw 也是一條指令,手冊上說:Store FPU status word in AX register after checking for
pending unmasked floating-point exceptions.
7~9 行:把 eax 的值和 110b 異或,也就是翻轉 MP、EM,即設置成上面的表格中沒有數學協處理器的配置(EM=1,MP=0)
第12行:0xDB,0xE4 這兩個字節是 80287 協處理器指令 fsetpm 的機器碼。其作用是通知 80287 :處理器處于保護模式。80387 無需該指令,它會把該指令看作是空操作。
With the 32-bit Intel Architecture FPUs, the FSETPM instruction is treated as NOP (no opera-
tion). This instruction informs the Intel 287 math coprocessor that the processor is in protected
mode.
關于異或
按位異或的3個特點
按位異或的幾個常見用途
1. 使某些特定的位翻轉
? 例如要使 EAX 的 b1 位和 b2 位翻轉:
EAX = EAX ^ 00000110
? 代碼第8行就是這種用法,把 EM 和 MP 翻轉。
2. 不使用臨時變量就可以實現兩個值的交換
? 例如 a=11110000,b=00001111,要交換a、b的值,可通過下列語句實現:
a = a^b; //a=11111111 b = b^a; //b=11110000 a = a^b; //a=000011113. 在匯編語言中經常用于將變量置零
? xor eax,eax
4. 快速判斷兩個值是否相等
? 例如判斷兩個整數a、b是否相等,可通過下列 C 語句實現:
? return ((a ^ b) == 0);
七、開啟分頁,跳轉到 main()
Linus 將內核的頁表直接放在頁目錄之后,使用了 4 個頁表來尋址 16MB 的物理內存。如果你有多于 16MB 的內存,就需要在這里進行擴充修改。關于分頁機制,說來話長,不了解的朋友可以參考我的博文:
簡單的分頁模型
x86分頁機制詳解
一個頁表有 1024 個表項,每個表項對應一頁,大小是 4KB,所以,一個頁表對應 1024*4KB,即 4MB。所以,4 個頁表就是 16MB。
Linus 在物理地址 0x0 處開始存放 1 頁頁目錄,這就導致偏移地址從 0 開始到 0x1000 的代碼會被覆蓋。
頁目錄后面是 4 頁頁表。
頁目錄是系統所有進程公用的,而其后的 4 頁頁表則屬于內核專用,它們把線性地址 0x000000~0xFFFFFF 一一映射到物理地址 0x000000~0xFFFFFF(大小是 16MB)。
.org 0x1000 #從偏移 0x1000 處開始放第1個頁表(偏移0開始處存放頁目錄) pg0:.org 0x2000 #從偏移 0x2000 處開始放第2個頁表 pg1:.org 0x3000 #從偏移 0x3000 處開始放第3個頁表 pg2:.org 0x4000 #從偏移 0x4000 處開始放第4個頁表 pg3:.org 0x5000 #定義下面的內存數據塊從偏移 0x5000 處開始.ORG偽指令用來表示起始的偏移地址,緊接著ORG的數值就是偏移地址的起始值。ORG偽操作常用來指定數據的存儲地址,有時也用來指定代碼段的起始地址。更詳細的解釋可以參考我的博文:
ORG 偽指令
/** 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 #共保留1024項,每項1字節,填充數值0fill偽指令的格式是 .fill repeat,size,value
表示產生 repeat 個大小為 size 字節的重復拷貝。size 最大是 8,size 字節的值是 value.
當 DMA (直接存儲器訪問)不能訪問緩沖塊時,tmp_floppy_area 內存塊就可供軟盤驅動程序使用。其地址需要對齊,這樣就不會跨越 64KB 邊界。
這是趙炯老師的翻譯,我不甚理解。暫不深究,以后再說。
為調用 main()函數做準備
after_page_tables:pushl $0 # These are the parameters to main :-)pushl $0pushl $0pushl $L6 # return address for main, if it decides to.pushl $mainjmp setup_paging # 設置頁目錄和頁表,并開啟分頁 L6:jmp L6 # main should never return here, but# just in case, we know what happens.2~6行:為跳轉到 init/main.c 中的 main() 函數作準備工作。
2~4行:前3個入棧 0 值應該分別表示 envp、argv 指針和 argc 的值,但 main() 沒有用到。
函數原型是:
int main(int argc, char *argv[], char *envp[])第5行:壓入返回地址。模擬調用 main.c 程序時首先將返回地址入棧的操作,如果 main.c 程序真的退出,就會返回到標號 L6 處繼續執行下去,即死循環。
第6行:壓入 main() 函數代碼的起始地址。當后面執行 ret 指令時(后文會講),就會彈出 main() 的地址,并把控制權轉移到 init/main.c 程序中。
如果對這里的壓棧不理解的話,可以參考我的博文: 在匯編程序中調用C函數
設置頁目錄和頁表
再次強調,頁目錄是系統所有進程公用的,而其后的 4 頁頁表則屬于內核專用,它們把線性地址 0x000000~0xFFFFFF 一一映射到物理地址 0x000000~0xFFFFFF(大小是 16MB)。
一個頁表有 1024 個表項,每個表項對應一頁,大小是 4KB,所以,一個頁表對應 1024*4KB,即 4MB。
如何映射呢,也就是如何構建頁目錄和頁表才能實現這種原地映射?
根據分頁的原理,0x00FFFFFF 寫成二進制就是
0000 0000 1111 1111 1111 1111 1111 1111b
按照高 10 位,中間 10 位,低 12 位排列就是
0000000011 1111111111 111111111111
高 10 位用來索引頁目錄,00~11,即頁目錄的 0-3 項;
中間 10 位用來索引頁表,00000000~1111111111 ,即頁表的 0~1023 項;
低 12 位作為頁內偏移;
為了實現原地映射,頁目錄的第 0 項必然指向某個頁表,這個頁表的第 0 項應該指向物理地址 0x0000,第 1 項應該指向物理地址 0x1000,第 2 項應該指向物理地址 0x2000,…,第 1023 項應該指向物理地址 0x3FF000;
同理,頁目錄的第 1 項必然指向某個頁表,這個頁表的第 0 項應該指向物理地址 0x400000,第 1 項應該指向物理地址 0x401000,第 2 項應該指向物理地址 0x402000,…,第 1023 項應該指向物理地址 0x7FF000;
……
頁目錄的第 3 項必然指向某個頁表,這個頁表的第 0 項應該指向物理地址 0xC00000,第 1 項應該指向物理地址 0xC01000,第 2 項應該指向物理地址 0xC02000,…,第 1023 項應該指向物理地址 0xFFF000;
根據 Linus 的安排,頁目錄的第 0 項指向第一個頁表(位于內存 0x1000),頁目錄的第 1 項指向第二個頁表(位于內存 0x2000),……
所以,給頁表填寫物理地址的時候,可以從第一個頁表開始,一直填寫到第四個頁表,從第 0 項開始,順次往后填:0x0000,0x1000,0x2000,…,每次增加 0x1000;不過,Linus 是倒著填的。
思路弄清楚后,可以看代碼了
.org 0x1000 pg0:.org 0x2000 pg1:.org 0x3000 pg2:.org 0x4000 pg3: setup_paging:movl $1024*5,%ecx # 每個頁表占用1024個雙字(雙字=4B),共5個頁表xorl %eax,%eax # eax = 0xorl %edi,%edi # edi = 0cldrep; stosl # eax -> es:[edi],edi每次增加4,重復ecx次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,%edimovl $0xfff007,%eax /* 16Mb - 4096 + 7 (r/w user,p) */std 1: stosl /* fill pages backwards - more efficient :-) */subl $0x1000,%eaxjge 1bxorl %eax,%eax /* pg_dir is at 0x0000 */movl %eax,%cr3 /* cr3 - page directory start */movl %cr0,%eaxorl $0x80000000,%eaxmovl %eax,%cr0 /* set paging (PG) bit */ret /* this also flushes prefetch-queue */2~5行,把頁目錄和頁表清零。
stosl:Store EAX at address ES:EDI
STOSL 指令將 EAX 中的值保存到 ES:EDI 指向的地址中,若 EFLAGS 中的方向位置位(即在 STOSL 指令前使用 STD 指令),則 EDI 自減 4,若清空方向位(即在 STOSL 指令前使用 CLD 指令),則 EDI 自增 4;
單純的 STOSL 只能執行一次,如果希望處理器自動地反復執行,可以加上指令前綴 rep;
在寄存器 CX(16 位模式)或者 ECX(32 位模式)中設置傳送的次數。當 CX/ECX 不等于0時,則執行STOSL ,執行后,CX/ECX 的值減一,直到減為 0 為止。
7~10行,填寫頁目錄表的前 4 項。pg_dir 就是 0,在開頭就定義了;pg0 就是 0x1000,表示頁表的基地址(物理地址)
/** 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.*/ .text .globl _idt,_gdt,_pg_dir,_tmp_floppy_area _pg_dir: startup_32:“+7” 是什么意思?
關于表項的格式,如下圖。(詳細內容可以參考我的博文 頁目錄項和頁表項)
| 0 | P | 存在位。為1表示頁表或者頁位于內存中,為0表示不在內存中,必須先予以創建或者從磁盤調入內存后方可使用。 |
| 1 | R/W | 讀寫標志。為1表示頁面可以被讀寫,為0表示只讀。當處理器運行在0、1、2特權級時,此位不起作用。頁目錄中的這個位對其所映射的所有頁面起作用。 |
| 2 | U/S | 用戶/超級用戶標志。為1時,允許所有特權級別的程序訪問;為0時,僅允許特權級為0、1、2的程序訪問。頁目錄中的這個位對其所映射的所有頁面起作用。 |
根據上表,可以知道 setup_paging 這段代碼 7~10 行中的“+7”表示:頁表存在,可讀可寫,允許所有特權級別的程序訪問。
填寫后示意圖如下:
繼續看代碼
movl $pg3+4092,%edi # 頁表最后一項的地址 => edimovl $0xfff007,%eax std # 設置方向位DF=1 1: stosl # Store EAX at address ES:EDIsubl $0x1000,%eax # 更新下一個表項的值,因為一個表項對應 0x1000B 的內存,所以要把頁基址減去0x1000jge 1b # 用于有符號數大小的比較,eax 大于等于 0x1000 則跳轉到1處以上代碼的目的是填寫4個頁表。
頁表項的格式如下圖,0、1、2 比特位的含義見前文的表格。
movl $pg3+4092,%edi
一張頁表最多可以容納 1024 個表項,每項占 4 個字節。下圖左邊是表項的序號,從 0 到 1023,右邊是偏移地址(= 序號*4),4092 是最后一個表項的偏移地址。
上面的代碼表示把頁表 3(最后一個頁表)的最后一項的地址傳入 edi. 作者的意圖是從最后一個表項開始,倒著填寫,直到填完頁表 0 的第 0 個表項。
movl $0xfff007,%eax 中的 0xfff007 是頁表 3 的最后一項的值,“7”就不用再解釋了,解釋一下為什么是 0xfff000:
其實前文用列舉法已經列出來是 0xfff000 了,這里再算一遍。
頁表的每一項對應 4KB(2^12=4K)的內存,一個頁表有 1024(=1K)項,共對應 4KB*1K=4MB 的內存。代碼中安排了4個頁表,即共可以映射 4*4MB=16M 內存。
16M - 4K = 0xFFF000
或者:
16M - 1 = 0x1000000-1 = 0xFFFFFF 算出了最后一個物理地址
根據 4KB 對齊,0xFFFFFF & 0xFFFFF000 = 0xFFF000
jge 用于有符號數大小的比較,當 DEST(這里是 eax) 大于等于 SRC(這里是 0x1000) 則跳轉。當 eax = 0x1007 時,eax>=0x1000,跳轉之后 eax = 0x0007,這時候條件不再成立,則結束跳轉。所以,最后填寫的表項值是 0x0007。
xorl %eax,%eax /* pg_dir is at 0x0000 */movl %eax,%cr3 # 把頁目錄的物理地址寫入CR3movl %cr0,%eaxorl $0x80000000,%eaxmovl %eax,%cr0 #以上三行使 CR0 的 PG=1, 開啟分頁機制retCR3寄存器的格式如下:
The P6 family and Pentium ? processors support page-level cache management in the same
manner as the Intel486? processor by using the PCD and PWT flags in control register CR3,
the page-directory entries, and the page-table entries. The Intel486? processor, however, is not
affected by the state of the PWT flag since the internal cache of the Intel486? processor is a
write-through cache.
PCD 和 PWT 和頁級別的緩存管理有關,這里沒有用,就寫 0 了。
CR0 的格式如下,第 31 位是 PG 位,為 1 的時候開啟分頁機制。
從movl %eax,%cr0執行后,段部件產生的線性地址就不再被看成物理地址,而是要送往頁部件進行變換,以得到真正的物理地址。
注意,現在內核工作在分頁機制的一個特殊情況下,線性地址和經過頁部件轉換后的物理地址相同,這是作者精心安排的。
最后的ret指令有2個作用。
在改變分頁處理標志后要求使用轉移指令刷新預取指令隊列,這里用的是返回指令ret。
將之前壓入棧中的 main() 程序入口地址彈出,并跳轉到 init/main.c 程序去運行。
本程序到這里就分析結束了。
八、總結
本程序作的工作有:
參考資料
《Linux內核完全剖析》(趙炯,機械工業出版社,2006)
總結
以上是生活随笔為你收集整理的head.s 剖析——Linux-0.11 剖析笔记(五)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: x86分页机制详解
- 下一篇: python queue的用法_pyth