[英]How Do I Use Labels In GCC Inline Assembly?
我正在嘗試學習 x86-64 內聯匯編,並決定實現這個非常簡單的交換方法,只需按升序對a
和b
進行排序:
#include <stdio.h>
void swap(int* a, int* b)
{
asm(".intel_syntax noprefix");
asm("mov eax, DWORD PTR [rdi]");
asm("mov ebx, DWORD PTR [rsi]");
asm("cmp eax, ebx");
asm("jle .L1");
asm("mov DWORD PTR [rdi], ebx");
asm("mov DWORD PTR [rsi], eax");
asm(".L1:");
asm(".att_syntax noprefix");
}
int main()
{
int input[3];
scanf("%d%d%d", &input[0], &input[1], &input[2]);
swap(&input[0], &input[1]);
swap(&input[1], &input[2]);
swap(&input[0], &input[1]);
printf("%d %d %d\n", input[0], input[1], input[2]);
return 0;
}
當我使用以下命令運行時,上面的代碼按預期工作:
> gcc main.c
> ./a.out
> 3 2 1
> 1 2 3
但是,一旦我打開優化,我就會收到以下錯誤消息:
> gcc -O2 main.c
> main.c: Assembler messages:
> main.c:12: Error: symbol `.L1' is already defined
> main.c:12: Error: symbol `.L1' is already defined
> main.c:12: Error: symbol `.L1' is already defined
如果我理解正確的話,這是因為gcc
在優化打開時嘗試內聯我的swap
函數,導致標簽.L1
在程序集文件中被定義多次。
我試圖找到這個問題的答案,但似乎沒有任何效果。 在這個以前問過的問題中,建議改用本地標簽,我也試過了:
#include <stdio.h>
void swap(int* a, int* b)
{
asm(".intel_syntax noprefix");
asm("mov eax, DWORD PTR [rdi]");
asm("mov ebx, DWORD PTR [rsi]");
asm("cmp eax, ebx");
asm("jle 1f");
asm("mov DWORD PTR [rdi], ebx");
asm("mov DWORD PTR [rsi], eax");
asm("1:");
asm(".att_syntax noprefix");
}
但是當嘗試運行程序時,我現在得到了一個分段錯誤:
> gcc -O2 main.c
> ./a.out
> 3 2 1
> Segmentation fault
我也嘗試了建議的解決方案, 這previusly被問到的問題,改變了名稱.L1
到CustomLabel1
的情況下,將有一個名稱沖突,但它仍然給我的老錯誤:
> gcc -O2 main.c
> main.c: Assembler messages:
> main.c:12: Error: symbol `CustomLabel1' is already defined
> main.c:12: Error: symbol `CustomLabel1' is already defined
> main.c:12: Error: symbol `CustomLabel1' is already defined
最后我也嘗試了這個建議:
void swap(int* a, int* b)
{
asm(".intel_syntax noprefix");
asm("mov eax, DWORD PTR [rdi]");
asm("mov ebx, DWORD PTR [rsi]");
asm("cmp eax, ebx");
asm("jle label%=");
asm("mov DWORD PTR [rdi], ebx");
asm("mov DWORD PTR [rsi], eax");
asm("label%=:");
asm(".att_syntax noprefix");
}
但是后來我得到了這些錯誤:
main.c: Assembler messages:
main.c:9: Error: invalid character '=' in operand 1
main.c:12: Error: invalid character '%' in mnemonic
main.c:9: Error: invalid character '=' in operand 1
main.c:12: Error: invalid character '%' in mnemonic
main.c:9: Error: invalid character '=' in operand 1
main.c:12: Error: invalid character '%' in mnemonic
main.c:9: Error: invalid character '=' in operand 1
main.c:12: Error: invalid character '%' in mnemonic
所以,我的問題是:
如何在內聯匯編中使用標簽?
這是優化版本的反匯編輸出:
> gcc -O2 -S main.c
.file "main.c"
.section .text.unlikely,"ax",@progbits
.LCOLDB0:
.text
.LHOTB0:
.p2align 4,,15
.globl swap
.type swap, @function
swap:
.LFB23:
.cfi_startproc
#APP
# 5 "main.c" 1
.intel_syntax noprefix
# 0 "" 2
# 6 "main.c" 1
mov eax, DWORD PTR [rdi]
# 0 "" 2
# 7 "main.c" 1
mov ebx, DWORD PTR [rsi]
# 0 "" 2
# 8 "main.c" 1
cmp eax, ebx
# 0 "" 2
# 9 "main.c" 1
jle 1f
# 0 "" 2
# 10 "main.c" 1
mov DWORD PTR [rdi], ebx
# 0 "" 2
# 11 "main.c" 1
mov DWORD PTR [rsi], eax
# 0 "" 2
# 12 "main.c" 1
1:
# 0 "" 2
# 13 "main.c" 1
.att_syntax noprefix
# 0 "" 2
#NO_APP
ret
.cfi_endproc
.LFE23:
.size swap, .-swap
.section .text.unlikely
.LCOLDE0:
.text
.LHOTE0:
.section .rodata.str1.1,"aMS",@progbits,1
.LC1:
.string "%d%d%d"
.LC2:
.string "%d %d %d\n"
.section .text.unlikely
.LCOLDB3:
.section .text.startup,"ax",@progbits
.LHOTB3:
.p2align 4,,15
.globl main
.type main, @function
main:
.LFB24:
.cfi_startproc
subq $40, %rsp
.cfi_def_cfa_offset 48
movl $.LC1, %edi
movq %fs:40, %rax
movq %rax, 24(%rsp)
xorl %eax, %eax
leaq 8(%rsp), %rcx
leaq 4(%rsp), %rdx
movq %rsp, %rsi
call __isoc99_scanf
#APP
# 5 "main.c" 1
.intel_syntax noprefix
# 0 "" 2
# 6 "main.c" 1
mov eax, DWORD PTR [rdi]
# 0 "" 2
# 7 "main.c" 1
mov ebx, DWORD PTR [rsi]
# 0 "" 2
# 8 "main.c" 1
cmp eax, ebx
# 0 "" 2
# 9 "main.c" 1
jle 1f
# 0 "" 2
# 10 "main.c" 1
mov DWORD PTR [rdi], ebx
# 0 "" 2
# 11 "main.c" 1
mov DWORD PTR [rsi], eax
# 0 "" 2
# 12 "main.c" 1
1:
# 0 "" 2
# 13 "main.c" 1
.att_syntax noprefix
# 0 "" 2
# 5 "main.c" 1
.intel_syntax noprefix
# 0 "" 2
# 6 "main.c" 1
mov eax, DWORD PTR [rdi]
# 0 "" 2
# 7 "main.c" 1
mov ebx, DWORD PTR [rsi]
# 0 "" 2
# 8 "main.c" 1
cmp eax, ebx
# 0 "" 2
# 9 "main.c" 1
jle 1f
# 0 "" 2
# 10 "main.c" 1
mov DWORD PTR [rdi], ebx
# 0 "" 2
# 11 "main.c" 1
mov DWORD PTR [rsi], eax
# 0 "" 2
# 12 "main.c" 1
1:
# 0 "" 2
# 13 "main.c" 1
.att_syntax noprefix
# 0 "" 2
# 5 "main.c" 1
.intel_syntax noprefix
# 0 "" 2
# 6 "main.c" 1
mov eax, DWORD PTR [rdi]
# 0 "" 2
# 7 "main.c" 1
mov ebx, DWORD PTR [rsi]
# 0 "" 2
# 8 "main.c" 1
cmp eax, ebx
# 0 "" 2
# 9 "main.c" 1
jle 1f
# 0 "" 2
# 10 "main.c" 1
mov DWORD PTR [rdi], ebx
# 0 "" 2
# 11 "main.c" 1
mov DWORD PTR [rsi], eax
# 0 "" 2
# 12 "main.c" 1
1:
# 0 "" 2
# 13 "main.c" 1
.att_syntax noprefix
# 0 "" 2
#NO_APP
movl 8(%rsp), %r8d
movl 4(%rsp), %ecx
movl $.LC2, %esi
movl (%rsp), %edx
xorl %eax, %eax
movl $1, %edi
call __printf_chk
movq 24(%rsp), %rsi
xorq %fs:40, %rsi
jne .L6
xorl %eax, %eax
addq $40, %rsp
.cfi_remember_state
.cfi_def_cfa_offset 8
ret
.L6:
.cfi_restore_state
call __stack_chk_fail
.cfi_endproc
.LFE24:
.size main, .-main
.section .text.unlikely
.LCOLDE3:
.section .text.startup
.LHOTE3:
.ident "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.4) 5.4.0 20160609"
.section .note.GNU-stack,"",@progbits
有很多教程 - 包括這個(可能是我所知道的最好的),以及一些關於操作數大小修飾符的信息。
這是第一個實現 - swap_2
:
void swap_2 (int *a, int *b)
{
int tmp0, tmp1;
__asm__ volatile (
"movl (%0), %k2\n\t" /* %2 (tmp0) = (*a) */
"movl (%1), %k3\n\t" /* %3 (tmp1) = (*b) */
"cmpl %k3, %k2\n\t"
"jle %=f\n\t" /* if (%2 <= %3) (at&t!) */
"movl %k3, (%0)\n\t"
"movl %k2, (%1)\n\t"
"%=:\n\t"
: "+r" (a), "+r" (b), "=r" (tmp0), "=r" (tmp1) :
: "memory" /* "cc" */ );
}
一些注意事項:
volatile
(或__volatile__
)是必需的,因為編譯器只“看到” (a)
和(b)
(並且不“知道”您可能正在交換它們的內容),否則可以自由優化整個asm
語句離開 - tmp0
和tmp1
否則也會被視為未使用的變量。
"+r"
表示這是一個可以修改的輸入和輸出; 只是它不是在這種情況下,並且它們只能嚴格輸入- 稍后會詳細介紹......
'movl' 上的 'l' 后綴並不是必需的; 寄存器的“k”(32 位)長度修飾符也不是。 由於您使用的是 Linux (ELF) ABI,因此對於 IA32 和 x86-64 ABI, int
都是 32 位。
%=
令牌為我們生成一個唯一的標簽。 順便說一句,跳轉語法<label>f
表示向前跳轉,而<label>b
表示向后跳轉。
為了正確性,我們需要"memory"
因為編譯器無法知道來自解除引用的指針的值是否已更改。 在被 C 代碼包圍的更復雜的內聯匯編中,這可能是一個問題,因為它會使內存中所有當前保存的值無效 - 並且通常是一種大錘方法。 以這種方式出現在函數的末尾,這不會成為問題 - 但您可以在此處閱讀更多相關信息(請參閱: Clobbers )
"cc"
標志寄存器 clobber 在同一部分中有詳細說明。 在 x86 上,它什么也不做。 一些作者為了清楚起見將它包含在內,但由於實際上所有非平凡的asm
語句都會影響標志寄存器,因此它只是默認情況下被破壞。
這是 C 實現 - swap_1
:
void swap_1 (int *a, int *b)
{
if (*a > *b)
{
int t = *a; *a = *b; *b = t;
}
}
使用gcc -O2
for x86-64 ELF 編譯,我得到相同的代碼。 幸運的是,編譯器選擇tmp0
和tmp1
來使用相同的空閑寄存器作為臨時寄存器......消除噪音,如 .cfi 指令等,給出:
swap_2:
movl (%rdi), %eax
movl (%rsi), %edx
cmpl %edx, %eax
jle 21f
movl %edx, (%rdi)
movl %eax, (%rsi)
21:
ret
如前所述, swap_1
代碼是相同的,只是編譯器選擇了.L1
作為其跳轉標簽。 使用-m32
編譯代碼會生成相同的代碼(除了以不同的順序使用 tmp 寄存器)。 開銷更大,因為 IA32 ELF ABI 在堆棧上傳遞參數,而 x86-64 ABI 分別傳遞%rdi
和%rsi
的前兩個參數。
僅將(a)
和(b)
視為輸入 - swap_3
:
void swap_3 (int *a, int *b)
{
int tmp0, tmp1;
__asm__ volatile (
"mov (%[a]), %[x]\n\t" /* x = (*a) */
"mov (%[b]), %[y]\n\t" /* y = (*b) */
"cmp %[y], %[x]\n\t"
"jle %=f\n\t" /* if (x <= y) (at&t!) */
"mov %[y], (%[a])\n\t"
"mov %[x], (%[b])\n\t"
"%=:\n\t"
: [x] "=&r" (tmp0), [y] "=&r" (tmp1)
: [a] "r" (a), [b] "r" (b) : "memory" /* "cc" */ );
}
我已經取消了此處的“l”后綴和“k”修飾符,因為它們不是必需的。 我還對操作數使用了“符號名稱”語法,因為它通常有助於使代碼更具可讀性。
(a)
和(b)
現在確實是僅輸入寄存器。 那么"=&r"
語法是什么意思呢? &
表示早期的clobber操作數。 在這種情況下,可能會在我們使用完輸入操作數之前寫入該值,因此編譯器必須選擇與為輸入操作數選擇的寄存器不同的寄存器。
再一次,編譯器生成與swap_1
和swap_2
相同的代碼。
我在這個答案上寫的比我計划的要多,但正如你所看到的,很難保持對編譯器必須知道的所有信息的認識,以及每個指令集 (ISA) 和 ABI 的特性。
你不能像這樣將一堆asm
語句放在內聯。 優化器可以根據它知道的約束自由地重新排序、復制和刪除它們。 (在你的情況下,它不知道。)
因此,首先,您應該使用適當的讀/寫/clobber 約束將 asm 合並在一起。 其次,有一個特殊的asm goto
形式,它可以將匯編到 C 級標簽。
void swap(int *a, int *b) {
int tmp1, tmp2;
asm(
"mov (%2), %0\n"
"mov (%3), %1\n"
: "=r" (tmp1), "=r" (tmp2)
: "r" (a), "r" (b)
: "memory" // pointer in register doesn't imply that the pointed-to memory has to be "in sync"
// or use "m" memory source operands to let the compiler pick the addressing mode
);
asm goto(
"cmp %1, %0\n"
"jle %l4\n"
"mov %1, (%2)\n"
"mov %0, (%3)\n"
:
: "r" (tmp1), "r" (tmp2), "r" (a), "r" (b)
: "cc", "memory"
: L1
);
L1:
return;
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.