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,則 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 一樣。
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 只有 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 具有保護模式,在該模式下,其所有 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 位地址可以覆蓋高達 4 GiB 的記憶體。這意味著我們不需要在 32 位處理器中使用偏移地址。相反,我們使用所謂的“扁平定址”方案,其中暫存器中的地址直接指向物理記憶體位置。段暫存器用於定義不同的段,這樣程式就不會嘗試執行堆疊部分,也不會意外地嘗試在資料部分執行堆疊操作。
如前所述,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,你可以訪問向上延伸至 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 上也能正常執行,只是速度更快。