寫這個系列文章的主要目的是記錄書中重要的知識點,並和大家分享一些個人理解與實踐。由於筆記中的知識點比較零散,而書中系統的介紹了一個 x86-16 處理器在實模式下的工作原理以及如何使用匯編語言與其進行“溝通”,所以推薦想要系統學習的朋友們去學習這本書。當我們掌握了實模式的工作原理之後,就可以進一步研究後來出現的其他運行模式(如保護模式)。除此之外,熟悉彙編語言有助於我們掌握上層語言(如 C)的執行原理,因為它們都要對彙編(機器碼)進行抽象,而彙編程序就是基於 CPU 的執行機理寫出來的。
前言
學習彙編的兩個最根本目的
- 充分獲得底層編程的體驗(在一個沒有操作系統的環境中面向硬件編程。所以本書中不會涉及彙編器)
- 深刻理解機器運行程序的機理
一門課程是由相互關聯的知識構成的,這些知識在一本書中如何組織則是一種信息組織和加工的藝術。學習是一個循序漸進的過程(物有本末,事有終始,之所先後,則近道矣!),但並不是所有的教學都是以這種方式完成的,這並不是我們所希望的事情,因為任何不以循序漸進的方式進行的學習,都將出現盲目探索和不成系統的情況,最終學習到的也大都是相對零散的知識,並不能建立起一個系統的知識結構。非循序漸進的學習,也達不到循序漸進學習所能達到的深度,因為後者是步步深入的,每一步都以前一步為基礎。
第一章
彙編語言的組成
- 彙編指令:機器碼的助記符(與 ISA 中的機器碼一一對應)
- 偽指令:沒有對應的機器碼,由編譯器執行,CPU 並不執行(主要作用是引導彙編器進行編譯)
- 標號(Label):一個標號代表了一個地址
- 其他符號:如
+ - * /等,由編譯器識別,沒有對應的機器碼
系統總線
電子計算機能處理、傳輸的信息都是電信號,電信號要用導線傳送。所以總線從物理上來講就是一根根導線的集合。
- 地址總線的寬度決定了尋址能力(8086 地址總線寬度為 20 位)
- 數據總線的寬度決定了 CPU 和外界的數據傳送速度(傳送數據需要的次數)
- 控制總線是一些不同控制線的集合。有多少根控制線,就意味着 CPU 提供了對外部器件的多少種控制。所以,它的寬度決定了 CPU 對外部器件的控制能力
存儲器
存儲器可以理解為由 N 個大小相同的存儲單元組成,所以説存儲單元是存儲器最小的存儲單位。
存儲單元的大小取決於存儲器的編址方式。在現代計算機中:每個存儲單元的大小通常是 1 字節。
一個存儲單元有地址和內容兩個屬性:
- 地址:每個存儲單元的唯一編號(用於供 CPU 訪存時來定位存儲單元)
- 內容:存儲單元存放的內容即 指令 或 數據
輸入/輸出設備
I/O 設備通常被劃分為兩個部分:外設 和 I/O 接口。
- 外設就是連接在計算機外部的設備,如:鼠標、鍵盤、打印機、顯示器、網絡等。由於外設的種類繁多,不同外設之間的差異很大(比如:接口、信號、數據傳輸率等差異),而 CPU 與外部器件的交互方式比較單一(CPU 的想法是:我給出一個地址,來讓我操作對應的數據即可),導致它無法與外設直接連接。這時,I/O 接口就出現了。
-
I/O 接口就是連接在 CPU 和外設之間的“中轉站”。它的出現讓 CPU 和外設之間實現了“解耦”:CPU 只需像往常一樣,給出一個地址與之交互;它來幫助 CPU 完成與不同外設之間的交互。
- I/O 接口主要負責信號轉換(如:數字信號<->模擬信號、串行信號<->並行信號)、協調、數據緩衝(解決 CPU 和外設的速度差異)等工作。
- I/O 接口中包含許多寄存器,如:數據輸入寄存器用來保存來自外設輸入的數據、數據輸出寄存器用來保存 CPU 向外設輸出的數據、控制寄存器、狀態寄存器等等,這些寄存器被稱為 I/O 端口。與存儲器的存儲單元一樣,每個 I/O 端口都會對應一個地址。
存儲器和 I/O 接口的編址方式
存儲器和 I/O 接口的編址方式通用有兩種:
- Memory Mapped I/O——存儲器和 I/O 接口統一編址(使用同一個地址空間)
- I/O Mapped I/O——存儲器和 I/O 接口分開編址(使用兩個獨立的地址空間。常用於 RISC 中,因為 RISC 指令的特徵能避免這種編址方式存在的一些弊端)
本書中介紹的是 Memory Mapped I/O 方式:
- 8086 主存的地址空間為:
0~9FFFF (640KB) - 8086 顯存的地址空間為:
A0000~BFFFF (128KB) - 8086 只讀存儲器的地址空間為:
C0000~FFFFF (256KB)
地址空間總大小:1024KB (1MB) = 2^20 bytes, 所以 8086 對外提供了 20 根地址總線。
FFFF0H單元中的指令是 8086 開機後執行的第一條指令。
第二章
N 位結構(N 位機、字長為 N 位)特性
- 運算器每次最多能支持 N 位的算數運算或邏輯運算
- 寄存器的最大位寬通常是 N 位
- 寄存器和運算器之間的內部通路支持 N 位的數據傳輸
- 外部數據總線的寬度通常是 N 位(8088 比較特殊,對外提供的數據總線是 8 位)
8086的段
注意:物理內存並沒有分段,段的邏輯劃分來自於 CPU(8086 物理地址的生成方式:基地址(段基址*16) + 偏移地址 = 物理地址”),使得可以用分段的方式來管理內存。(這種分段方式的弊端是:不同段對應的內存可能是:完全獨立、部分重合、完全重合的,如 2000:0 和 1001:FFF0 物理地址完全重合)
一個段的起始地址(基地址)一定是 16 的倍數;偏移地址為 16 位,16 位地址的尋址能力為 64KB, 所以在 8086 處理器中一個段的長度最大為 64KB.
8086 中 CS:IP 表示的內存區域代表着指令。8086 中沒有提供 mov 類指令來修改 IP 寄存器,只能通過特殊指令(如轉移指令)改變。轉移指令詳見第九章內容。
實驗一:查看 CPU 和內存,用機器指令和彙編指令編程
MacOS 中搭建 DOS 環境
- 下載 dosbox: https://www.dosbox.com/download.php?main=1
-
創建目錄,作為掛載點
mkdir ~/DOS - 將下載的
DOSBox-0.74-3-3.dmg包裏面的內容拷貝至~/DOS目錄中 - 下載
debug.exe, 並將其拷貝至~/DOS/bin目錄中 -
修改 dosbox 的配置文件,將掛載等操作加入到
[autoexec]中(這樣每次啓動 dosbox 會自動執行這些命令)# vi ~/Library/Preferences/DOSBox\ 0.74-3-3\ Preferences # 在 [autoexec] 中加入以下命令: mount c ~/DOS C: set PATH=%PATH%;C:\bin\ - 啓動
DOSBox.app -
執行
debug, 可以進行調試了!debug
debug 的常用參數選項(可以和 GDB 進行類比)
# 寄存器
-r # 查看所有寄存器的內容
-r ax # 修改寄存器內容
# 內存
-d 1000:0 # 從指定內存單元開始,顯示 128 個內存單元的內容(128字節)
-d # 繼續顯示“接下來的” 128 個內存單元的內容
-d 1000:0 9 # 格式————“d 段地址:偏移地址 結尾的偏移地址”
-e 1000:0 1 2 3 'a' 'b' 'c' "str" # 修改指定內存單元開始的連續字節內容
-e 1000:0 # 修改指定內存單元開始的連續字節內容(通過“回車、空格、空格……回車”進行交互)
# 指令(使用 -e 直接輸入機器碼的16進制進行編輯)
-u 1000:0 # 從指定內存單元開始,顯示其對應的指令(機器碼+彙編)
-u # 繼續顯示“接下來的”指令
-t # 執行一條指令
-a 1000:0 # 以彙編的形式在指定內存中寫入指令
-a # 以彙編的形式在“接下來的”內存中寫入指令
# 退出 debug
-quit
-q
# 補充
-d|e|u|a 段寄存器名:偏移地址(邏輯地址) # 如:-a ds:0, -e cs:0
-g 偏移地址 # 如:-g 0012 執行 [CS:IP~CS:0012) 之間的指令,相當於打【斷點】
-p # 執行 `int 21H` 指令時,需要用 -p; 在循環中,使用 -p 可以完成所有循環的執行
第三章
將起始地址為 N 的字單元簡稱為:N 地址字單元
8086 指令操作數規則(瞭解即可,必要時查閲《Intel 開發手冊(第二卷)》來確定具體的格式):
- 不支持將【立即數】直接送入【段寄存器】的操作
- 不支持將【立即數】直接送入【內存單元】的操作
- 不支持操作數不能同時是兩個【內存單元】
- 支持的形式這裏不一一列舉,參見書中 3.4(第53頁)
棧
處理器通過 ss(棧基址) 和 sp(棧指針) 來確定一個棧。
入棧指令 push 對 sp 的影響,比如:push ax
- sp=sp-2
- ax 內容寫入棧頂
出棧指令 pop 對 sp 的影響,比如:pop ax
- 棧頂內容寫入 ax
- sp=sp+2(注:剛剛棧頂的內容不會被擦除,下次使用時覆蓋即可)
棧頂越界問題
書中説 8086 硬件層面沒有做這種控制,在 debug 中測試了確實是。但在保護模式中,提供了“段界限檢查”機制。