linux內聯彙編

來源:互聯網
上載者:User
 

如果您是 Linux 核心的開發人員,您會發現自己經常要對與體繫結構高度相關的功能進行編碼或最佳化代碼路徑。您很可能是通過將組合語言指令插入到 C 語句的中間(又稱為內聯彙編的一種方法)來執行這些任務的。讓我們看一下 Linux 中內聯彙編的特定用法。(我們將討論限制在 IA32 彙編。)

GNU 組譯工具簡述

讓我們首先看一下 Linux 中使用的基本組譯工具文法。GCC(用於 Linux 的 GNU C 編譯器)使用 AT&T 彙編文法。下面列出了這種文法的一些基本規則。(該列表肯定不完整;只包括了與內聯彙編相關的那些規則。)

寄存器命名
寄存器名稱有 % 首碼。即,如果必須使用 eax,它應該用作 %eax。

源運算元和目的運算元的順序
在所有指令中,先是源運算元,然後才是目的運算元。這與將源運算元放在目的運算元之後的 Intel 文法不同。

mov %eax, %ebx, transfers the contents of eax to ebx.

運算元大小
根據運算元是位元組 (byte)、字 (word) 還是長型 (long),指令的尾碼可以是 b、w 或 l。這並不是強制性的;GCC 會嘗試通過讀取運算元來提供相應的尾碼。但手工指定尾碼可以改善代碼的可讀性,並可以消除編譯器猜測不正確的可能性。

movb %al, %bl -- Byte move    movw %ax, %bx -- Word move    movl %eax, %ebx -- Longword move

立即運算元
通過使用 $ 指定直接運算元。

movl $0xffff, %eax -- will move the value of 0xffff into eax register.

間接記憶體引用
任何對記憶體的間接引用都是通過使用 ( ) 來完成的。

movb (%esi), %al -- will transfer the byte in the memory  pointed by esi into alregister


回頁首

內聯彙編

GCC 為內聯彙編提供特殊結構,它具有以下格式:

GCG 的 "asm" 結構

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

本例中,組譯工具模板由彙編指令組成。輸入運算元是充當指令輸入運算元使用的 C 運算式。輸出運算元是將對其執行彙編指令輸出的 C 運算式。

內聯彙編的重要性體現在它能夠靈活操作,而且可以使其輸出通過 C 變數顯示出來。因為它具有這種能力,所以 "asm" 可以用作彙編指令和包含它的 C 程式之間的介面。

一個非常基本但很重要的區別在於 簡單內聯彙編只包括指令,而 擴充內聯彙編包括運算元。要說明這一點,考慮以下樣本:

內聯彙編的基本要素

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

在上例中,我們使用彙編指令使 "b" 的值等於 "a"。請注意以下幾點:

  • "b" 是輸出運算元,由 %0 引用,"a" 是輸入運算元,由 %1 引用。
  • "r" 是運算元的約束,它指定將變數 "a" 和 "b" 儲存在寄存器中。請注意,輸出運算元約束應該帶有一個約束修飾符 "=",指定它是輸出運算元。
  • 要在 "asm" 內使用寄存器 %eax,%eax 的前面應該再加一個 %,換句話說就是 %%eax,因為 "asm" 使用 %0、%1 等來標識變數。任何帶有一個 % 的數都看作是輸入/輸出運算元,而不認為是寄存器。
  • 第三個冒號後的修飾寄存器 %eax 告訴將在 "asm" 中修改 GCC %eax 的值,這樣 GCC 就不使用該寄存器儲存任何其它的值。
  • movl %1, %%eax 將 "a" 的值移到 %eax 中, movl %%eax, %0 將 %eax 的內容移到 "b" 中。
  • 因為 "b" 被指定成輸出運算元,因此當 "asm" 的執行完成後,它將反映出更新的值。換句話說,對 "asm" 內 "b" 所做的更改將在 "asm" 外反映出來。

現在讓我們更詳細的瞭解每一項的含義。



回頁首

組譯工具模板

組譯工具模板是一組插入到 C 程式中的彙編指令(可以是單個指令,也可以是一組指令)。每條指令都應該由雙引號括起,或者整組指令應該由雙引號括起。每條指令還應該用一個定界符結尾。有效定界符為新行 (/n) 和分號 (;)。 '/n' 後可以跟一個 tab(/t) 作為格式化符號,增加 GCC 在彙編檔案中產生的指令的可讀性。 指令通過數 %0、%1 等來引用 C 運算式(指定為運算元)。

