簡體   English   中英

GCC 內聯匯編從數組中讀取值

[英]GCC inline assembly read value from array

在學習 gcc 內聯匯編時,我玩了一下 memory 訪問。 我正在嘗試使用來自不同數組的值作為索引從數組中讀取值。 arrays 都被初始化為某些東西。

初始化:

uint8_t* index = (uint8_t*)malloc(256);
memset(index, 33, 256);

uint8_t* data = (uint8_t*)malloc(256);
memset(data, 44, 256);

數組訪問:

unsigned char read(void *index,void *data) {
        unsigned char value;

        asm __volatile__ (
        "  movzb (%1), %%edx\n"
        "  movzb (%2, %%edx), %%eax\n"
        : "=r" (value)
        : "c" (index), "c" (data)
        : "%eax", "%edx");

        return value;
    }

這就是我使用 function 的方式:

unsigned char value = read(index, data);

現在我希望它返回 44。但它實際上返回了一些隨機值。 我是在讀取未初始化的 memory 嗎? 另外我不確定如何告訴編譯器它應該將值從eax分配給變量value

您告訴編譯器您要將 output 放入%0 ,它可以為該"=r"選擇任何寄存器。 但是,您永遠不會在模板中寫%0

當您可以使用%0作為臨時對象時,您會無緣無故地使用兩個臨時對象。

像往常一樣,您可以通過添加# 0 = %0之類的注釋並查看編譯器的 asm output 來調試內聯 asm。 (不是反匯編,只是gcc -S看看它填充了什么。例如# 0 = %ecx 。(你沒有使用早期的clobber "=&r" ,所以它可以選擇與輸入相同的寄存器)。


此外,這還有 2 個其他錯誤:

  1. 不編譯。 除非編譯器可以在編譯時證明它們具有相同的值,因此%1%2可以是同一個寄存器,否則在 ECX 中使用"c"約束請求 2 個不同的操作數是無效的。 https://godbolt.org/z/LgR4xS

  2. 您取消引用指針輸入而不告訴編譯器您正在讀取指向的 memory。 使用"memory"破壞者或虛擬 memory 操作數。 我如何表明可以使用內聯 ASM 參數*指向*的 memory?

或者更好的https://gcc.gnu.org/wiki/DontUseInlineAsm因為它對此沒用; 讓 GCC 自己發出 movzb 負載。 unsigned char*不受嚴格別名 UB 的影響,因此您可以安全地將任何指針轉換為unsigned char*並取消引用它,甚至無需使用memcpy或其他黑客來對抗更廣泛的未對齊或類型雙關訪問的語言規則。

但是,如果您堅持使用內聯匯編,請閱讀手冊和教程,以及https://stackoverflow.com/tags/inline-assembly/info上的鏈接 在堅持使用內聯匯編之前,您不能只是將代碼扔到牆上:您必須了解為什么您的代碼是安全的,才能寄希望於它是安全的。 內聯匯編有很多方法可以正常工作,但實際上被破壞了,或者正在等待與不同的周圍代碼一起破壞。


這是一個安全且不完全糟糕的版本(除了內聯 asm 中不可避免的優化失敗部分)。 即使返回值只有 8 位,您仍然需要兩個加載的 movzbl 加載。 movzbl是加載字節的自然有效方式,替換而不是與完整寄存器的舊內容合並。

unsigned char read(void *index, void *data)
{
    uintptr_t value;
    asm (
        " movzb (%[idx]), %k[out] \n\t"
        " movzb (%[arr], %[out]), %k[out]\n"
        : [out] "=&r" (value)              // early-clobber output
        : [idx] "r" (index), [arr] "r" (data)
        : "memory"  // we deref some inputs as pointers
    );

    return value;
}

請注意 output 上的早期破壞:這會阻止 gcc 為 output 選擇相同的寄存器作為輸入之一。 在第一次加載時破壞[idx]寄存器是安全的,但我不知道如何在一個 asm 語句中告訴 GCC。 您可以您的 asm 語句拆分為兩個單獨的語句,每個語句都有自己的輸入和 output 操作數,通過局部變量將第一個的 output 連接到第二個的輸入。 那么任何人都不需要 early-clobber,因為它們只是包裝單個指令,如 GNU C 內聯 asm 語法旨在做得很好。

