擴展的行內匯編
在擴展的行內匯編中,可以將 C 語言表達式指定為匯編指令的操作數,而且不用去管如何將 C 語言表達式的值讀入寄存器,以及如何將計算結果寫回 C 變量,你只要告訴程序中 C 語言表達式與匯編指令操作數之間的對應關系即可,GCC 會自動插入代碼完成必要的操作。
使用內嵌匯編,要先編寫匯編指令模板,然后將 C 語言表達式與指令的操作數相關聯,並告訴 GCC 對這些操作有哪些限制條件。例如在下面的匯編語句:
__asm__ __volatile__ ("movl %1,%0" : "=r" (result) : "r" (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”表示該表達式需要先放入某個寄存器,然后在指令中使用該寄存器參加運算。
限制字符必須與指令對操作數的要求相匹配,否則產生的匯編代碼將會有錯,讀者可以將上例中的兩個“r”,都改為“m” (m,表示操作數放在內存,而不是寄存器中),編譯后得到的結果是:
movl input, result
很明顯這是一條非法指令,因此限制字符串必須與指令對操作數的要求匹配。例如指令movl 允許寄存器到寄存器,立即數到寄存器等,但是不允許內存到內存的操作,因此兩個操作數不能同時使用“m”作為限定字符。
擴展的行內匯編的語法
內嵌匯編語法如下:
__asm__(
匯編語句模板:
輸出部分:
輸入部分:
破壞描述部分);
即格式為 asm ( "statements" : output_regs : input_regs : clobbered_regs);
共四個部分:匯編語句模板,輸出部分,輸入部分,破壞描述部分,各部分使用“:”格開,匯編語句模板必不可少,其他三部分可選,如果使用了后面的部分,而前面部分為空,也需要用“:”格開,相應部分內容為空。
下面是一個簡單的例子:
int main(void)
{
int dest;
int value=1;
asm(
"movl %1, %0"
: "=a"(dest)
: "c" (value)
: "%ebx");
printf("%d\n", dest);
return 0;
}
在這段內嵌匯編的意思是將 value 變量的值復制到變量 dest 中,並指定在匯編中使用 eax
與 ecx 寄存器,同時在最后標識這兩個寄存器的值有被改變。
1) 匯編語句模板
匯編語句模板由匯編語句序列組成,語句之間使用“;”、“\n”或“\n\t”分開。指令中的操作數可以使用占位符引用 C 語言變量, 操作數占位符最多 10 個, 名稱如下: %0, %1…,%9。指令中使用占位符表示的操作數,總被視為 long 型(4,個字節),但對其施加的操作根據指令可以是字或者字節,當把操作數當作字或者字節使用時,默認為低字或者低字節。對字節操作可以顯式的指明是低字節還是次字節。方法是在%和序號之間插入一個字母,“b”
代表低字節,“h”代表高字節,例如: %h1。
2) 輸出部分
輸出部分描述輸出操作數,不同的操作數描述符之間用逗號格開,每個操作數描述符由
限定字符串和 C 語言變量組成。每個輸出操作數的限定字符串必須包含“=”表示它是一個
輸出操作數。例:
__asm__ __volatile__ ("pushfl ; popl %0 ; cli":"=g" (x) )
在這里“x”便是最終存放輸出結果的 C 程序變量,而“=g”則是限定字符串,限定字符串表示了對它之后的變量的限制條件,這樣 GCC 就可以根據這些條件決定如何分配寄存器,如何產生必要的代碼處理指令,以及如何處理操作數與 C 表達式或 C 變量之間的關系。
3) 輸入部分
輸入部分描述輸入操作數,不同的操作數描述符之間使用逗號格開,每個操作數描述符
同樣也由限定字符串和 C 語言表達式或者 C 語言變量組成。例:
__asm__ __volatile__ ("lidt %0" : : "m" (real_mode_idt));
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,把 eax 和 edx,合成一個 64 位的寄存器(uselong 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” |
操作數類型 |
“=” |
操作數在指令中是只寫的(輸出操作數) |
“+” |
操作數在指令中是讀寫類型的(輸入輸出操作數) |
|
浮點數 |
“=” |
操作數在指令中是只寫的(輸出操作數) |
“+” |
操作數在指令中是讀寫類型的(輸入輸出操作數) |
|
“f” |
浮點數 |
|
“t” |
第一個浮點寄存器 |
|
“u” |
第二個浮點寄存器 |
|
“G” |
標准的 80387 |
|
% |
該操作數可以和下一個操作數交換位置 |
另外“0”,“1”, ..., “9”表示用它限制的操作數與某個指定的操作數匹配,也即該操作數就是指定的那個操作數,例如用“0”去描述“%1”操作數,那么“%1”引用的其實就是“%0”操作數,注意作為限定符字母的 0-9 ,與指令中的“%0”-“%9”的區別,前者描述操作數,后者代表操作數。
5) 破壞描述部分
修改描述部分可以防止內嵌匯編在使用某些寄存器時導致錯誤。修改描述符是由逗號隔開的字符串組成的,每個字符串描述一種情況,一般是寄存器,有時也會有“memory”。例如:“%eax”、“%ebx”、“memory”等。具體的意思就是告訴編譯器在編譯內嵌匯編的時候不能使用某個寄存器或者不能使用內存的空間。
下面用一些具體的示例來講述 GCC 如何把內嵌匯編轉換成標准的 AT&T 匯編的。
首先看一個簡單的例子,這個例子:
int main(void)
{
int result = 2;
int input = 1;
__asm__ __volatile__ ("addl %1, %0": "=r"(result): "r"(input));
printf("%d\n", result);
return 0;
}
這段內嵌匯編原本的目的是輸出 1+2=3 的結果,也就是將 input 變量的值與 result 變量的值相加之后再存入 result 中。可以看到在匯編語句模板中的%1 與%0 分別代表 input 與 result變量,而“=r”與“r”則表示兩個變量在匯編中應該對應兩個寄存器, “=”表示 result 是輸出變量。然而實際運行后發現結果實際上是 2。這是為什么呢?我們用(objdump -j .text –S 可執行文件名)這樣的命令來查看編譯生成后的代碼發現這段內嵌匯編經 GCC 翻譯后所對應的AT&T 匯編是:
movl $0x2,0xfffffffc(%ebp)
movl $0x1,0xfffffff8(%ebp)
movl 0xfffffff8(%ebp),%eax
addl %eax,%eax
movl %eax,0xfffffffc(%ebp)
前兩句匯編分別是為 result 和 input 變量賦值。input 為輸入型變量,而且需要放在寄存
器中, GCC 給它分配的寄存器是%eax,在執行 addl 之前%eax 的內容已經是 input 的值。可見對於使用“r”限制的輸入型變量或者表達式,在使用之前 GCC 會插入必要的代碼將他們的值讀到寄存器; “m”型變量則不需要這一步。讀入 input 后執行 addl,顯然 addl %eax,%eax的值不對。再往后看: movl %eax,0xfffffffc(%ebp)的作用是將結果存回 result,分配給 result的寄存器與分配給 input 的一樣,都是%eax。
綜上可以總結出如下幾點:
1. 使用“r”限制的輸入變量, GCC 先分配一個寄存器,然后將值讀入寄存器,最后用該寄存器替換占位符;
2. 使用“r”限制的輸出變量, GCC 會分配一個寄存器,然后用該寄存器替換占位符,但是在使用該寄存器之前並不將變量值先讀入寄存器, GCC 認為所有輸出變量以前的值都沒有用處,不讀入寄存器,最后 GCC 插入代碼,將寄存器的值寫回變量;因為第二條,上面的內嵌匯編指令不能奏效,因此需要在執行 addl 之前把 result 的值讀入寄存器,也許再將 result 放入輸入部分就可以了(因為第一條會保證將 result 先讀入寄存器)。修改后的指令如下(為了更容易說明問題將 input 限制符由“r, ”改為“m”):
int main(void)
{
int result = 2;
int input = 1;
__asm__ __volatile__ ("addl %2,%0":"=r"(result):"r"(result),"m"(input));
printf("%d\n", result);
return 0;
}
這段內嵌匯編所對應的 AT&T 匯編如下:
movl $0x2,0xfffffffc(%ebp)
movl $0x1,0xfffffff8(%ebp)
movl 0xfffffffc(%ebp),%eax
addl 0xfffffff8(%ebp),%eax
movl %eax,0xfffffffc(%ebp)
看上去上面的代碼可以正常工作,因為我們知道%0 和%1 都和 result 相關,應該使用同一個寄存器,而且事實上在實際結果中 GCC 也確實是使用了同一個寄存器 eax,所以可以得到正確的結果 3。但是為了更保險起見,為了確保%0 與%1 與同一個寄存器關聯我們可以
使用如下的方法
int main(void)
{
int result = 2;
int input = 1;
__asm__ __volatile__ ("addl %2,%0":"=r"(result):"0"(result),"m"(input));
printf("%d\n", result);
return 0;
}
它所對應的 AT&T 匯編為:
movl $0x2,0xfffffffc(%ebp)
movl $0x1,0xfffffff8(%ebp)
movl 0xfffffffc(%ebp),%eax
addl 0xfffffff8(%ebp),%eax
movl %eax,0xfffffffc(%ebp)
輸入部分中的 result 用匹配限制符“0”限制,表示%1 與%0,代表同一個變量, 輸入部分
說明該變量的輸入功能,輸出部分說明該變量的輸出功能,兩者結合表示 result 是讀寫型。%0和%1,表示同一個 C 變量,所以放在相同的位置,無論是寄存器還是內存。
至此讀者應該明白了匹配限制符的意義和用法。在新版本的 GCC 中增加了一個限制字符“+”,它表示操作數是讀寫型的, GCC 知道應將變量值先讀入寄存器,然后計算,最后寫回變量,而無需在輸入部分再去描述該變量。
int main(void)
{
int result = 2;
int input = 1;
__asm__ __volatile__ ("addl %1, %0": "+r"(result): "r"(input));
printf("%d\n", result);
return 0;
}
這段內嵌匯編所對應的 AT&T 匯編為:
movl $0x2,0xfffffffc(%ebp)
movl $0x1,0xfffffff8(%ebp)
mov 0xfffffffc(%ebp),%eax
mov 0xfffffff8(%ebp),%edx
add %edx,%eax
mov %eax,0xfffffffc(%ebp)
通過這段內嵌匯編所對應的 AT&T 匯編我們可以看出系統首先將 result 變量的值讀入了eax 寄存器,並為 input 變量分配了 edx 寄存器,然后將 eax 與 edx 的值相加后將結果寫入內存。
接下來的一個示例要較為復雜一些:
int main(void)
{
int count=3;
int value=1;
int buf[10];
asm(
"cld \n\t"
"rep \n\t"
"stosl"
:
: "c" (count), "a" (value) , "D" (buf) );
printf("%d %d %d\n", buf[0],buf[1],buf[2]);
}
經 GCC 翻譯后所對應的 AT&T 匯編是:
movl 0xfffffff4(%ebp),%ecx
movl 0xfffffff0(%ebp),%eax
lea 0xffffffb8(%ebp),%edi
cld
repz stos %eax,%es:(%edi)
在這里 count、 value 和 buf 是三個輸入變量,它們都是 C 程序中的變量,“c”、“a”和
“D”表示這三個輸入值分別被存放入寄存器 ECX、 EAX 與 EDI;“cld rep stosl”是需要
執行的匯編指令;而“%ecx、 %edi”表示這兩個寄存器在指令中被改變了。這段內嵌匯編
要做的就是向 buf 中寫 count 個 value 值。
最后我們給出一個比較綜合一點的例子:
int main(void)
{
int input, output, temp;
input = 1;
__asm__ __volatile__ ("movl $0, %%eax;\n\t"
"movl %%eax, %1;\n\t"
"movl %2, %%eax;\n\t"
"movl %%eax, %0;\n\t"
: "=m" (output), "=m"(temp)
: "r" (input)
:"eax");
printf("%d %d\n", temp,output);
return 0;
}
這段內嵌匯編經由 GCC 轉化成的匯編代碼如下:
movl $0x1,0xfffffffc(%ebp)
mov 0xfffffffc(%ebp),%edx
mov $0x0,%eax
mov %eax,0xfffffff4(%ebp)
mov %edx,%eax
mov %eax,0xfffffff8(%ebp)
可以看到,由於 input、 output、 temp 都是程序局部整型數變量,於是它們實際上是存放在堆棧中的,也就是內存中的某個部分。其中 output 和 temp 是輸出變量,而且“=m”表
明它們應該在內存中, input 是輸入變量,“r”表明它應存放在寄存器中,於是首先把 1 存入 input 變量,然后將變量的值復制給了 edx 寄存器,在這里我們可以看到內嵌匯編中使用了破環描述符“eax”,這是告訴編譯器在程序中 eax 寄存器已被使用,這樣編譯器為了避免沖突會將輸入變量存放在除 eax 以外別的寄存器中,如像我們最后看到的 edx 寄存器。看看內嵌匯編的代碼編譯生成的 AT&T 匯編,我們可以發現第二句內嵌匯編中的%1 轉化成了0xfffffff4 (%ebp),對應的就是 temp 變量;第三句中的%2 則對應到了%edx,即 input 變量所存放的寄存器;而%0 就對應到 output 變量所存放的內存位置 0xfffffff8 (%ebp)。