Linux中國

GCC 內聯彙編 HOWTO

v0.1, 01 March 2003.

本 HOWTO 文檔將講解 GCC 提供的內聯彙編特性的用途和用法。對於閱讀這篇文章,這裡只有兩個前提要求,很明顯,就是 x86 彙編語言和 C 語言的基本認識。

1. 簡介

1.1 版權許可

Copyright (C) 2003 Sandeep S.

本文檔自由共享;你可以重新發布它,並且/或者在遵循自由軟體基金會發布的 GNU 通用公共許可證下修改它;也可以是該許可證的版本 2 或者(按照你的需求)更晚的版本。

發布這篇文檔是希望它能夠幫助別人,但是沒有任何擔保;甚至不包括可售性和適用於任何特定目的的擔保。關於更詳細的信息,可以查看 GNU 通用許可證。

1.2 反饋校正

請將反饋和批評一起提交給 Sandeep.S 。我將感謝任何一個指出本文檔中錯誤和不準確之處的人;一被告知,我會馬上改正它們。

1.3 致謝

我對提供如此棒的特性的 GNU 人們表示真誠的感謝。感謝 Mr.Pramode C E 所做的所有幫助。感謝在 Govt Engineering College 和 Trichur 的朋友們的精神支持和合作,尤其是 Nisha Kurur 和 Sakeeb S 。 感謝在 Gvot Engineering College 和 Trichur 的老師們的合作。

另外,感謝 Phillip , Brennan Underwood 和 colin@nyx.net ;這裡的許多東西都厚顏地直接取自他們的工作成果。

2. 概覽

在這裡,我們將學習 GCC 內聯彙編。這裡 內聯 inline 表示的是什麼呢?

我們可以要求編譯器將一個函數的代碼插入到調用者代碼中函數被實際調用的地方。這樣的函數就是內聯函數。這聽起來和宏差不多?這兩者確實有相似之處。

內聯函數的優點是什麼呢?

這種內聯方法可以減少函數調用開銷。同時如果所有實參的值為常量,它們的已知值可以在編譯期允許簡化,因此並非所有的內聯函數代碼都需要被包含進去。代碼大小的影響是不可預測的,這取決於特定的情況。為了聲明一個內聯函數,我們必須在函數聲明中使用 inline 關鍵字。

現在我們正處於一個猜測內聯彙編到底是什麼的點上。它只不過是一些寫為內聯函數的彙編程序。在系統編程上,它們方便、快速並且極其有用。我們主要集中學習(GCC)內聯彙編函數的基本格式和用法。為了聲明內聯彙編函數,我們使用 asm 關鍵詞。

內聯彙編之所以重要,主要是因為它可以操作並且使其輸出通過 C 變數顯示出來。正是因為此能力, "asm" 可以用作彙編指令和包含它的 C 程序之間的介面。

3. GCC 彙編語法

Linux上的 GNU C 編譯器 GCC ,使用 AT&T / UNIX 彙編語法。在這裡,我們將使用 AT&T 語法 進行彙編編碼。如果你對 AT&T 語法不熟悉的話,請不要緊張,我會教你的。AT&T 語法和 Intel 語法的差別很大。我會給出主要的區別。

  1. 源操作數和目的操作數順序

AT&T 語法的操作數方向和 Intel 語法的剛好相反。在Intel 語法中,第一操作數為目的操作數,第二操作數為源操作數,然而在 AT&T 語法中,第一操作數為源操作數,第二操作數為目的操作數。也就是說,

Intel 語法中的 Op-code dst src 變為 AT&T 語法中的 Op-code src dst

  1. 寄存器命名

寄存器名稱有 % 前綴,即如果必須使用 eax,它應該用作 %eax

  1. 立即數

AT&T 立即數以 $ 為前綴。靜態 "C" 變數也使用 $ 前綴。在 Intel 語法中,十六進位常量以 h 為後綴,然而 AT&T 不使用這種語法,這裡我們給常量添加前綴 0x。所以,對於十六進位,我們首先看到一個 $,然後是 0x,最後才是常量。

  1. 操作數大小

在 AT&T 語法中,存儲器操作數的大小取決於操作碼名字的最後一個字元。操作碼後綴 』b』 、』w』、』l』 分別指明了 位元組 byte (8位)、 word (16位)、 長型 long (32位)存儲器引用。Intel 語法通過給存儲器操作數添加 byte ptrword ptrdword ptr 前綴來實現這一功能。

