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