2017-03-08 56 views
1

我努力学习的x86-64内联汇编,并决定实施这个非常简单的交换方法简单单ab按升序排列:如何在GCC内联装配中使用标签?

#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并更名.L1CustomLabel1的情况下,将有一个名称冲突,但它仍然给我的老错误:

> 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 
+0

你试过命名没有点的标签吗?从C生成的装配标签通常具有圆点形式。我认为它可能是保留的。 – PSkocik

+0

后一种解决方案是正确的语法。只是你的代码无法正常工作而崩溃。 –

+0

@PSkocik是的,我尝试命名它“CustomLabel1”(没有点),它仍然给我同样的错误。 –

回答

6

有很多教程 - 包括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__)是必需的,因为编译器只 '看到' (a)(b)(并且不'知道'你有可能交换他们的内容),否则可以自由地优化整个asm声明 - tmp0tmp1否则将被视为未使用的变量。

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

  • 上“MOVL”的“L”后缀是不是真的有必要;寄存器的'k'(32位)长度修饰符也不是。由于您使用的是Linux(ELF)ABI,因此对于IA32和x86-64 ABI,int均为32位。

  • %=令牌为我们生成一个唯一的标签。 BTW,跳转语法<label>f表示转发跳转,而<label>b表示返回

  • 为了正确,我们需要"memory",因为编译器无法知道取消引用的指针的值是否已更改。这可能是由C代码包围的更复杂的内联asm中的一个问题,因为它会使内存中所有当前保存的值无效 - 并且通常是大锤方法。在这种方式的函数的最后面,它不会是一个问题 - 但你可以阅读更多关于它here(参见:则会覆盖

  • "cc"标志寄存器撞是在同一节详细。在x86上,它没有没有。有些作者为了清楚起见而将其包括在内,但由于几乎所有不重要的语句都会影响标志寄存器,所以它只是假设在默认情况下被破坏。

这里的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使用相同的自由寄存器临时工......交织出噪音,像.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"语法是什么意思? &表示早期触发器操作数。在这种情况下,可以在之前将值写入,因为我们使用输入操作数,因此编译器必须选择与为输入操作数选择的寄存器不同的寄存器。

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


我写的比我计划这个答案的方式较多,但你可以看到,这是非常难以维持的编译器必须知道的所有信息意识,以及每个指令的特质设置(ISA)和ABI。

+0

非常详细的解释和代码工作没有任何segfaults并产生正确的输出,所以我会选择这一个作为答案。虽然,我可以问,为什么编译器不会自动执行此操作?编译器不应该能够读取我们编写的asm指令,并理解哪些寄存器和内存位置已被更改?我也可以问,这在英特尔语法中看起来如何?再次感谢您的详细解答。 –

+0

@ fighting_falcon93根据内嵌汇编程序模板的复杂性,代码分析工具可能无法准确确定汇编程序模板中的变化以及副作用。 GCC开发者并没有在可能无法工作的东西上浪费自己的努力,而是强迫开发者明确并强制他们准确地描述输入,输出,破坏者和其他副作用。 –

3

你不能只是把一堆asm报表内联这样的。优化器可以根据知道的约束条件自由地重新排序,复制和删除它们。 (在你的情况下,它什么都不知道)。

所以首先,你应该把asm合并在一起,并且要有正确的读/写/ 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) 
    ); 
    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; 
} 
+1

我试过了你的建议,但是它在用'-O2'编译时仍然会产生分段错误,但是它没有任何优化就可以正常运行。 –

+0

@ fighting_falcon93也许试着编译成汇编代码(gcc -S),这样你就可以看看编译器实际在做什么 – immibis

+3

我可能试过类似的东西(没有goto);避免“记忆”闯入;并让汇编程序选择临时寄存器:'int temp1,temp2; __asm__( “MOV%[A],%[TMP1] \ n上\ t” 的 “MOV%[B],%[TMP2] \ n上\ t” 的 “CMP%[TMP2],%[TMP1] \ n“t” “mov%[tmp1],%[b] \ n \ t” “mov%[tmp2],%[a] \ n \ t” “ 1:\ n“ :[a]”+ m“(* a),[b]”+ m“(* b),[tmp1]”= r“(temp1),[tmp2]”= r“( temp2) : :“cc” );' –

-2

以下提示将允许您成功使用内联装配中的标签。 (我没有调试汇编代码,而且我知道它是错误的,但这不是问题的一部分。)

  • 请勿使用像.L1这样的标签,因为它可能会与GCC生成的标签发生冲突。
  • 您的标签应该包含%= token,这样如果GCC在多个地方使用asm语句,每个版本都会有自己的唯一标签。
  • 您需要降低你的代码只是有一个asm声明,因为%=生成每个asm声明它是在一个不同的唯一编号。
  • 最后,我不知道为什么,但是你需要把在你的asm语句结尾处冒号,否则GCC不会扩展其中的任何%=标记。

这里是代码,使用GCC编译成功在x86_64(尽管它在运行时崩溃):

#include <stdio.h> 

void swap(int* a, int* b) 
{ 
    asm(
     ".intel_syntax noprefix\n\t" 
     "mov eax, DWORD PTR [rdi]\n\t" 
     "mov ebx, DWORD PTR [rsi]\n\t" 
     "cmp eax, ebx\n\t" 
     "jle swap_end_%=\n\t" 
     "mov DWORD PTR [rdi], ebx\n\t" 
     "mov DWORD PTR [rsi], eax\n\t" 
     "swap_end_%=:\n\t" 
     ".att_syntax noprefix\n\t" 
    : 
    ); 
} 

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; 
} 
+0

我尝试了你的建议,但是当使用-O2编译时它仍然会产生分段错误,但是它运行良好,没有任何优化。 –

+0

asm语句需要指定它的操作数以及它的破坏程度。 – interjay

+0

fighting_falcon93:我回答了您的问题,即“我如何在内联装配中使用标签?”。我没有调试过你的汇编代码。如果您仍然需要帮助,应该将其作为一个新问题提出。 (问题很可能是由于你认为交换函数的参数存储在特定的寄存器中,你可以使用汇编操作数来让GCC选择使用哪个寄存器来代替对它们进行硬编码。) –

0

你不能假设值是在你的汇编代码中的任何特定的寄存器 - 你需要使用约束来告诉gcc你想读取和写入什么值,并让它告诉你他们在哪个寄存器中。gcc docs告诉你你需要知道的大部分内容,但是非常密集。也有教程在那里,你可以很容易地在网上搜索(herehere)找到

相关问题