跳至內容

x86 彙編/16、32 和 64 位

來自 Wikibooks,開放的書籍,面向開放的世界

使用 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,則 EIP 將不準確,即使只有 4 GiB 或更少的 RAM 也是如此)。
R8–15
這些是 64 位的新增暫存器。它們被計數為上面的暫存器是 0 到 7 號暫存器(包含),而不是 1 到 8 號。

R8–R15 可以被訪問為 8 位、16 位或 32 位暫存器。以 R8 為例,對應於這些寬度的名稱分別是 R8B、R8W 和 R8D。64 位版本的 x86 還允許直接訪問 RSP、RBP、RSI、RDI 的低位元組。例如,可以使用 SPL 訪問 RSP 的低位元組。無法直接訪問這些暫存器的第 8–15 位,就像 AH 允許訪問 AX 一樣。

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

[編輯 | 編輯原始碼]

64 位 x86 包括 SSE2(32 位 x86 的擴充套件),它為特定指令提供 128 位暫存器。自 2011 年以來製造的大多數 CPU 也具有 AVX,這是一個進一步的擴充套件,它將這些暫存器的長度擴充套件到 256 位。一些 CPU 還具有 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 mebibyte 的記憶體。這意味著所有暫存器本身都不足以利用地址匯流排的整個寬度,留下了 4 位未用,將可用地址的數量縮小了 16 倍(1024 KiB / 64 KiB = 16)。

問題是:如何透過 16 位暫存器引用 20 位地址空間?為了解決這個問題,英特爾的工程師提出了段暫存器 CS(程式碼段)、DS(資料段)、ES(擴充套件段)和 SS(堆疊段)。要從 20 位地址轉換,首先將其除以 16,並將商放在段暫存器中,並將餘數放在偏移暫存器中。這表示為 CS:IP(這意味著,CS 是段,IP 是偏移量)。同樣,當寫入地址 SS:SP 時,意味著 SS 是段,SP 是偏移量。

這也適用於反向操作。如果一個人要建立 20 位地址,而不是從 20 位地址轉換,則可以將段暫存器的 16 位值放在地址總線上,但將其左移 4 次(因此實際上將暫存器乘以 16),然後將另一個暫存器的未修改的偏移量新增到總線上的值,從而建立一個完整的 20 位地址。

如果 CS = 258C,IP = 001216,那麼 CS:IP 將指向一個 20 位地址,相當於“CS × 16 + IP”,即

258C × 1016 + 001216 = 258C0 + 001216 = 258D2(記住:16 十進位制 = 1016)。

20 位地址稱為絕對(或線性)地址,Segment:Offset 表示法(CS:IP)稱為分段地址。這種分離是必要的,因為暫存器本身無法儲存需要超過 16 位編碼的值。在 32 位或 64 位處理器上以保護模式程式設計時,暫存器足夠大,可以完全填充地址匯流排,從而消除了分段地址 - 只有線性/邏輯地址通常在這種“平面定址”模式中使用,儘管為了向後相容,仍然支援Segment:Offset 體系結構。

需要注意的是,物理地址與分段地址之間不存在一一對映關係;對於任何物理地址,都可能存在多個分段地址。例如:考慮分段表示 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+)

[編輯 | 編輯原始碼]

雖然這看起來很醜陋,但實際上它是在朝著以後晶片中使用的保護地址方案邁出的一步。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 位定址

[編輯 | 編輯原始碼]

32 位地址可以覆蓋高達 4 GiB 的記憶體。這意味著我們不需要在 32 位處理器中使用偏移地址。相反,我們使用所謂的“扁平定址”方案,其中暫存器中的地址直接指向物理記憶體位置。段暫存器用於定義不同的段,這樣程式就不會嘗試執行堆疊部分,也不會意外地嘗試在資料部分執行堆疊操作。

A20 門事件

[編輯 | 編輯原始碼]

如前所述,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,你可以訪問向上延伸至 1 MiB 邊界的 32 KiB,然後訪問再向上的 32 KiB,最終會繞回到最底部的 32 KiB。

現在,這些非常棒的程式設計師忽視了一個事實,那就是將會製造出具有更多地址線的處理器。(注意:比爾·蓋茨被認為說過“誰會需要超過 640 KB 的記憶體?”,這些程式設計師可能也持有類似的想法。) 1982 年,僅僅在 8086 推出兩年後,英特爾釋出了具有 24 條地址線的 80286 處理器。雖然從理論上來說,它與傳統的 8086 程式向後相容,因為它也支援真實模式,但許多 8086 程式不能正常工作,因為它們依賴於越界地址繞回到較低的記憶體段。因此,為了相容性,IBM 工程師將 A20 地址線 (8086 具有 A0 - A19 線) 透過鍵盤控制器進行路由,並提供了一種機制來啟用/停用 A20 相容模式。現在如果你想知道為什麼是鍵盤控制器,答案是它有一個未使用的引腳。由於 80286 將被宣傳為與 8086 完全相容 (甚至還沒有推出很久),所以如果 80286 不能實現完全的 bug-for-bug 相容性,升級後的客戶將非常生氣,這樣為 8086 設計的程式碼在 80286 上也能正常執行,只是速度更快。

華夏公益教科書