作業系統——初始MBR(二)
作業系統——初識MBR(二)
2020-09-0918:31:39 hawk
概述
實際上這節主要簡單介紹一下彙編方面的基礎知識,為後面完成MBR程式做鋪墊,主要包括彙編指令的規則講解,會比較枯燥,有基礎的或者對於這些沒有興趣的可以直接跳過,把這個當作彙編手冊即可。
CPU的真實模式
前面已經分析過了,計算機學科的傳統優良傳統就是相容性——實際上真實模式指的是8086CPU的工作環境、工作方式以及工作狀態等。這裡我們簡單總結一下相關的知識點。
真實模式下的暫存器
首先,在真實模式下,預設用到的暫存器都是16位寬的(這裡指的是當下的CPU會相容16位CPU的特性,從而確保可以按照16位CPU的特性正常工作)。
CPU中的暫存器,大體上可以分為兩大類——程式設計師可見的;程式設計師不可見的。對於程式設計師可見的暫存器,也就是在組合語言程式設計的時候可以直接操作的暫存器,如段暫存器、通用暫存器等。而對於程式設計師不可見的暫存器,說的是程式設計師沒有辦法直接使用。雖然這些暫存器沒有辦法是用,但往往需要通過程式設計師進行初始化,比如全域性描述符表暫存器GDTR、中斷描述符表暫存器和IDTR區域性描述符表暫存器LDTR,都可以通過lgdt指令初始化;任務暫存器TR,可以通過ltr指令初始化。而對於flags暫存器,通過pushf和popf指令,將flags暫存器的內容進行入棧和出棧。
除了上面的分類以外,實際上CPU中的暫存器還可以分為段暫存器、flags暫存器和通用暫存器。
對於段暫存器來說,其產生和CPU的工作模式相關——CPU一般通過分段機制來訪問duan記憶體,即“段基址:段內偏移地址”表示相關的記憶體中的地址,而這個段基址則是用段暫存器來進行儲存的。段暫存器中的段基址就相當於指定的一片記憶體的起始地址。需要說明的是,無論是在真實模式,還是在保護模式(即我們平常使用),段暫存器都是16位的。
真實模式下的段暫存器主要是CS程式碼段暫存器、DS資料段暫存器、ES、FS、GS附加段暫存器和SS棧段暫存器。
對於flags暫存器,其展示了CPU內部各項設定、指標等,會在後面進行介紹。
對於通用暫存器,其在保護模式和真實模式下,都為AX、BX、CX、DX、SI、DI、BP、SP這8個。通用指的是每個暫存器的功能不單一,可以有多種用途,但是一般約定了通用暫存器的慣用功能,如下表所示
暫存器 | 助記名稱 | 功能描述 |
AX | 累加器 | 常用於算數運算、邏輯運算、儲存與外設輸入輸出的資料 |
BX | 基址暫存器 | 常用於儲存記憶體地址,將其作為基址進行遍歷 |
CX | 計數器 | 迴圈指令中的迴圈次數 |
DX | 資料暫存器 | 通常用來儲存外設控制器的埠號地址 |
SI | 源變址暫存器 | 被操作的資料來源地址 |
DI | 目的變址暫存器 | 被操作的資料的目的地址 |
SP | 棧指標暫存器 | 段基址是ss,用來指向棧頂 |
BP | 基址指標 | 通過ss:bp的方式將棧當作普通資料段進行訪問 |
這樣子,我們基本上完成了對應的暫存器的介紹。下面我們稍微學習和介紹一下真實模式下CPU記憶體定址方式。
真實模式下CPU記憶體定址
實際上目前看到這,可能很多人會比較困惑,為什麼要介紹這些無聊的東西,並且看起來和作業系統沒有關係。實際上並不是這樣的,我們要實現的MBR中的很多程式碼到需要對於記憶體進行訪問,而由於我們編寫的是較為底層的彙編程式碼,並且直接執行在CPU上,因此並沒有作業系統、編譯器等幫助我們管理和完善對於記憶體的使用,因此我們需要補充從高階語言到直接執行在CPU上這個落差中對於記憶體的處理,這樣我們才能最終沒有障礙的實現MBR程式。
下面我們來具體分析一下CPU記憶體定址,這裡定址指的是尋找資料的地址。8086下主要分為三大類,而最後一類中又包含四小類。
1. 暫存器定址
2. 立即數定址
3. 記憶體定址
(1). 直接定址
(2). 基址定址
(3). 變址定址
(4). 基址變址定址
下面我們將簡單分析一下對應的定址方式。
1. 首先是暫存器定址,其指的是資料就儲存在暫存器中,也就是直接從暫存器中使用資料即可,如下所示
mov ax, 0x10
這就是一條暫存器定址指令。
2. 其次是立即數定址,即常數,如下所示
mov ax, 0x18
可以看到,這條指令即是立即數定址,同樣也是暫存器定址。
3. 直接定址,這是記憶體定址中的一個小類。其將直接在運算元中給出的數字作為記憶體地址,通過中括號的形式表示取此地址中的值作為運算元,如下所示
mov ax, [0x5678]
可以看到,由於0x5678代表記憶體地址,而真實模式下CPU使用分段記憶體來訪問記憶體,因此其需要通過段暫存器:段偏移暫存器來表示記憶體,這裡如果沒有特殊明確,預設段暫存器為DS段暫存器,這條指令將地址為ds * 16 + 0x5678處的值賦給了ax暫存器,是直接定址
4. 基址定址,也是記憶體定址中的一個小類。其將bx暫存器或bp暫存器作為基址來尋找地址(注意,在真實模式下對於基址定址來說,僅僅只能用bx暫存器或bp暫存器進行基址定址)。這裡需要說明的是,當bx暫存器或bp暫存器當作地址時,其同樣遵循真實模式的分段記憶體原則——即”段基址:段偏移“,這裡bx暫存器的預設段暫存器為DS,bp預設的段暫存器為SS,如下所示
mov ax, [bx]
5. 變址定址,同樣是記憶體定址中的一個小類。其和基址定址十分類似,但是理解稍稍不同——其將DS段暫存器所包含的段基址作為基址,而將si暫存器或di暫存器以及可能的立即數運算後的結果作為基址的偏移,從而獲取對應的記憶體中的地址,如下所示
mov [si+0x1234], ax
可以看到,實際上這裡的基址為ds * 16,而基址的偏移為si + 0x1234,這樣共同組成了一個地址,即ds * 16 + si + 0x1234。
6. 基址變址定址,同樣是記憶體定址中的一個小類。根據名字即可知道,這種定址方式結合了基址定址和變址定址,實際上也確實如此。其將bx暫存器和DS段暫存器或bp暫存器和SS段暫存器所生成的地址當作基址,將si暫存器或di暫存器當作基址的偏移,從而確定對應的地址,如下所示
mov [bx+di], ax
可以看到,實際上這裡的基址為ds:bx,即ds * 16 + bx,而這裡的地址偏移為di,因此其共同組成的地址為ds * 16 + bx + di。
可能有人還是對於基址定址和變址定址比較迷惑,實際上我一開始也比較迷惑——這如果變址定址的立即數為0,那麼變址定址不就是基址定址麼,不過是更換了對應的暫存器和段暫存器而已麼?實際上其結果確實是這樣的,但並不能因此就將其混為一談:這就好比數學和物理有部分交集,那麼數學和物理是一樣的東西麼?這裡實際上是兩種思路,
對於基址定址來說,其將bx暫存器或者bp暫存器就當作基址,只不過由於真實模式下是記憶體分段,只要是地址就需要通過分段表示,因此bx暫存器或bp暫存器標識基址是預設帶上了段暫存器;
而對於變址定址來說,其將di暫存器或者si暫存器以及可能的立即數當作地址偏移,但同樣由於真實模式下是記憶體分段,只要是地址就需要通過分段表示,因此需要帶上對應的預設段暫存器,DS段暫存器。需要說明的是,我們一定需要注意一下各個定址方式的暫存器的要求,否則可能無法正常編譯出CPU可執行的指令。
真實模式下棧結構
實際上真實模式下棧結構和保護模式下棧結構並沒有什麼大的區別,除了處理資料的字長不同而已。其同樣使用push指令和pop指令。這裡分別簡單介紹一下。
1. push指令,即將資料入棧。由於sp暫存器對應的表明棧頂(同樣由於真實模式下CPU的分段記憶體基址,同樣需要段暫存器表明地址,這裡sp暫存器預設的段暫存器為SS段暫存器),並且由於棧頂處於低地址(記憶體中棧向下生長),為了避免破壞棧頂的資料,首先將sp暫存器減去一個字長(16位),然後將值寫入sp暫存器對應地址的記憶體中(這裡實際上相當於訪問了[sp],即類似於基址定址法,實際上這個命令在真實模式下是非法的,因為暫存器並不在上面講到的基址定址或變址定址所給定的暫存器中,這裡就簡單理解為CPU內部實現的,但外部無法呼叫即可)。
2. pop指令,即將資料出棧。類似於上面的分析,由於sp暫存器指向棧頂,並且棧頂處於低地址,因此我們直接輸出sp暫存器對應地址的記憶體中的資料即可,但是為了維護棧結構,還需要將sp暫存器減去一個字長(16位)。
實際上這樣相當於實現了記憶體中的棧結構,這裡說明一下,實際上棧底相當於SS段暫存器對應的地址,根據分段記憶體訪問基址,也就是SS * 16是棧底地址,其餘和普通的棧結構並沒有什麼太大的區別。
真實模式下跳轉
實際上真實模式下跳轉和保護模式下並沒有什麼大的區別。同樣大體分為兩類——無返回的jmp型別和有返回的call型別。
call型別
在8086處理器中,決定程式流程的是cs:ip暫存器,因此如果我們能直接修改cs段暫存器或者ip暫存器的話,我們自然也就完成了程式流程的轉變。根據這個,實際上call指令中包含了4種方式,其中兩種為近呼叫,即在同一個段中,只需要修改段偏移即可,其返回的話通過ret指令;另外兩種為遠呼叫,即跨段呼叫,需要同時修改段基址和段偏移。
1. 16位真實模式相對近呼叫。其指令形式如下所示
call near near_proc
實際上其轉換為機器碼為e8llhh,其中e8是操作碼,表明是相對近呼叫,而ll和hh表示一個數值,根據小端序位元組,實際上該數值為hhll,等於目標函式的地址-當前相對近呼叫指令地址-相對近呼叫地址指令大小(這裡是3位元組)。
相對近呼叫指令實現的功能很簡單,首先將當前相對近呼叫指令的下一條指令地址的段偏移入棧(16位),然後修改ip暫存器,將其值修改為當前相對近呼叫指令地址+相對近呼叫地址指令大小(這裡是3位元組)+相對近呼叫地址指令中的偏移值,即將ip暫存器修改為目標函式地址。
2. 16位真實模式間接絕對近呼叫
實際上,16位真實模式間接絕對近呼叫和前面分析到的16位真實模式相對近呼叫十分相似,都是近呼叫—即在同一個段中,只需要修改段偏移即可。但是不同點在於16位真實模式間接絕對近呼叫中包含的段偏移地址是絕對地址,並且其是間接的,即通過暫存器或者記憶體給出來。其指令形式如下所示
call ax call [0x7c00]
實際上這兩條指令都是16位真實模式間接絕對近呼叫。對於第一條指令來說,其會呼叫地址為ax暫存器處的函式;而對於第二條指令來說,其會呼叫地址為ds * 16 + 0x7c00(真實模式CPU分段記憶體基址)處的函式。如果用記憶體定址,該指令的機器碼為ff16llhh,其中ff16為操作碼,表示使用記憶體定址的間接絕對近呼叫,而ll和hh表示一個數值,根據小端序位元組,實際上該數值為hhll,等於目標函式的地址;而如果使用暫存器定址,該指令的機器碼為ff**,其中ff為操作碼,表示使用暫存器定址的間接絕對近呼叫,而**表示暫存器的表示。
間接絕對近呼叫指令實現的功能也很簡單,首先將當前間接絕對近呼叫指令的下一條指令地址的段偏移入棧(16位),然後修改ip暫存器,將其值從暫存器或者對應的記憶體地址處獲取,即將ip暫存器修改為對應的目標函式地址。
3. 16位真實模式直接絕對遠呼叫
這個不同於前面所介紹的呼叫,一方面是遠呼叫,即需要同時修改段基址和段偏移;另一方面是直接絕對地址,即直接將絕對地址以立即數形式。其指令形式如下所示
call 0x0000:0x7c00
實際上其轉換為機器碼為9a007c0000,其中9a是操作碼,表明是直接絕對地址遠呼叫,而後緊跟的是32位運算元,其中前16位是段偏移地址,後16位是段基址地址。
直接絕對地址遠呼叫指令實現的功能也很簡單,首先將當前直接絕對地址遠呼叫的下一條指令地址的段基址(16位)、段偏移(16位)先後入棧,然後直接將段基址、段偏移修改為直接絕對地址遠呼叫命令中所包含的地址即可。
4. 16位真實模式間接絕對遠呼叫
由於是間接地址,因此其地址一般儲存於記憶體中或者暫存器中,而由於是絕對地址遠呼叫,因此其地址需要32位(段地址和段偏移),這也就決定了16位真實模式間接絕對地址遠呼叫通過記憶體定址,其中資料的地址儲存在記憶體中。其指令形式如下所示
call far [0x7c00]
上述指令便是一個16位真實模式間接絕對地址遠呼叫,其機器碼ff1ellhh,其中ff1e是間接絕對地址遠呼叫的操作碼,而ll和hh表示一個數值,根據小端序位元組,實際上該數值為hhll(需要附帶段暫存器,預設為ds段暫存器),地址為該值的記憶體中的值才是函式所在的地址。其中低2個位元組是段偏移,高兩個位元組是段基址。
間接絕對遠呼叫指令實現的功能也很簡單,首先將當前直接絕對地址遠呼叫的下一條指令地址的段基址(16位)、段偏移(16位)先後入棧,然後根據指令中包含的地址,將該地址處的記憶體低2位元組作為段偏移,高2位元組作為段基址,從而呼叫該地址處的函式即可。這裡需要特別區別一下16位真實模式間接絕對地址遠呼叫和16位真實模式間接絕對近呼叫,其呼叫的方式和儲存的值的不同。
實際上對於有返回的呼叫來說,單單有呼叫還不行,還需要有返回——即ret和retf指令。這裡僅僅簡單介紹一下這兩個指令——由於call指令分了遠呼叫和近呼叫,其在棧中的返回地址儲存的形式也各不相同,因此需要不同的指令進行返回。對於近呼叫來說,其通過ret進行返回,即其將將彈出的16位資料寫入ip暫存器中,從而完成段偏移的修復;而對於遠呼叫來說,其將彈出的2個16位資料分別寫入ip暫存器和cs段暫存器中,從而完成段偏移和段基址的修復。
無條件jmp型別
實際上類似於上面的call型別,jmp型別也同樣按照遠近來進行劃分,主要可以劃分為短轉移、近轉移和遠轉移這三大類。
1. 16位真實模式相對短轉移。實際上其有點類似於相對近呼叫,其指令形式如下所示
jmp short start
實際上相對短轉移指令的機器碼是ebll,其中eb是相對短轉移的操作碼,而ll表示偏移,其數值等於目標函式地址-當前相對短轉移指令地址-相對短轉移指令大小(這裡是2位元組)。
所以相對短轉移指令實現的功能也很簡單,就是修改ip暫存器,將其值修改為當前相對短轉移指令地址+相對短轉移指令大小(這裡是2位元組)+短轉移指令中的偏移值,即將ip暫存器修改為目標函式地址。
2. 16位真實模式相對近轉移。實際上和16位真實模式相對短轉移十分類似,僅僅是擴大了偏移的大小,從而可以完成更大範圍的跳轉,其指令形式如下所示
jmp near short
實際上近轉移指令的機器碼是e9llhh,其中e9是近轉移的操作碼,而ll和hh表示一個數值,根據小端序位元組,實際上該數值為hhll,其數值等於目標函式地址-當前近轉移指令地址-近轉移指令大小(這裡是3位元組)。
所以近轉移指令實現的功能也很簡單,就是修改ip暫存器,將其值修改為當前近轉移指令地址+近轉移指令大小(這裡是3位元組)+近轉移指令中的偏移值,即將ip暫存器修改為目標函式地址。
3. 16位真實模式間接絕對近轉移。
對於16位真實模式間接絕對近轉移來說,其和16位真實模式間接絕對近呼叫十分地相似,即將段偏移的絕對地址存放在暫存器中或者記憶體中。指令形式如下所示
jmp near ax jmp near [0x7c00]
實際上這兩條指令都屬於間接絕對近轉移,其將暫存器或者記憶體中的值直接寫入到ip暫存器中,從而完成段基址偏移的修改。對於記憶體訪問方式來說,需要注意CPU的記憶體分段基址,需要帶預設的段暫存器ds段暫存器。
4. 16位真實模式直接絕對遠轉移
類似於call命令的直接絕對遠呼叫,直接將段基址和段偏移位通過立即數進行設定,其指令格式如下所示
jmp 0x0:start
上述即為一個直接絕對遠轉移,其中start是彙編中的偽指令,用來表示地址,在進行編譯的時候會直接轉換為對應的立即數。
因此實際上該指令的作用也很簡單,就是直接將段基址和段偏移設定為對應的立即數即可。
5. 16位真實模式間接絕對遠轉移
仍然類似於call命令的間接絕對遠呼叫,其需要同時修改段基址和段偏移,因此只能通過記憶體中儲存目標地址。其指令格式如下所示
jmp far [addr]
由於CPU的記憶體分段基址,其要表示地址的話需要段暫存器,預設為ds段暫存器。這樣子,將ds * 16 + addr的低2個位元組寫入段偏移暫存器,高2個位元組寫入段基址暫存器,從而完成了轉移。
可以看出來,實際上jmp系列和call系列十分相似。
條件jmp型別
條件跳轉,即根據條件情況進行對應的跳轉,其中這些條件被存放在標誌暫存器中,包括CF位(判斷無符號加減法溢位)、PF位(奇偶位)等標誌位。大家可以查閱更多資料來了解標誌暫存器。下面具體介紹一下條件jmp指令。其是一個指令族,簡稱為jxx。如果條件滿足,jxx將會跳轉到指定的位置去執行,否則繼續順序地執行下一條指令。其指令格式如下所示
jxx address
這裡需要說明的是,address只能是段內偏移地址,因此可以理解為短轉移或近轉移。自然不需要額外的段暫存器表明段基址。下面將各個指令具體列出,如下所示
轉移指令 | 條件 | 意義 | 英文助記 |
jz/je | ZF=1 | 相減結果為0/相等時轉移 | Jump if Zero/Equal |
jnz/jne | ZF=0 | 不等於0/不相等時轉移 | Jump if Not Zero/Not Equal |
js | SF=1 | 負數時轉移 | Jump if Sign |
jns | SF=0 | 正數時轉移 | Jump if Not Sign |
jo | OF=1 | 溢位時轉移 | Jump if Overflow |
jno | OF=0 | 未溢位時轉移 | Jump if Not Overflow |
jp/jpe | PF=1 | 低位元組中有偶數個1時轉移 | Jump if Parity/Parity Even |
jnp/jnpe | PF=0 | 低位元組中有奇數個1時轉移 | Jump if Not Parity/Parity Odd |
jbe/jna | CF=1或ZF=1 | 小於等於/不大於時轉移 | Jump if Below or Equal/Not Above |
jnbe/ja | CF=ZF=0 | 不小於等於/大於時轉移 | Jump if Not Below or Equal/Above |
jc/jb/jnae | CF=1 | 進位/小於/不大於等於時轉移 | Jump if Carry/Below/Not Above Equal |
jnc/jnb/jae | CF=0 | 未進位/不小於/大於等於時轉移 | Jump if Not Carry/Not Below/Above Equal |
jl/jnge | SF!=OF | 小於/不大於等於時轉移 | Jump Less/Not Great Equal |
jnl/jge | SF=OF | 不小於/大於等於時轉移 | Jump if Not Less/Great Equal |
jle/jng | SF!=OF或ZF=1 | 小於等於/不大於時轉移 | Jump if Less or Equal/Not Great |
jnle/jg | SF=OF且ZF=0 | 不小於等於/大於時轉移 | Jump Not Less Equal/Grea |
jcxz | CX暫存器=0 | cs暫存器值為0時轉移 | Jump if register CX‘s value is Zero |