簡體   English   中英

為什么這個內聯匯編不能為每條指令使用單獨的asm volatile語句?

[英]Why is this inline assembly not working with a separate asm volatile statement for each instruction?

對於以下代碼:

long buf[64];

register long rrax asm ("rax");
register long rrbx asm ("rbx");
register long rrsi asm ("rsi");

rrax = 0x34;
rrbx = 0x39;

__asm__ __volatile__ ("movq $buf,%rsi");
__asm__ __volatile__ ("movq %rax, 0(%rsi);");
__asm__ __volatile__ ("movq %rbx, 8(%rsi);");

printf( "buf[0] = %lx, buf[1] = %lx!\n", buf[0], buf[1] );

我得到以下輸出:

buf[0] = 0, buf[1] = 346161cbc0!

它應該是:

buf[0] = 34, buf[1] = 39!

任何想法為什么它不能正常工作,以及如何解決它?

你破壞了內存,但沒有告訴GCC,所以GCC可以在匯編調用中緩存buf值。 如果您想使用輸入和輸出,請告訴GCC一切。

__asm__ (
    "movq %1, 0(%0)\n\t"
    "movq %2, 8(%0)"
    :                                /* Outputs (none) */
    : "r"(buf), "r"(rrax), "r"(rrbx) /* Inputs */
    : "memory");                     /* Clobbered */

您通常也希望讓GCC處理大部分的mov ,寄存器選擇等 - 即使您明確約束寄存器(rrax是stil %rax )讓信息流經GCC,否則您將獲得意外結果。

__volatile__錯了。

__volatile__存在的原因是,您可以保證編譯器將您的代碼准確放置在原來的位置......這對此代碼來說是完全不必要的保證。 實現內存屏障等高級功能是必要的,但如果只修改內存和寄存器,幾乎完全沒有價值。

GCC已經知道它不能在printf之后移動這個程序集,因為printf調用訪問buf ,並且buf可能被程序集破壞。 GCC已經知道它在rrax=0x39;之前無法移動程序集rrax=0x39; 因為rax是匯編代碼的輸入。 那么__volatile__會給你帶來什么? 沒有。

如果你的代碼在沒有__volatile__情況下不起作用,那么代碼中的錯誤應該被修復,而不是僅僅添加__volatile__並希望這會使一切變得更好。 __volatile__關鍵字不是魔術,不應該這樣對待。

替代修復:

原始代碼需要__volatile__嗎? 不。只需正確標記輸入和clobber值。

/* The "S" constraint means %rsi, "b" means %rbx, and "a" means %rax
   The inputs and clobbered values are specified.  There is no output
   so that section is blank.  */
rsi = (long) buf;
__asm__ ("movq %%rax, 0(%%rsi)" : : "a"(rrax), "S"(rssi) : "memory");
__asm__ ("movq %%rbx, 0(%%rsi)" : : "b"(rrbx), "S"(rrsi) : "memory");

為什么__volatile__在這里沒有幫助你:

rrax = 0x34; /* Dead code */

GCC完全有權完全刪除上述行,因為上述問題中的代碼聲稱它從未使用過rrax

一個更清晰的例子

long global;
void store_5(void)
{
    register long rax asm ("rax");
    rax = 5;
    __asm__ __volatile__ ("movq %%rax, (global)");
}

反匯編或多或少與您期望的-O0

movl $5, %rax
movq %rax, (global)

但是在優化的情況下,你可以對裝配相當邋。 我們試試-O2

movq %rax, (global)

哎呦! rax = 5; 走? 這是死代碼,因為%rax從未在函數中使用 - 至少就GCC而言。 海灣合作委員會沒有偷看內部裝配。 當我們刪除__volatile__時會發生什么?

; empty

好吧,你可能會認為__volatile__通過讓GCC放棄你寶貴的裝配來為你服務,但它只是掩蓋了GCC認為你的裝配沒有任何事情的事實。 GCC認為你的程序集不需要輸入,不產生輸出,並且沒有內存。 你最好把它理順:

long global;
void store_5(void)
{
    register long rax asm ("rax");
    rax = 5;
    __asm__ __volatile__ ("movq %%rax, (global)" : : : "memory");
}

現在我們得到以下輸出:

movq %rax, (global)

更好。 但是如果你告訴GCC關於輸入,它將確保%rax首先正確初始化:

long global;
void store_5(void)
{
    register long rax asm ("rax");
    rax = 5;
    __asm__ ("movq %%rax, (global)" : : "a"(rax) : "memory");
}

輸出,優化:

movl $5, %eax
movq %rax, (global)

