1. 程式人生 > 實用技巧 >linux0.11boot之head.s

linux0.11boot之head.s

/*
* linux/boot/head.s
*
* (C) 1991 Linus Torvalds
*/

/*
* 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 位啟動程式碼。
* 注意!!! 32 位啟動程式碼是從絕對地址0x00000000 開始的,這裡也同樣是頁目錄將存在的地方,
* 因此這裡的啟動程式碼將被頁目錄覆蓋掉。
*/
.text
.globl _idt,_gdt,_pg_dir,_tmp_floppy_area
_pg_dir: # 頁目錄將會存放在這裡。 startup_32: # 18-22 行設定各個資料段暫存器。 movl $0x10,%eax # 對於GNU 彙編來說,每個直接數要以'$'開始,否則是表示地址。 # 每個暫存器名都要以'%'開頭,eax 表示是32 位的ax 暫存器。 # 再次注意!!! 這裡已經處於32 位執行模式,因此這裡的$0x10 並不是把地址0x10 裝入各個 # 段暫存器,它現在其實是全域性段描述符表中的偏移值,或者更正確地說是一個描述符表項 # 的選擇符。有關選擇符的說明請參見setup.s 中193 行下的說明。這裡$0x10 的含義是請求 # 特權級0(位0-1=0)、選擇全域性描述符表(位2=0
)、選擇表中第2 項(位3-15=2)。它正好指 在當前的Linux 作業系統中,gas 和gld 已經分別更名為as 和ld。 # 向表中的資料段描述符項。(描述符的具體數值參見前面setup.s 中212,213 行) # 下面程式碼的含義是:置ds,es,fs,gs 中的選擇符為setup.s 中構造的資料段(全域性段描述符表 # 的第2 項)=0x10,並將堆疊放置在資料段中的_stack_start 陣列內,然後使用新的中斷描述 # 符表和全域性段描述表.新的全域性段描述表中初始內容與setup.s 中的完全一樣。 mov %ax,%ds mov %ax,%es mov %ax,%fs
mov %ax,%gs lss _stack_start,%esp # 表示_stack_start??ss:esp,設定系統堆疊。 # stack_start 定義在kernel/sched.c,69 行。 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 # 因為修改了gdt,所以需要重新裝載所有的段暫存器。 mov %ax,%gs # CS 程式碼段暫存器已經在setup_gdt 中重新載入過了。 lss _stack_start,%esp # 32-36 行用於測試A20 地址線是否已經開啟。採用的方法是向記憶體地址0x000000 處寫入任意 # 一個數值,然後看記憶體地址0x100000(1M)處是否也是這個數值。如果一直相同的話,就一直 # 比較下去,也即死迴圈、宕機。表示地址A20 線沒有選通,結果核心就不能使用1M 以上記憶體。 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 去。 /* * 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 # check math chip andl $0x80000011,%eax # Save PG,PE,ET /* "orl $0x10020,%eax" here for 486 might be good */ orl $2,%eax # set MP movl %eax,%cr0 call check_x87 jmp after_page_tables # 跳轉到135 行。 /* * We depend on ET to be correct. This checks for 287/387. */ /* * 我們依賴於ET 標誌的正確性來檢測287/387 存在與否。 */ check_x87: fninit fstsw %ax cmpb $0,%al je 1f /* no coprocessor: have to set bits */ movl %cr0,%eax # 如果存在的則向前跳轉到標號1 處,否則改寫cr0。 xorl $6,%eax /* reset MP, set EM */ movl %eax,%cr0 ret .align 2 # 這裡".align 2"的含義是指儲存邊界對齊調整。"2"表示調整到地址最後2 位為零, # 即按4 位元組方式對齊記憶體地址。 1: .byte 0xDB,0xE4 /* fsetpm for 287, ignored by 387 */ # 287 協處理器碼。 ret /* * 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 指令)。真正實用的中斷門以後再安裝。當我們在其它地方認為一切 * 都正常時再開啟中斷。該子程式將會被頁表覆蓋掉。 */ # 中斷描述符表中的項雖然也是8 位元組組成,但其格式與全域性表中的不同,被稱為門描述符 # (Gate Descriptor)。它的0-1,6-7 位元組是偏移量,2-3 位元組是選擇符,4-5 位元組是一些標誌。 setup_idt: lea ignore_int,%edx # 將ignore_int 的有效地址(偏移值)值??edx 暫存器 movl $0x00080000,%eax # 將選擇符0x0008 置入eax 的高16 位中。 movw %dx,%ax /* selector = 0x0008 = cs */ # 偏移值的低16 位置入eax 的低16 位中。此時eax 含有 #門描述符低4 位元組的值。 movw $0x8E00,%dx /* interrupt gate - dpl=0, present */ # 此時edx 含有門描述符高4 位元組的值。 lea _idt,%edi # _idt 是中斷描述符表的地址。 mov $256,%ecx rp_sidt: movl %eax,(%edi) # 將啞中斷門描述符存入表中。 movl %edx,4(%edi) addl $8,%edi # edi 指向表中下一項。 dec %ecx jne rp_sidt lidt idt_descr # 載入中斷描述符表暫存器值。 ret /* * 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 # 載入全域性描述符表暫存器(內容已設定好,見232-238 行)。 ret /* * 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 的記憶體,就需要在這裡進行擴充修改。 */ # 每個頁表長為4 Kb 位元組,而每個頁表項需要4 個位元組,因此一個頁表共可以存放1000 個表項, # 如果一個表項定址4 Kb 的地址空間,則一個頁表就可以定址4 Mb 的實體記憶體。 # 頁表項的格式為:項的前0-11 位存放一些標誌,如是否在記憶體中(P 位0)、讀寫許可(R/W 位1)、 # 普通使用者還是超級使用者使用(U/S 位2)、是否修改過(是否髒了)(D 位6)等;表項的位12-31 是 # 頁框地址,用於指出一頁記憶體的物理起始地址。 .org 0x1000 # 從偏移0x1000 處開始是第1 個頁表(偏移0 開始處將存放頁表目錄)。 pg0: .org 0x2000 pg1: .org 0x3000 pg2: .org 0x4000 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 isn't * on a 64kB border. */ /* 當DMA(直接儲存器訪問)不能訪問緩衝塊時,下面的tmp_floppy_area 記憶體塊 * 就可供軟盤驅動程式使用。其地址需要對齊調整,這樣就不會跨越64kB 邊界。 */ _tmp_floppy_area: .fill 1024,1,0 # 共保留1024 項,每項1 位元組,填充數值0。 # 下面這幾個入棧操作(pushl)用於為呼叫/init/main.c 程式和返回作準備。 # 前面3 個入棧指令不知道作什麼用的,也許是Linus 用於在除錯時能看清機器碼用的?。 # 139 行的入棧操作是模擬呼叫main.c 程式時首先將返回地址入棧的操作,所以如果 # main.c 程式真的退出時,就會返回到這裡的標號L6 處繼續執行下去,也即死迴圈。 # 140 行將main.c 的地址壓入堆疊,這樣,在設定分頁處理(setup_paging)結束後 # 執行'ret'返回指令時就會將main.c 程式的地址彈出堆疊,並去執行main.c 程式去了。 after_page_tables: pushl $0 # These are the parameters to main :-) pushl $0 # 這些是呼叫main 程式的引數(指init/main.c)。 pushl $0 pushl $L6 # return address for main, if it decides to. pushl $_main # '_main'是編譯程式對main 的內部表示方法。 jmp setup_paging # 跳轉至第198 行。 L6: jmp L6 # main should never return here, but # just in case, we know what happens. /* This is the default interrupt "handler" :-) */ /* 下面是預設的中斷“向量控制代碼”? */ int_msg: .asciz "Unknown interrupt\n\r" # 定義字串“未知中斷(回車換行)”。 .align 2 # 按4 位元組方式對齊記憶體地址。 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'是printk 編譯後模組中的內部表示法。 popl %eax pop %fs pop %es pop %ds popl %edx popl %ecx popl %eax iret # 中斷返回(把中斷呼叫時壓入棧的CPU 標誌暫存器(32 位)值也彈出)。 /* * 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 # 按4 位元組方式對齊記憶體地址邊界。 setup_paging: # 首先對5 頁記憶體(1 頁目錄 + 4 頁頁表)清零 movl $1024*5,%ecx /* 5 pages - pg_dir+4 page tables */ xorl %eax,%eax xorl %edi,%edi /* pg_dir is at 0x000 */ # 頁目錄從0x000 地址開始。 cld;rep;stosl # 下面4 句設定頁目錄中的項,我們共有4 個頁表所以只需設定4 項。 # 頁目錄項的結構與頁表中項的結構一樣,4 個位元組為1 項。參見上面113 行下的說明。 # "$pg0+7"表示:0x00001007,是頁目錄表中的第1 項。 # 則第1 個頁表所在的地址 = 0x00001007 & 0xfffff000 = 0x1000; # 第1 個頁表的屬性標誌 = 0x00001007 & 0x00000fff = 0x07,表示該頁存在、使用者可讀寫。 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 /* --------- " " --------- */ # 下面6 行填寫4 個頁表中所有項的內容,共有:4(頁表)*1024(項/頁表)=4096 項(0 - 0xfff), # 也即能對映實體記憶體 4096*4Kb = 16Mb。 # 每項的內容是:當前項所對映的實體記憶體地址 + 該頁的標誌(這裡均為7)。 # 使用的方法是從最後一個頁表的最後一項開始按倒退順序填寫。一個頁表的最後一項在頁表中的 # 位置是1023*4 = 4092。因此最後一頁的最後一項的位置就是$pg3+4092。 movl $pg3+4092,%edi # edi??最後一頁的最後一項。 movl $0xfff007,%eax /* 16Mb - 4096 + 7 (r/w user,p) */ # 最後1 項對應實體記憶體頁面的地址是0xfff000, # 加上屬性標誌7,即為0xfff007. std # 方向位置位,edi 值遞減(4 位元組)。 1: stosl /* fill pages backwards - more efficient :-) */ subl $0x1000,%eax # 每填寫好一項,實體地址值減0x1000。 jge 1b # 如果小於0 則說明全添寫好了。 # 設定頁目錄基址暫存器cr3 的值,指向頁目錄表。 xorl %eax,%eax /* pg_dir is at 0x0000 */ # 頁目錄表在0x0000 處。 movl %eax,%cr3 /* cr3 - page directory start */ # 設定啟動使用分頁處理(cr0 的PG 標誌,位31) movl %cr0,%eax orl $0x80000000,%eax # 添上PG 標誌。 movl %eax,%cr0 /* set paging (PG) bit */ ret /* this also flushes prefetch-queue */ # 在改變分頁處理標誌後要求使用轉移指令重新整理預取指令佇列,這裡用的是返回指令ret。 # 該返回指令的另一個作用是將堆疊中的main 程式的地址彈出,並開始執行/init/main.c 程式。 # 本程式到此真正結束了。 .align 2 # 按4 位元組方式對齊記憶體地址邊界。 .word 0 idt_descr: #下面兩行是lidt 指令的6 位元組運算元:長度,基址。 .word 256*8-1 # idt contains 256 entries .long _idt .align 2 .word 0 gdt_descr: # 下面兩行是lgdt 指令的6 位元組運算元:長度,基址。 .word 256*8-1 # so does gdt (not that that's any .long _gdt # magic number, but it works for me :^) .align 3 # 按8 位元組方式對齊記憶體地址邊界。 _idt: .fill 256,8,0 # idt is uninitialized # 256 項,每項8 位元組,填0。 # 全域性表。前4 項分別是空項(不用)、程式碼段描述符、資料段描述符、系統段描述符,其中 # 系統段描述符linux 沒有派用處。後面還預留了252 項的空間,用於放置所建立任務的 # 區域性描述符(LDT)和對應的任務狀態段TSS 的描述符。 # (0-nul, 1-cs, 2-ds, 3-sys, 4-TSS0, 5-LDT0, 6-TSS1, 7-LDT1, 8-TSS2 etc...) _gdt: .quad 0x0000000000000000 /* NULL descriptor */ .quad 0x00c09a0000000fff /* 16Mb */ # 程式碼段最大長度16M。 .quad 0x00c0920000000fff /* 16Mb */ # 資料段最大長度16M。 .quad 0x0000000000000000 /* TEMPORARY - don't use */ .fill 252,8,0 /* space for LDT's and TSS's etc */