【轉貼】GCC內聯匯編基礎


原文作者 Sandeep.S
英文原文 [https://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html]

本文將介紹GCC編譯環境下,在C語言代碼中嵌入匯編代碼的基本方法。閱讀本文需要您具備80X86匯編語言和C語言的基礎知識。為了使中文描述更加清楚自然,翻譯過程中加入了稍許解釋和意譯部分。

簡介

版權/反饋/勘誤/感謝等信息。[^ 1]
[^ 1]:這里信息價值不大,沒有翻譯。具體參加原文:https://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html#s1

概要

在討論GCC內聯匯編之前,我們先來搞搞清楚,到底什么是內聯匯編?

先看在C語言中,我們可以指定編譯器將一個函數代碼直接復制到調用其代碼的地方執行。這種函數調用方式和默認壓棧調用方式不同,我們稱這種函數為內聯函數。內聯函數看起來很像宏?兩者確實有許多共同之處。

那么,內聯函數有哪些優點呢?

很明顯,內聯函數降低了函數的調用開銷:如果多次被調用的某個函數實參相同,那么它的返回值一定是相同的,這就給編譯器留下了優化空間。此時編譯器完全可以直接用這個返回值替代這個函數,而不必把該函數的代碼插入到調用者的代碼中再去計算結果了。如此一來,不但減少了代碼量,還節省了計算資源。指定編譯器將一個函數處理為內聯函數,我們只要在函數申明前加上inline關鍵字即可。

基於對上述內聯函數的認知,我們大概可以想象出內聯匯編到底是怎么一回事了。內聯匯編相當於用匯編語句寫成的內聯函數。它方便,快速,對系統編程有着舉足輕重的作用。本文主要就GCC內聯函數的格式和使用方法展開討論。在GCC中聲明一個內聯匯編函數,我們要用asm這個關鍵字。

之所以內聯匯編如此有用,主要是因為它可以操作C語言變量,比如可以輸出值到C語言變量。這個特性使內聯匯編成為匯編代碼和調用其C程序之間的橋梁。

GCC匯編格式。

GCC (GNU Compiler for Linux) 使用AT&T/UNIX匯編語法。所以這篇文章將會用AT&T匯編格式來寫匯編代碼。如果你不熟悉AT&T匯編語法也沒有關系,下面會有些簡單的介紹。AT&T和Intel匯編語法差別比較大,二者主要不同之處如下:

  1. 源操作數和目的操作數的方向
    AT&T和Intel匯編語法源操作數和目的操作數的方向正好相反。Intel中第一個操作數作為目的操作數,第二個操作數作為源操作數。而在AT&T中,第一個操作數是源操作數,第二個是目的操作數:
    OP-code dst src //Intel語法
    Op-code src dst //AT&T語法
  1. 寄存器命名
    在AT&T匯編中, 寄存器名前有%前綴。例如,如果要使用eax,得寫作: %eax。

  2. 立即數 (Immediate Operand)
    在AT&T語法中,立即數(Immediate Operand)都有'$'前綴。引用的C語言靜態變量 (static C variables) 也必須放上'$'前綴;
    此外,在Intel語法中, 16進制的常數是以’h’作為后綴的,但是在AT&T語法中, 是以'0x’作為前綴的。因此,在AT&T語法中,一個16進制常數的寫法是:首先以$開頭接着是0x,最后是常數本身。

  3. 操作數大小
    在AT&T語法中,操作符的最后一個字符決定着操作數訪問內存的長度:以’b’, 'w'和 'l'為后綴指明內存訪問長度是 byte(8-bit), word(16-bit)還是long(32-bit)。而Intel語法在操作數前加上'byte ptr', 'word ptr'和'dword ptr'的內存操作符來達到相同目的。
    因此 Intel匯編寫法:
    mov al, byte ptr foo

    用AT&T語法寫就是:
    movb foo, %al

  4. 內存操作數
    在Intel語法中,基址寄存器是放在方括號‘[]’中的,但AT&T是放在小括弧’()’內的。
    因此,在Intel語法中,一個間接內存尋址是這么寫的:
    section:[base + index * scale + disp]。

    而在AT&T中則應該寫成這樣:
    section:disp(base, index, scale)

    此外對於AT&T匯編,當一個常數被用作disp或者scale時,不需要'$'前綴。這點需要記住。

以上就是AT&T和Intel匯編語法的一些主要不同點。這只是一小部分,具體內容需要參考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

基本內聯匯編 (Basic Inline)

基本內聯匯編格式比較直觀,可以直接這樣寫:
asm("assembly code");

例如:
asm("movl %ecx, %eax"); /* 把 ecx 內容移動到 eax */ __asm__("movb %bh , (%eax)"); /* 把bh中一個字節的內容移動到eax指向的內存 */

你可能注意到了這里使用了兩個不同的關鍵字 asm 和 __asm__。這兩個關鍵字都可以使用。不過當遇到asm關鍵字與程序其他變量有沖突的時候就必須用__asm__了。如果內聯匯編有多條指令,則每行要加上雙引號,並且該行要以\n\t結尾。這是因為GCC會將每行指令作為一個字符串傳給as(GAS),使用換行和TAB可以將正確且格式良好的代碼行傳遞給匯編器。
舉個例子:

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

如果在內聯代碼中操作了一些寄存器,比如你修改了寄存器內容(而之后也沒有進行還原操作),程序很可能會產生一些難以預料的情況。因為此時GCC並不知道你已經將寄存器內容修改了。這點尤其是在編譯器對代碼進行了一些優化的情況下而導致問題。因為編譯器注意不到寄存器內容已經被改掉,程序將當作它沒有被修改過而繼續執行。所以此時我們盡量不要使用這些會產生附加影響的操作,或者當我們退出的時候還原這些操作。否則很可能會造成程序崩潰。可是如果我們必須要這樣操作該怎么辦呢?我們可以通過下面的討論的擴展內聯匯編進行。

擴展內聯匯編 (Extended Asm)

前面討論的基本內聯匯編只涉及到嵌入匯編指令,而在擴展形式中,我們還可以指定操作數,並且可以選擇輸入輸出寄存器,以及指明要修改的寄存器列表。對於要訪問的寄存器,並不一定要要顯式指明,也可以留給GCC自己去選擇,這可能讓GCC更好去優化代碼。擴展內聯匯編格式如下:

asm ( assembler template
        : output operands                /* optional */
        : input operands                   /* optional */
        : list of clobbered registers   /* optional */
);

其中assembler template為匯編指令部分。括號內的操作數都是C語言表達式中常量字符串。不同部分之間使用冒號分隔。相同部分語句中的每個小部分用逗號分隔。最多可以指定10個操作數,不過可能有的計算機平台有額外的文檔說明可以使用超過10個操作數。

此外,如果沒有輸出部分但是有輸入部分,我們還得保留輸出部分前面的冒號。就像下面這樣:

asm ( "cld\n\t"
          "rep\n\t"
          "stosl"
         : /* no output registers */
         : "c" (count), "a" (fill_value), "D" (dest)
         : "%ecx", "%edi"
      );

上述代碼做了些什么呢?它主要是循環count次把fill_value的值到填充到edi寄存器指定的內存位置。並且告訴GCC,寄存器ecx[^ 2]和edi中的內容可能已經被改變了。 為了有一個更清晰的理解,我們再來看一個例子:
[^ 2]:原文有誤,原文是這里是eax。

int a=10, b; asm ( "movl %1, %%eax; movl %%eax, %0;" :"=r"(b) /* output */ :"r"(a) /* input */ :"%eax" /* clobbered register */ ); 

上面代碼實現的功能就是用匯編代碼把a的值賦給b。值得注意的幾點有:

  • “b”是輸出操作數,用%0來訪問,”a”是輸入操作數,用%1來訪問。
  • “r” 是一個constraint, 關於constraint后面有詳細的介紹。這里我們只要記住這里”r”的意思就是讓GCC自己去選擇一個寄存器去存儲變量a。輸出部分constraint前必須要有個 ”=”修飾,用來說明是一個這是一個輸出操作數,並且是只寫(write only)的。
  • 你可能已經注意到,有的寄存器名字前面用了”%%”,這是用來讓GCC區分操作數和寄存器的:操作數已經用了一個%作為前綴,寄存器只能用“%%”做前綴了。
  • 第三個冒號后面的clobbered register部分有個%eax,意思是內聯匯編代碼中會改變寄存器eax的內容,如此一來GCC在調用內聯匯編前就不會依賴保存在寄存器eax中的內容了。

當這段代碼執行結束后,變量”b”的值將會被改寫,因為它是被指定作為輸出操作數的。這里可以看出在“asm”內部對b的改動將影響到asm外了,正如之前所說的內聯匯編起到橋梁作用。

下面我們將對擴展內聯匯編各個部分分別進行詳細的討論。

匯編模板

匯編模板部分就是嵌入在C程序中的匯編指令,格式如下:

  • 每條指令放在一個雙引號內,或者將所有的指令都放着一個雙引號內。
  • 每條指令都要包含一個分隔符。合法的分隔符是換行符(\n)或者分號。用換行符的時候通常后面放一個制表符\t。對此前文已經有所說明。
  • 訪問C語言變量用%0,%1…等等。

操作數

”asm”內部使用C語言字符串作為操作數。操作數都要放在雙引號中。對於輸出操作數,還要用“=”修飾。constraint和修飾都放在雙引號內。之后是C表達式了。就像下面這樣:
"constraint" (C expression) //"=r"(result)

對於輸出操作數一定要用 “=“修飾。 constraint主要用來指定操作數的尋址類型 (內存尋址或寄存器尋址),也用來指明使用哪個寄存器。

如果有多個操作數,使用逗號隔開。

在匯編模板部分,我們按順序用數字去引用操作數,引用規則如下:
如果總共有n個操作數(包括輸入輸出操作數),那么第一個輸出操作引用數字為0,依次遞增,然后最后一個操作數是n-1。關於操作數數量限制參見前面的章節。

輸出操作數表達式必須是左值,輸入操作數沒有這個限制。注意這里可以使表達式,不僅僅指一個變量。當編譯器不知道有這個機器指令的時候(比如新CPU指令出來的時候,編譯器還沒有支持該指令),擴展匯編形式就能發揮其用武之地了。如果輸出表達式不能直接尋址(比如是[bit-field]), constraint就必須指定一個寄存器。這種情況下,GCC將使用寄存器作為asm的輸出。然后保存這個寄存器的值到輸出表達式中。

如前文所描述,一般輸出操作數必須是只寫 (write only)的;GCC將認為在這條指令之前,保存在這種操作數中的值已經過期和不再需要了。當然也支持輸入輸出類型或者可讀可寫類型的操作數。

現在我們來看一些例子:
要求把一個數字乘以5,我們可以使用匯編指令lea來實現,具體方法如下:

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

這里輸入操作數是 ‘x’,因為沒有指定具體要使用那個寄存器,GCC會自己選擇合適的輸入輸出寄存器。我們也可以修改constraint部分內容,讓GCC固定使用同一個寄存器,具體方法如下:

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

上面例子中指定GCC始終使用在相同的寄存器來處理輸入輸出操作數。當然這時我們也不知道GCC具體使那個寄存器,如果需要的話我們也可以像這樣指定一個:

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

上面的三個例子中,我都沒有在clobber list部分指定何寄存器。為什么?前兩個例子中,因為指定GCC自己選擇合適的寄存器,並且GCC知道會改寫什么。第三個例子中我們也沒有必要把ecx放在clobber list中是因為GCC知道x將存入其中,GCC完全知道ecx的值。所以我們也不用寫在clobber list中。

Clobber List

如果某個指令改變了某個寄存器的值,我們就必須在asm中第三個冒號后的Clobber List中標示出該寄存器。為的是通知GCC,讓其不再假定之前存入這些寄存器中的值依然合法。輸入輸出寄存器不用放Clobber List中(看上面就是個例子),因為GCC能知道asm將使用這些寄存器。(因為它們已經顯式被指定輸入輸出標出在輸入輸出部分) 。其他使用到的寄存器,無論是顯示還是隱式的使用,必須在clobbered list中標明。

如果指令中以無法預料的形式修改了內存值,需要在clobbered list中加上”memory”。從而使得GCC不去緩存在這些內存值。此外,如果要改變沒有被列在輸入和出部分的內存內容時,需要加上volatile關鍵字說明。clobbered list中列出的寄存器可以被多次讀寫。

來看一個內聯匯編實現乘法的例子,這里內聯匯編調用函數_foo,並且接受存在eax和ecx值作為參數:

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

Volatile

如果你熟悉內核代碼或者一些類似優秀的代碼,你一定見過很多在asm或者asm后的函數聲明前加了volatile 或者volatile。前面已經討論了asm和asm的用途,那volatile有什么用途呢?

如果我們要求匯編代碼必須在被放置的位置執行(例如不能被循環優化而移出循環),我們就要在asm之后的“()”前,放一個volatile關鍵字。 這樣可以禁止這些代碼被移動或刪除,像這樣聲明:
asm volatile ( ... : ... : ... : ...);
同樣,如果擔心volatile有變量沖突,可以使用__volatile__關鍵字。

如果匯編代碼只是做一些運算而沒有什么附加影響的時候最好不要使用volatile修飾。不用volatile能給GCC留下優化代碼的空間。

在“常用技巧”章節中的代碼示例里有更多的關於volatile的使用詳情。[^ 3]
[^ 3]:原文有誤: 原文是clobber-list,這樣應該是volatile

constraints詳解

你可能已經感到我們之前經常提到的constraint是個很重要的內容了。不過之前我們並沒有過多的討論。constraint中可以指明一個操作數是否在寄存器中,在哪個寄存器中;可以指明操作數是否是內存引用,如何尋址;可以說明操作數是否是立即數常量,和其可能是的值(或值范圍)。

常用constraints

雖然constraints有很多,但常用的並不多。下面我們就來看看這些常用的constraints。

  1. 寄存器操作數constraints: r
    如果操作數指定了這個constraints,操作數將被存儲在通用寄存器中。看下面的例子:
    asm ( "movl %%eax, %0" : "=r" (myval));

    上面變量myval會被被保存在一個由GCC自己選擇的寄存器中,eax中的值被拷貝到這個寄存器中去,並且在內存中的myval的值也會按這個寄存器值被更新。當constraints ”r” 被指定時,GCC可能會在任何一個可用的通用寄存器中保存這個值。當然,你也可以指定具體使用那個寄存器,用下表所列出的constraints:

r Register(s)
a %eax, %ax, %al
b %ebx, %bx, %bl
c %ecx, %cx, %cl
d %edx, %dx, %adl
S %esi, %si
D %edi, %di
  1. 內存操作數constraint: m
    當操作數在內存中時,任何對其操作會直接在內存中進行。與寄存器constraint不同的是:指定寄存器constraint時,內存操作時先把值存在一個寄存器中,修改后再將該值寫回到該內存中去。寄存器constraint通常只用於必要的匯編指令,或者用於能明顯加快操作速度的情況,因為內存constraint能提升C語言變量更新效率,完全沒必要通過一個寄存器來中轉。下面這個例子中,sidt的值會被直接存儲到loc所指向的內存:
    asm (“sidt” %0” : : “m”(loc) );

  2. 匹配constraint
    在某些情況下,一個變量可能被用來傳遞輸入也用來保存輸出。這種情況下我們需要用到匹配constraint。
    asm (“incl %0” :”=a”(var) : “0”(var) );

    在之前章節中我們已經看過類似的例子。上面的例子中,%eax被用來傳遞輸入也用來保存輸出。輸入變量先被讀入eax中,incl執行之后,%eax被更新並且保存到變量var中。這里的constraint ”0”就是指定使用和第一個輸出相同的寄存器,即輸入變量指定放在eax中。這種constraint可以使用在如下場景:

    • 輸入值從一個變量讀入, 這個變量將被修改並且修改過的值要寫回同一個變量;
    • 沒有必要把輸入和輸出操作數分開。
      使用匹配constraint最重要的好處是可以更高效地使用變量寄存器。

其他可能用到的constraint有:

  1. “m”: 使用一個內存操作數,內存地址可以是機器支持的范圍內。
  2. “o”: 使用一個內存操作數,但是要求內存地址范圍在在同一段內。例如,加上一個小的偏移量來形成一個可用的地址。
  3. “V”: 內存操作數,但是不在同一個段內。換句話說,就是使用除了”o” 以外的”m”的所有的情況。
  4. “i”: 使用一個立即整數操作數(值固定);也包含僅在編譯時才能確定其值的符號常量。
  5. “n”: 一個確定值的立即數。很多系統不支持匯編常數操作數小於一個字(word)的長度的情況。這時候使用n就比使用i好。
  6. “g”: 除了通用寄存器以外的任何寄存器,內存和立即整數。

這里是一些x86特有的constraint:

  1. ”r” : Register operand constraint, look table given above.
  2. ”q” : Registers a, b, c or d.
  3. ”I” : Constant in range 0 to 31 (for 32-bit shifts).
  4. ”J” : Constant in range 0 to 63 (for 64-bit shifts).
  5. ”K” : 0xff.
  6. ”L” : 0xffff.
  7. ”M” : 0, 1, 2, or 3 (shifts for lea instruction).
  8. ”N” : Constant in range 0 to 255 (for out instruction).
  9. ”f” : Floating point register
  10. ”t” : First (top of stack) floating point register
  11. ”u” : Second floating point register
  12. ”A” : Specifies the “a” or “d” registers. This is primarily useful for 64-bit integer values intended to be returned with the “d” register holding the most significant bits and the “a” register holding the least significant bits.

constraint修飾符(Constraint Modifiers)

在使用constraint的時候,為了更精確的控制約束,GCC提供了一些修飾符,常用的修飾符有:

  1. “=” 指明這個操作數是只寫的;之前保存在其中的值將被廢棄而被輸出值所代替。
  2. “&” 指明這個操作事數是一個會在使用之前被修改的操作數,這個操作數將在輸入指令用過輸入操作數之前被修改。因此,該操作數不能被放在一個被用作輸入操作數的寄存器或者內存處。只有在該操作數被寫入之前完成輸入指令的情況下,可以被綁定在該操作數上。[^ 4]
    [^ 4]:因為譯者對此處內容不了解,故翻譯的不好。本段的英文原文是 “&” Means that this operand is an earlyclobber operand, which is modified before the instruction is finished using the input operands. Therefore, this operand may not lie in a register that is used as an input operand or as part of any memory address. An input operand can be tied to an earlyclobber operand if its only use as an input occurs before the early result is written.

對於cosntraint的解釋還遠遠沒完。代碼本身是理解內聯匯編最好的老師。下一小結中我們就來看一些代碼示例。通過這些示例我們能學到更多關於clobber-list和constraint使用情況。

常用代碼示例

到目前為止,GCC內聯匯編基礎知識就已經講完了。接下來讓我們通過一些簡單的例子來鞏固我們所學到到知識。內聯匯編函數可以很方便的用宏的形式來編寫,linux內核代碼中有很多這樣的實例(在/usr/src/linux/asm/*.h)。

  1. 我們從一個簡單的例子看起。我們來寫一個把兩個數字加起來的一個程序。
        int main(void) { int foo = 10, bar = 15; __asm__ __volatile__ ( "addl %%ebx, %%eax" : ”=a”(foo) : ”a”(foo), “b”(bar) ); prinft(“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)
                        : /* no clobber-list */
    );
上面代碼是一個原子加法操作。要移除該原子操作可以刪除lock指令。在輸出部分“=m”指出直接輸出到內存my_var。類似的,”ir”是指my_int是一個整型數並且要保存到一個寄存器中(可以參考上面關於constraint的列表)。這里clobber list中沒有指定任何寄存器。
  1. 我們在一些寄存器或變量上來執行一些操作來對比下它們的值。
         __asm__ __volatile__ ( "decl %0; sete %1"
                          : "=m" (my_var), "=q" (cond)
                          : "m" (my_var)
                          : "memory"
                          );
    
    
    上面的程序將~my_var~減一並且如果減一的最終結果為零就將cond置位。我們可以在匯編語句之前加上~”lock;\n\t”~讓其變成原子操作。
     
    類似的,我們可以用”incl %0”替換”decl %0”來增加~my_var~的值。

    這里值得注意的幾點有:
    - my_var是存在內存中的變量;
    - cond存在通用寄存器中(eax,ebx,ecx,edx),因為有限制條件”=q”;
    - clobber list中指定了“memory”,說明代碼將改變內存值。
 
3. 如何設置和清除寄存器中的某一位?來看看下面這個技巧。
    ```
         __asm__ __volatile__( “btsl %1, %0”
                         : “=m” (ADDR)
                         : “Ir” (pos)
                         : “cc”
                         );
上面例子中變量ADDR(一個內存變量)的’pos’位置值被設置成了1。我們可以使用btrl指令來清除由btsl設置的位。pos變量的限定符constraint為”Ir”說明pos放在寄存器中,並且取值范圍是0-31(I是一個x86相關constraint)。因此我們可以設置或者清除ADDR變量中從第0到第31位的值。因為這個操作涉會改變相關寄存器的內容,因此我們加上”cc”在clobberlist中。
  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結束。約束符constraint 為”&S”,”&D”,”&a”,指定了使用的寄存器為esi,edi和eax。很明顯這些寄存器是clobber寄存器,因為它們的內容會在函數執行后被改變。此外我們也能看出為什么memory被放在clobber list中,因為d0, d1, d2被更新了。
 
    我們再來看一個類似的函數。該函數用來移動一塊雙字(double word)。注意這個函數是用宏來定義的。
    `#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內容被改變,所以我們必須把它們放在clobber list中。
 
5. 在Linux中,系統調用是用GCC內聯匯編的形式實現的。讓我們來看看一個系統調用是如何實現的。所有的系統調用都是用宏來寫的 (在linux/unistd.h)。例如,一個帶三個參數的系統調用的定義如下:
    ```
      #define _syscall3(type,name,type1,arg1,type2,arg2,type3,arg3) \
    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中。
     
    Linux中所有的系統調用都是用上面類似的方式實現的。比如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,所以我們把1放到eax中並且把0放到ebx中,最后通過調用int $0x80,exit(0)就被執行了。這就是exit函數的全部。

結束語

這篇文章講述了GCC內聯匯編的基礎知識。一旦你理解了這些基礎內容,自己再一步步的看下去就沒有什么困難了。通過這些例子可以更好的幫助我們理解內聯匯編的常用特性。

GCC內聯匯編是一個很大的主題,這片文章的討論還遠遠不夠。本篇文章我們提到的大多數語法都可以在官方文檔GNU Assembler中看到。完整的constraint也可以在GCC官方文檔中找到。

Linux內核大范圍內使用了GCC內聯匯編,我們可以從中找到各種各樣的例子來學習。這對我們也很有幫助。

如果你找到任何低級的排版打字錯誤或者過期的內容,請聯系我。

參考文獻

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM