簡體   English   中英

gcc刪除內聯匯編程序代碼

[英]gcc removes inline assembler code

似乎gcc 4.6.2刪除了它認為從函數中未使用的代碼。

test.c的

int main(void) {
  goto exit;
  handler:
    __asm__ __volatile__("jmp 0x0");
  exit:
  return 0;
}

拆卸main()

   0x08048404 <+0>:     push   ebp
   0x08048405 <+1>:     mov    ebp,esp
   0x08048407 <+3>:     nop    # <-- This is all whats left of my jmp.
   0x08048408 <+4>:     mov    eax,0x0
   0x0804840d <+9>:     pop    ebp
   0x0804840e <+10>:    ret

編譯器選項

沒有啟用優化,只有gcc -m32 -o test test.c-m32因為我在64位機器上)。

我怎么能阻止這種行為?

編輯:最好通過使用編譯器選項,而不是通過修改代碼。

看起來就是這樣 - 當gcc看到函數中的代碼無法訪問時,它會刪除它。 其他編譯器可能不同。
gcc ,編譯的早期階段是構建“控制流圖” - 一個“基本塊”的圖形,每個條件都沒有條件,通過分支連接。 在發出實際代碼時,將丟棄無法從根訪問的圖形部分。
這不是優化階段的一部分,因此不受編譯選項的影響。

所以任何解決方案都會讓gcc認為代碼是可以訪問的。

我的建議:

您可以將它放在可到達的地方,並跳過有問題的指令,而不是將匯編代碼放在無法到達的地方(GCC可能會刪除它)。

int main(void) {
     goto exit;

     exit:
     __asm__ __volatile__ (
        "jmp 1f\n"
        "jmp $0x0\n"
        "1:\n"
    );
    return 0;
}

另外,請參閱此主題有關該問題

我不相信有一種可靠的方法只使用編譯選項來解決這個問題。 無論用於編譯的選項如何,優選的機制都可以完成工作並在未來版本的編譯器上工作。


關於公認答案的評論

在接受的答案中,對原始文件進行了編輯,建議使用此解決方案:

int main(void) {
  __asm__ ("jmp exit");

  handler:
      __asm__ __volatile__("jmp $0x0");
  exit:
  return 0;
}

首先關閉jmp $0x0應該是jmp 0x0 其次, C標簽通常會被翻譯成本地標簽。 jmp exit實際上沒有跳轉到C函數中的標簽exit ,它跳轉到C庫中的exit函數,有效地繞過main底部的return 0 使用Godbolt和GCC 4.6.4我們得到了這個非優化的輸出(我已經修剪了我們不關心的標簽):

main:
        pushl   %ebp
        movl    %esp, %ebp
        jmp exit
        jmp 0x0
.L3:
        movl    $0, %eax
        popl    %ebp
        ret

.L3實際上是exit的本地標簽。 您將無法在生成的程序集中找到exit標簽。 如果存在C庫,它可以編譯和鏈接。 不要像這樣在內聯匯編中使用C本地goto標簽。


使用asm goto作為解決方案

從GCC 4.5(OP使用4.6.x)開始,支持asm goto擴展程序集模板 asm goto允許您指定內聯匯編可能使用的跳轉目標:

6.45.2.7轉到標簽

asm goto允許匯編代碼跳轉到一個或多個C標簽。 asm goto語句中的GotoLabels部分包含匯編代碼可能跳轉到的所有C標簽的逗號分隔列表。 GCC假定asm執行落到下一個語句(如果不是這種情況,請考慮在asm語句之后使用__builtin_unreachable內在函數)。 可以通過使用熱標簽屬性和冷標簽屬性來改進asm goto的優化(請參閱標簽屬性)。

asm goto語句不能有輸出。 這是由於編譯器的內部限制:控制傳輸指令不能有輸出。 如果匯編代碼確實修改了任何內容,請使用“memory”clobber強制優化器將所有寄存器值刷新到內存,並在asm語句之后根據需要重新加載它們。

另請注意,asm goto語句始終隱式地被視為volatile。

要在匯編程序模板中引用標簽,請在其前面加上'%l'(小寫'L'),后跟在GotoLabels中的(從零開始)位置加上輸入操作數的數量。 例如,如果asm有三個輸入並引用兩個標簽,請將第一個標簽稱為“%l3”,將第二個標簽稱為“%l4”。

或者,您可以使用括在括號中的實際C標簽名稱來引用標簽。 例如,要引用名為carry的標簽,可以使用'%l [carry]'。 使用此方法時,標簽仍必須列在GotoLabels部分中。

代碼可以這樣寫:

int main(void) {
  __asm__ goto ("jmp %l[exit]" :::: exit);
  handler:
      __asm__ __volatile__("jmp 0x0");
  exit:
  return 0;
}

我們可以使用asm goto 我更喜歡__asm__ over asm因為如果使用-ansi-std=?編譯它不會發出警告-std=? 選項。 在clobbers之后,您可以列出內聯匯編可能使用的跳轉目標。 C實際上並不知道我們是否跳轉,因為GCC不分析內聯匯編模板中的實際代碼。 它不能刪除這個跳轉,也不能假設死代碼之后的內容。 使用Godbolt與GCC 4.6.4未經優化的代碼(修剪)看起來像:

main:
        pushl   %ebp
        movl    %esp, %ebp
        jmp .L2                   # <------ this is the goto exit
        jmp 0x0
