這篇博文是對Lab 1中的Exercise 2的解答~
Lab 1 Exercise 2:
使用GDB的'si'命令,去追蹤ROM BIOS幾條指令,並且試圖去猜測,它是在做什么。但是不需要把每個細節都弄清楚。
答:
在這里我們將盡可能的去分析每一條指令,由於題目中說我們只需要知道BIOS的幾條指令在做什么就夠了,所以我們也會盡可能的去分析,由於能力有限,這里面有很多問題還沒有解決,希望大家諒解。以后有機會會盡可能的把沒分析的命令去補全。
首先注意這里是緊接着Lab 1 Part 1.2那篇博文中建立了qemu和gdb調試環境之后進行了,至於如何建立調試環境,大家可以再回到那篇博文中具體查看~
在Lab 1 Part 1.2中我們建立了調試環境后,在運行gdb的窗口中會將顯示出PC機啟動后運行BIOS的第一條命令,如下:
1. 0xffff0: ljmp $0xf000, $0xe05b
這是運行的第一條指令,是一條跳轉指令,跳轉到0xfe05b地址處。至於要知道這個地址是怎么通過指令中的值計算出來的,我們需要先知道,當PC機啟動時,CPU運行在實模式(real mode)下,而當進入操作系統內核后,將會運行在保護模式下(protected mode)。實模式是早期CPU,比如8088處理器的工作模式,這類處理器由於只有20根地址線,所以它們只能訪問1MB的內存空間。但是CPU也在不斷的發展,之后的80286/80386已經具備32位地址總線,能夠訪問4GB內存空間,為了能夠很好的管理這么大的內存空間,保護模式被研發出來。所以現代處理器都是工作在保護模式下的。但是為了實現向后兼容性,即原來運行在8088處理器上的軟件仍舊能在現代處理器上運行,所以現代的CPU都是在啟動時運行於實模式,啟動完成后運行於保護模式。BIOS就是PC剛啟動時運行的軟件,所以它必然工作在實模式。
至於這兩個模式下的運行原理,可以看這個鏈接:http://blog.csdn.net/zdwzzu2006/article/details/4030948
這里先簡單介紹一下地址的計算方法,在實模式下,指令中出現的地址都是采用
(段基址:段內偏移)
的形式的。其中這兩個字段的值,通常是存放寄存器中的。其中段基址必須放在段寄存器中,包括CS(代碼段), DS(數據段), SS(堆棧段), ES(擴展段)。不同的段寄存器存放的是你程序不同的段的起始地址。
但是由於8088CPU中寄存器都是16位,而CPU地址總線是20位的,我們怎么通過16位的寄存器去拼接20位的地址呢?
所以我們需要采用下面的方法:把段寄存器中的值左移4位,形成20位段基址,然后和16位段內偏移相加,就得到了真實地址。比如上面的指令中段寄存器的內容為0xf000,所以真實地址為 0xf000<<4+0xe05b = 0xfe05b。
2. 0xfe05b: cmpl $0x0, $cs:0x6ac8
下一條指令,把0x0這個立即數和$cs:0x6ac8所代表的內存地址處的值比較,至於為什么這樣比較,現在還不是很清楚。其中$cs:0x6ac8就是我們剛剛介紹的在實模式下地址形成的格式,其中$cs就代表CS段寄存器的值。
3. 0xfe062: jne 0xfd2e1
jne指令:如果ZF標志位為0的時候跳轉,即上一條指令cmpl的結果不是0時跳轉,也就是$cs:0x6ac8地址處的值不是0x0時跳轉。
4. 0xfe066: xor %dx, %dx
下一條指令地址是0xfe066,可見上面的跳轉指令並沒有跳轉。這條指令的功能是把dx寄存器清零。
5. 0xfe068: mov %dx %ss 6. 0xfe06a: mov $0x7000, %esp 7. 0xfe070: mov $0xf34d2, %edx 8. 0xfe076: jmp 0xfd15c 9. 0xfd15c: mov %eax, %ecx
接下來的這些指令就是設置一些寄存器的值,具體含義現在不明白..
這里要注意第8條指令,進行了絕對跳轉。
10. 0xfd15f: cli
關閉中斷指令。這個比較好理解,啟動時的操作是比較關鍵的,所以肯定是不能被中斷的。這個關中斷指令用於關閉那些可以屏蔽的中斷。比如大部分硬件中斷。
11. 0xfd160: cld
設置方向標識位為0,表示后續的串操作比如MOVS操作,內存地址的變化方向,如果為0代表從低地址值變為高地址。
具體什么是串操作,可以看這個鏈接:http://www.tyut.edu.cn/kecheng1/2008/site04/courseware/chapter3/3-3-4.html
12. 0xfd161: mov $0x8f, %eax 13. 0xfd167: out %al, $0x70 14. 0xfd169: in $0x71, %al
這三個操作中涉及到兩個新的指令out, in。這兩個操作是用於操作IO端口的。這種IO端口的操作我們后面會經常接觸到,這里大致說下。
CPU與外部設備通訊時,通常是通過訪問,修改設備控制器中的寄存器來實現的。那么這些位於設備控制器當中的寄存器也叫做IO端口。為了方便管理,80x86CPU采用IO端口單獨編址的方式,即所有設備的端口都被命名到一個IO端口地址空間中。這個空間是獨立於內存地址空間的。所以必須采用和訪問內存的指令不一樣的指令來訪問端口。
所以這里引入in,out操作:
in %al, PortAddress 向端口地址為PortAddress的端口寫入值,值為al寄存器中的值
out PortAddres,%al 把端口地址為PortAddress的端口中的值讀入寄存器al中
標准規定端口操作必須要用al寄存器作為緩沖。
那么這三條命令就是要操作端口0x70,0x71,它們對應的是什么設備呢?根據下面的鏈接中所提供的清單(這個連接之后會經常用到,建議大家收藏):http://bochs.sourceforge.net/techspec/PORTS.LST
我們知道了,0x70端口和0x71端口是用於控制系統中一個叫做CMOS的設備,這個設備是一個低功耗的存儲設備,它可以用於在計算機關閉時存儲一些信息,它是由獨立的電池供電的。這個鏈接有詳細介紹http://wiki.osdev.org/CMOS
這個CMOS中可以控制跟PC相關的多個功能,其中最重要的就是時鍾設備(Real Time Clock)的 ,它還可以控制是否響應不可屏蔽中斷NMI(Non-Maskable Interrupt)。
操作CMOS存儲器中的內容需要兩個端口,一個是0x70另一個就是0x71。其中0x70可以叫做索引寄存器,這個8位寄存器的最高位是不可屏蔽中斷(NMI)使能位。如果你把這個位置1,則NMI不會被響應。低7位用於指定CMOS存儲器中的存儲單元地址,所以如果你想訪問第1號存儲單元,並且在訪問時,我要使能NMI,那么你就應該向端口0x70里面送入0b10000001 = 0x81。
即mov $0x81, %al
然后對於這個地址單元的操作,比如讀或者寫就可以由0x71端口完成,比如你現在想從1號存儲單元里面讀出它的值,在完成上面的兩條指令后,就可以輸入這條指令
in $0x71, %al
再回到我們的系統,這三條指令可以看出,它首先關閉了NMI中斷,並且要訪問存儲單元0xF的值,並且把值讀到al中,但是在后面我們發現這個值並沒有被利用,所以可以認為這三條指令是用來關閉NMI中斷的。
15. 0xfd16b: in $0x92, %al 16. 0xfd16d: or $0x2, %al 17. 0xfd16f: out %al, $0x92
這三步操作又是在控制端口,此時被控制的端口號為0x92,通過上面那個鏈接 http://bochs.sourceforge.net/techspec/PORTS.LST
我們可以查看到,它控制的是 PS/2系統控制端口A,而第16,17步的操作明顯是在把這個端口的1號bit置為1。這個端口的bit1的功能是
bit 1= 1 indicates A20 active
即A20位,即第21個地址線被使能,了解實模式和保護模式的同學肯定清楚,如果A20地址線被激活,那么系統工作在保護模式下。但是在之后的boot loader程序中,計算機首先要工作在實模式下啊。所以這里的這個操作,根據網上 http://kernelx.weebly.com/a20-address-line.html 所說應該是去測試可用內存空間。在boot loader之前,它肯定還會轉換回實模式。
18. 0xfd171: lidtw %cs:0x6ab8
lidt指令:加載中斷向量表寄存器(IDTR)。這個指令會把從地址0xf6ab8起始的后面6個字節的數據讀入到中斷向量表寄存器(IDTR)中。中斷是操作系統中非常重要的一部分,有了中斷操作系統才能真正實現進程。每一種中斷都有自己對應的中斷處理程序,那么這個中斷的處理程序的首地址就叫做這個中斷的中斷向量。中斷向量表自然是存放所有中斷向量的表了。關於中斷向量表的介紹,大家可以戳這個鏈接 http://wiki.osdev.org/Interrupt_Descriptor_Table
19. 0xfd177: lgdtw %cs:0x6a74
把從0xf6a74為起始地址處的6個字節的值加載到全局描述符表格寄存器中GDTR中。這個表實現保護模式非常重要的一部分,我們在介紹boot loader時會具體介紹它。
20. 0xfd17d: mov %cr0, %eax 21. 0xfd180: or $0x1, %eax 22. 0xfd184: mov %eax, %cr0
計算機中包含CR0~CR3四個控制寄存器,用來控制和確定處理器的操作模式。其中這三個語句的操作明顯是要把CR0寄存器的最低位(0bit)置1。CR0寄存器的0bit是PE位,啟動保護位,當該位被置1,代表開啟了保護模式。但是這里出現了問題,我們剛剛說過BIOS是工作在實模式之下,后面的boot loader開始的時候也是工作在實模式下,所以這里把它切換為保護模式,顯然是自相矛盾。所以只能推測它在檢測是否機器能工作在保護模式下。
23. 0xfd187: ljmpl $0x8, $0xfd18f 24. 0xfd18f: mov $0x10, %eax 25. 0xfd194: mov %eax, %ds 26. 0xfd196: mov %eax, %es 27. 0xfd198: mov %eax, %ss 28. 0xfd19a: mov %eax, %fs 29. 0xfd19c: mov %eax, %gs
修改這些寄存器的值。這些寄存器都是段寄存器。大家可以戳這個鏈接看一下具體介紹 http://www.eecg.toronto.edu/~amza/www.mindsec.com/files/x86regs.html
這里的23~29步之所以這么做是按照規定來的,https://en.wikibooks.org/wiki/X86_Assembly/Global_Descriptor_Table鏈接中指出,如果剛剛加載完GDTR寄存器我們必須要重新加載所有的段寄存器的值,而其中CS段寄存器必須通過長跳轉指令,即23號指令來進行加載。所以這些步驟是在第19步完成后必須要做的。這樣才能是GDTR的值生效。
。。。。。
非常抱歉,到目前為止我只能理解到這里,后面的代碼雖然操作上容易,但是它們的目的變得非常難以琢磨,所以我只能把自己比較理解的一部分展示給大家,在之后我會盡力把這部分的知識補全!
綜上,我們可以看到BIOS的操作就是在控制,初始化,檢測各種底層的設備,比如時鍾,GDTR寄存器。以及設置中斷向量表。這都和Lab 1 Part 1.2最后兩段說的一樣。但是作為PC啟動后運行的第一段程序,它最重要的功能是把操作系統從磁盤中導入內存,然后再把控制權轉交給操作系統。所以BIOS在運行的最后會去檢測可以從當前系統的哪個設備中找到操作系統,通常來說是我們的磁盤。也有可能是U盤等等。當BIOS確定了,操作系統位於磁盤中,那么它就會把這個磁盤的第一個扇區,通常把它叫做啟動區(boot sector)先加載到內存中,這個啟動區中包括一個非常重要的程序--boot loader,它會負責完成整個操作系統從磁盤導入內存的工作,以及一些其他的非常重要的配置工作。最后操作系統才會開始運行。
可見PC啟動后的運行順序為 BIOS --> boot loader --> 操作系統內核
以上就是對Exercise 2的分析,老規矩
zzqwf12345@163.com
歡迎騷擾~