head.s 分析——Linux-0.11 学习笔记(三)
題目:head.s 分析
head.s 程序在被編譯生成目標文件后會與內(nèi)核其他程序一起被鏈接成 system 模塊,它位于 system 模塊的最開始部分,這也就是為什么稱其為“頭部(head)”程序的原因。
從這里開始,內(nèi)核完全是在保護模式下運行了。head.s 匯編程序與前面的語法格式不同,它采用的是AT&T匯編語言格式,并且需要使用 GNU 的 as 和 ld 進行編譯和連接。因此請注意代碼中賦值的方向是從左到右。
這段程序?qū)嶋H上處于內(nèi)存地址0處,在理解代碼的時候,請務(wù)必記住。
一、加載段寄存器
.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 是數(shù)據(jù)段的選擇子,在 setup.s 文件的末尾處定義,基地址是 0,段界限是 0x7FF,粒度 4KB,可讀可寫,向上擴展。如果讀者忘了,可以參考我的博文:setup.s 分析
7~10行:令ds,es,fs,gs指向數(shù)據(jù)段。
第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
含義是用內(nèi)存中的長指針加載 SS:r32
Load SS:r32 with far pointer from memory
m16:32表示一個內(nèi)存操作數(shù),這個操作數(shù)是一個長指針,由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.
注意,長指針在內(nèi)存中的布局如下:低4字節(jié)是偏移,高2字節(jié)是段選擇子。
stack_start 處的6字節(jié)是long * a和short b.
a被賦值為user_stack[]數(shù)組最末端的地址,b被賦值為0x10.
所以,第11行代碼表示用a的值加載ESP,用b的值加載SS,即棧的初始化。
二、設(shè)置中斷描述符表(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 */lea idt,%edi # 取idt的偏移給edimov $256,%ecx # 循環(huán)256次 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。
2~5行:組裝中斷門,示意圖如下,藍色圓圈是行號。
2~5行:在edx、eax中組合設(shè)置出8字節(jié)默認的中斷描述符值。eax 含有描述符低4字節(jié),edx 含有高4字節(jié)。
9~14行:在idt表每一項中都放置該描述符,共 256 項。內(nèi)核在隨后的初始化過程中會替換那些真正使用的中斷描述符項。
中斷處理過程 ignore_int
/* This is the default interrupt "handler" :-) */ int_msg:.asciz "Unknown interrupt\n\r" .align 2 ignore_int:pushl %eaxpushl %ecxpushl %edxpush %ds # 這里請注意ds,es,fs,gs等雖然是16位的寄存器,# 但仍然會以32位的形式入棧,即需要占用4個字節(jié)的棧空間。 push %espush %fs # 以上用于保存寄存器movl $0x10,%eax # 0x10是數(shù)據(jù)段選擇子mov %ax,%dsmov %ax,%esmov %ax,%fs # ds,es,fs均指向數(shù)據(jù)段pushl $int_msg call printk # 該函數(shù)在 kernel/printk.c 中popl %eax # 清理參數(shù)pop %fspop %espop %dspopl %edxpopl %ecxpopl %eaxiret第17行:把 printk 函數(shù)的參數(shù)入棧。注意:若符號 int_msg 前不加 $,則表示把 int_msg 符號處的雙字Unkn入棧。
第18行:調(diào)用 printk 函數(shù),該函數(shù)在 kernel/printk.c 中,以后再具體分析。
第19行:清理參數(shù) $int_msg.
說明:匯編程序調(diào)用C函數(shù)時,函數(shù)的入口參數(shù)使用棧來傳送,參數(shù)的傳遞順序是從右到左。調(diào)用者負責(zé)清除參數(shù)占用的棧空間。C函數(shù)的返回值如果是32位整數(shù),則保存在eax寄存器;如果是64位整數(shù),則保存在edx:eax寄存器。
具體可以參考我的博文: 在匯編程序中調(diào)用C函數(shù)
三、設(shè)置全局描述符表(GDT),加載 GDTR
call setup_gdt setup_gdt:lgdt gdt_descr # 加載GDTRretLGDT指令的格式是:
LGDT m16&32
該指令的操作數(shù)是一個 48 位(6字節(jié))的內(nèi)存區(qū)域。在這6字節(jié)的內(nèi)存區(qū)域中,要求前(低)16位是 GDT 的界限值,后(高)32 位是 GDT 的基地址 。該指令在實模式和保護模式下都可以執(zhí)行。
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 | 代碼段 | 0xFFF | 1(表示4KB) | 0 | 非一致,可讀 |
| 2 | 0x10 | 數(shù)據(jù)段 | 0xFFF | 1(表示4KB) | 0 | 向上擴展,可寫 |
段長度可以這樣算:(Limit + 1)* 4KB = (0xFFF + 1) * 4KB = 0x1000 * 4KB = 16MB
四、重新加載段寄存器
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 lss stack_start,%esp由于段描述符中的段限長(Limit)從setup.s中的 8MB 改成了本程序設(shè)置的 16MB,因此這里再次對所有段寄存器執(zhí)行加載操作是必須的。另外,如果不對 CS 再次加載,那么在執(zhí)行到第1行時,CS段寄存器的“描述符高速緩存器”中的段限長還是 8MB。這樣看來應(yīng)該重新加載CS。
但是由于 setup.s 中的代碼段描述符與本程序中重新設(shè)置的代碼段描述符除了段限長以外其余部分完全一樣,8MB 的限長在內(nèi)核初始化階段不會有問題,而且在以后內(nèi)核執(zhí)行段間跳轉(zhuǎn)時會重新加載 CS,因此這里沒有加載它并沒有讓程序出錯。
針對該問題,目前內(nèi)核中就在movl $0x10,%eax之前添加了一條長跳轉(zhuǎn)指令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)。這條指令會跳轉(zhuǎn)到第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 地址線是否已開啟。采用的方法是向內(nèi)存地址 0x0_0000 處寫入任意一個數(shù)值,然后看內(nèi)存地址 Ox10_0000(1M)處是否也是這個數(shù)值。如果一直相同的話,就一直比較下去。死機表示 A20 線沒有選通。
六、檢測x87協(xié)處理器
為了彌補x86系列在進行浮點運算時的不足,Intel于1980年推出了x87系列數(shù)學(xué)協(xié)處理器,那時x87是一個外置的、可選的芯片。1989年,Intel發(fā)布了486處理器。從486開始,以后的CPU一般都內(nèi)置了協(xié)處理器。這樣,對于486以前的計算機而言,操作系統(tǒng)檢測x87協(xié)處理器是否存在就非常必要了。
注:1991年,一名21歲的就讀于芬蘭赫爾辛基大學(xué)的計算機科學(xué)專業(yè)學(xué)生—— Linus Torvalds 基于 gcc、bash 開發(fā)了針對 386 機器的 Liniux內(nèi)核。
下面這段程序用于檢查數(shù)學(xué)協(xié)處理器芯片是否存在。方法是修改控制寄存器CR0,在假設(shè)協(xié)處理器存在的情況下執(zhí)行一個協(xié)處理器指令,如果出錯的話則說明協(xié)處理器芯片不存在。
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位,其他位都清零。
PE 指示是否開啟保護模式。PG 指示是否開啟分頁。
關(guān)于ET,Intel 手冊如是說:
Extension Type (bit 4 of CR0). Reserved in the P6 family and Pentium ? processors. (In the P6 family processors, this flag is hardcoded to 1.) In the Intel 386? and Intel 486? processors, this flag indicates support of Intel 387 DX math coprocessor instructions when set.
第3行,設(shè)置MP=1.
這塊我不是很明白,根據(jù)下面的表格,在數(shù)學(xué)協(xié)處理器存在的時候,推薦設(shè)置EM=0,MP=1.
既然作者的意圖是假設(shè)數(shù)學(xué)協(xié)處理器存在,那么就設(shè)置 EM=0,MP=1 吧。
check_x87:fninit # 向協(xié)處理器發(fā)出初始化命令fstsw %ax # 把FPU的狀態(tài)字保存到AX中# 初始化后狀態(tài)字應(yīng)該為0,否則說明協(xié)處理器不存在cmpb $0,%alje 1f # 存在則跳轉(zhuǎn)到標號1處movl %cr0,%eax xorl $6,%eax # 把 eax 的值和 0110b 異或movl %eax,%cr0ret.align 2 1: .byte 0xDB,0xE4 /* fsetpm for 287, ignored by 387 */ret第12行:0xDB,0xE4這兩個字節(jié)是 80287 協(xié)處理器指令 fsetpm 的機器碼。其作用是把 80287 設(shè)置為保護模式。80387 無需該指令,它會把該指令看作是空操作。
關(guān)于異或
按位異或的3個特點
按位異或的幾個常見用途
1. 使某些特定的位翻轉(zhuǎn)
? 例如要使 EAX 的 b1 位和 b2 位翻轉(zhuǎn):
EAX = EAX ^ 00000110
? 代碼第8行就是這種用法,把 EM 和 MP 翻轉(zhuǎn)。
2. 不使用臨時變量就可以實現(xiàn)兩個值的交換
? 例如 a=11110000,b=00001111,要交換a、b的值,可通過下列語句實現(xiàn):
a = a^b; //a=11111111 b = b^a; //b=11110000 a = a^b; //a=000011113. 在匯編語言中經(jīng)常用于將變量置零
? xor eax,eax
4. 快速判斷兩個值是否相等
? 例如判斷兩個整數(shù)a、b是否相等,可通過下列語句實現(xiàn):
? return ((a ^ b) == 0);
關(guān)于.align
.align是匯編語言指示符。其含義是邊界對齊調(diào)整。”2”表示把隨后的代碼或數(shù)據(jù)的偏移位置調(diào)整到地址值最后 2 比特位為零的位置,即按 4(=2^2)字節(jié)方式對齊內(nèi)存地址。不過現(xiàn)在 GNU as 直接寫出對齊的值而非 2 的冪次。使用該指示符的目的是為了提高 32 位 CPU 訪問內(nèi)存中代碼或數(shù)據(jù)的效率。
七、開啟分頁,跳轉(zhuǎn)到 main()
Linus 將內(nèi)核的頁表直接放在頁目錄之后,使用了4個頁表來尋址16MB的物理內(nèi)存。如果你有多于16MB的內(nèi)存,就需要在這里進行擴充修改。關(guān)于分頁機制,說來話長,不了解的朋友可以參考我的博文:
x86分頁機制
Linus 在物理地址0x0處開始存放1頁頁目錄和4頁頁表。頁目錄是系統(tǒng)所有進程公用的,而其后的4頁頁表則屬于內(nèi)核專用,它們把線性地址 0x000000~0xFFFFFF 一一映射到物理地址 0x000000~0xFFFFFF。
.org 0x1000 #從偏移 0x1000 處開始放第1個頁表(偏移0開始處將存放頁目錄) pg0:.org 0x2000 #從偏移 0x2000 處開始放第2個頁表 pg1:.org 0x3000 #從偏移 0x3000 處開始放第3個頁表 pg2:.org 0x4000 #從偏移 0x4000 處開始放第4個頁表 pg3:.org 0x5000 #定義下面的內(nèi)存數(shù)據(jù)塊從偏移 0x5000 處開始.ORG偽指令用來表示起始的偏移地址,緊接著ORG的數(shù)值就是偏移地址的起始值。ORG偽操作常用來指定數(shù)據(jù)的存儲地址,有時也用來指定代碼段的起始地址。更詳細的解釋可以參考我的博文:
ORG 偽指令
tmp_floppy_area:.fill 1024,1,0 #共保留1024項,每項1字節(jié),填充數(shù)值0fill偽指令的格式是 .fill repeat,size,value
表示產(chǎn)生 repeat 個大小為 size 字節(jié)的重復(fù)拷貝。size 最大是 8,size 字節(jié)的值是 value.
“當(dāng) DMA (直接存儲器訪問)不能訪問緩沖塊時,tmp_floppy_area 內(nèi)存塊就可供軟盤驅(qū)動程序使用。其地址需要對齊,這樣就不會跨越 64KB 邊界。”
這是書上的話,我不甚理解。暫不深究,以后再說。
為調(diào)用main()函數(shù)做準備
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 # 設(shè)置頁目錄和頁表,并開啟分頁 L6:jmp L6 # main should never return here, but# just in case, we know what happens.2~6行:為跳轉(zhuǎn)到 init/main.c 中的 main() 函數(shù)作準備工作。
2~4行:前3個入棧 0 值應(yīng)該分別表示 envp、argv 指針和 argc 的值,但 main() 沒有用到。
第5行:壓入返回地址。模擬調(diào)用(其實是使用JMP指令) main.c 程序時首先將返回地址入棧的操作,如果 main.c 程序真的退出,就會返回到標號 L6 處繼續(xù)執(zhí)行下去,即死循環(huán)。
第6行:壓入 main() 函數(shù)代碼的地址。當(dāng)后面執(zhí)行 ret 指令時,就會彈出 main() 的地址,并把控制權(quán)轉(zhuǎn)移到 init/main.c 程序中。
依然可以參考我的那篇博文: 在匯編程序中調(diào)用C函數(shù)
設(shè)置頁目錄和頁表
setup_paging:movl $1024*5,%ecx # 每個頁表占用1024個雙字(4B),共5個頁表xorl %eax,%eax # eax = 0xorl %edi,%edi # edi = 0cldrep;stosl # eax -> es:[edi],edi每次增加4,重復(fù)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
7~10行,填寫頁目錄表的前4項。關(guān)于表項的格式,可以參考我的博文 頁目錄項和頁表項
| 0 | P | 存在位。為1表示頁表或者頁位于內(nèi)存中,為0表示不在內(nèi)存中,必須先予以創(chuàng)建或者從磁盤調(diào)入內(nèi)存后方可使用。 |
| 1 | R/W | 讀寫標志。為1表示頁面可以被讀寫,為0表示只讀。當(dāng)處理器運行在0、1、2特權(quán)級時,此位不起作用。頁目錄中的這個位對其所映射的所有頁面起作用。 |
| 2 | U/S | 用戶/超級用戶標志。為1時,允許所有特權(quán)級別的程序訪問;為0時,僅允許特權(quán)級為0、1、2的程序訪問。頁目錄中的這個位對其所映射的所有頁面起作用。 |
所以,根據(jù)上表,可以知道7~10行中的“+7”表示:頁表存在,可讀可寫,允許所有特權(quán)級別的程序訪問。
填寫后示意圖如下:
以上代碼的目的是填寫4個頁表。
頁表項的格式如下圖,0、1、2比特位的含義見前文的表格。
movl $pg3+4092,%edi
一張頁表最多可以容納1024個表項,每項占4個字節(jié)。下圖左邊是表項的序號,從0到1023,右邊是偏移地址(= 序號*4),4092是最后一個表項的偏移地址。
上面的代碼表示把頁表3(最后一個頁表)的最后一項的地址傳入edi. 作者的意圖是從最后一個表項開始,倒著填寫,直到填完頁表0的第0個表項。
movl $0xfff007,%eax中的0xfff007是頁表3的最后一項的值,“7”就不用再解釋了,解釋一下為什么是0xfff000:
頁表的每一項對應(yīng)4KB(2^12=4K)的內(nèi)存,一個頁表有1024(=1K)項,共對應(yīng)4KB*1K=4MB的內(nèi)存。代碼中安排了4個頁表,即共可以映射4*4MB=16M內(nèi)存。
16M - 4K = 0xFFF000
或者:
16M - 1 = 0x1000000-1 = 0xFFFFFF
0xFFFFFF & 0xFFFFF000 = 0xFFF000
jge 用于有符號數(shù)大小的比較,當(dāng) DEST(這里是eax) 大于等于 SRC(這里是0x1000) 則跳轉(zhuǎn)。當(dāng) eax=0x1007時,eax>=0x1000,跳轉(zhuǎn)之后 eax=0x0007,這時候條件不再成立,則結(jié)束跳轉(zhuǎn)。所以,最后填寫的表項值是 0x0007。
xorl %eax,%eax /* pg_dir is at 0x0000 */movl %eax,%cr3 # 把頁目錄的物理地址寫入CR3movl %cr0,%eaxorl $0x80000000,%eaxmovl %eax,%cr0 #以上三行使 CR0 的 PG=1, 開啟分頁機制retCR3寄存器的格式如下:
從movl %eax,%cr0執(zhí)行后,段部件產(chǎn)生的地址就不再被看成物理地址,而是要送往頁部件進行變換,以得到真正的物理地址。
注意,現(xiàn)在內(nèi)核工作在分頁機制的一個特殊情況下,線性地址和經(jīng)過頁部件轉(zhuǎn)換后的物 理地址相同,這是作者精心安排的。
最后的ret指令有2個作用。
在改變分頁處理標志后要求使用轉(zhuǎn)移指令刷新預(yù)取指令隊列,這里用的是返回指令ret。
將之前壓入棧中的 main() 程序入口地址彈出,并跳轉(zhuǎn)到 init/main.c 程序去運行。
?
本程序到這里就分析結(jié)束了。
參考資料
《Linux內(nèi)核完全剖析》(趙炯,機械工業(yè)出版社,2006)
與50位技術(shù)專家面對面20年技術(shù)見證,附贈技術(shù)全景圖總結(jié)
以上是生活随笔為你收集整理的head.s 分析——Linux-0.11 学习笔记(三)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 百万数据php7取出循环_PHP7带来了
- 下一篇: kernel_mktime() 详解 —