1. 輸入變量與佔位符
根據限定符和破壞描述部分,爲輸入和輸出部分的變量分配合適的寄存器,如果限定符指定爲立即數(“i”)或內存變量(“m”)則不需要該步驟,如果限定符沒有具體指定輸入操作數的類型(如“g”),GCC 會視需要決定是否將該操作數輸入到某個寄存器。這樣每個佔位符都與某個寄存器、內存變量或立即數形成了一一對應的關係。對分配了寄存器的輸入變量需要增加代碼將它的值讀入寄存器。另外還要根據破壞描述符的部分增加額外代碼。
2. 指令模板部分
然後根據這種一一對應的關係,用這些寄存器、內存變量或立即數來取代彙編代碼中的佔位符。
3. 變量輸出
按照輸出限定符的指定將寄存器的內容輸出到某個內存變量中,如果輸出操作數的限定符指定爲內存變量(“m”),則該步驟被省略。
__asm__ __volatile__("hlt");
“__asm__”表示後面的代碼爲內嵌彙編, “asm”是“__asm__” 的別名。 “__volatile__”表示編譯器不要優化代碼,後面的指令保留原樣, “volatile”是它的別名。括號裏面是彙編指令。
在內嵌彙編中,可以將 C語言表達式指定爲彙編指令的操作數,而且不用去管如何將 C語言表達式的值讀入哪個寄存器,以及如何將計算結果寫回 C 變量,你只要告訴程序中 C語言表達式與彙編指令操作數之間的對應關係即可, GCC會自動插入代碼完成必要的操作。
使用內嵌彙編,要先編寫彙編指令模板,然後將 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 –c –S 得到該C 文件源代碼
相對應的彙編代碼,然後查看一下彙編代碼,看看 GCC是如何處理的。
C源文件如下內容如下,注意該代碼沒有實際意義,僅僅作爲例子。
extern int input,result;
void test(void)
{
input = 1;
__asm__ __volatile__ ("movl %1,%0" : "=r" (result) : "r" (input));
return ;
}
對應的彙編代碼如下;
行號 代碼 解釋
1-7 ……
8 movl $1, input 對應C語言語句input = 1;
9 movl input, %eax
10 #APP GCC插入的註釋,表示內嵌彙編開始
11 movl %eax,%eax 我們的內嵌彙編語句
12 #NO_APP GCC插入的註釋,表示內嵌彙編結束
13 movl %eax, result 將結果存入 result變量
14-18 。。。。。。
從彙編代碼可以看出,第 9 行和第 13 行是 GCC 自動增加的代碼,GCC 根據限定字符串決定如何處理C表達式,本例兩個表達式都被指定爲“r”型,所以先使用指令:
movl input, %eax
將input讀入寄存器%eax;GCC也指定一個寄存器與輸出變量 result相關,本例也是%eax,等得到操作結果後再使用指令:
movl %eax, result
將寄存器的值寫回 C 變量 result 中。從上面的彙編代碼我們可以看出與 result 和 input相關連的寄存器都是%eax,GCC使用%eax替換內嵌彙編指令模板中的%0,%1:
movl %eax,%eax
顯然這一句可以不要。但是沒有優化,所以這一句沒有被去掉。 由此可見,C表達式或者變量與寄存器的關係由 GCC自動處理,我們只需使用限制字符串指導GCC如何處理即可。限制字符必須與指令對操作數的要求相匹配,否則產生的彙編代碼將會有錯,讀者可以將上例中的兩個“r” ,都改爲“m”(m表示操作數放在內存,而不是寄存器中),編譯後得到的結果是:
movl input, result
很明顯這是一條非法指令, 因此限制字符串必須與指令對操作數的要求匹配。 例如指令 movl允許寄存器到寄存器,立即數到寄存器等,但是不允許內存到內存的操作,因此兩個操作數不能同時使用“m”作爲限定字符。
限制字符列表
限制字符有很多種,有些是與特定體系結構相關,此處僅列出常用的限定字符和 i386中可能用到的一些常用的限定符。 它們的作用是指示編譯器如何處理其後的 C語言變量與指令操作數之間的關係,例如是將變量放在寄存器中還是放在內存中等,下表列出了常用的限定字母。
分類 限定符 描述
“a” 將輸入變量放入eax 這裏有一個問題:假設 eax已經被使用,那怎麼辦? 其實很簡單:因爲 GCC 知道 eax 已經被使用,它在這段彙編代碼的起始處插入一條語句 pushl %eax,將 eax 內容保存到堆棧,然
後在這段代碼結束處再增加一條語句popl %eax,恢復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” ... “9” 表示用它限制的操作數與某個指定的操作數匹配,也即該操作數
就是指定的那個操作數,例如用“0”去描述“%1”操作數,那麼“%1”引用的其實就是“%0”操作數,注意作爲限定符字母的0-9與指令中的“%0”-“%9”的區別,前者描述操作數,後者代表操作數。 後面有詳細描述
& 該輸出操作數不能使用過和輸入操作數相同的寄存器後面有詳細描述
“=” 操作數在指令中是隻寫的(輸出操作數) 操作數類型 “+” 操作數在指令中是讀寫類型的(輸入輸出操作數)
“f” 浮點寄存器
“t” 第一個浮點寄存器
“u” 第二個浮點寄存器 浮點數
“G” 標準的80387浮點常數
% 該操作數可以和下一個操作數交換位置,例如addl的兩個操作數可以交換順序(當然兩個操作數都不能是立即數)
# 部分註釋,從該字符到其後的逗號之間所有字母被忽略
* 表示如果選用寄存器,則其後的字母被忽略
現在繼續看上面的例子,"=m" (ADDR)表示 ADDR 爲內存變量(“m”) ,而且是輸出變量(“=” );"Ir" (nr)表示nr爲 0-31之間的立即數(“I”)或者一個寄存器操作數(“r”) 。