稍微偏離主題,但我想跟進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);
這告訴編譯器,彙編:
- 在寄存器一個參數,
"+r"(...)
這兩個都需要在彙編語句之前初始化,並且由彙編語句修改,並將變量bar
與它關聯。
- 具有在寄存器中,
"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:
- 的語句具有輸出操作數,變量
bar
,該語句後,將在寄存器中發現,"=r"(...)
- 語句具有輸入操作數,可變
foo
,這是被放置到一個寄存器中,"r"(...)
- 操作數零也輸入操作數,並與0123被初始化
或者,再次可替換:
asm("add %1, %0" : "+r"(bar) : "g"(foo));
它告訴GCC:
- BLA(打哈欠 - 以前一樣,
bar
兩個輸入/輸出)
- 的語句有一個輸入操作數,變量
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內嵌程序集約束是一種「擴展函數原型」 - 它告訴編譯器參數/返回值的類型和位置,以及更多。如果你沒有指定這些約束條件,那麼你的內聯程序集正在創建類似於僅對全局變量/狀態進行操作的函數 - 正如我們大家都同意的那樣,它很少會按照你的意圖執行。
爲什麼不通過gdb代碼,以便您可以*看到*發生了什麼? – 2012-01-17 07:47:58