如何在GCC内联汇编中使用标签?

我正在尝试学习x86-64内联汇编并决定实现这个非常简单的交换方法,只需按升序排序ab

 #include  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  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 

我也尝试了这个普遍问的问题的建议解决方案,并将名称.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语句之外 - tmp0tmp1也将被视为未使用的变量。

  • "+r"表示这是一个可以修改的输入和输出; 只有它不是在这种情况下,他们可以严格只输入 - 更多的是在...

  • 'movl'上的'l'后缀并不是必需的; 既不是寄存器的'k'(32位)长度修改器。 由于您使用的是Linux(ELF)ABI,因此IA32和x86-64 ABI的int为32位。

  • %=令牌为我们生成唯一标签。 顺便说一句,跳转语法表示向前跳转, 表示向后跳转。

  • 为了正确,我们需要"memory"因为编译器无法知道来自解引用指针的值是否已被更改。 这可能是由C代码包围的更复杂的内联asm中的问题,因为它使内存中所有当前保持的值无效 - 并且通常是大锤方法。 以这种方式出现在函数的末尾,它不会成为一个问题 - 但你可以在这里阅读更多内容(参见: 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编译x86-64 ELF,我得到相同的代码。 运气好,编译器选择tmp0tmp1来为temps使用相同的自由寄存器...切断噪声,如.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_1swap_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) ); 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; } 

您不能假设值在您的asm代码中的任何特定寄存器中 – 您需要使用约束来告诉gcc您想要读取和写入的值,并让它告诉您它们所在的寄存器.gcc docs告诉您大部分你需要知道什么,但相当密集。 还有一些教程可以通过网络搜索轻松找到( 这里或这里 )