1. 程式人生 > >[翻譯] GCC 內聯匯編 HOWTO

[翻譯] GCC 內聯匯編 HOWTO

學習 unistd.h 讀寫 直接 應該 不能 sub 位移 inux


GCC 內聯匯編 HOWTO

v0.1, 01 March 2003.
* * *

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


[TOC]

原文鏈接與說明

  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 ,或者(按照你的需求)更晚的版本。

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


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

[翻譯] GCC 內聯匯編 HOWTO