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