因此,Intel的 mov al, byte ptr foo 在 AT&T 語法中為 movb foo, %al

  1. 存儲器操作數

在 Intel 語法中,基址寄存器包含在 [] 中,然而在 AT&T 中,它們變為 ()。另外,在 Intel 語法中, 間接內存引用為

section:[base + index*scale + disp],在 AT&T中變為 section:disp(base, index, scale)

需要牢記的一點是,當一個常量用於 disp 或 scale,不能添加 $ 前綴。

現在我們看到了 Intel 語法和 AT&T 語法之間的一些主要差別。我僅僅寫了它們差別的一部分而已。關於更完整的信息,請參考 GNU 彙編文檔。現在為了更好地理解,我們可以看一些示例。

+------------------------------+------------------------------------+
|       Intel Code             |      AT&T Code                     |
+------------------------------+------------------------------------+
| mov     eax,1                |  movl    $1,%eax                   |   
| mov     ebx,0ffh             |  movl    $0xff,%ebx                |   
| int     80h                  |  int     $0x80                     |   
| mov     ebx, eax             |  movl    %eax, %ebx                |
| mov     eax,[ecx]            |  movl    (%ecx),%eax               |
| mov     eax,[ebx+3]          |  movl    3(%ebx),%eax              | 
| mov     eax,[ebx+20h]        |  movl    0x20(%ebx),%eax           |
| add     eax,[ebx+ecx*2h]     |  addl    (%ebx,%ecx,0x2),%eax      |
| lea     eax,[ebx+ecx]        |  leal    (%ebx,%ecx),%eax          |
| sub     eax,[ebx+ecx*4h-20h] |  subl    -0x20(%ebx,%ecx,0x4),%eax |
+------------------------------+------------------------------------+

4. 基本內聯

基本內聯彙編的格式非常直接了當。它的基本格式為

asm("彙編代碼");

示例

asm("movl %ecx %eax"); /* 將 ecx 寄存器的內容移至 eax  */
__asm__("movb %bh (%eax)"); /* 將 bh 的一個位元組數據 移至 eax 寄存器指向的內存 */

你可能注意到了這裡我使用了 asm__asm__。這兩者都是有效的。如果關鍵詞 asm 和我們程序的一些標識符衝突了,我們可以使用 __asm__。如果我們的指令多於一條,我們可以每個一行,並用雙引號圈起,同時為每條指令添加 』n』 和 』t』 後綴。這是因為 gcc 將每一條當作字元串發送給 as(GAS)(LCTT 譯註: GAS 即 GNU 彙編器),並且通過使用換行符/製表符發送正確格式化後的行給彙編器。

示例

__asm__ ("movl %eax, %ebxnt"
         "movl $56, %esint"
         "movl %ecx, $label(%edx,%ebx,$4)nt"
         "movb %ah, (%ebx)");

如果在代碼中,我們涉及到一些寄存器(即改變其內容),但在沒有恢復這些變化的情況下從彙編中返回,這將會導致一些意想不到的事情。這是因為 GCC 並不知道寄存器內容的變化,這會導致問題,特別是當編譯器做了某些優化。在沒有告知 GCC 的情況下,它將會假設一些寄存器存儲了一些值——而我們可能已經改變卻沒有告知 GCC——它會像什麼事都沒發生一樣繼續運行(LCTT 譯註:什麼事都沒發生一樣是指GCC不會假設寄存器裝入的值是有效的,當退出改變了寄存器值的內聯彙編後,寄存器的值不會保存到相應的變數或內存空間)。我們所可以做的是使用那些沒有副作用的指令,或者當我們退出時恢復這些寄存器,要不就等著程序崩潰吧。這是為什麼我們需要一些擴展功能,擴展彙編給我們提供了那些功能。

5. 擴展彙編

在基本內聯彙編中,我們只有指令。然而在擴展彙編中,我們可以同時指定操作數。它允許我們指定輸入寄存器、輸出寄存器以及修飾寄存器列表。GCC 不強制用戶必須指定使用的寄存器。我們可以把頭疼的事留給 GCC ,這可能可以更好地適應 GCC 的優化。不管怎麼說,基本格式為:

asm ( 彙編程序模板 
    : 輸出操作數                   /* 可選的 */
    : 輸入操作數                  /* 可選的 */
    : 修飾寄存器列表             /* 可選的 */
    );