.L2:                              # <------ exit label
        movl    $0, %eax
        popl    %ebp
        ret

具有GCC 4.6.4輸出的Godbolt看起來仍然正確並顯示為:

main:
        jmp .L2                   # <------ this is the goto exit
        jmp 0x0
.L2:                              # <------ exit label
        xorl    %eax, %eax
        ret

無論您是打開還是關閉優化,此機制也應該起作用,無論您是編譯64位還是32位x86目標都無關緊要。


其他觀察

  • 當擴展內聯匯編模板中沒有輸出約束時, asm語句是隱式volatile。 這條線

     __asm__ __volatile__("jmp 0x0"); 

    可以寫成:

     __asm__ ("jmp 0x0"); 
  • asm goto語句被認為是隱式不穩定的。 它們也不需要volatile改性劑。

這會有用嗎,讓它如此gcc無法知道它無法到達

int main(void)  
{ 
    volatile int y = 1;
    if (y) goto exit;
handler:
    __asm__ __volatile__("jmp 0x0");  
exit:   
    return 0; 
}

如果編譯器認為它可以欺騙你,只需作弊:(僅限GCC)

int main(void) {
    {
        /* Place this code anywhere in the same function, where
         * control flow is known to still be active (such as at the start) */
        extern volatile unsigned int some_undefined_symbol;
        __asm__ __volatile__(".pushsection .discard" : : : "memory");
        if (some_undefined_symbol) goto handler;
        __asm__ __volatile__(".popsection" : : : "memory");
    }
    goto exit;
handler:
    __asm__ __volatile__("jmp 0x0");
    exit:
    return 0;
}

此解決方案不會為無意義指令添加任何額外開銷,但僅在與AS一起使用時才適用於GCC(默認情況下)。

解釋: .pushsection將編譯器的文本輸出切換到另一個部分,在本例中為.discard (默認情況下在鏈接期間刪除)。 "memory" clobber阻止GCC嘗試移動將被丟棄的部分中的其他文本。 但是,GCC沒有意識到(並且永遠不可能因為__asm____volatile__ )2個語句之間發生的任何事情都將被丟棄。

對於some_undefined_symbol ,這實際上只是從未定義的任何符號(或實際定義的,它應該無關緊要)。 並且由於使用它的代碼段將在鏈接期間被丟棄,因此它也不會產生任何未解析的引用錯誤。

最后,條件跳轉到您想要制作的標簽看起來好像是可以到達的那樣。 除了它根本不會出現在輸出二進制文件中之外,GCC意識到它對some_undefined_symbol ,這意味着它別無選擇,只能假設兩個if的分支都是可達的,這意味着值得關注的是,控制流可以通過到達goto exit或跳轉到handler來繼續(即使沒有任何代碼甚至可以執行此操作)

但是,在鏈接器ld --gc-sections啟用垃圾收集時要小心(默認情況下禁用它),否則它可能會想到擺脫仍然未使用的標簽。

編輯:忘記這一切。 這樣做:

int main(void) {
    __asm__ __volatile__ goto("" : : : : handler);
    goto exit;
handler:
    __asm__ __volatile__("jmp 0x0");
exit:
    return 0;
}

更新2012/6/18

考慮一下,可以將goto exit放在asm塊中,這意味着只需要更改一行代碼:

int main(void) {
  __asm__ ("jmp exit");

  handler:
    __asm__ __volatile__("jmp $0x0");
  exit:
  return 0;
}

這比我下面的其他解決方案要清晰得多(也可能比@ ugoren當前的更好)。


這是非常hacky,但它似乎工作:將處理程序隱藏在正常條件下永遠不會被遵循的條件中,但是通過阻止編譯器能夠使用某些內聯匯編程序正確地進行分析來阻止它被消除。

int main (void) {
  int x = 0;
  __asm__ __volatile__ ("" : "=r"(x));
  // compiler can't tell what the value of x is now, but it's always 0

  if (x) {
handler:
    __asm__ __volatile__ ("jmp $0x0");
  }

  return 0;
}

即使使用-O3jmp也會被保留:

    testl   %eax, %eax   
    je      .L2     
.L3:
    jmp $0x0
.L2:
    xorl    %eax, %eax 
    ret

(這看起來狡猾,所以我希望有更好的方法來做到這一點。 編輯只是在x前面放一個volatile可以了,所以不需要做內聯asm技巧。)

我從來沒有聽說過阻止gcc刪除無法訪問的代碼的方法; 似乎無論你做什么,一旦gcc檢測到無法訪問的代碼,它總是將其刪除(使用gcc的-Wunreachable-code選項來查看它認為無法訪問的內容)。

也就是說,您仍然可以將此代碼放在靜態函數中,並且不會進行優化:

static int func()
{
    __asm__ __volatile__("jmp $0x0");
}

int main(void)
{
    goto exit;

handler:
    func();

exit:
    return 0;
}

PS
如果您希望在原始代碼中的多個位置植入相同的“處理程序”代碼塊時避免代碼冗余,則此解決方案特別方便。

gcc可以在函數內復制asm語句並在優化期間刪除它們(即使在-O0),因此這將永遠無法可靠地工作。

可靠地執行此操作的一種方法是使用全局asm語句(即任何函數之外的asm語句)。 gcc會將此直接復制到輸出中,您可以毫無問題地使用全局標簽。

暫無
暫無

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

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