[英]MOVZX missing 32 bit register to 64 bit register
這是復制(轉換)未簽名寄存器的指令: http : //www.felixcloutier.com/x86/MOVZX.html
基本上指令有 8->16、8->32、8->64、16->32 和 16->64。
32->64 轉換在哪里? 我必須為此使用簽名版本嗎?
如果是這樣,您如何將完整的 64 位用於無符號整數?
如果您還不能保證 RDI 的高位全為零mov eax, edi
請使用mov eax, edi
將 EDI 零擴展到 RAX 。 請參閱: 為什么 32 位寄存器上的 x86-64 指令將完整 64 位寄存器的上半部分置零?
更喜歡使用不同的源/目標寄存器,因為mov eax,eax
在 Intel 和 AMD CPU 上的mov-elimination 失敗。 當移動到不同的寄存器時,您會在不需要執行單元的情況下產生零延遲。 (gcc 顯然不知道這一點,通常零擴展到位。)不過,不要花費額外的指令來實現這一點。
摘要: movzx 和 movsx 的每個不同的源寬度都需要不同的操作碼。 目標寬度由前綴控制。 由於mov
可以完成這項工作,因此movzx dst, r/m32
的新操作碼將是多余的。
在設計 AMD64 匯編語法時,AMD 選擇不讓movzx rax, edx
作為mov eax, edx
的偽指令工作。 這可能是一件好事,因為知道編寫 32 位寄存器將高位字節歸零對於為 x86-64 編寫高效代碼非常重要。
AMD64 確實需要一個新的操作碼用於帶有 32 位源操作數的符號擴展。 他們出於某種原因將助記符命名為movsxd
,而不是將其作為movsx
助記符的第三個操作碼。 英特爾將它們全部記錄在一個 ISA 參考手冊條目中。 他們重新利用了 32 位模式下ARPL
的 1 字節操作碼,因此movsxd
實際上比來自 8 位或 16 位源的movsx
短 1 個字節(假設您仍然需要一個 REX 前綴來擴展到 64 位)。
不同的目標大小使用相同的操作碼和不同的操作數大小1 。 ( 66
或REX.W
前綴用於 16 位或 64 位而不是默認的 32 位。)例如movsx eax, bl
和movsx rax, bl
僅在 REX 前綴上有所不同; 相同的操作碼。 ( movsx ax, bl
也是一樣的,但有一個 66 前綴使操作數大小為 16 位。)
在 AMD64 之前,不需要讀取 32 位源的操作碼,因為最大目標寬度是 32 位,而相同大小的“符號擴展”只是一個副本。 注意movsxd eax, eax
是合法的但不推薦。 您甚至可以使用66
前綴對其進行編碼以讀取 32 位源並寫入 16 位目標2 。
不鼓勵在 64 位模式下使用沒有 REX.W 的 MOVSXD。 應該使用常規 MOV 而不是使用沒有 REX.W 的 MOVSXD。
32->64 位符號擴展可以使用cdq
來完成,以將 EAX 符號擴展為 EDX:EAX(例如在 32 位idiv
之前)。 這是 x86-64 之前的唯一方法(當然除了復制和使用算術右移來廣播符號位)。
但是 AMD64 已經通過任何寫入 32 位寄存器的指令從 32 零擴展到 64 。 這避免了亂序執行的錯誤依賴,這就是 AMD 打破 8086 / 386 傳統的原因,即在寫入部分寄存器時保持高字節不變。 ( 為什么 GCC 不使用部分寄存器? )
由於每個源寬度需要不同的操作碼,因此沒有前綴可以使兩個movzx
操作碼中的任何一個讀取 32 位源。
您有時確實需要花費一條指令來零擴展某些東西。 這在小函數的編譯器輸出中很常見,因為 x86-64 SysV 和 Windows x64 調用約定允許 args 和返回值中的高垃圾。
像往常一樣,如果您想知道如何在 asm 中執行某些操作,請詢問編譯器,尤其是當您沒有看到要查找的說明時。 我省略了每個函數末尾的ret
。
來自 Godbolt 編譯器資源管理器的 Source + asm,用於 System V 調用約定(RDI、RSI、RDX 中的參數...) :
#include <stdint.h>
uint64_t zext(uint32_t a) { return a; }
uint64_t extract_low(uint64_t a) { return a & 0xFFFFFFFF; }
# both compile to
mov eax, edi
int use_as_index(int *p, unsigned a) { return p[a]; }
# gcc
mov esi, esi # missed optimization: mov same,same can't be eliminated on Intel
mov eax, DWORD PTR [rdi+rsi*4]
# clang
mov eax, esi # with signed int a, we'd get movsxd
mov eax, dword ptr [rdi + 4*rax]
uint64_t zext_load(uint32_t *p) { return *p; }
mov eax, DWORD PTR [rdi]
uint64_t zext_add_result(unsigned a, unsigned b) { return a+b; }
lea eax, [rdi+rsi]
x86-64 中的默認地址大小為 64。 高垃圾不會影響加法的低位,因此與lea eax, [edi+esi]
相比,這節省了一個字節lea eax, [edi+esi]
需要一個 67 地址大小前綴,但對每個輸入都給出相同的結果。 當然, add edi, esi
會在 RDI 中產生零擴展結果。
uint64_t zext_mul_result(unsigned a, unsigned b) { return a*b; }
# gcc8.1
mov eax, edi
imul eax, esi
# clang6.0
imul edi, esi
mov rax, rdi # silly: mov eax,edi would save a byte here
英特爾建議銷毀的結果mov
馬上當你有選擇,釋放微架構資源mov
在剔除占用和提高成功率mov
在剔除(這是不是100%的SandyBridge系列,不像AMD銳龍) 。 GCC 的選擇mov
/ imul
是最好的。
此外,在沒有MOV,消除CPU中, mov
IMUL之前可能不會在關鍵路徑上,如果它是另一個輸入這還沒有准備好(即,如果關鍵路徑經過輸入不得到mov
版)。 但是imul
之后的mov
取決於兩個輸入,因此它始終位於關鍵路徑上。
當然,當這些函數內聯時,編譯器通常會知道寄存器的完整狀態,除非它們來自函數返回值。 而且它不需要在特定寄存器中生成結果(RAX 返回值)。 但是,如果您的源代碼草率地將unsigned
與size_t
或uint64_t
混合在一起,則編譯器可能會被迫發出截斷 64 位值的指令。 (查看編譯器 asm 輸出是一個很好的方法來捕捉它並弄清楚如何調整源代碼以讓編譯器保存指令。)
腳注 1 :有趣的事實:AT&T 語法(使用不同的助記符,如movswl
(符號擴展字->long (dword) 或movzbl
)可以從寄存器中推斷目標大小,如movzb %al, %ecx
,但不會組裝movz %al, %ecx
即使沒有歧義。因此它將movzb
視為自己的助記符,具有可以推斷或顯式的常用操作數大小后綴。這意味着每個不同的操作碼在 AT&T 語法中都有自己的助記符。
有關 EAX->RAX 的 CDQE 和任何寄存器的 MOVSXD 之間冗余的歷史教訓,另請參閱匯編 cltq 和 movslq 差異。 請參閱cltq 在匯編中做什么? 或 AT&T 與 Intel 零/符號擴展的助記符的 GAS 文檔。
腳注 2:使用movsxd ax, [rsi]
愚蠢計算機技巧:
匯編程序拒絕匯編movsxd eax, eax
或movsxd ax, eax
,但可以對其進行手動編碼。 ndisasm
甚至不會反匯編它(只是db 0x63
),但 GNU objdump
會。 實際的 CPU 也會對其進行解碼。 我試過 Skylake 只是為了確保:
; NASM source ; register value after stepi in GDB
mov rdx, 0x8081828384858687
movsxd rax, edx ; RAX = 0xffffffff84858687
db 0x63, 0xc2 ;movsxd eax, edx ; RAX = 0x0000000084858687
xor eax,eax ; RAX = 0
db 0x66, 0x63, 0xc2 ;movsxd ax, edx ; RAX = 0x0000000000008687
那么CPU內部是如何處理的呢? 它是否真的讀取 32 位然后截斷到操作數大小? 事實證明,英特爾的 ISA 參考手冊將 16 位格式記錄為63 /r
MOVSXD r16, r/m16
,因此movsxd ax, [unmapped_page - 2]
不會movsxd ax, [unmapped_page - 2]
。 (但它錯誤地將非 REX 形式記錄為在兼容/傳統模式下有效;實際上0x63
在那里解碼為 ARPL。這不是英特爾手冊中的第一個錯誤。)
這是完全有道理的:當沒有 REX.W 前綴時mov r32, r/m32
硬件可以簡單地將其解碼為與mov r16, r/m16
或mov r32, r/m32
的 uop。 或不! Skylake 的movsxd eax,edx
(但不是movsxd rax, edx
)對目標寄存器具有輸出依賴性,就像它合並到目標中一樣! times 4
db 0x63, 0xc2 ; movsx eax, edx
循環db 0x63, 0xc2 ; movsx eax, edx
db 0x63, 0xc2 ; movsx eax, edx
每次迭代運行 4 個時鍾(每個movsxd
1 個,所以 1 個周期延遲)。 uops 相當均勻地分布到所有 4 個整數 ALU 執行端口。 帶有movsxd eax,edx
/ movsxd ebx,edx
/ 2 個其他目的地的循環每次迭代運行約 1.4 個時鍾(如果您使用普通的 4x mov eax, edx
或 4x movsxd rax, edx
mov eax, edx
則比每次迭代前端瓶頸的 1.25 個時鍾movsxd rax, edx
)。 在 i7-6700k 上的 Linux 上使用perf
計時。
我們知道movsxd eax, edx
將 RAX 的高位清零,所以它實際上並沒有使用它正在等待的目標寄存器中的任何位,但大概在內部處理 16 位和 32 位類似地簡化了解碼,並簡化了這種極端情況的處理沒有人應該使用的編碼。 16 位形式總是必須實際合並到目標中,因此它確實對輸出 reg 有真正的依賴性。 (Skylake 不會將 16 位寄存器與完整寄存器分開重命名。)
GNU binutils 反匯編不正確:gdb 和 objdump 將源操作數顯示為 32 位,例如
4000c8: 66 63 c2 movsxd ax,edx
4000cb: 66 63 06 movsxd ax,DWORD PTR [rsi]
什么時候應該
4000c8: 66 63 c2 movsxd ax,dx
4000cb: 66 63 06 movsxd ax,WORD PTR [rsi]
在 AT&T 語法中, objdump 有趣地仍然使用movslq
。 所以我猜它把它當作一個完整的助記符,而不是一個帶有q
操作數大小的movsl
指令。 或者這只是沒有人關心氣體無論如何都不會聚集的特殊情況的結果(它拒絕movsll
,並檢查movslq
寄存器寬度)。
在查看手冊之前,我實際上使用 NASM 在 Skylake 上進行了測試,以查看負載是否會出現故障。 它當然不會:
section .bss
align 4096
resb 4096
unmapped_page:
; When built into a static executable, this page is followed by an unmapped page on my system,
; so I didn't have to do anything more complicated like call mmap
...
_start:
lea rsi, [unmapped_page-2]
db 0x66, 0x63, 0x06 ;movsxd ax, [rsi]. Runs without faulting on Skylake! Hardware only does a 2-byte load
o16 movsxd rax, dword [rsi] ; REX.W prefix takes precedence over o16 (0x66 prefix); this faults
mov eax, [rsi] ; definitely faults if [rsi+2] isn't readable
請注意, movsx al, ax
是不可能的:字節操作數大小需要單獨的操作碼。 前綴只能在 32(默認)、16 位 (0x66) 和長模式 64 位 (REX.W) 之間進行選擇。 movs/zx ax, word [mem]
自 386 年以來就已成為可能,但讀取比目標更寬的源是 x86-64 中的新特性,僅用於符號擴展。 (事實證明,16 位目標編碼實際上只讀取 16 位源。)
順便說一句,AMD可以(但沒有)將 AMD64 設計為在 32 位寄存器寫入時始終進行符號擴展而不是始終零擴展。 在大多數情況下,它對軟件來說不太方便,並且可能還需要一些額外的晶體管,但它仍然可以避免對寄存器中舊值的錯誤依賴。 它可能會在某處添加額外的門延遲,因為結果的高位取決於低位,而不像零擴展,零擴展只取決於它是 32 位操作的事實。 (但這可能並不重要。)
如果AMD 是這樣設計的,他們就需要movzxd
而不是movsxd
。 我認為這種設計的主要缺點是在將位域打包到更寬的寄存器中時需要額外的指令。 例如,免費零擴展對於shl rax,32
/ or rax, rdx
后一個rdtsc
寫入edx
和eax
方便。 如果它是符號擴展,則需要在or
之前將rdx
的高字節置零的指令。
其他 ISA 做出了不同的選擇:MIPS III(約 1995 年)將架構擴展到 64 位,而沒有引入新模式。 與 x86 非常不同的是,在固定寬度的 32 位指令字格式中,有足夠的操作碼空間未使用。
MIPS 一開始是一個 32 位架構,從來沒有像 32 位 x86 那樣從其 16 位 8086 繼承和 8086 完全支持 8 位操作數大小和 AX = AH :AL 部分 regs 等,以便輕松移植 8080 源代碼。
MIPS 32 位算術指令(如 64 位 CPU 上的addu
要求其輸入正確進行符號擴展,並產生符號擴展輸出。 (在不知道更寬寄存器的情況下運行傳統 32 位代碼時,一切正常,因為移位是特殊的。)
ADDU rd, rs, rt
( 來自 MIPS III 手冊,第 A-31 頁)限制:
在 64 位處理器上,如果 GPR rt 或 GPR rs 不包含符號擴展的 32 位值(位 63..31 相等),則運算結果未定義。手術:
if (NotWordValue(GPR[rs]) or NotWordValue(GPR[rt])) then UndefinedResult() endif temp ←GPR[rs] + GPR[rt] GPR[rd]← sign_extend(temp31..0)
(請注意,正如手冊所指出的,U 表示addu
中的 unsigned 確實是用詞不當。您也可以將它用於有符號算術,除非您確實想要add
到有符號溢出的陷阱。)
有一個用於雙字 ADDU 的DADDU
指令,它可以滿足您的期望。 同樣DDIV/DMULT/DSUBU,以及DSLL等移位。
按位運算保持不變:現有的 AND 操作碼變為 64 位 AND; 不需要 64 位 AND,也不需要 32 位 AND 結果的免費符號擴展。
MIPS 32 位移位是特殊的(SLL 是 32 位移位。DSLL 是單獨的指令)。
SLL 字左移 邏輯
手術:
s ← sa temp ← GPR[rt] (31-s)..0 || 0 s GPR[rd]← sign_extend(temp)
編程注意事項:
與幾乎所有其他字操作不同,輸入操作數不必是正確符號擴展的字值,以產生有效的符號擴展 32 位結果。 結果字總是符號擴展到 64 位目標寄存器中; 此具有零移位量的指令將 64 位值截斷為 32 位並對其進行符號擴展。
我認為 SPARC64 和 PowerPC64 在保持窄結果的符號擴展方面類似於 MIPS64。 (a & 0x80000000) +- 12315
for int a
的int a
(使用-fwrapv
所以編譯器不能假設a
是非負的,因為有符號溢出 UB)顯示了 PowerPC64 維護或重做符號擴展的 clang 和 clang -target sparc64
ANDing 然后 ORing 以確保僅設置低 32 中的正確位,再次保持符號擴展。 將返回類型或 arg 類型更改為long
或在 AND 掩碼常量上添加L
后綴會導致 MIPS64 和 PowerPC64 以及有時 SPARC64 的代碼差異; 也許只有 MIPS64 實際上在輸入未正確符號擴展的 32 位指令上會出錯,而在其他指令中,這只是軟件調用約定的要求。
但是AArch64采取的做法更像是X86-64,與w0..31
寄存器是低一半x0..31
,並在兩個操作數的大小可用的指令。
對於這些示例函數,我在上面的 Godbolt 鏈接中包含了 MIPS64 編譯器輸出。 (還有其他一些告訴我們更多關於調用約定和編譯器的信息。)從 32 位到 64 位通常需要dext
來零擴展; 但是直到 mips64r2 才添加該指令。 使用-march=mips3
,為無符號a
return p[a]
必須使用兩個雙字移位(左移然后右移 32 位)到零擴展! 它還需要一個額外的指令來零擴展添加結果,即實現從 unsigned 到uint64_t
。
所以我認為我們很高興 x86-64 被設計成免費的零擴展,而不是只為某些事情提供 64 位操作數大小。 (就像我說的,x86 的傳統非常不同;它已經為使用前綴的相同操作碼提供了可變的操作數大小。)當然,更好的位域指令會更好。 其他一些 ISA,如 ARM 和 PowerPC,讓 x86 因高效的位域插入/提取而蒙羞。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.