正確! 我們甚至不需要使用__volatile__

為什么__volatile__存在?

__volatile__的主要正確用法是,如果匯編代碼除了輸入,輸出或破壞內存之外還執行其他操作。 也許它與GCC不了解或影響IO的特殊寄存器相混淆。 你在Linux內核中看到了很多東西,但它經常在用戶空間中被濫用。

__volatile__關鍵字非常誘人,因為我們C程序員經常喜歡認為我們幾乎已經使用匯編語言進行編程。 不是。 C編譯器進行了大量的數據流分析 - 因此您需要向編譯器解釋匯編代碼的數據流。 這樣,編譯器可以安全地操縱你的程序集塊,就像它操縱它生成的程序集一樣。

如果您發現自己__volatile__使用__volatile__ ,作為替代方法,您可以在匯編文件中編寫整個函數或模塊。

編譯器使用寄存器,它可以覆蓋您放入它們的值。

在這種情況下,編譯器可能在rrbx賦值之后和內聯匯編部分之前使用rbx寄存器。

通常,您不應期望寄存器在內聯匯編代碼序列之后和之間保留其值。

稍微偏離主題,但我想跟進gcc內聯匯編。

(非)需要__volatile__來自GCC 優化內聯匯編的事實。 GCC檢查匯編語句的副作用/先決條件,如果發現它們不存在,它可能會選擇移動匯編指令,甚至決定將其刪除 所有__volatile__都是告訴編譯器“停止關懷並把它放在那里”。

這通常不是你真正想要的。

