有時(shí)為了高效,有時(shí)為了直接控制硬件,有些模塊我們不得不直接用匯編語言來編寫,并且對(duì)外提供調(diào)用的接口,隱藏細(xì)節(jié),這其實(shí)就是內(nèi)聯(lián)匯編。如何使用內(nèi)聯(lián)匯編?我們就以 GCC 為例,一窺其中奧秘!
一、關(guān)鍵字
如何讓 GCC 知道代碼中內(nèi)嵌的匯編呢? 借助關(guān)鍵字!來看下面的例子:
__asm__ __volatile__("hlt");
__asm__ 表示后面的代碼為內(nèi)嵌匯編,asm 是 __asm__ 的別名。__volatile__ 表示編譯器不要優(yōu)化代碼,后面的指令保留原樣,volatile 是它的別名。括號(hào)里面是匯編指令。
二、示例分析
使用內(nèi)嵌匯編,要先編寫匯編指令模板,然后將 C 語言表達(dá)式與指令的操作數(shù)相關(guān)聯(lián),并告訴 GCC 對(duì)這些操作有哪些限制條件。示例如下:
__asm__ __violate__ ("movl %1,%0" : "=r" (result) : "m" (input));
movl %1,%0 是指令模板;%0 和 %1 代表指令的操作數(shù),稱為占位符,內(nèi)嵌匯編靠它們將C 語言表達(dá)式與指令操作數(shù)相對(duì)應(yīng)。
指令模板后面用小括號(hào)括起來的是 C 語言表達(dá)式,本例中只有兩個(gè):result 和 input ,他們按照出現(xiàn)的順序分別與指令操作數(shù) %0 、%1 對(duì)應(yīng);注意對(duì)應(yīng)順序:第一個(gè) C 表達(dá)式對(duì)應(yīng) %0 ;第二個(gè)表達(dá)式對(duì)應(yīng) %1 ,依次類推,操作數(shù)至多有10 個(gè),分別用 %0, %1 …. %9 表示。
在每個(gè)操作數(shù)前面有一個(gè)用引號(hào)括起來的字符串,字符串的內(nèi)容是對(duì)該操作數(shù)的限制或者說要求。result 前面的限制字符串是 =r ,其中 = 表示 result 是輸出操作數(shù), r 表示需要將 result 與某個(gè)通用寄存器相關(guān)聯(lián),先將操作數(shù)的值讀入寄存器,然后在指令中使用相應(yīng)寄存器,而不是 result 本身,當(dāng)然指令執(zhí)行完后需要將寄存器中的值存入變量 result ,從表面上看好像是指令直接對(duì) result 進(jìn)行操作,實(shí)際上 GCC 做了隱式處理,這樣我們可以少寫一些指令。 input 前面的 r 表示該表達(dá)式需要先放入某個(gè)寄存器,然后在指令中使用該寄存器參加運(yùn)算。
C 表達(dá)式或者變量與寄存器的關(guān)系由 GCC 自動(dòng)處理,我們只需使用限制字符串指導(dǎo) GCC 如何處理即可。限制字符必須與指令對(duì)操作數(shù)的要求相匹配,否則產(chǎn)生的匯編代碼將會(huì)有錯(cuò),讀者可以將上例中的兩個(gè) r,都改為 m (m表示操作數(shù)放在內(nèi)存,而不是寄存器中),編譯后得到的結(jié)果是:
movl input, result
很明顯這是一條非法指令,因此限制字符串必須與指令對(duì)操作數(shù)的要求匹配。例如指令 movl 允許寄存器到寄存器,立即數(shù)到寄存器等,但是不允許內(nèi)存到內(nèi)存的操作,因此兩個(gè)操作數(shù)不能同時(shí)使用 m 作為限定字符。
內(nèi)嵌匯編語法如下:
__asm__(匯編語句模板: 輸出部分: 輸入部分: 破壞描述部分)
共四個(gè)部分:匯編語句模板,輸出部分,輸入部分,破壞描述部分,各部分使用“:”格開,匯編語句模板必不可少,其他三部分可選,如果使用了后面的部分,而前面部分為空,也需要用“:”格開,相應(yīng)部分內(nèi)容為空。例如:
__asm__ __volatile__("cli": : :"memory")
具體這幾部分都有什么限制呢?這得從細(xì)處著手!
三、語法細(xì)節(jié)
1、匯編語句模板
匯編語句模板由匯編語句序列組成,語句之間使用“;”、“\n” 或 “\n\t” 分開。指令中的操作數(shù)可以使用占位符引用 C 語言變量,操作數(shù)占位符最多10 個(gè),名稱如下:%0,%1,…,%9。指令中使用占位符表示的操作數(shù),總被視為 long 型(4個(gè)字節(jié)),但對(duì)其施加的操作根據(jù)指令可以是字或者字節(jié),當(dāng)把操作數(shù)當(dāng)作字或者字節(jié)使用時(shí),默認(rèn)為低字或者低字節(jié)。對(duì)字節(jié)操作可以顯式的指明是低字節(jié)還是次字節(jié)。方法是在 % 和序號(hào)之間插入一個(gè)字母,b 代表低字節(jié),h 代表高字節(jié),例如:%h1。
2、輸出部分
輸出部分描述輸出操作數(shù),不同的操作數(shù)描述符之間用逗號(hào)格開,每個(gè)操作數(shù)描述符由限定字符串和 C 語言變量組成。每個(gè)輸出操作數(shù)的限定字符串必須包含“=”表示他是一個(gè)輸出操作數(shù)。 例如:
__asm__ __volatile__("pushfl ; popl %0 ; cli":"=g" (x) )
描述符字符串表示對(duì)該變量的限制條件,這樣 GCC 就可以根據(jù)這些條件決定如何分配寄存器,如何產(chǎn)生必要的代碼處理指令操作數(shù)與 C 表達(dá)式或 C 變量之間的聯(lián)系。
3、輸入部分
輸入部分描述輸入操作數(shù),不同的操作數(shù)描述符之間使用逗號(hào)格開,每個(gè)操作數(shù)描述符由限定字符串和 C 語言表達(dá)式或者 C 語言變量組成。 示例如下:
例 1 :
__asm__ __volatile__ ("lidt %0" : : "m" (real_mode_idt));
例 2:
Static __inline__ void __set_bit(int nr, volatile void * addr)
{
__asm__(
"btsl %1,%0"
:"=m" (ADDR)
:"Ir" (nr));
}
后例功能是將 (*addr) 的第 nr 位設(shè)為 1。第一個(gè)占位符 %0 與 C 語言變量 ADDR 對(duì)應(yīng),第二個(gè)占位符 %1 與 C 語言變量 nr 對(duì)應(yīng)。因此上面的匯編語句代碼與下面的偽代碼等價(jià):btsl nr, ADDR,該指令的兩個(gè)操作數(shù)不能全是內(nèi)存變量,因此將 nr 的限定字符串指定為“Ir”,將 nr 與立即數(shù)或者寄存器相關(guān)聯(lián),這樣兩個(gè)操作數(shù)中只有 ADDR 為內(nèi)存變量。
4、限制字符
限制字符有很多種,有些是與特定體系結(jié)構(gòu)相關(guān),此處僅列出常用的限定字符和i386中可能用到的一些常用的限定符。它們的作用是指示編譯器如何處理其后的 C 語言變量與指令操作數(shù)之間的關(guān)系。
分類
|
限定符
|
描述
|
通用寄存器
|
“a”
|
將輸入變量放入eax
|
“b”
|
將輸入變量放入ebx
|
“c”
|
將輸入變量放入ecx
|
“d”
|
將輸入變量放入edx
|
“s”
|
將輸入變量放入esi
|
“d”
|
將輸入變量放入edi
|
“q”
|
將輸入變量放入eax,ebx,ecx,edx中的一個(gè)
|
“r”
|
將輸入變量放入通用寄存器,即eax,ebx,ecx,edx,esi,edi之一
|
“A”
|
把eax和edx合成一個(gè)64 位的寄存器(use long longs)
|
內(nèi)存
|
“m”
|
內(nèi)存變量
|
“o”
|
操作數(shù)為內(nèi)存變量,但其尋址方式是偏移量類型, 也即基址尋址
|
“V”
|
操作數(shù)為內(nèi)存變量,但尋址方式不是偏移量類型
|
“ ”
|
操作數(shù)為內(nèi)存變量,但尋址方式為自動(dòng)增量
|
“p”
|
操作數(shù)是一個(gè)合法的內(nèi)存地址(指針)
|
寄存器或內(nèi)存
|
“g”
|
將輸入變量放入eax,ebx,ecx,edx之一,或作為內(nèi)存變量
|
“X”
|
操作數(shù)可以是任何類型
|
立即數(shù)
|
“I”
|
0-31之間的立即數(shù)(用于32位移位指令)
|
“J”
|
0-63之間的立即數(shù)(用于64位移位指令)
|
“N”
|
0-255之間的立即數(shù)(用于out指令)
|
“i”
|
立即數(shù)
|
“n”
|
立即數(shù),有些系統(tǒng)不支持除字以外的立即數(shù),則應(yīng)使用“n”而非 “i”
|
匹配
|
“ 0 ”
|
表示用它限制的操作數(shù)與某個(gè)指定的操作數(shù)匹配
|
“1” ...
|
也即該操作數(shù)就是指定的那個(gè)操作數(shù),例如“0”
|
“9”
|
去描述“%1”操作數(shù),那么“%1”引用的其實(shí)就是“%0”操作數(shù),注意作為限定符字母的0-9 與指令中的“%0”-“%9”的區(qū)別,前者描述操作數(shù), 后者代表操作數(shù)。
|
&
|
該輸出操作數(shù)不能使用過和輸入操作數(shù)相同的寄存器
|
操作數(shù)類型
|
“=”
|
操作數(shù)在指令中是只寫的(輸出操作數(shù))
|
“+”
|
操作數(shù)在指令中是讀寫類型的(輸入輸出操作數(shù))
|
浮點(diǎn)數(shù)
|
“f”
|
浮點(diǎn)寄存器
|
“t”
|
第一個(gè)浮點(diǎn)寄存器
|
“u”
|
第二個(gè)浮點(diǎn)寄存器
|
“G”
|
標(biāo)準(zhǔn)的80387浮點(diǎn)常數(shù)
|
%
|
該操作數(shù)可以和下一個(gè)操作數(shù)交換位置,例如addl的兩個(gè)操作數(shù)可以交換順序(當(dāng)然兩個(gè)操作數(shù)都不能是立即數(shù))
|
#
|
部分注釋,從該字符到其后的逗號(hào)之間所有字母被忽略
|
*
|
表示如果選用寄存器,則其后的字母被忽略
|
5、破壞描述部分
破壞描述符用于通知編譯器我們使用了哪些寄存器或內(nèi)存,由逗號(hào)格開的字符串組成,每個(gè)字符串描述一種情況,一般是寄存器名;除寄存器外還有 “memory”。例如:“%eax”,“%ebx”,“memory” 等。