x86 彙編/16 位、32 位和 64 位
在使用 x86 彙編時,重要的是要考慮 16 位、32 位和 64 位架構之間的差異。本頁將討論不同位寬架構之間的一些基本差異。
在 8086 和所有後續的 x86 處理器上發現的暫存器如下:AX、BX、CX、DX、SP、BP、SI、DI、CS、DS、SS、ES、IP 和 FLAGS。這些都是 16 位寬的。
在 DOS 和 32 位 Windows 上,您可以從 DOS shell 執行一個非常方便的程式,稱為“debug.exe”,這對於學習 8086 非常有用。如果您使用的是 DOSBox 或 FreeDOS,則可以使用“debug.exe” 如 FreeDOS 提供的那樣。
- AX、BX、CX、DX
- 這些通用暫存器也可以作為 8 位暫存器定址。因此 AX = AH(高 8 位)和 AL(低 8 位)。
- SI、DI
- 這些暫存器通常用作資料空間的偏移量。預設情況下,SI 是從 DS 資料段偏移,DI 是從 ES 附加段偏移,但這兩個或其中之一都可以被覆蓋。
- SP
- 這是堆疊指標,偏移通常來自堆疊段 SS。資料被推送到堆疊以進行臨時儲存,並在再次需要時從堆疊彈出。
- BP
- 堆疊幀,通常被視為堆疊段 SS 的偏移量。子例程的引數通常在呼叫子例程時被推送到堆疊,並且在子例程開始時 BP 被設定為 SP 的值。然後可以使用 BP 從堆疊中找到引數,無論在此期間使用了多少堆疊。
- CS、DS、SS、ES
- 段指標。這些分別是當前程式碼段、資料段、堆疊段和附加段的記憶體偏移量。
- IP
- 指令指標。從程式碼段 CS 偏移,它指向當前正在執行的指令。
- FLAGS (F)
- 一些單位元標誌,指示(或有時設定)處理器的當前狀態。
隨著晶片開始支援 32 位資料匯流排,暫存器也被擴充套件到 32 位。32 位暫存器的名稱只是在 16 位名稱前加上一個“E”。
- EAX、EBX、ECX、EDX、ESP、EBP、ESI、EDI
- 這些是上面顯示的暫存器的 32 位版本。
- EIP
- IP 的 32 位版本。在 32 位系統上始終使用它而不是 IP。
- EFLAGS
- 16 位 FLAGS 暫存器的擴充套件版本。
64 位暫存器的名稱與 32 位暫存器的名稱相同,只是以“R”開頭。
- RAX、RBX、RCX、RDX、RSP、RBP、RSI、RDI
- 這些是上面顯示的暫存器的 64 位版本。
- RIP
- 這是完整的 64 位指令指標,應該使用它而不是 EIP(如果地址空間大於 4 GiB,這可能會發生,即使只有 4 GiB 或更少的 RAM)。
- R8–15
- 這些是 64 位的新增暫存器。它們被計數,就像上面的暫存器是第零到第七個暫存器,包括在內,而不是第一個到第八個。
R8–R15 可以作為 8 位、16 位或 32 位暫存器訪問。以 R8 為例,對應於這些位寬的名稱分別是 R8B、R8W 和 R8D。64 位版本的 x86 還允許直接訪問 RSP、RBP、RSI、RDI 的低位元組。例如,RSP 的低位元組可以使用 SPL 訪問。沒有辦法直接訪問這些暫存器的第 8-15 位,就像 AH 允許 AX 一樣。
64 位 x86 包括 SSE2(對 32 位 x86 的擴充套件),它為特定指令提供 128 位暫存器。自 2011 年以來製造的大多數 CPU 也有 AVX,這是進一步的擴充套件,將這些暫存器延長到 256 位。有些還具有 AVX-512,它將它們延長到 512 位,並添加了 16 個暫存器。
- XMM0~7
- SSE2 及更新版本。
- XMM8~15
- SSE3 及更新版本和 AMD(但不是 Intel)SSE2。
- YMM0~15
- AVX。每個 YMM 暫存器都包含相應的 XMM 暫存器作為其下半部分。
- ZMM0~15
- AVX-512F。每個 ZMM 暫存器都包含相應的 YMM 暫存器作為其下半部分。
- ZMM16~31
- AVX-512F。512 位暫存器,除非實現了 AVX-512VL,否則在較窄的模式下不可定址。
- XMM16~31
- AVX-512VL。每個都是相應 ZMM 暫存器的下四分之一。
- YMM16~31
- AVX-512VL。每個都是相應 ZMM 暫存器的下半部分。
最初的 8086 只有 16 位大小的暫存器,實際上允許儲存 [0 - (216 - 1)] 範圍內的值(或更簡單地說:它可以定址最多 65536 個不同的位元組,或 64 kibibytes) - 但地址匯流排(到記憶體控制器的連線,它接收地址,然後載入來自給定地址的內容,並將資料透過資料匯流排返回到 CPU)是 20 位大小,實際上允許定址最多 1 兆位元組的記憶體。這意味著所有暫存器本身都不足以利用地址匯流排的全部寬度,留下 4 位未使用,將可用地址的數量縮減了 16 倍(1024 KiB / 64 KiB = 16)。
問題在於:如何透過 16 位暫存器引用 20 位地址空間?為了解決這個問題,英特爾的工程師提出了段暫存器 CS(程式碼段)、DS(資料段)、ES(附加段)和 SS(堆疊段)。要從 20 位地址轉換,首先要將它除以 16,並將商放在段暫存器中,並將餘數放在偏移暫存器中。這表示為 CS:IP(這意味著 CS 是段,IP 是偏移量)。同樣,當寫入地址 SS:SP 時,它意味著 SS 是段,SP 是偏移量。
這也適用於相反的方式。如果一個人是,而不是從,建立 20 位地址,它將透過取段暫存器的 16 位值並將其放在地址總線上,但向左移動 4 次(因此實際上將暫存器乘以 16),然後透過將另一個暫存器的偏移量保持不變地新增到總線上的值,從而建立完整的 20 位地址。
如果 CS = 258C 且 IP = 001216,則 CS:IP 將指向一個等於“CS × 16 + IP”的 20 位地址,即
258C × 1016 + 001216 = 258C0 + 001216 = 258D2(記住:16 進位制 = 1016)。
20 位地址被稱為絕對地址(或線性地址),而段:偏移表示法(CS:IP)被稱為分段地址。這種分離是必要的,因為暫存器本身無法儲存需要超過 16 位編碼的值。在 32 位或 64 位處理器上使用保護模式程式設計時,暫存器足夠大,可以完全填充地址匯流排,從而消除了分段地址——在這種“扁平定址”模式下,通常只使用線性/邏輯地址,儘管為了向後相容,仍然支援段:偏移架構。
需要注意的是,物理地址和分段地址之間沒有一一對應關係;對於任何物理地址,都有多個可能的分段地址。例如:考慮分段表示 B000:8000 和 B200:6000。經過計算,它們都對映到物理地址 B8000。
B000:8000 = B000 × 1016 + 800016 = B0000 + 800016 = B8000,以及
B200:6000 = B200 × 1016 + 600016 = B2000 + 600016 = B8000。
但是,使用適當的對映方案可以避免此問題:這種對映將線性變換應用於物理地址,為每個物理地址建立唯一的分段地址。要反轉轉換,只需反轉對映 [f(x)]。
例如,如果段部分等於物理地址除以 1016,而偏移部分等於餘數,那麼只會生成一個分段地址。(任何偏移都不會大於 0F16。)物理地址 B8000 對映到 (B8000 / 1016):(B8000 mod 1016) 或 B800:0。這種分段表示法有一個特殊的名字:這種地址被稱為“規範地址”。
CS:IP(程式碼段:指令指標)表示下一條要執行的指令將從中獲取的物理記憶體的 20 位地址。類似地,SS:SP(堆疊段:堆疊指標)指向一個 20 位絕對地址,該地址將被視為堆疊頂端(8086 使用它來壓入/彈出值)。
保護模式 (80286+)
[edit | edit source]儘管這看起來很醜陋,但它實際上是邁向後來晶片中使用的保護定址方案的一步。80286 具有保護模式的操作,其中所有 24 個地址線都可用,允許定址高達 16 MiB 的記憶體。在保護模式下,CS、DS、ES 和 SS 暫存器不是段,而是選擇器,指向一個表,該表提供有關程式正在使用的物理記憶體塊的資訊。在這種模式下,指標值 CS:IP = 0010:2400 的使用方法如下:
CS 值 001016 是選擇器表中的一個偏移量,指向特定的選擇器。該選擇器將具有一個 24 位值來指示記憶體塊的起始位置,一個 16 位值來指示塊的長度,以及標誌來指定塊是否可寫、是否當前駐留在記憶體中以及其他資訊。假設指向的記憶體塊實際從 24 位地址 16440016 開始,那麼引用的實際地址為 16440016 + 240016 = 16680016。如果選擇器還包含關於塊長度為 240016 位元組的資訊,則引用將指向該塊後面的位元組,這將導致異常:作業系統不應允許程式讀取它不擁有的記憶體。如果塊被標記為只讀,則程式碼段記憶體應該是這樣,以防止程式覆蓋自身,嘗試寫入該地址同樣會導致異常。
隨著 CS 和 IP 在 386 中擴充套件到 32 位,這種方案變得不再必要;選擇器指向物理地址 0000000016,32 位暫存器可以定址高達 4 GiB 的記憶體。然而,選擇器仍然被用來保護記憶體免受惡意程式的侵害。例如,如果 Windows 中的一個程式試圖讀取或寫入它不擁有的記憶體,它將違反選擇器設定的規則,觸發異常,Windows 將顯示“通用保護故障”訊息將其關閉。
32 位定址
[edit | edit source]32 位地址可以覆蓋高達 4 GiB 的記憶體。這意味著我們不需要在 32 位處理器中使用偏移地址。相反,我們使用所謂的“扁平定址”方案,其中暫存器中的地址直接指向物理記憶體位置。段暫存器用於定義不同的段,這樣程式就不會嘗試執行堆疊部分,也不會意外地在資料部分執行堆疊操作。
A20 門傳奇
[edit | edit source]如前所述,8086 處理器有 20 條地址線(從 A0 到 A19),因此它可定址的總記憶體為 1 MiB(或 2 的 20 次方)。但由於它只有 16 位暫存器,他們想出了段:偏移方案,否則使用單個 16 位暫存器,他們不可能訪問超過 64 KiB(或 2 的 16 次方)的記憶體。因此,這使得程式可以訪問整個 1 MiB 的記憶體。
但這種分段方案也帶來了副作用。使用這種方案,你的程式碼不僅可以引用整個 1 MiB,實際上可以引用更多。讓我們看看如何實現…
讓我們記住如何從段:偏移表示法轉換為線性 20 位表示法。
轉換
段:偏移 = 段 × 16 + 偏移。
現在要檢視可以定址的最大記憶體量,讓我們將段和偏移都填充到其最大值,然後將其值轉換為 20 位絕對物理地址。
因此,段的最大值為 FFFF16,偏移的最大值為 FFFF16。
現在,讓我們將 FFFF:FFFF 轉換為其 20 位線性地址,記住 1610 在十六進位制中表示為 10。
所以我們得到,FFFF:FFFF -> FFFF × 1016 + FFFF = FFFF0(1 MiB - 16 位元組)+ FFFF(64 KiB)= FFFFF + FFF0 = 1 MiB + FFF0 位元組。
- 注意:FFFFF 為十六進位制,等於 1 MiB,FFF0 等於 64 KiB 減去 16 位元組。
故事的寓意:從真實模式下,程式實際上可以引用 (1 MiB + 64 KiB - 16) 位元組的記憶體。
注意“引用”這個詞的使用,而不是“訪問”。程式可以引用這麼多記憶體,但它是否可以訪問,取決於實際存在的地址線數量。因此,對於 8086 來說,這絕對不可能,因為當程式引用 1 MiB 以上的記憶體時,放在地址線上的地址實際上超過了 20 位,導致地址發生環繞。
例如,如果程式碼引用 1 MiB,它將發生環繞,指向記憶體中的位置 0,類似地,1 MiB + 1 將發生環繞,指向地址 1(或 0000:0001)。
當時有一些超級時髦的程式設計師利用了程式碼中的這個特性,即地址發生環繞,使得他們的程式碼更快,而且位元組更少。使用這種技術,他們可以訪問 32 KiB 的頂部記憶體區域(即與 1 MiB 邊界相鄰的 32 KiB)和 32 KiB 的底部記憶體區域,而無需重新載入段暫存器!
你看,簡單的數學,如果你在段:偏移表示法中使段保持不變,那麼由於偏移是一個 16 位值,因此你可以遍歷 64 KiB(或 2 的 16 次方)的記憶體區域。現在,如果你讓你的段暫存器指向 1 MiB 標記下方的 32 KiB,你可以向上訪問 32 KiB,以接觸 1 MiB 邊界,然後進一步訪問 32 KiB,最終將環繞到最底部的 32 KiB。
這些超級時髦的程式設計師忽視了一個事實,那就是會建立具有更多地址線的處理器。(注意:比爾·蓋茨被認為說過,“誰會需要超過 640 KB 的記憶體?”,這些程式設計師可能也是這樣想的。)1982 年,在 8086 釋出僅僅兩年後,英特爾釋出了具有 24 條地址線的 80286 處理器。儘管從理論上來說,它向後相容舊的 8086 程式,因為它也支援真實模式,但許多 8086 程式無法正常執行,因為它們依賴於越界地址環繞到較低的記憶體段。為了相容性,IBM 工程師將 A20 地址線(8086 具有 A0 - A19 線)路由到鍵盤控制器,並提供了一種機制來啟用/停用 A20 相容模式。如果你想知道為什麼是鍵盤控制器,答案是它有一個未使用的引腳。由於 80286 將被市場宣傳為與 8086 完全相容(8086 還沒有上市很久),升級後的客戶會很憤怒,如果 80286 不完全相容,以至於為 8086 設計的程式碼在 80286 上執行時速度會更快。