Godbolt與測試調用者一起使用 i386 clang 和 x86-64 gcc 來查看它在調用兩次時如何內聯/優化 例如,在寄存器中請求index會強制執行 LEA,而不是讓編譯器看到 deref 並讓它為*index選擇尋址模式。 還有額外的movzbl %al, %eax在添加到unsigned sum時由編譯器完成,因為我們使用了窄返回類型。

我使用了uintptr_t value ,因此它可以編譯為 32 位和 64 位 x86。 There's no harm in making the output from the asm statement wider than the return value of the function, and that saves us from having to use size modifiers like movzbl (%1), %k0 to get GCC to print the 32-bit register name (如 EAX)如果它選擇 AL 作為 8 位 output 變量,例如。

我確實決定實際使用%k[out]來為 64 位模式帶來好處:我們想要movzbl (%rdi), %eax ,而不是movzb (%rdi), %rax (浪費 REX 前綴)。

不過,您不妨聲明 function 以返回unsigned intuintptr_t ,因此編譯器知道它不必重做 zero-extension OTOH 有時它可以幫助編譯器知道值范圍僅為 0..255。 你可以告訴它你使用if(retval>255) __builtin_unreachable()或其他東西產生了一個正確的零擴展值。 或者你不能使用內聯 asm

你不需要asm volatile (假設您希望在結果未使用時讓它優化掉,或者被提升出循環以獲得恆定輸入)。 你只需要一個"memory" clobber,所以如果它被使用了,編譯器就會知道它讀取的是 memory。

"memory"破壞者將所有 memory 視為輸入,所有 memory 視為 output。所以它不能讀取 CSE 的內容,因為編譯器可能知道之前的一個循環寫。所以在實踐中, "memory"破壞者與asm volatile一樣糟糕。即使在不接觸輸入數組的情況下對這個 function 進行兩次背靠背調用,也會強制編譯器發出兩次指令。)

您可以使用虛擬內存輸入操作數來避免這種情況,因此編譯器知道這個 asm 塊不會修改memory,只能讀取它。 但是如果你真的關心效率,你不應該為此使用內聯匯編。


但就像我說的那樣,使用內聯 asm 的理由為零:

這將在 100% 便攜和安全的 ISO C 中做同樣的事情:

// safe from strict-aliasing violations 
// because  unsigned char* can alias anything
inline
unsigned char read(void *index, void *data) {
    unsigned idx = *(unsigned char*)index;
    unsigned char * dp = data;
    return dp[idx];
}

如果您堅持每次都發生訪問並且不被優化掉,您可以將一個或兩個指針轉換為volatile unsigned char*

或者甚至可能是atomic<unsigned char> *取決於你在做什么。 (這是一個 hack,更喜歡 C++20 atomic_ref以原子方式加載/存儲通常不是原子的對象。)

在仔細閱讀手冊后,我想出了兩個解決方案:

1)

unsigned char forward(void *index, void *data) {
    unsigned char value;

    asm (
    "  mov %1, %%eax            \n"
    "  movzb (%%eax), %%eax     \n"
    "  mov %2, %%edx            \n"
    "  mov (%%edx, %%eax), %0   \n"
    : "=r" (value)
    : "m" (index), "m" (data)
    : "%edx");

    return value;
}

這里還是要告訴gcc要用什么寄存器。

2)

如果我想讓 gcc 決定,我需要將 output 操作數的表達式更改為 32 位類型。 否則基址+索引尋址不正確。

unsigned char forward2(void *index, void *data) {
    size_t value;

    asm (
    "  mov %1, %0           \n"
    "  movzb (%0), %0       \n"
    "  mov %2, %%edx        \n"
    "  mov (%%edx, %0), %0  \n"
    : "=r" (value)
    : "m" (index), "m" (data)
    : "%edx");

    return (unsigned char)value;
}

暫無
暫無

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

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