如果希望確保編譯器不會在 "asm" 內部最佳化指令,可以在 "asm" 後使用關鍵字 "volatile"。如果程式必須與 ANSI C 相容,則應該使用 __asm__ 和 __volatile__,而不是 asm 和 volatile。



回頁首

運算元

C 運算式用作 "asm" 內的彙編指令運算元。在彙編指令通過對 C 程式的 C 運算式進行操作來執行有意義的作業的情況下,運算元是內聯彙編的主要特性。

每個運算元都由運算元約束字串指定,後面跟用括弧括起的 C 運算式,例如:"constraint" (C expression)。運算元約束的主要功能是確定運算元的定址方式。

可以在輸入和輸出部分中同時使用多個運算元。每個運算元由逗號分隔開。

在組譯工具模板內部,運算元由數字引用。如果總共有 n 個運算元(包括輸入和輸出),那麼第一個輸出運算元的編號為 0,逐項遞增,最後那個輸入運算元的編號為 n -1。總運算元的數目限制在 10,如果機器描述中任何指令模式中的最大運算元數目大於 10,則使用後者作為限制。



回頁首

修飾寄存器列表

如果 "asm" 中的指令指的是硬體寄存器,可以告訴 GCC 我們將自己使用和修改它們。這樣,GCC 就不會假設它裝入到這些寄存器中的值是有效值。通常不需要將輸入和輸出寄存器列為 clobbered,因為 GCC 知道 "asm" 使用它們(因為它們被明確指定為約束)。不過,如果指令使用任何其它的寄存器,無論是明確的還是隱含的(寄存器不在輸入約束列表中出現,也不在輸出約束列表中出現),寄存器都必須被指定為修飾列表。修飾寄存器列在第三個冒號之後,其名稱被指定為字串。

至於關鍵字,如果指令以某些不可預知且不明確的方式修改了記憶體,則可能將 "memory" 關鍵字添加到修飾寄存器列表中。這樣就告訴 GCC 不要在不同指令之間將記憶體值快取在寄存器中。



回頁首

運算元約束

前面提到過,"asm" 中的每個運算元都應該由運算元約束字串描述,後面跟用括弧括起的 C 運算式。運算元約束主要是確定指令中運算元的定址方式。約束也可以指定:

  • 是否允許運算元位於寄存器中,以及它可以包括在哪些種類的寄存器中
  • 運算元是否可以是記憶體引用,以及在這種情況下使用哪些種類的地址
  • 運算元是否可以是立即數

約束還要求兩個運算元匹配。



回頁首

常用約束

在可用的運算元約束中,只有一小部分是常用的;下面列出了這些約束以及簡要描述。有關運算元約束的完整列表,請參考 GCC 和 GAS 手冊。

寄存器運算元約束 (r)
使用這種約束指定運算元時,它們儲存在通用寄存器中。請看下例:

asm ("movl %%cr3, %0/n" :"=r"(cr3val));

這裡,變數 cr3val 儲存在寄存器中,%cr3 的值複製到寄存器上,cr3val 的值從該寄存器更新到記憶體中。指定 "r" 約束時,GCC 可以將變數 cr3val 儲存在任何可用的 GPR 中。要指定寄存器,必須通過使用特定的寄存器約束直接指定寄存器名。

a   %eaxb   %ebxc   %ecxd   %edxS   %esiD   %edi

記憶體運算元約束 (m)
當運算元位於記憶體中時,任何對它們執行的操作都將在記憶體位置中直接發生,這與寄存器約束正好相反,後者先將值儲存在要修改的寄存器中,然後將它寫回記憶體位置中。但寄存器約束通常只在對於指令來說它們是絕對必需的,或者它們可以大大提高進程速度時使用。當需要在 "asm" 內部更新 C 變數,而您又確實不希望使用寄存器來儲存其值時,使用記憶體約束最為有效。例如,idtr 的值儲存在記憶體位置 loc 中:

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

匹配(數字)約束
在某些情況下,一個變數既要充當輸入運算元,也要充當輸出運算元。可以通過使用匹配約束在 "asm" 中指定這種情況。

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

在匹配約束的樣本中,寄存器 %eax 既用作輸入變數,也用作輸出變數。將 var 輸入讀取到 %eax,增加後將更新的 %eax 再次儲存在 var 中。這裡的 "0" 指定第 0 個輸出變數相同的約束。即,它指定 var 的輸出執行個體只應該儲存在 %eax 中。該約束可以用於以下情況:

  • 輸入從變數中讀取,或者變數被修改後,修改寫回到同一變數中
  • 不需要將輸入運算元和輸出運算元的執行個體分開

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



回頁首

一般內聯彙編用法樣本

以下樣本通過各種不同的運算元約束說明了用法。有如此多的約束以至於無法將它們一一列出,這裡只列出了最經常使用的那些約束類型。

"asm" 和寄存器約束 "r" 讓我們先看一下使用寄存器約束 r 的 "asm"。我們的樣本顯示了 GCC 如何分配寄存器,以及它如何更新輸出變數的值。

int main(void){    int x = 10, y;        asm ("movl %1, %%eax;     "movl %%eax, %0;"        :"=r"(y)  /* y is output operand */        :"r"(x)       /* x is input operand */        :"%eax"); /* %eax is clobbered register */}

在該例中,x 的值複製為 "asm" 中的 y。x 和 y 都通過儲存在寄存器中傳遞給 "asm"。為該例產生的彙編代碼如下:

main:pushl %ebpmovl %esp,%ebpsubl $8,%espmovl $10,-4(%ebp)    movl -4(%ebp),%edx  /* x=10 is stored in %edx */#APP    /* asm starts here */   movl %edx, %eax     /* x is moved to %eax */movl %eax, %edx     /* y is allocated in edx and updated */#NO_APP /* asm ends here */movl %edx,-8(%ebp)  /* value of y in stack is updated with                  the value in %edx */ 

當使用 "r" 約束時,GCC 在這裡可以自由分配任何寄存器。在我們的樣本中,它選擇 %edx 來儲存 x。在讀取了 %edx 中 x 的值後,它為 y 也分配了相同的寄存器。

因為 y 是在輸出運算元部分中指定的,所以 %edx 中更新的值儲存在 -8(%ebp),堆棧上 y 的位置中。如果 y 是在輸入部分中指定的,那麼即使它在 y 的臨時寄存器儲存值 (%edx) 中被更新,堆棧上 y 的值也不會更新。

因為 %eax 是在修飾列表中指定的,GCC 不在任何其它地方使用它來儲存資料。

輸入 x 和輸出 y 都分配在同一個 %edx 寄存器中,假設輸入在輸出產生之前被消耗。請注意,如果您有許多指令,就不是這種情況了。要確保輸入和輸出分配到不同的寄存器中,可以指定 & 約束修飾符。下面是添加了約束修飾符的樣本。

int main(void){    int x = 10, y;        asm ("movl %1, %%eax;     "movl %%eax, %0;"        :"=&r"(y) /* y is output operand, note the                     & constraint modifier. */        :"r"(x)       /* x is input operand */        :"%eax"); /* %eax is clobbered register */}

以下是為該樣本產生的彙編代碼,從中可以明顯地看出 x 和 y 儲存在 "asm" 中不同的寄存器中。

main:pushl %ebpmovl %esp,%ebpsubl $8,%espmovl $10,-4(%ebp)movl -4(%ebp),%ecx  /* x, the input is in %ecx */#APP    movl %ecx, %eax    movl %eax, %edx     /* y, the output is in %edx */#NO_APPmovl %edx,-8(%ebp)


回頁首

特定寄存器約束的使用

現在讓我們看一下如何將個別寄存器作為運算元的約束指定。在下面的樣本中,cpuid 指令採用 %eax 寄存器中的輸入,然後在四個寄存器中給出輸出:%eax、%ebx、%ecx、%edx。對 cpuid 的輸入(變數 "op")傳遞到 "asm" 的 eax 寄存器中,因為 cpuid 希望它這樣做。在輸出中使用 a、b、c 和 d 約束,分別收集四個寄存器中的值。

asm ("cpuid": "=a" (_eax),"=b" (_ebx),"=c" (_ecx),"=d" (_edx): "a" (op));

在下面可以看到為它產生的彙編代碼(假設 _eax、_ebx 等... 變數都儲存在堆棧上):

movl -20(%ebp),%eax /* store 'op' in %eax -- input */#APPcpuid#NO_APPmovl %eax,-4(%ebp)  /* store %eax in _eax -- output */movl %ebx,-8(%ebp)  /* store other registers inmovl %ecx,-12(%ebp)  respective output variables */movl %edx,-16(%ebp)

strcpy 函數可以通過以下方式使用 "S" 和 "D" 約束來實現:

asm ("cld/n    rep/n    movsb"    : /* no input */    :"S"(src), "D"(dst), "c"(count));

通過使用 "S" 約束將源指標 src 放入 %esi 中,使用 "D" 約束將目的指標 dst 放入 %edi 中。因為 rep 首碼需要 count 值,所以將它放入 %ecx 中。

在下面可以看到另一個約束,它使用兩個寄存器 %eax 和 %edx 將兩個 32 位的值合并在一起,然後產生一個64 位元的值:

#define rdtscll(val) / __asm__ __volatile__ ("rdtsc" : "=A" (val))The generated assembly looks like this (if val has a 64 bit memory space).#APPrdtsc#NO_APPmovl %eax,-8(%ebp)  /* As a result of A constraint movl %edx,-4(%ebp)   %eax and %edx serve as outputs */Note here that the values in %edx:%eax serve as 64 bit output.


回頁首

使用匹配約束

在下面將看到系統調用的代碼,它有四個參數:

#define _syscall4(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4) /type name (type1 arg1, type2 arg2, type3 arg3, type4 arg4) /{ /long __res; /__asm__ volatile ("int $0x80" /: "=a" (__res) /: "0" (__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2)), /"d" ((long)(arg3)),"S" ((long)(arg4))); /__syscall_return(type,__res); /}

在上例中,通過使用 b、c、d 和 S 約束將系統調用的四個自變數放入 %ebx、%ecx、%edx 和 %esi 中。請注意,在輸出中使用了 "=a" 約束,這樣,位於 %eax 中的系統調用的傳回值就被放入變數 __res 中。通過將匹配約束 "0" 用作輸入部分中第一個運算元約束,syscall 號 __NR_##name 被放入 %eax 中,並用作對系統調用的輸入。這樣,這裡的 %eax 既可以用作輸入寄存器,又可以用作輸出寄存器。沒有其它寄存器用於這個目的。另請注意,輸入(syscall 號)在產生輸出(syscall 的傳回值)之前被消耗(使用)。



回頁首

記憶體運算元約束的使用

請考慮下面的原子遞減操作:

__asm__ __volatile__("lock; decl %0":"=m" (counter):"m" (counter));

為它產生的彙編類似於:

#APP    lock    decl -24(%ebp) /* counter is modified on its memory location */#NO_APP.

您可能考慮在這裡為 counter 使用寄存器約束。如果這樣做,counter 的值必須先複製到寄存器,遞減,然後對其記憶體更新。但這樣您會無法理解鎖定和原子性的全部意圖,這些明確顯示了使用記憶體約束的必要性。



回頁首

使用修飾寄存器

請考慮記憶體拷貝的基本實現。

   asm ("movl $count, %%ecx;    up: lodsl;      stosl;    loop up;"        :           /* no output */        :"S"(src), "D"(dst) /* input */        :"%ecx", "%eax" );  /* clobbered list */    

當 lodsl 修改 %eax 時,lodsl 和 stosl 指令隱含地使用它。%ecx 寄存器明確裝入 count。但 GCC 在我們通知它以前是不知道這些的,我們是通過將 %eax 和 %ecx 包括在修飾寄存器集中來通知 GCC 的。在完成這一步之前,GCC 假設 %eax 和 %ecx 是自由的,它可能決定將它們用作儲存其它的資料。請注意,%esi 和 %edi 由 "asm" 使用,它們不在修飾列表中。這是因為已經聲明 "asm" 將在輸入運算元列表中使用它們。這裡最低限度是,如果在 "asm" 內部使用寄存器(無論是明確還是隱含地),既不出現在輸入運算元列表中,也不出現在輸出運算元列表中,必須將它列為修飾寄存器。



回頁首

結束語

總的來說,內聯彙編非常巨大,它提供的許多特性我們甚至在這雷根本沒有涉及到。但如果掌握了本文描述的基本材料,您應該可以開始對自己的內聯彙編進行編碼了

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.