彙編程序模板由彙編指令組成。每一個操作數由一個操作數約束字元串所描述,其後緊接一個括弧括起的 C 表達式。冒號用於將彙編程序模板和第一個輸出操作數分開,另一個(冒號)用於將最後一個輸出操作數和第一個輸入操作數分開(如果存在的話)。逗號用於分離每一個組內的操作數。總操作數的數目限制在 10 個,或者機器描述中的任何指令格式中的最大操作數數目,以較大者為準。

如果沒有輸出操作數但存在輸入操作數,你必須將兩個連續的冒號放置於輸出操作數原本會放置的地方周圍。

示例:

asm ("cldnt"
     "repnt"
     "stosl"
     : /* 無輸出寄存器 */
     : "c" (count), "a" (fill_value), "D" (dest)
     : "%ecx", "%edi" 
     );

現在來看看這段代碼是幹什麼的?以上的內聯彙編是將 fill_value 值連續 count 次拷貝到寄存器 edi 所指位置(LCTT 譯註:每執行 stosl 一次,寄存器 edi 的值會遞增或遞減,這取決於是否設置了 direction 標誌,因此以上代碼實則初始化一個內存塊)。 它也告訴 gcc 寄存器 ecxedi 一直無效(LCTT 譯註:原文為 eax ,但代碼修飾寄存器列表中為 ecx,因此這可能為作者的紕漏。)。為了更加清晰地說明,讓我們再看一個示例。

int a=10, b;
asm ("movl %1, %%eax; 
      movl %%eax, %0;"
     :"=r"(b)        /* 輸出 */
     :"r"(a)         /* 輸入 */
     :"%eax"         /* 修飾寄存器 */
     );       

這裡我們所做的是使用彙編指令使 』b』 變數的值等於 』a』 變數的值。一些有意思的地方是:

  • "b" 為輸出操作數,用 %0 引用,並且 "a" 為輸入操作數,用 %1 引用。
  • "r" 為操作數約束。之後我們會更詳細地了解約束(字元串)。目前,"r" 告訴 GCC 可以使用任一寄存器存儲操作數。輸出操作數約束應該有一個約束修飾符 "=" 。這修飾符表明它是一個只讀的輸出操作數。
  • 寄存器名字以兩個 % 為前綴。這有利於 GCC 區分操作數和寄存器。操作數以一個 % 為前綴。
  • 第三個冒號之後的修飾寄存器 %eax 用於告訴 GCC %eax 的值將會在 "asm" 內部被修改,所以 GCC 將不會使用此寄存器存儲任何其他值。

當 「asm」 執行完畢, "b" 變數會映射到更新的值,因為它被指定為輸出操作數。換句話說, 「asm」 內 "b" 變數的修改應該會被映射到 「asm」 外部。

現在,我們可以更詳細地看看每一個域。

5.1 彙編程序模板

彙編程序模板包含了被插入到 C 程序的彙編指令集。其格式為:每條指令用雙引號圈起,或者整個指令組用雙引號圈起。同時每條指令應以分界符結尾。有效的分界符有換行符(n)和分號(;)。n 可以緊隨一個製表符(t)。我們應該都明白使用換行符或製表符的原因了吧(LCTT 譯註:就是為了排版和分隔)?和 C 表達式對應的操作數使用 %0、%1 ... 等等表示。

5.2 操作數

C 表達式用作 「asm」 內的彙編指令操作數。每個操作數前面是以雙引號圈起的操作數約束。對於輸出操作數,在引號內還有一個約束修飾符,其後緊隨一個用於表示操作數的 C 表達式。即,「操作數約束」(C 表達式)是一個通用格式。對於輸出操作數,還有一個額外的修飾符。約束字元串主要用於決定操作數的定址方式,同時也用於指定使用的寄存器。

如果我們使用的操作數多於一個,那麼每一個操作數用逗號隔開。

在彙編程序模板中,每個操作數用數字引用。編號方式如下。如果總共有 n 個操作數(包括輸入和輸出操作數),那麼第一個輸出操作數編號為 0 ,逐項遞增,並且最後一個輸入操作數編號為 n - 1 。操作數的最大數目在前一節我們講過。

