[翻譯] GCC 內聯彙編 HOWTO


GCC 內聯彙編 HOWTO

v0.1, 01 March 2003.
* * *linux

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


原文連接與說明

  1. http://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html
  2. 本翻譯文檔原文選題自 Linux中國 ,翻譯文檔版權歸屬 Linux中國 全部

1. 簡介

1.1 版權許可

Copyright (C)2003 Sandeep S.緩存

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

發佈這篇文檔是但願它可以幫助別人,可是沒有任何保證;甚至不包括可售性和適用於任何特定目的的保證。關於更詳細的信息,能夠查看 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 ;這裏的許多東西都厚顏地直接取自他們的工做成果。ui


2. 概覽

在這裏,咱們將學習 GCC 內聯彙編。這內聯表示的是什麼呢?編碼

咱們能夠要求編譯器將一個函數的代碼插入到調用者代碼中函數被實際調用的地方。這樣的函數就是內聯函數。這聽起來和宏差很少?這二者確實有類似之處。

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

這種內聯方法能夠減小函數調用開銷。同時若是全部實參的值爲常量,它們的已知值能夠在編譯期容許簡化,所以並不是全部的內聯函數代碼都須要被包含。代碼大小的影響是不可預測的,這取決於特定的狀況。爲了聲明一個內聯函數,咱們必須在函數聲明中使用 inline 關鍵字。

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

內聯彙編之因此重要,主要是由於它能夠操做而且使其輸出經過 C 變量顯示出來。正是由於此能力, asm 能夠用做彙編指令和包含它的 C 程序之間的接口。


3. GCC 彙編語法

GCC , Linux上的 GNU C 編譯器,使用 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

  2. 寄存器命名

    寄存器名稱有 % 前綴,即若是必須使用 eax,它應該用做 %eax。

  3. 當即數

    AT&T 當即數以 $ 爲前綴。靜態 "C" 變量 也使用 $ 前綴。在 Intel 語法中,十六進制常量以 h 爲後綴,然而AT&T不使用這種語法,這裏咱們給常量添加前綴 0x。因此,對於十六進制,咱們首先看到一個 $,而後是 0x,最後纔是常量。

  4. 操做數大小

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

    所以,Intel的 mov al, byte ptr foo 在 AT&T 語法中爲 movb foo, %al

  5. 存儲器操做數

    在 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)( GAS 即 GNU 彙編器 ——譯者注),而且經過使用換行符/製表符發送正確地格式化行給彙編器。

示例

__asm__ ("movl %eax, %ebx\n\t"
           "movl $56, %esi\n\t"
           "movl %ecx, $label(%edx,%ebx,$4)\n\t"
           "movb %ah, (%ebx)");

若是在代碼中,咱們涉及到一些寄存器(即改變其內容),但在沒有固定這些變化的狀況下從彙編中返回,這將會致使一些很差的事情。這是由於 GCC 並不知道寄存器內容的變化,這會致使問題,特別是當編譯器作了某些優化。在沒有告知 GCC 的狀況下,它將會假設一些寄存器存儲了咱們可能已經改變的變量的值,它會像什麼事都沒發生同樣繼續運行(什麼事都沒發生同樣是指GCC不會假設寄存器裝入的值是有效的,當退出改變了寄存器值的內聯彙編後,寄存器的值不會保存到相應的變量或內存空間 ——譯者注)。咱們所能夠作的是使用這些沒有反作用的指令,或者當咱們退出時固定這些寄存器,或者等待程序崩潰。這是爲何咱們須要一些擴展功能。擴展彙編正好給咱們提供了那樣的功能。


5. 擴展彙編

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

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

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

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

示例:

asm ("cld\n\t"
              "rep\n\t"
              "stosl"
              : /* 無輸出寄存器 */
              : "c" (count), "a" (fill_value), "D" (dest)
              : "%ecx", "%edi" 
              );

