基本內聯匯編
基本內聯匯編格式比較直觀,可以直接這樣寫:
asm("assembly code");
例如:
asm("movl %ecx, %eax"); /* 把 ecx 內容移動到 eax */
__asm__("movb %bh , (%eax)"); /* 把bh中一個字節的內容移動到eax指向的內存 */
擴展內聯匯編
前面討論的基本內聯匯編只涉及到嵌入匯編指令,而在擴展形式中,我們還可以指定操作數,並且可以選擇輸入輸出寄存器,以及指明要修改的寄存器列表。對於要訪問的寄存器,並不一定要顯式指明,也可以留給GCC自己去選擇,這可能讓GCC更好去優化代碼。擴展內聯匯編格式如下:
asm 匯編限定符 (匯編模板
:"限定符1"(輸出操作數1),"限定符2"(輸出操作數2)....... //可選的
:"限定符1"(輸入操作數1),"限定符2"(輸入操作數2)....... //可選的
:已改寫寄存器列表 //可選的
);
其中匯編模板為匯編指令部分。括號內的操作數都是C語言表達式中常量字符串。不同部分之間使用冒號分隔。相同部分語句中的每個小部分用逗號分隔。最多可以指定10個操作數,不過可能有的計算機平台有額外的文檔說明可以使用超過10個操作數。
- 第一個關鍵字
asm
,有時為了避免命名沖突,gcc也支持寫作__asm__
- 匯編限定符,例如限定符可以是
__volatile__
匯編模板
#include <stdio.h>
void foo(void)
{
int foo = 0; bar = 10;
__asm__ __volatile__ (
"movl %1, %%eax;\n"
"movl %%eax, %0;\n"
:"=r"(foo)
:"r"(bar)
:"%eax");
printf("foo = bar; foo = %d\n", foo);
}
截取自上面冒號之前的兩句就是匯編模板,他們可以一行寫一句,甚至加上換行符\n
,\r
。非常類似printf的第一個參數,格式化字符串部分。
"movl %1, %%eax;\n"
"movl %%eax, %0;\n"
操作數
"constraint" (C expression) //"=r"(result)
接下來兩個冒號之后的分別是輸出/入操作數和它的約束屬性。
"約束屬性"(操作數)
"=r"(foo)
多個操作數的描述可以以,
逗號隔開
"約束屬性1"(操作數1),"約束屬性2"(操作數2),"約束屬性3"(操作數3)............
這些操作數最終會被匯編模板里面的%0, %1, %2, %3
依次引用,例如上列中,匯編模板最終變為:
movl bar, %eax;
movl %eax, foo;
是不是和printf的參數列表特像?
輸出操作數
- 當內聯匯編被執行完后,作為輸出操作數的變量foo,值發生了改變。在
__asm__
內聯匯編內部對操作數foo的修改,反應在了外部的C語言中。
movl bar, %eax; @ bar(10)賦值給eax
movl %eax, foo; @ eax(10)賦值給foo
經過上面兩句,foo的值由原來的0變為了10。輸出操作數必須是左值,因為它最終要被內部的內聯匯編賦值的。
輸入操作數
- 同理,輸入操作數就是由外部的C表達式或者變量對內部內聯匯編進行一個數值的傳入。
movl bar, %eax; @ bar(10)賦值給eax
movl %eax, foo; @ eax(10)賦值給foo
bar作為輸入操作數,將值10帶入了內聯匯編內部。
已改寫寄存器
- 這個寄存器的內容在內聯匯編中已經被修改。所以gcc不會用這個寄存器來存儲其它值。例如上列里面的
eax
寄存器。
一些指令會修改一些寄存器的值。我們必須在受影響列表中列出那些寄存器,即內聯匯編第三個:
后的區域。這用於指示gcc我們將使用並修改它們。所以gcc將不會假設它加載到這些寄存器中的值是合法的。我們不應該列出輸入和輸出寄存器,因為gcc知道內聯匯編使用它們(因為它們被明確指定為限制符(constraints))。如果指令使用了任何其他寄存器,顯式或隱式的(並且這些寄存器沒有出現在輸入和輸出列表上),那么那些寄存器必須在受影響列表中指定。
約束(constraints)
你可能已經感到我們之前經常提到的constraint是個很重要的內容了。不過之前我們並沒有過多的討論。constraint中可以指明一個操作數是否在寄存器中,在哪個寄存器中;可以指明操作數是否是內存引用,如何尋址;可以說明操作數是否是立即數常量,和其可能是的值(或值范圍)。
常用constraints
雖然constraints有很多,但常用的並不多。下面我們就來看看這些常用的constraints。
a. 寄存器操作數限制符("r")
如果操作數指定了這個constraints,操作數將被存儲在通用寄存器中。看下面的例子:
__asm__( "movl %%eax, %0" : "=r" (myval));
上面變量myval會被被保存在一個由GCC自己選擇的寄存器中,eax中的值被拷貝到這個寄存器中去,並且在內存中的myval的值也會按這個寄存器值被更新。當constraints ”r” 被指定時,GCC可能會在任何一個可用的通用寄存器中保存這個值。當然,你也可以指定具體使用那個寄存器,用下表所列出的constraints:
限制符 | 寄存器 |
---|---|
r | 隨機的通用寄存器 |
a | %eax, %ax, %al |
b | %ebx, %bx, %bl |
c | %ecx, %cx, %cl |
d | %edx, %dx, %dl |
S | %esi, %si |
D | %edi, %di |
b. 內存操作數限制符("m")
當操作數是在內存中時,任何在它上的操作將直接在內存位置進行,而寄存器限制符,則優先存於寄存器而后修改再寫回內存。但寄存器限制符通常只在指令必需或者明顯提升性能時使用。當C變量需在asm中修改且無需寄存器保持其值時,內存限制符可最大化性能。如,將idtr的值存儲於loc的內存位置中:
__asm__("sidt %0\n" : :"m"(loc));
c. 匹配(數字)限制符
有時,一個單獨變量既是輸入也是輸出操作符,這時可使用匹配限制符。
asm ("incl %0" :"=a"(var):"0"(var));
我們在操作數一節看到了類似的例子,在這個例子中寄存器%eax既是輸入也是輸出變量。var輸入讀入%eax並更新到%eax最后在自增后存入var。這里的"0"指定了和輸出變量一樣的第0個限制符。也就是說,它指定了var的輸出過程應該只存於%eax中。這類限制符可用於:
- 輸入輸出是統一變量,或變量被修改並被寫會同一變量時。
- 將輸入和輸出操作符分開是不必要的時候。
使用匹配限制符最重要的效果是使可用寄存器的使用更有效。
一些其他的限制符有:
- m: 接受內存操作數,機器支持任意的地址。
- o: 接受內存操作數,只接受偏移地址(offsettable)。即對某個合法地址添加一個微小的偏移量。
- V: 非偏移內存操作數。換句話說,任何符合"m"但不符合"o"限制符的地址。
- i: 立即整型操作數,允許在編譯期(assembly-time)可知常量符號。
- n: 立即整型操作數,允許已知數字值。許多系統不支持小於16-bit的(word wide)編譯期(assembly-time)常量作為操作數。這些操作數應該使用n而不是i。
- g: 任何寄存器,內存或立即整型操作數都可用,要求寄存器不是常規寄存器(general registers)。
- cc:如果我們的指令可以修改條件碼寄存器(the condition code register),我們必須增加
cc
到受影響寄存器列表。 - memory:如果我們的指令用一個不可預期的方法(fashion)修改了內存,添加
memory
到受影響寄存器。這會使gcc在匯編指令期間不在寄存器內保持內存值的緩存。我們也必須添加__volatile__
關鍵字,如果內存影響(memory affected)未列在asm的輸入和輸出中。
對於ARM處理器核,GCC v4提供了如下一系列的限定性字符串:
Constraint | Usage in ARM state | Usage in Thumb state |
---|---|---|
f | Floating point registers f0 … f7 | Not available |
h | Not available | Registers r8…r15 |
G | Immediate floating point constant | Not available |
H | Same a G, but negated | Not available |
I(大寫i) | Immediate value in data processing instructions e.g. ORR R0, R0, #operand | Constant in the range 0 … 255 e.g. SWI operand |
J | Indexing constants -4095 … 4095 e.g. LDR R1, [PC, #operand] | Constant in the range -255 … -1 e.g. SUB R0, R0, #operand |
K | Same as I, but inverted | Same as I, but shifted |
L | Same as I, but negated | Constant in the range -7 … 7 e.g. SUB R0, R1, #operand |
l(小寫L) | Same as r Registers r0…r7 e.g. | PUSH operand |
M | Constant in the range of 0 … 32 or a power of 2 e.g. MOV R2, R1, ROR #operand | Constant that is a multiple of 4 in the range of 0 … 1020 e.g. ADD R0, SP, #operand |
m | Any valid memory address | |
N | Not available | Constant in the range of 0 … 31 e.g. LSL R0, R1, #operand |
O | Not available | Constant that is a multiple of 4 in the range of -508 … 508 e.g. ADD SP, #operand |
r | General register r0 … r15 e.g. | SUB operand1, operand2, operand3 Not available |
w | Vector floating point registers s0 … s31 | Not available |
X | Any operand |
限制符之前可以添加單個的限定性修飾符。如果不添加修飾符,則表明該操作數是只讀的。可以使用的修飾符如下所示:
修飾符 | 說明 |
---|---|
無 | 默認操作數是只讀的 |
= | 只寫操作數,通常被用來修飾輸出操作數 |
+ | 可讀可寫操作數,只能用來修飾輸出操作數 |
& | 意味着這個操作數為一個早期的改動操作數,其在該指令完成前通過使用輸入操作數被修改了。因此,這個操作數不可以位於一個被用作輸出操作數或任何內存地址部分的寄存器。如果在舊值被寫入之前它僅用作輸入而已,一個輸入操作數可以為一個早期改動操作數。 |