輸出操作數表達式必須為左值。輸入操作數的要求不像這樣嚴格。它們可以為表達式。擴展彙編特性常常用於編譯器所不知道的機器指令 ;-)。如果輸出表達式無法直接定址(即,它是一個位域),我們的約束字元串必須給定一個寄存器。在這種情況下,GCC 將會使用該寄存器作為彙編的輸出,然後存儲該寄存器的內容到輸出。

正如前面所陳述的一樣,普通的輸出操作數必須為只寫的; GCC 將會假設指令前的操作數值是死的,並且不需要被(提前)生成。擴展彙編也支持輸入-輸出或者讀-寫操作數。

所以現在我們來關注一些示例。我們想要求一個數的5次方結果。為了計算該值,我們使用 lea 指令。

asm ("leal (%1,%1,4), %0"
     : "=r" (five_times_x)
     : "r" (x) 
     );

這裡我們的輸入為 x。我們不指定使用的寄存器。 GCC 將會選擇一些輸入寄存器,一個輸出寄存器,來做我們預期的工作。如果我們想要輸入和輸出放在同一個寄存器里,我們也可以要求 GCC 這樣做。這裡我們使用那些讀-寫操作數類型。這裡我們通過指定合適的約束來實現它。

asm ("leal (%0,%0,4), %0"
     : "=r" (five_times_x)
     : "0" (x) 
     );

現在輸出和輸出操作數位於同一個寄存器。但是我們無法得知是哪一個寄存器。現在假如我們也想要指定操作數所在的寄存器,這裡有一種方法。

asm ("leal (%%ecx,%%ecx,4), %%ecx"
     : "=c" (x)
     : "c" (x) 
     );

在以上三個示例中,我們並沒有在修飾寄存器列表裡添加任何寄存器,為什麼?在頭兩個示例, GCC 決定了寄存器並且它知道發生了什麼改變。在最後一個示例,我們不必將 'ecx' 添加到修飾寄存器列表(LCTT 譯註: 原文修飾寄存器列表這個單詞拼寫有錯,這裡已修正),gcc 知道它表示 x。因此,因為它可以知道 ecx 的值,它就不被當作修飾的(寄存器)了。

5.3 修飾寄存器列表

一些指令會破壞一些硬體寄存器內容。我們不得不在修飾寄存器中列出這些寄存器,即彙編函數內第三個 』:』 之後的域。這可以通知 gcc 我們將會自己使用和修改這些寄存器,這樣 gcc 就不會假設存入這些寄存器的值是有效的。我們不用在這個列表裡列出輸入、輸出寄存器。因為 gcc 知道 「asm」 使用了它們(因為它們被顯式地指定為約束了)。如果指令隱式或顯式地使用了任何其他寄存器,(並且寄存器沒有出現在輸出或者輸出約束列表裡),那麼就需要在修飾寄存器列表中指定這些寄存器。

如果我們的指令可以修改條件碼寄存器(cc),我們必須將 "cc" 添加進修飾寄存器列表。

如果我們的指令以不可預測的方式修改了內存,那麼需要將 "memory" 添加進修飾寄存器列表。這可以使 GCC 不會在彙編指令間保持緩存於寄存器的內存值。如果被影響的內存不在彙編的輸入或輸出列表中,我們也必須添加 volatile 關鍵詞。

我們可以按我們的需求多次讀寫修飾寄存器。參考一下模板內的多指令示例;它假設子常式 _foo 接受寄存器 eaxecx 里的參數。

asm ("movl %0,%%eax;
      movl %1,%%ecx;
      call _foo"
     : /* no outputs */
     : "g" (from), "g" (to)
     : "eax", "ecx"
     );

5.4 Volatile ...?

如果你熟悉內核源碼或者類似漂亮的代碼,你一定見過許多聲明為 volatile 或者 __volatile__的函數,其跟著一個 asm 或者 __asm__。我之前提到過關鍵詞 asm__asm__。那麼什麼是 volatile 呢?

如果我們的彙編語句必須在我們放置它的地方執行(例如,不能為了優化而被移出循環語句),將關鍵詞 volatile 放置在 asm 後面、()的前面。以防止它被移動、刪除或者其他操作,我們將其聲明為 asm volatile ( ... : ... : ... : ...);

如果擔心發生衝突,請使用 __volatile__

如果我們的彙編只是用於一些計算並且沒有任何副作用,不使用 volatile 關鍵詞會更好。不使用 volatile 可以幫助 gcc 優化代碼並使代碼更漂亮。

