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

How Do I Use Labels In GCC Inline Assembly?

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

#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被定义多次在程序集文件中。

我试图找到这个问题的答案,但似乎没有任何效果。在this previusly asked question中建议改用本地标签,我也试过了:

#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

我也尝试了 this previusly asked question 的建议解决方案并将名称 .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    , %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    , %edi
    call    __printf_chk
    movq    24(%rsp), %rsi
    xorq    %fs:40, %rsi
    jne .L6
    xorl    %eax, %eax
    addq    , %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

你不能像那样将一堆 asm 语句内联。优化器可以根据它知道的约束自由地重新排序、复制和删除它们。 (在你的例子中,它知道 none。)

所以首先,您应该将 asm 合并在一起,并使用适当的 read/write/clobber 约束。其次,有一种特殊的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;
}

您不能假设值在您的 asm 代码中的任何特定寄存器中——您需要使用约束来告诉 gcc 您想要读取和写入哪些值,并让它告诉您它们在哪个寄存器中。gcc docs tell you most of what you need to know, but are pretty dense. There are also tutorials out there that you can easily find with a web search (here or here)

有很多教程 - 包括 this one (probably the best I know of), and some info on

这是第一个实现 - 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__)是必需的,因为编译器仅 'sees' (a)(b)(并且不t 'know' 你可能会交换它们的内容),否则可以自由地优化整个 asm 语句 - tmp0tmp1 否则也将被视为未使用的变量.

  • "+r"表示这既是输入又是输出,可以修改;在这种情况下,只有它 不是 ,它们可以严格地 仅输入 - 稍后会详细介绍...

  • 'movl' 上的 'l' 后缀并不是真正必要的;寄存器的 'k'(32 位)长度修饰符也不是。由于您使用的是 Linux (ELF) ABI,因此对于 IA32 和 x86-64 ABI,int 都是 32 位。

  • %= 令牌为我们生成了一个唯一的标签。顺便说一句,跳转语法 <label>f 表示 forward 跳转,而 <label>b 表示 back.

  • 为了正确性,我们需要 "memory",因为编译器无法知道解引用指​​针的值是否已更改。这可能是被 C 代码包围的更复杂的内联 asm 中的一个问题,因为它会使内存中所有当前保存的值无效 - 并且通常是一种大锤方法。以这种方式出现在函数的末尾,这不会成为问题 - 但您可以阅读更多内容 here(参见:Clobbers

  • "cc" flags register clobber 在同一节中有详细说明。在 x86 上,它 什么都不做 。为了清楚起见,一些作者将其包括在内,但由于实际上所有重要的 asm 语句都会影响标志寄存器,因此它只是假设默认情况下会被破坏。

这是 C 实现 - swap_1 :

void swap_1 (int *a, int *b)
{
    if (*a > *b)
    {
        int t = *a; *a = *b; *b = t;
    }
}

为 x86-64 ELF 使用 gcc -O2 编译,我得到相同的代码。编译器选择 tmp0tmp1 为临时使用相同的自由寄存器只是有点幸运...消除噪音,如 .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' 修饰符,因为不需要它们。我还对操作数使用了 'symbolic name' 语法,因为它通常有助于提高代码的可读性。

(a)(b) 现在确实是仅输入寄存器。那么 "=&r" 语法是什么意思呢? & 表示 early clobber 操作数。在这种情况下,在我们完成使用输入操作数之前,可能会将值写入,因此编译器必须选择与为输入操作数选择的寄存器不同的寄存器。

编译器再次生成与 swap_1swap_2 相同的代码。


我在这个答案上写的比我计划的要多,但正如你所看到的,很难保持对编译器必须知道的所有信息的认识,以及每个指令集的特性( ISA) 和 ABI.