這就是需要約束的地方。名稱被重載並實際用於GCC內聯匯編中的不同內容:

  • 約束指定asm()塊中使用的輸入/輸出操作數
  • 約束指定“clobber列表”,其詳細說明asm()影響“狀態”(寄存器,條件代碼,內存asm()
  • 約束指定操作數的類(寄存器,地址,偏移量,常量,......)
  • 約束聲明匯編器實體和C / C ++變量/表達式之間的關聯/綁定

在許多情況下,開發人員濫用 __volatile__因為他們注意到他們的代碼要么被移動,要么在沒有它的情況下消失。 如果發生這種情況,通常是開發人員試圖告訴GCC有關裝配的副作用/先決條件的信號。 例如,這個錯誤的代碼:

register int foo __asm__("rax") = 1234;
register int bar __adm__("rbx") = 4321;

asm("add %rax, %rbx");
printf("I'm expecting 'bar' to be 5555 it is: %d\n", bar);

它有幾個錯誤:

  • 首先,它只是由於gcc bug(!)而編譯。 通常,要/tmp/ccYPmr3g.s:22: Error: bad register name '%%rax'聯匯編中寫入寄存器名稱,需要雙%% ,但在上面如果實際指定它們,則會出現編譯器/匯編器錯誤,/ /tmp/ccYPmr3g.s:22: Error: bad register name '%%rax' / /tmp/ccYPmr3g.s:22: Error: bad register name '%%rax'
  • 第二,它沒有告訴編譯器何時何地需要/使用變量。 相反,它假設編譯器從字面上尊重asm() 對於Microsoft Visual C ++可能也是如此,但gcc 不是這種情況

如果在沒有優化的情況下編譯它,它會創建:

0000000000400524 <main>:
[ ... ]
  400534:       b8 d2 04 00 00          mov    $0x4d2,%eax
  400539:       bb e1 10 00 00          mov    $0x10e1,%ebx
  40053e:       48 01 c3                add    %rax,%rbx
  400541:       48 89 da                mov    %rbx,%rdx
  400544:       b8 5c 06 40 00          mov    $0x40065c,%eax
  400549:       48 89 d6                mov    %rdx,%rsi
  40054c:       48 89 c7                mov    %rax,%rdi
  40054f:       b8 00 00 00 00          mov    $0x0,%eax
  400554:       e8 d7 fe ff ff          callq  400430 <printf@plt>
[...]
你可以找到你的add指令,以及兩個寄存器的初始化,它將打印出預期的。 另一方面,如果您進行優化,則會發生其他情況:
  0000000000400530 <main>:\n   400530:48 83 ec 08 sub $ 0x8,%rsp\n   400534:48 01 c3添加%rax,%rbx\n   400537:是e1 10 00 00 mov $ 0x10e1,%esi\n   40053c:bf 3c 06 40 00 mov $ 0x40063c,%edi\n   400541:31 c0 xor%eax,%eax\n   400543:e8 e8 fe ff ff callq 400430 <printf @ plt>\n [...] 
您對“已使用”寄存器的初始化不再存在。 編譯器放棄了它們,因為它沒有看到它們正在使用它們,並且它保留了匯編指令,它使用這兩個變量之前就把它放了。 它在那里,但它沒有做任何事情(幸運的是......如果rax / rbx 一直在使用誰可以告訴發生了什么......)。

原因是你實際上沒有告訴 GCC程序集正在使用這些寄存器/這些操作數值。 這與volatile無關,但事實上你使用的是一個無約束的asm()表達式。

正確執行此操作的方法是通過約束,即您使用:

 int foo = 1234; int bar = 4321; asm("add %1, %0" : "+r"(bar) : "r"(foo)); printf("I'm expecting 'bar' to be 5555 it is: %d\\n", bar); 

這告訴編譯器匯編:

  1. 在寄存器中有一個參數, "+r"(...) ,它們都需要在匯編語句之前初始化,並由匯編語句修改,並將變量bar與它相關聯。
  2. 在寄存器中有第二個參數, "r"(...)需要在匯編語句之前初始化,並被聲明視為readonly / not modified。 在這里,將foo與此聯系起來。

注意,沒有指定寄存器賦值 - 編譯器根據編譯的變量/狀態選擇它。 上面的(優化的)輸出:

  0000000000400530 <main>:\n   400530:48 83 ec 08 sub $ 0x8,%rsp\n   400534:b8 d2 04 00 00 mov $ 0x4d2,%eax\n   400539:是e1 10 00 00 mov $ 0x10e1,%esi\n   40053e:bf 4c 06 40 00 mov $ 0x40064c,%edi\n   400543:01 c6添加%eax,%esi\n   400545:31 c0 xor%eax,%eax\n   400547:e8 e4 fe ff ff callq 400430 <printf @ plt>\n [...] 
GCC內聯匯編約束幾乎總是以某種形式或其他形式存在,但是可以有多種可能的方式來描述編譯器的相同要求; 而不是上述,你也可以寫:

0000000000400530 <main>:
  400530:       48 83 ec 08             sub    $0x8,%rsp
  400534:       bf 4c 06 40 00          mov    $0x40064c,%edi
  400539:       31 c0                   xor    %eax,%eax
  40053b:       be e1 10 00 00          mov    $0x10e1,%esi
  400540:       81 c6 d2 04 00 00       add    $0x4d2,%esi
  400546:       e8 e5 fe ff ff          callq  400430 <printf@plt>
[ ... ]

這告訴gcc:

  1. 該語句有一個輸出操作數,即變量bar ,在語句之后將在寄存器中找到"=r"(...)
  2. 該語句有一個輸入操作數,即變量foo ,它將放入寄存器"r"(...)
  3. 操作數零也是一個輸入操作數,並用bar初始化

或者,再一個替代方案:

 asm("add %1, %0" : "+r"(bar) : "g"(foo)); 

告訴gcc:

  1. BLA(打哈欠-以前一樣, bar兩個輸入/輸出)
  2. 該語句有一個輸入操作數,即變量foo ,該語句不關心它是在寄存器中,在內存中還是在編譯時常量中(即"g"(...)約束)

結果與前者不同:

  0000000000400530 <main>:\n   400530:48 83 ec 08 sub $ 0x8,%rsp\n   400534:bf 4c 06 40 00 mov $ 0x40064c,%edi\n   400539:31 c0 xor%eax,%eax\n   40053b:是e1 10 00 00 mov $ 0x10e1,%esi\n   400540:81 c6 d2 04 00 00添加$ 0x4d2,%esi\n   400546:e8 e5 fe ff ff callq 400430 <printf @ plt>\n [...] 
因為現在,GCC 實際上已經發現 foo 是一個編譯時常量,只是將值嵌入到 add 指令中 這不是很整潔嗎?

不可否認,這很復雜,需要習慣。 優點是讓編譯器選擇哪些寄存器用於哪些操作數允許整體優化代碼; 例如,如果在宏和/或static inline函數中使用內聯匯編語句,則編譯器可以根據調用上下文在代碼的不同實例中選擇不同的寄存器。 或者,如果某個值在一個地方是編譯時可評估/常量而在另一個地方沒有,則編譯器可以為其定制創建的程序集。

將GCC內聯匯編約束視為“擴展函數原型” - 它們告訴編譯器參數/返回值的類型和位置,以及更多。 如果你沒有指定這些約束,你的內聯匯編就會創建僅對全局變量/狀態進行操作的函數的模擬 - 正如我們可能都認為的那樣,它們很少完全按照你的意圖行事。

暫無
暫無

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

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