我正在關注 Linux 64 系統中的“Beginning x64 Assembly Programming”一書。我正在使用 NASM 和 gcc。
在關于浮點運算的章節中,本書指定了以下用于添加 2 個浮點數的代碼。在本書和其他在線資源中,我已經閱讀了暫存器 RAX 根據呼叫約定指定要使用的 XMM 暫存器的數量。
書中的代碼如下:
extern printf
section .data
num1 dq 9.0
num2 dq 73.0
fmt db "The numbers are %f and %f",10,0
f_sum db "%f %f = %f",10,0
section .text
global main
main:
push rbp
mov rbp, rsp
printn:
movsd xmm0, [num1]
movsd xmm1, [num2]
mov rdi, fmt
mov rax, 2 ;for printf rax specifies amount of xmm registers
call printf
sum:
movsd xmm2, [num1]
addsd xmm2, [num2]
printsum:
movsd xmm0, [num1]
movsd xmm1, [num2]
mov rdi, f_sum
mov rax, 3
call printf
這按預期作業。
然后,在最后一次printf通話之前,我嘗試改變
mov rax, 3
為了
mov rax, 1
然后我重新組裝并運行程式。
我期待一些不同的廢話輸出,但令我驚訝的是輸出完全相同。printf正確輸出 3 個浮點值:
The numbers are 9.000000 and 73.000000 9.000000 73.000000 = 82.000000
我想當期望使用多個 XMM 暫存器時存在某種覆寫printf,只要 RAX 不為 0,它將使用連續的 XMM 暫存器。我在呼叫約定和 NASM 手冊中搜索了解釋,但沒有找到。
這行得通的原因是什么?
uj5u.com熱心網友回復:
x86-64 SysV ABI的嚴格規則允許實作只保存指定的 XMM regs 的確切數量,但當前實作只檢查零/非零,因為這樣很有效,特別是對于 AL=0 常見情況。
如果您在 AL 1中傳遞的數字低于 XMM 暫存器引數的實際數量,或者高于 8 的數字,則您將違反 ABI,并且只有此實作細節才能阻止您的代碼中斷。(即它“恰好可以作業”,但不受任何標準或檔案的保證,并且不能移植到其他一些實際實作中,例如使用 GCC4.5 或更早版本構建的舊 GNU/Linux 發行版。)
此問答顯示了 glibc printf 的當前版本,它只檢查AL!=0,而舊版本的 glibc 將跳轉目標計算到一系列movaps存盤中。(那個問答是關于那個代碼在什么時候被破壞AL>8,使計算跳轉到它不應該去的地方。)
為什么eax包含向量引數的數量?參考 ABI 檔案,并顯示 ICC 代碼生成,該代碼生成類似地使用與舊 GCC 相同的指令進行計算跳轉。
Glibc 的printf實作是從 C 源代碼編譯的,通常由 GCC 編譯。 當現代 GCC 編譯像 printf 這樣的可變引數函式時,它會生成只檢查零與非零 AL 的 asm,如果非零,則將所有 8 個 arg-passing XMM 暫存器轉儲到堆疊上的陣列中。
GCC4.5 和更早版本實際上確實使用 AL 中的數字來計算跳轉到movaps存盤序列中,以實際只保存必要的 XMM regs。
Nate 的簡單示例來自對帶有GCC4.5與 GCC11 的 Godbolt 的評論,顯示出與舊/新 glibc(由 GCC 構建)反匯編的鏈接答案相同的差異,這不足為奇。此函式只使用va_arg(v, double);,從不使用整數型別,因此它不會將傳入的 RDI...R9 轉儲到任何地方,這與printf. 它是一個葉函式,因此它可以使用紅色區域(低于 RSP 的 128 個位元組)。
# GCC4.5.3 -O3 -fPIC to compile like glibc would
add_them:
movzx eax, al
sub rsp, 48 # reserve stack space, needed either way
lea rdx, 0[0 rax*4] # each movaps is 4 bytes long
lea rax, .L2[rip] # code pointer to after the last movaps
lea rsi, -136[rsp] # used later by va_arg. test/jz version does the same, but after the movaps stores
sub rax, rdx
lea rdx, 39[rsp] # used later by va_arg, test/jz version also does an LEA like this
jmp rax # AL=0 case jumps to L2
movaps XMMWORD PTR -15[rdx], xmm7 # using RDX as a base makes each movaps 4 bytes long, vs. 5 with RSP
movaps XMMWORD PTR -31[rdx], xmm6
movaps XMMWORD PTR -47[rdx], xmm5
movaps XMMWORD PTR -63[rdx], xmm4
movaps XMMWORD PTR -79[rdx], xmm3
movaps XMMWORD PTR -95[rdx], xmm2
movaps XMMWORD PTR -111[rdx], xmm1
movaps XMMWORD PTR -127[rdx], xmm0 # xmm0 last, will be ready for store-forwading last
.L2:
lea rax, 56[rsp] # first stack arg (if any), I think
## rest of the function
對比
# GCC11.2 -O3 -fPIC
add_them:
sub rsp, 48
test al, al
je .L15 # only one test&branch macro-fused uop
movaps XMMWORD PTR -88[rsp], xmm0 # xmm0 first
movaps XMMWORD PTR -72[rsp], xmm1
movaps XMMWORD PTR -56[rsp], xmm2
movaps XMMWORD PTR -40[rsp], xmm3
movaps XMMWORD PTR -24[rsp], xmm4
movaps XMMWORD PTR -8[rsp], xmm5
movaps XMMWORD PTR 8[rsp], xmm6
movaps XMMWORD PTR 24[rsp], xmm7
.L15:
lea rax, 56[rsp] # first stack arg (if any), I think
lea rsi, -136[rsp] # used by va_arg. done after the movaps stores instead of before.
...
lea rdx, 56[rsp] # used by va_arg. With a different offset than older GCC, but used somewhat similarly. Redundant with the LEA into RAX; silly compiler.
GCC 可能改變了策略,因為計算的跳轉需要更多的靜態代碼大小(I-cache 占用空間),并且 test/jz 比間接跳轉更容易預測。更重要的是,在常見的 AL=0 (no-XMM) case 2中執行的微指令更少。即使對于 AL=1 最壞的情況(7 個死movaps存盤但沒有完成計算分支目標的作業),也沒有更多。
相關問答:
- 匯編可執行檔案不顯示任何內容 (x64) AL != 0 與 glibc printf 的計算跳轉代碼生成
- 為什么在呼叫 printf 之前將
轉載請註明出處,本文鏈接:https://www.uj5u.com/caozuo/463857.html下一篇:我遇到分段錯誤-裝配