在「一些實用的訣竅」一節中,我提供了多個內聯彙編函數的例子。那裡我們可以了解到修飾寄存器列表的細節。

6. 更多關於約束

到這個時候,你可能已經了解到約束和內聯彙編有很大的關聯。但我們對約束講的還不多。約束用於表明一個操作數是否可以位於寄存器和位於哪種寄存器;操作數是否可以為一個內存引用和哪種地址;操作數是否可以為一個立即數和它可能的取值範圍(即值的範圍),等等。

6.1 常用約束

在許多約束中,只有小部分是常用的。我們來看看這些約束。

  1. 寄存器操作數約束(r)

當使用這種約束指定操作數時,它們存儲在通用寄存器(GPR)中。請看下面示例:

asm ("movl %%eax, %0n" :"=r"(myval));

這裡,變數 myval 保存在寄存器中,寄存器 eax 的值被複制到該寄存器中,並且 myval 的值從寄存器更新到了內存。當指定 "r" 約束時, gcc 可以將變數保存在任何可用的 GPR 中。要指定寄存器,你必須使用特定寄存器約束直接地指定寄存器的名字。它們為:

+---+--------------------+
| r |    Register(s)     |
+---+--------------------+
| a |   %eax, %ax, %al   |
| b |   %ebx, %bx, %bl   |
| c |   %ecx, %cx, %cl   |
| d |   %edx, %dx, %dl   |
| S |   %esi, %si        |
| D |   %edi, %di        |
+---+--------------------+
  1. 內存操作數約束(m)

當操作數位於內存時,任何對它們的操作將直接發生在內存位置,這與寄存器約束相反,後者首先將值存儲在要修改的寄存器中,然後將它寫回到內存位置。但寄存器約束通常用於一個指令必須使用它們或者它們可以大大提高處理速度的地方。當需要在 「asm」 內更新一個 C 變數,而又不想使用寄存器去保存它的值,使用內存最為有效。例如,IDTR 寄存器的值存儲於內存位置 loc 處:

asm("sidt %0n" : :"m"(loc));

  1. 匹配(數字)約束

在某些情況下,一個變數可能既充當輸入操作數,也充當輸出操作數。可以通過使用匹配約束在 "asm" 中指定這種情況。

asm ("incl %0" :"=a"(var):"0"(var));

在操作數那一節中,我們也看到了一些類似的示例。在這個匹配約束的示例中,寄存器 "%eax" 既用作輸入變數,也用作輸出變數。 var 輸入被讀進 %eax,並且等遞增後更新的 %eax 再次被存儲進 var。這裡的 "0" 用於指定與第 0 個輸出變數相同的約束。也就是,它指定 var 輸出實例應只被存儲在 "%eax" 中。該約束可用於:

* 在輸入從變數讀取或變數修改後且修改被寫回同一變數的情況
* 在不需要將輸入操作數實例和輸出操作數實例分開的情況使用匹配約束最重要的意義在於它們可以有效地使用可用寄存器。

其他一些約束:

  1. "m" : 允許一個內存操作數,可以使用機器普遍支持的任一種地址。
  2. "o" : 允許一個內存操作數,但只有當地址是可偏移的。即,該地址加上一個小的偏移量可以得到一個有效地址。
  3. "V" : 一個不允許偏移的內存操作數。換言之,任何適合 "m" 約束而不適合 "o" 約束的操作數。
  4. "i" : 允許一個(帶有常量)的立即整形操作數。這包括其值僅在彙編時期知道的符號常量。
  5. "n" : 允許一個帶有已知數字的立即整形操作數。許多系統不支持彙編時期的常量,因為操作數少於一個字寬。對於此種操作數,約束應該使用 'n' 而不是'i'。
  6. "g" : 允許任一寄存器、內存或者立即整形操作數,不包括通用寄存器之外的寄存器。

以下約束為 x86 特有。

  1. "r" : 寄存器操作數約束,查看上面給定的表格。
  2. "q" : 寄存器 a、b、c 或者 d。
  3. "I" : 範圍從 0 到 31 的常量(對於 32 位移位)。
  4. "J" : 範圍從 0 到 63 的常量(對於 64 位移位)。
  5. "K" : 0xff。
  6. "L" : 0xffff。
  7. "M" : 0、1、2 或 3 (lea 指令的移位)。
  8. "N" : 範圍從 0 到 255 的常量(對於 out 指令)。
  9. "f" : 浮點寄存器
  10. "t" : 第一個(棧頂)浮點寄存器
  11. "u" : 第二個浮點寄存器
  12. "A" : 指定 ad 寄存器。這主要用於想要返回 64 位整形數,使用 d 寄存器保存最高有效位和 a 寄存器保存最低有效位。

