跳轉到內容

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 一樣。

128 位、256 位和 512 位 (SSE/AVX)

[編輯 | 編輯原始碼]

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 和 80186

[編輯 | 編輯原始碼]

最初的 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 MiBFFF0 等於 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 上執行時速度會更快。

華夏公益教科書