目錄
- 初始化IDT、IDTR和GDT、GDTR
- 檢查協處理器并設置CR0寄存器
- 初始化頁表和CR3寄存器,開啟分頁
初始化IDT、IDTR和GDT、GDTR
startup_32:movl $0x10,%eaxmov %ax,%dsmov %ax,%esmov %ax,%fsmov %ax,%gslss _stack_start,%espcall setup_idtcall setup_gdtmovl $0x10,%eax # reload all the segment registersmov %ax,%ds # after changing gdt. CS was alreadymov %ax,%es # reloaded in 'setup_gdt'mov %ax,%fsmov %ax,%gslss _stack_start,%espxorl %eax,%eax
1: incl %eax # check that A20 really IS enabledmovl %eax,0x000000 # loop forever if it isn'tcmpl %eax,0x100000je 1b
個人閱讀上面代碼的知識點:
- lss _stack_start,%esp 首先lss是遠指針加載指令,不了解的查deepseek就懂了。“_stack_start”這個標號找了很久都找不到在哪兒定義的,搜索整個源碼都沒有。最后還是求助deepseek終于搞懂了(deepseek真自學神器)。要把“_stack_start”中下劃線去掉,直接搜索“stack_start”,才知道這個標號是定義在kernel/sched.c中的結構體變量名。之所以要加下劃線,是因為早期的編譯器編譯時會在C中變量名前面加,現在不加了 。由于對kernel代碼不懂,所以沒過多研究,知道是在初始化棧就行了。
call setup_idt 調用子程序初始化IDT表,通過指令lidt將IDT表的長度和地址加載到IDTR寄存器。
還處在內核初始化階段,所以只是簡單的將IDT表中的描述符初始化為同一個,都指向ignore_int這個中斷處理程序。ignore_int子程序就是本文件中,功能就是打印一段字符。- lidt指令加載的IDT表的地址是線性地址,初始化階段,此時還沒開啟分頁模式,線性地址等于物理地址。開啟分頁后,IDTR中的線性地址要經過MMU查頁表轉化成IDT表在內存中的物理地址。
- 需要對IDT表描述符了解,IDT表描述符總共8字節,小端法存入內存,先存低4字節,再高4字節,先低2字節,再高2字節。之所以提小端法是因為我一開始沒注意,把描述符的選擇子判斷錯了。
call setup_gdt 調用子程序初始化GDT表,通過指令lgdt將GDT表的長度和地址加載到GDTR寄存器。
GDT表自己構造。- lgdt指令加載的GDT表的地址是線性地址,只不過初始化階段,此時還沒開啟分頁模式,線性地址等于物理地址。開啟分頁后,GDTR中的線性地址要經過MMU查頁表轉化成GDT表在內存中的物理地址。
- 構造GDT表時,GDT表中的描述符大小為8字節,第一個描述符為全0,后面描述符的基地址base=0X00000000,limit=最大值(0X000FFF),把內存管理就設置成了平坦模式, 現代操作系統支持這種模式,這樣一個進程的
代碼段、數據段、棧段就共享同一個線性地址空間了,方便虛擬內存管理
。
- 保護模式下,指令執行過程中,查詢IDT表、GDT表的工作是有硬件來實現的,不是軟件模擬的,所以對IDT、GDT中描述符的結構也是硬件規定的。我們在構造這兩個表時要按照硬件的規定來。
- movl %eax,0x000000;cmpl %eax,0x100000 這段是負責檢查A20地址線是否打開,如果沒打開那么0X100000地址就等于0X000000地址,cmpl比較的結果就是相等,那就卡在這個執行死循環。
- je 1b 這個條件跳轉指令開始看了我也很蒙,還是靠deepseek,這是以前的寫法“b”表示滿足條件跳轉到后面的標號,“f”表示滿足條件跳轉到前面的標號。b和f指示跳轉的方向的。現在好像不用這種用法了。
檢查協處理器并設置CR0寄存器
movl %cr0,%eax # check math chipandl $0x80000011,%eax # Save PG,PE,ET
/* "orl $0x10020,%eax" here for 486 might be good */orl $2,%eax # set MPmovl %eax,%cr0call check_x87jmp after_page_tables/** We depend on ET to be correct. This checks for 287/387.*/
check_x87:fninitfstsw %axcmpb $0,%alje 1f /* no coprocessor: have to set bits */movl %cr0,%eaxxorl $6,%eax /* reset MP, set EM */movl %eax,%cr0ret
個人閱讀上面代碼的知識點:
- 了解CR0寄存器,CR0寄存器中,PE位開啟保護模式、PG位開啟分頁機制、WP位寫保護位。還有其它涉及協處理器的位,我一知半解就不寫了。上面這段代碼主要就是檢查協處理器,然后進行設置。 指令call check_x87就是調用子程序檢查是否存在287/387協處理器。
- fninit fstsw這兩個是協處理器指令,自己查deepseek能看懂,不贅述了。check_x87這個子程序功能就是檢查協處理器的。
初始化頁表和CR3寄存器,開啟分頁
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.
-----------------------------------------------------------------------------------------
setup_paging:movl $1024*5,%ecx /* 5 pages - pg_dir+4 page tables */xorl %eax,%eaxxorl %edi,%edi /* pg_dir is at 0x000 */cld;rep;stoslmovl $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 */
個人閱讀上面代碼的知識點:
- jmp setup_paging 跳轉到初始化頁表的子程序。該指令前面的push指令是在壓棧,pushl $main 目的是從setup_paging子程序返回時,ret指令能返回到main程序。 我還沒看過main.c的代碼,不贅述了。功能就是結束head.s的初始化任務。開啟main.c
- 強化我自己的一個知識點,call和jmp都是跳轉指令,區別就是call跳轉前要將返回地址壓棧,jmp無需壓棧直接跳轉無法返回。開始我還疑問為什么不用call指令,然后返回到main.c,仔細一想call指令是將下一條指令的地址壓棧,只能返回到本源文件call的下一條指令。而我們需要的是結束head.s,返回到另一個源文件main.c。所以通過pushl $main壓入返回地址,不用call調用,而用jmp直接跳轉到setup_paging初始化頁表子程序。
setup_paging子程序初始化頁表、CR3寄存器,置CR0寄存器的PG為1開啟分頁
。了解頁表和頁表項,即使有些匯編指令不熟,查一下就是看懂這段了。- stosl指令,我忽略了它執行時會自動增加EDI。stos、movs、cmps、scas都是字符串操作指令,可以順便都了解一下。偉大的deepseek很好用的。
- CR3寄存器是用來存放頂級頁表的物理地址的。MMU將線性地址轉換位物理地址時需要通過CR3寄存器找到頁表進行映射。CR3寄存器的改變,會引起TLB表的改變。CR3改變代表頁表的切換,TLB相當于部分頁表項的緩存,自然也要切換。
.org 0x1000
pg0:.org 0x2000
pg1:.org 0x3000
pg2:.org 0x4000
pg3:.org 0x5000
- 剛看見這段代碼時,感覺整齊有規律,就是不知道作用是什么。即使知道了.org這個偽指令的功能,也不知道寫這段代碼的意義。直到最后看到初始化頁表的子程序setup_paging。補充點因為這段代碼學到的零碎知識點。
- (1).org(Origin)是匯編語言中的一條偽指令(Directive),用于指定程序或數據在內存中的起始地址。 在需要直接控制內存布局的場景(如裸機開發、嵌入式開發)中,.org 是一個簡單有效的工具,但在高層編程中通常由鏈接器管理地址分配。
- (2) 這段代碼其實就是在劃分頁表的頁。它們之間的地址差是0X1000(4KB),從0地址開始到0X5000共20KB,分成5個頁,每頁4KB,第一個頁是頁目錄表,其余4個頁都是普通頁表。setup_paging子程序的開始將5個頁20KB用0填充,接著構造4個普通物理頁的頁表項填充到0地址的頁目錄表,最后構造一個普通物理頁的頁表項填充4個普通物理頁。
- (3)你還可以觀察到初始化IDT、IDTR和GDT、GDTR的代碼,還有檢查協處理器并設置CR0寄存器的代碼都寫在這段代碼之前,因為那些初始化、檢查的代碼執行完就沒用了,它們所在的內存空間可以被頁表覆蓋; 結束head.s跳轉到main.c的代碼、初始化頁表的子程序setup_paging、IDT表GDT表的位置,包括中斷處理程序ignore_int的代碼位置都在“.org 0X5000”之后,就是防止誤操作,初始化頁表時覆蓋比較重要的代碼、表空間。
- (4) 內存中IDT表、GDT表,各有1個,所有應用程序和操作系統共用;頁表有多個,每個應程序都有自己的頁表,任務切換時,通過修改CR3寄存器切換頁表,同時TLB表也要刷新。