2012-01-17 57 views
9

對於下面的代碼:這是什麼內聯程序集不工作?

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! 

任何爲什麼它不能正常工作思路,以及如何解決它?

+2

爲什麼不通過gdb代碼,以便您可以*看到*發生了什麼? – 2012-01-17 07:47:58

回答

22

你的clobber內存,但不告訴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電話訪問buf不能printf後移動本次大會,並可以buf由大會重挫。 GCC已經知道它不能在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知道。 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"); 
} 

輸出,與優化:

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

爲什麼__volatile__存在?

__volatile__的主要正確用法是如果您的彙編代碼除了輸入,輸出或破壞內存之外還有別的東西。也許它會與GCC不知道的特殊寄存器混淆,或影響IO。你在Linux內核中看到了很多,但它在用戶空間中經常被濫用。

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

如果您發現自己使用__volatile__很多,作爲替代方案,您可以在裝配文件中編寫整個功能或模塊。

+1

asm中的__volatile__是告訴編譯器將代碼放在它的放置位置。它不像易變的變量。 – MetallicPriest 2012-01-17 08:11:31

+3

@MetallicPriest:是的,這正是揮之不去的原因,這就是爲什麼在這裏沒有必要。如果你不明白這一點,那麼從頭到尾閱讀GCC內聯彙編HOWTO *是因爲它無法跳過塊。 – 2012-01-17 08:13:27

+0

如果不是粗體字,我會回答你的答案「__volatile__」是錯的。那是因爲這個組件是由原來的poster_實際上需要的。其他一些原因也是錯誤的(正如你注意到的那樣,缺失的clobber)。儘管如此,_separate_'asm()'語句,如果堅持使用它們(很少有一個好主意),就需要強制排序。是的,如果你喜歡,可以打電話給我nitpicky ;-) – 2012-01-17 09:16:31

4

編譯器使用寄存器,它可能會寫入你已經放入的值。

在這種情況下,編譯器可能會在rrbx分配之後和inline彙編部分之前使用rbx寄存器。

通常,您不應該期望寄存器在內聯彙編代碼序列之後和之間保持其值。

+0

但我使用rsi作爲printf前buf的指針。沒關係,如果printf使用它或不。無論如何buf [0]和buf [1]應該有正確的值,不是嗎?即使我從printf中刪除了rrsi,它仍會打印相同的錯誤值。 – MetallicPriest 2012-01-17 07:57:49

+1

@ugoren:如果你使用'asm'關鍵字使GCC分配寄存器,它將正確地將它們溢出並重新加載它們,以便在函數調用中保存它們。 – 2012-01-17 08:06:03

+0

@MetallicPriest,你是對的具體案件。我應該編輯我的答案,但我遇到了技術問題。但總體思路就像我寫的一樣。 – ugoren 2012-01-17 08:50:22

1

稍微偏離主題,但我想跟進gcc內聯程序集。

__volatile__的(非)需求來自GCC 優化內聯彙編的事實。 GCC檢查彙編語句中的副作用/先決條件,如果發現它們不存在,它可能會選擇移動彙編指令,甚至決定刪除它。所有__volatile__確實是告訴編譯器「停止關心並把它放在那裏」。

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

這是需要約束進來的名稱是重載和實際用於GCC聯彙編不同的事情:

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

在許多情況下,開發人員濫用__volatile__,因爲他們注意到他們的代碼被移動或甚至沒有它的消失。如果發生這種情況,開發人員試圖通過而不是告訴GCC有關裝配的副作用/先決條件。例如,該bug的代碼:

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錯誤。通常,要在內聯彙編中編寫寄存器名稱,需要雙重%%,但在上述中,如果實際指定它們,則會出現編譯器/彙編錯誤/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 <[email protected]> 
[...]
你可以找到你的 add指令,以及兩個寄存器的初始化,它會打印預期。另一方面,如果您進行優化,則會發生其他情況:
0000000000400530 <main>: 
    400530:  48 83 ec 08    sub $0x8,%rsp 
    400534:  48 01 c3    add %rax,%rbx 
    400537:  be e1 10 00 00   mov $0x10e1,%esi 
    40053c:  bf 3c 06 40 00   mov $0x40063c,%edi 
    400541:  31 c0     xor %eax,%eax 
    400543:  e8 e8 fe ff ff   callq 400430 <[email protected]> 
[ ... ]
您對「已用」寄存器的初始化不再存在。編譯器會丟棄它們,因爲它沒有看到它們在使用它們,並且在保留彙編指令時,它會在之前使用這兩個變量。它的存在,但它什麼都不做(幸運的是實際上...如果 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"(...)需要組裝語句之前被初始化和被視爲只讀/不通過的聲明修改的第二個參數。在這裏,聯繫foo與。

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

0000000000400530 <main>: 
    400530:  48 83 ec 08    sub $0x8,%rsp 
    400534:  b8 d2 04 00 00   mov $0x4d2,%eax 
    400539:  be e1 10 00 00   mov $0x10e1,%esi 
    40053e:  bf 4c 06 40 00   mov $0x40064c,%edi 
    400543:  01 c6     add %eax,%esi 
    400545:  31 c0     xor %eax,%eax 
    400547:  e8 e4 fe ff ff   callq 400430 <[email protected]> 
[ ... ]
GCC內聯彙編約束是 幾乎總是需要以某種形式或另一種形式,但是可以有多種可能的方式向編譯器描述相同的需求;代替上述情況,你也可以這樣寫:

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

這告訴GCC:

  1. 的語句具有輸出操作數,變量bar,該語句後,將在寄存器中發現,"=r"(...)
  2. 語句具有輸入操作數,可變foo,這是被放置到一個寄存器中,"r"(...)
  3. 操作數零也輸入操作數,並與0123被初始化

或者,再次可替換:

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

它告訴GCC:

  1. BLA(打哈欠 - 以前一樣,bar兩個輸入/輸出)
  2. 的語句有一個輸入操作數,變量foo,該語句不關心它是否在regis之三,內存或編譯時間常數(這是"g"(...)約束)

結果不同,前者:

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 <[email protected]> 
[ ... ]
因爲現在,GCC 實際上已經想通了 foo 是一個編譯時間常數和只需將該值嵌入 add 指令!那不整齊?

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

認爲GCC內嵌程序集約束是一種「擴展函數原型」 - 它告訴編譯器參數/返回值的類型和位置,以及更多。如果你沒有指定這些約束條件,那麼你的內聯程序集正在創建類似於僅對全局變量/狀態進行操作的函數 - 正如我們大家都同意的那樣,它很少會按照你的意圖執行。