簡體   English   中英

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

我試圖找到這個問題的答案,但似乎沒有任何效果。 這個以前問過的問題中,建議改用本地標簽,我也試過了:

#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

我也嘗試了建議的解決方案, 這previusly被問到的問題,改變了名稱.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

最后我也嘗試了這個建議

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 位。

  • %=令牌為我們生成一個唯一的標簽。 順便說一句,跳轉語法<label>f表示向前跳轉,而<label>b表示向后跳轉。

  • 為了正確性,我們需要"memory"因為編譯器無法知道來自解除引用的指針的值是否已更改。 在被 C 代碼包圍的更復雜的內聯匯編中,這可能是一個問題,因為它會使內存中所有當前保存的值無效 - 並且通常是一種大錘方法。 以這種方式出現在函數的末尾,這不會成為問題 - 但您可以在此處閱讀更多相關信息(請參閱: 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 for 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"語法是什么意思呢? &表示早期的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)
        : "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 文檔告訴你大部分你需要知道的,但非常密集。 還有一些教程可以通過網絡搜索輕松找到( 此處此處

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM