最牛X的 GCC 內聯匯編
正如大家知道的,在C語言中插入匯編語言,其是Linux中使用的基本匯編程序語法。本文將講解 GCC 提供的內聯匯編特性的用途和用法。對于閱讀這篇文章,這里只有兩個前提要求,很明顯,就是 x86 匯編語言和 C 語言的基本認識。
1. 簡介
1.1 版權許可
Copyright (C) 2003 Sandeep S.
本文檔自由共享;你可以重新發(fā)布它,并且/或者在遵循自由軟件基金會發(fā)布的 GNU 通用公共許可證下修改它;也可以是該許可證的版本 2 或者(按照你的需求)更晚的版本。
發(fā)布這篇文檔是希望它能夠幫助別人,但是沒有任何擔保;甚至不包括可售性和適用于任何特定目的的擔保。關于更詳細的信息,可以查看 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 內聯匯編。這里內聯表示的是什么呢?
我們可以要求編譯器將一個函數的代碼插入到調用者代碼中函數被實際調用的地方。這樣的函數就是內聯函數。這聽起來和宏差不多?這兩者確實有相似之處。
內聯函數的優(yōu)點是什么呢?
這種內聯方法可以減少函數調用開銷。同時如果所有實參的值為常量,它們的已知值可以在編譯期允許簡化,因此并非所有的內聯函數代碼都需要被包含進去。代碼大小的影響是不可預測的,這取決于特定的情況。為了聲明一個內聯函數,我們必須在函數聲明中使用 "inline" 關鍵字。
現在我們正處于一個猜測內聯匯編到底是什么的點上。它只不過是一些寫為內聯函數的匯編程序。在系統(tǒng)編程上,它們方便、快速并且極其有用。我們主要集中學習(GCC)內聯匯編函數的基本格式和用法。為了聲明內聯匯編函數,我們使用 "asm" 關鍵詞。
內聯匯編之所以重要,主要是因為它可以操作并且使其輸出通過 C 變量顯示出來。正是因為此能力, "asm" 可以用作匯編指令和包含它的 C 程序之間的接口。
3. GCC 匯編語法
Linux上的 GNU C 編譯器 GCC ,使用 AT&T / UNIX 匯編語法。在這里,我們將使用 AT&T 語法 進行匯編編碼。如果你對 AT&T 語法不熟悉的話,請不要緊張,我會教你的。AT&T 語法和 Intel 語法的差別很大。我會給出主要的區(qū)別。
1).源操作數和目的操作數順序
AT&T 語法的操作數方向和 Intel 語法的剛好相反。在Intel 語法中,第一操作數為目的操作數,第二操作數為源操作數,然而在 AT&T 語法中,第一操作數為源操作數,第二操作數為目的操作數。也就是說,
Intel 語法中的 "Op-code dst src" 變?yōu)?AT&T 語法中的 "Op-code src dst"。
2).寄存器命名
寄存器名稱有 "%" 前綴,即如果必須使用 "eax",它應該用作 "%eax"。
3).立即數
AT&T 立即數以 "$" 為前綴。靜態(tài) "C" 變量也使用 "$" 前綴。在 Intel 語法中,十六進制常量以 "h" 為后綴,然而 AT&T 不使用這種語法,這里我們給常量添加前綴 "0x"。所以,對于十六進制,我們首先看到一個 "$",然后是 "0x",最后才是常量。
4).操作數大小
在 AT&T 語法中,存儲器操作數的大小取決于操作碼名字的最后一個字符。操作碼后綴 ’b’ 、’w’、’l’ 分別指明了字節(jié)(8位)、字(16位)、長型(32位)存儲器引用。Intel 語法通過給存儲器操作數添加 "byte ptr"、 "word ptr" 和 "dword ptr" 前綴來實現這一功能。
因此,Intel的 "mov al, byte ptr foo" 在 AT&T 語法中為 "movb foo, %al"。
5).存儲器操作數
在 Intel 語法中,基址寄存器包含在 "[" 和 "]" 中,然而在 AT&T 中,它們變?yōu)?"(" 和 ")"。另外,在 Intel 語法中, 間接內存引用為
"section:[base + index*scale + disp]",在 AT&T中變?yōu)?"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 的一個字節(jié)數據 移至 eax 寄存器指向的內存 */
你可能注意到了這里我使用了 "asm" 和 "__asm__"。這兩者都是有效的。如果關鍵詞 "asm" 和我們程序的一些標識符沖突了,我們可以使用 "__asm__"。如果我們的指令多于一條,我們可以每個一行,并用雙引號圈起,同時為每條指令添加 ’/n’ 和 ’/t’ 后綴。這是因為 gcc 將每一條當作字符串發(fā)送給as(GAS)(LCTT 譯注: GAS 即 GNU 匯編器),并且通過使用換行符/制表符發(fā)送正確格式化后的行給匯編器。
示例
- __asm__ ("movl %eax, %ebx/n/t"
- "movl $56, %esi/n/t"
- "movl %ecx, $label(%edx,%ebx,$4)/n/t"
- "movb %ah, (%ebx)");
如果在代碼中,我們涉及到一些寄存器(即改變其內容),但在沒有恢復這些變化的情況下從匯編中返回,這將會導致一些意想不到的事情。這是因為 GCC 并不知道寄存器內容的變化,這會導致問題,特別是當編譯器做了某些優(yōu)化。在沒有告知 GCC 的情況下,它將會假設一些寄存器存儲了一些值——而我們可能已經改變卻沒有告知 GCC——它會像什么事都沒發(fā)生一樣繼續(xù)運行(LCTT 譯注:什么事都沒發(fā)生一樣是指GCC不會假設寄存器裝入的值是有效的,當退出改變了寄存器值的內聯匯編后,寄存器的值不會保存到相應的變量或內存空間)。我們所可以做的是使用那些沒有副作用的指令,或者當我們退出時恢復這些寄存器,要不就等著程序崩潰吧。這是為什么我們需要一些擴展功能,擴展匯編給我們提供了那些功能。
5. 擴展匯編
在基本內聯匯編中,我們只有指令。然而在擴展匯編中,我們可以同時指定操作數。它允許我們指定輸入寄存器、輸出寄存器以及修飾寄存器列表。GCC 不強制用戶必須指定使用的寄存器。我們可以把頭疼的事留給 GCC ,這可能可以更好地適應 GCC 的優(yōu)化。不管怎么說,基本格式為:
asm ( 匯編程序模板
: 輸出操作數 /* 可選的 */
: 輸入操作數 /* 可選的 */
: 修飾寄存器列表 /* 可選的 */
);
匯編程序模板由匯編指令組成。每一個操作數由一個操作數約束字符串所描述,其后緊接一個括弧括起的 C 表達式。冒號用于將匯編程序模板和第一個輸出操作數分開,另一個(冒號)用于將最后一個輸出操作數和第一個輸入操作數分開(如果存在的話)。逗號用于分離每一個組內的操作數。總操作數的數目限制在 10 個,或者機器描述中的任何指令格式中的最大操作數數目,以較大者為準。
如果沒有輸出操作數但存在輸入操作數,你必須將兩個連續(xù)的冒號放置于輸出操作數原本會放置的地方周圍。
示例:
- asm ("cld/n/t"
- "rep/n/t"
- "stosl"
- : /* 無輸出寄存器 */
- : "c" (count), "a" (fill_value), "D" (dest)
- : "%ecx", "%edi"
- );
現在來看看這段代碼是干什么的?以上的內聯匯編是將 "fill_value" 值連續(xù) "count" 次拷貝到寄存器 "edi" 所指位置(LCTT 譯注:每執(zhí)行 stosl 一次,寄存器 edi 的值會遞增或遞減,這取決于是否設置了 direction 標志,因此以上代碼實則初始化一個內存塊)。 它也告訴 gcc 寄存器 "ecx" 和 "edi" 一直無效。為了更加清晰地說明,讓我們再看一個示例。
- 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 區(qū)分操作數和寄存器。操作數以一個 % 為前綴。
- 第三個冒號之后的修飾寄存器 %eax 用于告訴 GCC %eax 的值將會在 "asm" 內部被修改,所以 GCC 將不會使用此寄存器存儲任何其他值。
當 “asm” 執(zhí)行完畢, "b" 變量會映射到更新的值,因為它被指定為輸出操作數。換句話說, “asm” 內 "b" 變量的修改應該會被映射到 “asm” 外部。
現在,我們可以更詳細地看看每一個域。
1.匯編程序模板
匯編程序模板包含了被插入到 C 程序的匯編指令集。其格式為:每條指令用雙引號圈起,或者整個指令組用雙引號圈起。同時每條指令應以分界符結尾。有效的分界符有換行符("/n")和分號(";")。"/n" 可以緊隨一個制表符("/t")。我們應該都明白使用換行符或制表符的原因了吧(LCTT 譯注:就是為了排版和分隔)?和 C 表達式對應的操作數使用 %0、%1 ... 等等表示。
2.操作數
C 表達式用作 “asm” 內的匯編指令操作數。每個操作數前面是以雙引號圈起的操作數約束。對于輸出操作數,在引號內還有一個約束修飾符,其后緊隨一個用于表示操作數的 C 表達式。即,“操作數約束”(C 表達式)是一個通用格式。對于輸出操作數,還有一個額外的修飾符。約束字符串主要用于決定操作數的尋址方式,同時也用于指定使用的寄存器。
如果我們使用的操作數多于一個,那么每一個操作數用逗號隔開。
在匯編程序模板中,每個操作數用數字引用。編號方式如下。如果總共有 n 個操作數(包括輸入和輸出操作數),那么第一個輸出操作數編號為 0 ,逐項遞增,并且最后一個輸入操作數編號為 n - 1 。操作數的最大數目在前一節(jié)我們講過。
輸出操作數表達式必須為左值。輸入操作數的要求不像這樣嚴格。它們可以為表達式。擴展匯編特性常常用于編譯器所不知道的機器指令 ;-)。如果輸出表達式無法直接尋址(即,它是一個位域),我們的約束字符串必須給定一個寄存器。在這種情況下,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 決定了寄存器并且它知道發(fā)生了什么改變。在最后一個示例,我們不必將 'ecx' 添加到修飾寄存器列表(LCTT 譯注: 原文修飾寄存器列表這個單詞拼寫有錯,這里已修正),gcc 知道它表示 x。因此,因為它可以知道 "ecx" 的值,它就不被當作修飾的(寄存器)了。
3.修飾寄存器列表
一些指令會破壞一些硬件寄存器內容。我們不得不在修飾寄存器中列出這些寄存器,即匯編函數內第三個 ’:’ 之后的域。這可以通知 gcc 我們將會自己使用和修改這些寄存器,這樣 gcc 就不會假設存入這些寄存器的值是有效的。我們不用在這個列表里列出輸入、輸出寄存器。因為 gcc 知道 “asm” 使用了它們(因為它們被顯式地指定為約束了)。如果指令隱式或顯式地使用了任何其他寄存器,(并且寄存器沒有出現在輸出或者輸出約束列表里),那么就需要在修飾寄存器列表中指定這些寄存器。
如果我們的指令可以修改條件碼寄存器(cc),我們必須將 "cc" 添加進修飾寄存器列表。
如果我們的指令以不可預測的方式修改了內存,那么需要將 "memory" 添加進修飾寄存器列表。這可以使 GCC 不會在匯編指令間保持緩存于寄存器的內存值。如果被影響的內存不在匯編的輸入或輸出列表中,我們也必須添加 "volatile" 關鍵詞。
我們可以按我們的需求多次讀寫修飾寄存器。參考一下模板內的多指令示例;它假設子例程 _foo 接受寄存器 "eax" 和 "ecx" 里的參數。
- asm ("movl %0,%%eax;
- movl %1,%%ecx;
- call _foo"
- : /* no outputs */
- : "g" (from), "g" (to)
- : "eax", "ecx"
- );
4.Volatile ...?
如果你熟悉內核源碼或者類似漂亮的代碼,你一定見過許多聲明為 "volatile" 或者 "__volatile__"的函數,其跟著一個 "asm" 或者 "__asm__"。我之前提到過關鍵詞 "asm" 和 "__asm__"。那么什么是 "volatile" 呢?
如果我們的匯編語句必須在我們放置它的地方執(zhí)行(例如,不能為了優(yōu)化而被移出循環(huán)語句),將關鍵詞 "volatile" 放置在 asm 后面、()的前面。以防止它被移動、刪除或者其他操作,我們將其聲明為 "asm volatile ( ... : ... : ... : ...);"
如果擔心發(fā)生沖突,請使用 "__volatile__"。
如果我們的匯編只是用于一些計算并且沒有任何副作用,不使用 "volatile" 關鍵詞會更好。不使用 "volatile" 可以幫助 gcc 優(yōu)化代碼并使代碼更漂亮。
在“一些實用的訣竅”一節(jié)中,我提供了多個內聯匯編函數的例子。那里我們可以了解到修飾寄存器列表的細節(jié)。
6. 更多關于約束
到這個時候,你可能已經了解到約束和內聯匯編有很大的關聯。但我們對約束講的還不多。約束用于表明一個操作數是否可以位于寄存器和位于哪種寄存器;操作數是否可以為一個內存引用和哪種地址;操作數是否可以為一個立即數和它可能的取值范圍(即值的范圍),等等。
6.1 常用約束/strong>
在許多約束中,只有小部分是常用的。我們來看看這些約束。
1. 寄存器操作數約束
當使用這種約束指定操作數時,它們存儲在通用寄存器(GPR)中。請看下面示例:
- asm ("movl %%eax, %0/n" :"=r"(myval));
這里,變量 myval 保存在寄存器中,寄存器 eax 的值被復制到該寄存器中,并且 myval 的值從寄存器更新到了內存。當指定 "r" 約束時, gcc 可以將變量保存在任何可用的 GPR 中。要指定寄存器,你必須使用特定寄存器約束直接地指定寄存器的名字。它們?yōu)椋?/p>
- +---+--------------------+
- | 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 |
- +---+--------------------+
2. 內存操作數約束
當操作數位于內存時,任何對它們的操作將直接發(fā)生在內存位置,這與寄存器約束相反,后者首先將值存儲在要修改的寄存器中,然后將它寫回到內存位置。但寄存器約束通常用于一個指令必須使用它們或者它們可以大大提高處理速度的地方。當需要在 “asm” 內更新一個 C 變量,而又不想使用寄存器去保存它的值,使用內存最為有效。例如,IDTR 寄存器的值存儲于內存位置 loc 處:
- asm("sidt %0/n" : :"m"(loc));
3. 匹配(數字)約束
在某些情況下,一個變量可能既充當輸入操作數,也充當輸出操作數。可以通過使用匹配約束在 "asm" 中指定這種情況。
- asm ("incl %0" :"=a"(var):"0"(var));
在操作數那一節(jié)中,我們也看到了一些類似的示例。在這個匹配約束的示例中,寄存器 "%eax" 既用作輸入變量,也用作輸出變量。 var 輸入被讀進 %eax,并且等遞增后更新的 %eax 再次被存儲進 var。這里的 "0" 用于指定與第 0 個輸出變量相同的約束。也就是,它指定 var 輸出實例應只被存儲在 "%eax" 中。該約束可用于:
在輸入從變量讀取或變量修改后且修改被寫回同一變量的情況
在不需要將輸入操作數實例和輸出操作數實例分開的情況
使用匹配約束最重要的意義在于它們可以有效地使用可用寄存器。
其他一些約束:
- "m" : 允許一個內存操作數,可以使用機器普遍支持的任一種地址。
- "o" : 允許一個內存操作數,但只有當地址是可偏移的。即,該地址加上一個小的偏移量可以得到一個有效地址。
- "V" : 一個不允許偏移的內存操作數。換言之,任何適合 "m" 約束而不適合 "o" 約束的操作數。
- "i" : 允許一個(帶有常量)的立即整形操作數。這包括其值僅在匯編時期知道的符號常量。
- "n" : 允許一個帶有已知數字的立即整形操作數。許多系統(tǒng)不支持匯編時期的常量,因為操作數少于一個字寬。對于此種操作數,約束應該使用 'n' 而不是'i'。
- "g" : 允許任一寄存器、內存或者立即整形操作數,不包括通用寄存器之外的寄存器。
以下約束為 x86 特有。
- "r" : 寄存器操作數約束,查看上面給定的表格。
- "q" : 寄存器 a、b、c 或者 d。
- "I" : 范圍從 0 到 31 的常量(對于 32 位移位)。
- "J" : 范圍從 0 到 63 的常量(對于 64 位移位)。
- "K" : 0xff。
- "L" : 0xffff。
- "M" : 0、1、2 或 3 (lea 指令的移位)。
- "N" : 范圍從 0 到 255 的常量(對于 out 指令)。
- "f" : 浮點寄存器
- "t" : 第一個(棧頂)浮點寄存器
- "u" : 第二個浮點寄存器
- "A" : 指定 "a" 或 "d" 寄存器。這主要用于想要返回 64 位整形數,使用 "d" 寄存器保存最高有效位和 "a" 寄存器保存最低有效位。
6.2 約束修飾符
當使用約束時,對于更精確的控制超過了對約束作用的需求,GCC 給我們提供了約束修飾符。最常用的約束修飾符為:
- "=" : 意味著對于這條指令,操作數為只寫的;舊值會被忽略并被輸出數據所替換。
- "&" : 意味著這個操作數為一個早期改動的操作數,其在該指令完成前通過使用輸入操作數被修改了。因此,這個操作數不可以位于一個被用作輸出操作數或任何內存地址部分的寄存器。如果在舊值被寫入之前它僅用作輸入而已,一個輸入操作數可以為一個早期改動操作數。
上述的約束列表和解釋并不完整。示例可以讓我們對內聯匯編的用途和用法更好的理解。在下一節(jié),我們會看到一些示例,在那里我們會發(fā)現更多關于修飾寄存器列表的東西。
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=%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" 表明 myvar 是一個輸出且位于內存。類似地,"ir" 表明 myint 是一個整型,并應該存在于其他寄存器(回想我們上面看到的表格)。沒有寄存器位于修飾寄存器列表中。
2).現在我們將在一些寄存器/變量上展示一些操作,并比較值。
- __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 位于修飾寄存器列表中。也就是說,代碼將改變內存中的內容。
3).如何置 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" 添加進修飾寄存器列表。
4).現在我們看看一些更為復雜而有用的函數。字符串拷貝。
- 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 的內容發(fā)生了改變,這是塊移動的副作用。因此我們必須將它們添加進修飾寄存器列表。
5).在 Linux 中,系統(tǒng)調用使用 GCC 內聯匯編實現。讓我們看看如何實現一個系統(tǒng)調用。所有的系統(tǒng)調用被寫成宏(linux/unistd.h)。例如,帶有三個參數的系統(tǒng)調用被定義為如下所示的宏。
- 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); /
- }
無論何時調用帶有三個參數的系統(tǒng)調用,以上展示的宏就會用于執(zhí)行調用。系統(tǒng)調用號位于 eax 中,每個參數位于 ebx、ecx、edx 中。最后 "int 0x80" 是一條用于執(zhí)行系統(tǒng)調用的指令。返回值被存儲于 eax 中。
每個系統(tǒng)調用都以類似的方式實現。Exit 是一個單一參數的系統(tǒng)調用,讓我們看看它的代碼看起來會是怎樣。它如下所示。
- {
- asm("movl $1,%%eax; /* SYS_exit is 1 */
- xorl %%ebx,%%ebx; /* Argument is in ebx, it is 0 */
- int $0x80" /* Enter kernel mode */
- );
- }
Exit 的系統(tǒng)調用號是 1,同時它的參數是 0。因此我們分配 eax 包含 1,ebx 包含 0,同時通過 "int $0x80" 執(zhí)行 "exit(0)"。這就是 exit 的工作原理。
8. 結束語
這篇文檔已經將 GCC 內聯匯編過了一遍。一旦你理解了基本概念,你就可以按照自己的需求去使用它們了。我們看了許多例子,它們有助于理解 GCC 內聯匯編的常用特性。
GCC 內聯是一個極大的主題,這篇文章是不完整的。更多關于我們討論過的語法細節(jié)可以在 GNU 匯編器的官方文檔上獲取。類似地,要獲取完整的約束列表,可以參考 GCC 的官方文檔。
當然,Linux 內核大量地使用了 GCC 內聯。因此我們可以在內核源碼中發(fā)現許多各種各樣的例子。它們可以幫助我們很多。
如果你發(fā)現任何的錯別字,或者本文中的信息已經過時,請告訴我們。