6.2 約束修飾符

當使用約束時,對於更精確的控制超過了對約束作用的需求,GCC 給我們提供了約束修飾符。最常用的約束修飾符為:

  1. "=" : 意味著對於這條指令,操作數為只寫的;舊值會被忽略並被輸出數據所替換。
  2. "&" : 意味著這個操作數為一個早期改動的操作數,其在該指令完成前通過使用輸入操作數被修改了。因此,這個操作數不可以位於一個被用作輸出操作數或任何內存地址部分的寄存器。如果在舊值被寫入之前它僅用作輸入而已,一個輸入操作數可以為一個早期改動操作數。

上述的約束列表和解釋並不完整。示例可以讓我們對內聯彙編的用途和用法更好的理解。在下一節,我們會看到一些示例,在那裡我們會發現更多關於修飾寄存器列表的東西。

7. 一些實用的訣竅

現在我們已經介紹了關於 GCC 內聯彙編的基礎理論,現在我們將專註於一些簡單的例子。將內聯彙編函數寫成宏的形式總是非常方便的。我們可以在 Linux 內核代碼里看到許多彙編函數。(usr/src/linux/include/asm/*.h)。

  1. 首先我們從一個簡單的例子入手。我們將寫一個兩個數相加的程序。
int main(void)
{
        int foo = 10, bar = 15;
        __asm__ __volatile__("addl  %%ebx,%%eax"
                             :"=a"(foo)
                             :"a"(foo), "b"(bar)
                             );
        printf("foo+bar=%dn", foo);
        return 0;
}

這裡我們要求 GCC 將 foo 存放於 %eax,將 bar 存放於 %ebx,同時我們也想要在 %eax 中存放結果。'=' 符號表示它是一個輸出寄存器。現在我們可以以其他方式將一個整數加到一個變數。

__asm__ __volatile__(
                     "   lock       ;n"
                     "   addl %1,%0 ;n"
                     : "=m"  (my_var)
                     : "ir"  (my_int), "m" (my_var)
                     :                                 /* 無修飾寄存器列表 */
                     );

這是一個原子加法。為了移除原子性,我們可以移除指令 'lock'。在輸出域中,"=m" 表明 myvar 是一個輸出且位於內存。類似地,"ir" 表明 myint 是一個整型,並應該存在於其他寄存器(回想我們上面看到的表格)。沒有寄存器位於修飾寄存器列表中。

  1. 現在我們將在一些寄存器/變數上展示一些操作,並比較值。
__asm__ __volatile__(  "decl %0; sete %1"
                     : "=m" (my_var), "=q" (cond)
                     : "m" (my_var) 
                     : "memory"
                     );

這裡,my_var 的值減 1 ,並且如果結果的值為 0,則變數 cond 置 1。我們可以通過將指令 "lock;nt" 添加為彙編模板的第一條指令以增加原子性。

以類似的方式,為了增加 my_var,我們可以使用 "incl %0" 而不是 "decl %0"。

這裡需要注意的地方是(i)my_var 是一個存儲於內存的變數。(ii)cond 位於寄存器 eax、ebx、ecx、edx 中的任何一個。約束 "=q" 保證了這一點。(iii)同時我們可以看到 memory 位於修飾寄存器列表中。也就是說,代碼將改變內存中的內容。

  1. 如何置 1 或清 0 寄存器中的一個比特位。作為下一個訣竅,我們將會看到它。
__asm__ __volatile__(   "btsl %1,%0"
                      : "=m" (ADDR)
                      : "Ir" (pos)
                      : "cc"
                      );

這裡,ADDR 變數(一個內存變數)的 'pos' 位置上的比特被設置為 1。我們可以使用 'btrl' 來清除由 'btsl' 設置的比特位。pos 的約束 "Ir" 表明 pos 位於寄存器,並且它的值為 0-31(x86 相關約束)。也就是說,我們可以設置/清除 ADDR 變數上第 0 到 31 位的任一比特位。因為條件碼會被改變,所以我們將 "cc" 添加進修飾寄存器列表。

  1. 現在我們看看一些更為複雜而有用的函數。字元串拷貝。
