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

时间:2017-03-08 20:53:07

标签: c gcc assembly 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

最后我还尝试了this suggestion

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

3 个答案:

答案 0 :(得分:7)

有很多教程 - 包括this one(可能是我所知道的最好的),以及operand size modifiers上的一些信息。

这是第一个实施 - 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__)是必需的,因为编译器只会看到&#39; (a)(b)(并且不知道您可能会交换他们的内容),否则可以自由优化整个asm tmp0tmp1 } statement off - "+r"int也会被视为未使用的变量。

  • %=表示这是一个可以修改的输入和输出;在这种情况下,只有不是,并且它们可以严格地仅输入 - 稍微多一点......

  • &#39; l&#39;后缀&#39; movl&#39;并非真的有必要;也不是&#39; k&#39;寄存器的(32位)长度修改器。由于您使用的是Linux(ELF)ABI,因此IA32和x86-64 ABI的<label>f为32位。

  • <label>b令牌为我们生成一个唯一的标签。顺便说一句,跳转语法"memory"表示转发跳转,而"cc"表示返回

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

  • swap_1标志寄存器clobber在同一节中详述。在x86上,它确实没有。一些作者为了清楚起见而将其包含在内,但由于几乎所有非平凡的void swap_1 (int *a, int *b) { if (*a > *b) { int t = *a; *a = *b; *b = t; } } 语句都会影响标志寄存器,因此默认情况下它只是被假定为。

这是C实现 - gcc -O2

tmp0

使用tmp1编译x86-64 ELF,我得到相同的代码。运气好,编译器选择swap_2: movl (%rdi), %eax movl (%rsi), %edx cmpl %edx, %eax jle 21f movl %edx, (%rdi) movl %eax, (%rsi) 21: ret swap_1来使用相同的临时寄存器...切断噪声,如.cfi指令等,给出:

.L1

如上所述,-m32代码是相同的,只是编译器为其跳转标签选择了%rdi。使用%rsi编译代码生成相同的代码(除了以不同的顺序使用tmp寄存器)。由于IA32 ELF ABI在堆栈上传递参数,因此有更多开销,而x86-64 ABI分别传递(a)(b)中的前两个参数。

仅将swap_3void 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" */ ); } 视为输入 - (a)

(b)

我已经废除了&#39; l&#39;后缀和&#39; k&#39;这里有修饰符,因为它们不需要。我还使用了这个符号名称&#39;操作数的语法,因为它通常有助于使代码更具可读性。

"=&r"&现在确实是仅输入寄存器。那么swap_1语法是什么意思呢? swap_2表示早期clobber 操作数。在这种情况下,可以在使用输入操作数之前将值写入,因此编译器必须选择与为输入操作数选择的寄存器不同的寄存器。

编译器再次生成与$(document).ready(function(){ if($('.txt-input').val().length === 0){ $('.erase').hide(); } else{ $('.erase').show(); }; $(".erase").click(function(){ $('.txt-input').val(''); }); });<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script> <form> <input type="text" class="txt-input"/> </form> <p class="erase">clear</p>相同的代码。

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

答案 1 :(得分:3)

你不能像这样内联一堆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;
}

答案 2 :(得分:0)

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