如今,這段代碼是幹什麼的?以上的內聯彙編是將 fill_value 值 連續 count 次 拷貝到 寄存器 edi 所指位置(每執行stosl一次,寄存器 edi 的值會遞增或遞減,這取決因而否設置了 direction 標誌,所以以上代碼實則初始化一個內存塊 ——譯者注)。 它也告訴 gcc 寄存器 ecxedi 一直無效(原文爲 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 變量會映射到更新的值,由於它被指定爲輸出操做數。換句話說, asmb 變量的修改 應該會被映射到 asm 外部。

如今,咱們能夠更詳細地看看每個域。

5.1 彙編程序模板

彙編程序模板包含了被插入到 C 程序的彙編指令集。其格式爲:每條指令用雙引號圈起,或者整個指令組用雙引號圈起。同時每條指令應以分界符結尾。有效的分界符有換行符(\n)和逗號(;)。\n 能夠緊隨一個製表符(\t)。咱們應該都明白使用換行符或製表符的緣由了吧?和 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 添加到修飾寄存器列表(原文修飾寄存器列表拼寫有錯,這裏已修正 ——譯者注), gcc 知道它表示x。所以,由於它能夠知道 ecx 的值,它就不被看成修飾的(寄存器)了。

5.3 修飾寄存器列表

一些指令會破壞一些硬件寄存器。咱們不得不在修飾寄存器中列出這些寄存器,即彙編函數內第三個 : 以後的域。這能夠通知 gcc 咱們將會本身使用和修改這些寄存器。因此 gcc 將不會假設存入這些寄存器的值是有效的。咱們不用在這個列表裏列出輸入輸出寄存器。由於 gcc 知道 asm 使用了它們(由於它們被顯式地指定爲約束了)。若是指令隱式或顯式地使用了任何其餘寄存器,(而且寄存器不能出如今輸出或者輸出約束列表裏),那麼不得不在修飾寄存器列表中指定這些寄存器。

若是咱們的指令能夠修改狀態寄存器,咱們必須將 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 優化代碼並使代碼更漂亮。

Some Useful Recipes 一節中,我提供了多個內聯彙編函數的例子。這兒咱們詳細查看修飾寄存器列表。


6. 更多關於約束

到這個時候,你可能已經瞭解到約束和內聯彙編有很大的關聯。但咱們不多說到約束。約束用於代表一個操做數是否能夠位於寄存器和位於哪一個寄存器;是否操做數能夠爲一個內存引用和哪一種地址;是否操做數能夠爲一個當即數和爲哪個可能的值(即值的範圍)。它能夠有...等等。

6.1 經常使用約束

在許多約束中,只有小部分是經常使用的。咱們將看看這些約束。

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

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

    asm ("movl %%eax, %0\n" :"=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 的值存儲於內存位置:

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

  2. 匹配(數字)約束

    在某些狀況下,一個變量可能既充當輸入操做數,也充當輸出操做數。能夠經過使用匹配約束在 asm 中指定這種狀況。

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

    在操做數子節中,咱們也看到了一些相似的示例。在這個匹配約束的示例中,寄存器 %eax 既用做輸入變量,也用做輸出變量。 var 輸入被讀進 %eax ,而且更新的 %eax 再次被存儲進 var。這裏的 0 用於指定與第0個輸出變量相同的約束。也就是,它指定 var 輸出實例應只被存儲在 %eax 中。該約束可用於:

  • 在輸入從變量讀取或變量修改後,修改被寫回同一變量的狀況
  • 在不須要將輸入操做數實例和輸出操做數實例分開的狀況

使用匹配約束最重要的意義在於它們能夠致使有效地使用可用寄存器。

其餘一些約束:

  1. m : 容許一個內存操做數使用機器廣泛支持的任一種地址。
  2. o : 容許一個內存操做數,但只有當地址是可偏移的。即,該地址加上一個小的偏移量能夠獲得一個地址。
  3. V : A memory operand that is not offsettable. In other words, anything that would fit the m constraint but not the o constraint.
  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, or 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 內聯彙編的基礎理論,如今咱們將專一於一些簡單的例子。將內聯彙編函數寫成宏的形式老是很是方便的。咱們能夠在內核代碼裏看到許多彙編函數。(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=%d\n", 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 代表 my_var 是一個輸出且位於內存。相似地,ir 代表 my_int 是一個整型,並應該存在於其餘寄存器(回想咱們上面看到的表格)。沒有寄存器位於修飾寄存器列表中。

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

這裏,my_var 的值減 1 ,而且若是結果的值爲 0,則變量 cond 置 1。咱們能夠經過添加指令 lock;\n\t 做爲彙編模板的第一條指令來添加原子性。

以相似的方式,爲了增長 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:\tlodsb\n\t"
                            "stosb\n\t"
                            "testb %%al,%%al\n\t"
                            "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__ (                                          \
                            "cld\n\t"                                \
                            "rep\n\t"                                \
                            "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

相關文章
相關標籤/搜索