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

fig*_*n93 2 c assembly 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;
}
Run Code Online (Sandbox Code Playgroud)

当我使用此命令运行它时,上面的代码按预期工作:

> gcc main.c
> ./a.out
> 3 2 1
> 1 2 3
Run Code Online (Sandbox Code Playgroud)

但是,一旦我转向优化,我就会收到以下错误消息:

> 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
Run Code Online (Sandbox Code Playgroud)

如果我理解正确,这是因为在打开优化时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");
}
Run Code Online (Sandbox Code Playgroud)

但是在尝试运行程序时,我现在得到了一个分段错误:

> gcc -O2 main.c
> ./a.out
> 3 2 1
> Segmentation fault
Run Code Online (Sandbox Code Playgroud)

我也尝试了建议的解决方案,这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
Run Code Online (Sandbox Code Playgroud)

最后我也尝试了这个建议:

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");
}
Run Code Online (Sandbox Code Playgroud)

但后来我得到了这些错误:

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
Run Code Online (Sandbox Code Playgroud)

所以,我的问题是:

如何在内联装配中使用标签?


这是优化版本的反汇编输出:

> 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
Run Code Online (Sandbox Code Playgroud)

Bre*_*ale 7

有很多教程 - 包括这个(可能是我所知道的最好的),以及操作数大小修饰符的一些信息.

这是第一个实现 - 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" */ );
}
Run Code Online (Sandbox Code Playgroud)

几点说明:

  • volatile(或__volatile__)是必需的,因为只有编译器"看到" (a)(b)(和不"知道"你有可能交换它们的内容),以及原本可以自由地优化整个asm陈述了- tmp0tmp1否则将被视为未使用的变量太.

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

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

  • %=令牌生成我们唯一的标签.顺便说一下,跳转语法<label>f意味着向前跳转,<label>b意味着回退.

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

  • "cc"标志寄存器撞是在同一节详细.在x86上,它什么都不做.一些作者为了清楚起见而将其包含在内,但由于几乎所有非平凡的asm语句都会影响标志寄存器,因此它被假定为默认情况下被破坏.

这是C实现 - swap_1:

void swap_1 (int *a, int *b)
{
    if (*a > *b)
    {
        int t = *a; *a = *b; *b = t;
    }
}
Run Code Online (Sandbox Code Playgroud)

使用gcc -O2x86-64 ELF进行编译,我得到相同的代码.只是运气好,编译器选择tmp0tmp1使用相同的自由寄存器临时...切除噪声,如.cfi指令等,给出:

swap_2:
        movl (%rdi), %eax
        movl (%rsi), %edx
        cmpl %edx, %eax
        jle  21f
        movl %edx, (%rdi)
        movl %eax, (%rsi)
        21:
        ret
Run Code Online (Sandbox Code Playgroud)

如上所述,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" */ );
}
Run Code Online (Sandbox Code Playgroud)

我已经废除了'l'后缀和'k'修饰符,因为它们不需要.我还为操作数使用了"符号名称"语法,因为它通常有助于使代码更具可读性.

(a)(b)现在确实只输入寄存器.那么"=&r"语法是什么意思呢?所述&表示早期撞操作数.在这种情况下,可以我们使用输入操作数之前写入该值,因此编译器必须选择与为输入操作数选择的寄存器不同的寄存器.

再次,编译器生成,因为它没有对相同的代码swap_1swap_2.


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