static inline char * strcpy(char * dest,const char *src)
{
int d0, d1, d2;
__asm__ __volatile__(  "1:tlodsbnt"
                       "stosbnt"
                       "testb %%al,%%alnt"
                       "jne 1b"
                     : "=&S" (d0), "=&D" (d1), "=&a" (d2)
                     : "0" (src),"1" (dest) 
                     : "memory");
return dest;
}

源地址存放於 esi,目標地址存放於 edi,同時開始拷貝,當我們到達 0 時,拷貝完成。約束 "&S"、"&D"、"&a" 表明寄存器 esi、edi 和 eax 早期修飾寄存器,也就是說,它們的內容在函數完成前會被改變。這裡很明顯可以知道為什麼 "memory" 會放在修飾寄存器列表。

我們可以看到一個類似的函數,它能移動雙字塊數據。注意函數被聲明為一個宏。

#define mov_blk(src, dest, numwords) 
__asm__ __volatile__ (                                          
                       "cldnt"                                
                       "repnt"                                
                       "movsl"                                  
                       :                                        
                       : "S" (src), "D" (dest), "c" (numwords)  
                       : "%ecx", "%esi", "%edi"                 
                       )

這裡我們沒有輸出,寄存器 ecx、esi和 edi 的內容發生了改變,這是塊移動的副作用。因此我們必須將它們添加進修飾寄存器列表。

  1. 在 Linux 中,系統調用使用 GCC 內聯彙編實現。讓我們看看如何實現一個系統調用。所有的系統調用被寫成宏(linux/unistd.h)。例如,帶有三個參數的系統調用被定義為如下所示的宏。
type name(type1 arg1,type2 arg2,type3 arg3) 
{ 
long __res; 
__asm__ volatile (  "int $0x80" 
                  : "=a" (__res) 
                  : "0" (__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2)), 
                    "d" ((long)(arg3))); 
__syscall_return(type,__res); 
}

無論何時調用帶有三個參數的系統調用,以上展示的宏就會用於執行調用。系統調用號位於 eax 中,每個參數位於 ebx、ecx、edx 中。最後 "int 0x80" 是一條用於執行系統調用的指令。返回值被存儲於 eax 中。

每個系統調用都以類似的方式實現。Exit 是一個單一參數的系統調用,讓我們看看它的代碼看起來會是怎樣。它如下所示。

{
        asm("movl $1,%%eax;         /* SYS_exit is 1 */
             xorl %%ebx,%%ebx;      /* Argument is in ebx, it is 0 */
             int  $0x80"            /* Enter kernel mode */
            );
}

Exit 的系統調用號是 1,同時它的參數是 0。因此我們分配 eax 包含 1,ebx 包含 0,同時通過 int $0x80 執行 exit(0)。這就是 exit 的工作原理。

8. 結束語

這篇文檔已經將 GCC 內聯彙編過了一遍。一旦你理解了基本概念,你就可以按照自己的需求去使用它們了。我們看了許多例子,它們有助於理解 GCC 內聯彙編的常用特性。

GCC 內聯是一個極大的主題,這篇文章是不完整的。更多關於我們討論過的語法細節可以在 GNU 彙編器的官方文檔上獲取。類似地,要獲取完整的約束列表,可以參考 GCC 的官方文檔。

當然,Linux 內核大量地使用了 GCC 內聯。因此我們可以在內核源碼中發現許多各種各樣的例子。它們可以幫助我們很多。

如果你發現任何的錯別字,或者本文中的信息已經過時,請告訴我們。

9. 參考

  1. Brennan』s Guide to Inline Assembly
  2. Using Assembly Language in Linux
  3. Using as, The GNU Assembler
  4. Using and Porting the GNU Compiler Collection (GCC)
  5. Linux Kernel Source

via: http://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html

作者:Sandeep.S 譯者:cposture 校對:wxy

本文由 LCTT 原創翻譯,Linux中國 榮譽推出


本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive

對這篇文章感覺如何?

太棒了
0
不錯
0
愛死了
0
不太好
0
感覺很糟
0
雨落清風。心向陽

    You may also like

    Leave a reply

    您的郵箱地址不會被公開。 必填項已用 * 標註

    此站點使用Akismet來減少垃圾評論。了解我們如何處理您的評論數據

    More in:Linux中國