最近在看“程序員的自我修養”,看到了gcc內嵌匯編,靜態鏈接那章的示例程序比較有趣,於是准備學習一下AT&T語法的gcc內嵌匯編。以前學微機原理的時候學習過匯編,現在基本上還給了老師,還是復習一下吧。
像大家一樣先來介紹一下AT&T語法與Intel asm語法的不同(順便也學學基本知識):
在 AT&T 匯編格式中,寄存器名要加上 '%' 作為前綴;而在 Intel 匯編格式中,寄存器名不需要加前綴。例如:
AT&T 格式 |
Intel 格式 |
pushl %eax |
push eax |
在 AT&T 匯編格式中,用 '$' 前綴表示一個立即操作數;而在 Intel 匯編格式中,立即數的表示不用帶任何前綴。例如:
AT&T 格式 |
Intel 格式 |
pushl $1 |
push 1 |
AT&T 和 Intel 格式中的源操作數和目標操作數的位置正好相反。在 Intel 匯編格式中,目標操作數在源操作數的左邊;而在 AT&T 匯編格式中,目標操作數在源操作數的右邊。例如:
AT&T 格式 |
Intel 格式 |
addl $1, %eax |
add eax, 1 |
在 AT&T 匯編格式中,操作數的字長由操作符的最后一個字母決定,后綴'b'、'w'、'l'分別表示操作數為字節(byte,8 比特)、字(word,16 比特)和長字(long,32比特);而在 Intel 匯編格式中,操作數的字長是用 "byte ptr" 和 "word ptr" 等前綴來表示的。例如:
AT&T 格式 |
Intel 格式 |
movb val, %al |
mov al, byte ptr val |
在 AT&T 匯編格式中,絕對轉移和調用指令(jump/call)的操作數前要加上'*'作為前綴,而在 Intel 格式中則不需要。
遠程轉移指令和遠程子調用指令的操作碼,在 AT&T 匯編格式中為 "ljump" 和 "lcall",而在 Intel 匯編格式中則為 "jmp far" 和 "call far",即:
AT&T 格式 |
Intel 格式 |
ljump $section, $offset |
jmp far section:offset |
lcall $section, $offset |
call far section:offset |
與之相應的遠程返回指令則為:
AT&T 格式 |
Intel 格式 |
lret $stack_adjust |
ret far stack_adjust |
基本的的內嵌格式:(每行用雙引號括起來,有多行的話用“\n\t”分開)
asm("assembly code");
比如:
asm("movl %ecx %eax"); /* moves the contents of ecx to eax */
__asm__ ("movl %eax, %ebx\n\t"
"movl $56, %esi\n\t"
"movl %ecx, $label(%edx,%ebx,$4)\n\t"
"movb %ah, (%ebx)");
注:使用asm或__asm__開頭都是可以的。
擴展asm格式:(Extended asm)
asm ( assembler template
: output operands /* optional */
: input operands /* optional */
: list of clobbered registers /* optional */
);
OR
asm("匯編語句"
:輸出寄存器
:輸入寄存器
:會被修改的寄存器);
如果沒有輸出的話,也需要使用“:”,那一行空着就行了:
asm ("cld\n\t"
"rep\n\t"
"stosl"
: /* no output registers */
: "c" (count), "a" (fill_value), "D" (dest)
: "%ecx", "%edi"
);
解釋一下上述代碼的作用:(cld,rep,stosl的具體使用方式請參加后面的說明)這幾條語句的功能是向buf中寫上count個value值.將count的值加載到ecx寄存器中(加載代碼是"c"),fill_value加載到eax中,dest放到edi中。 同時告知gcc,寄存器eax和edi的內容不再有效了(clobbered registers)。
進一步說明一下:
int a=10, b;
asm ("movl %1, %%eax \n\t"
"movl %%eax, %0 \n\t"
:"=r"(b)
:"r"(a)
:"%eax"
);
printf("Result: %d, %d\n", a, b);
b 是輸出操作符,%0 就是對b的一個引用,a是輸出操作符,被%1引用
r 是對操作符的一個限制,r告訴gcc使用寄存器來保存操作符。使用‘=’來指明輸出操作符
寄存器前面需要使用兩個‘%’,這幫助gcc區別操作符和寄存器,操作符前面只有一個‘%’
在第三個冒號之后的被改變的(the clobbered register) 寄存器 %eax 告訴gcc該寄存器會在asm中被修改,不要在該寄存器中存值。
這段代碼的效果是把a的值賦給b。
輸入: Result:10,10
操作數:
下面這個例子是將x的值擴大五倍之后存放到five_times_x中
int five_times_x = 0;
int x = 3;
asm ("leal (%1,%1,4), %0 "
: "=r" (five_times_x)
: "r" (x)
);
printf("After five times x is %d\n",five_times_x);
leal(%1,%1, 4),%0" :x + x * 4 -> five_times_x
這個段代碼中,x是輸入,我們並沒有指定使用的寄存器,gcc自動選擇不同的寄存器來完成這些操作。我們也可以讓gcc將輸入和輸出放在同樣的寄存器中,只要在代碼中稍加約束就可以辦到了:
int x = 3;
int five_times_x = 0;
asm ("leal (%0,%0,4), %0"
: "=r" (five_times_x)
: "0" (x)
);
printf("After five times x is %d\n",five_times_x);
這段代碼中輸入和輸出都是使用相同的寄存器,但是我們不知道使用的是哪個寄存器。
也可以指定一個寄存器被輸入和輸出共用:
int x = 3;
int five_times_x = 0;
asm ("leal (%%ecx,%%ecx,4), %%ecx"
: "=c" (five_times_x)
: "c" (x)
);
printf("After five times x is %d\n",five_times_x);
常用的寄存器約束的縮寫:
r:I/O,表示使用一個通用寄存器,由GCC在%eax/ %ax/ %al、%ebx/ %bx/ %bl、%ecx/ %cx /%cl、%edx/%dx/%dl中選取一個GCC認為是合適的;
q:I/O,表示使用一個通用寄存器,與r的意義相同;
g:I/O,表示使用寄存器或內存地址;
m:I/O,表示使用內存地址;
a:I/O,表示使用%eax/%ax/%al;
b:I/O,表示使用%ebx/%bx/%bl;
c:I/O,表示使用%ecx/%cx/%cl;
d:I/O,表示使用%edx/%dx/%dl;
D:I/O,表示使用%edi/%di;
S:I/O,表示使用%esi/%si;
f:I/O,表示使用浮點寄存器;
t:I/O,表示使用第一個浮點寄存器;
u:I/O,表示使用第二個浮點寄存器;
A:I/O,表示把%eax與%edx組合成一個64位的整數值;
o:I/O,表示使用一個內存位置的偏移量;
V:I/O,表示僅僅使用一個直接內存位置;
i:I/O,表示使用一個整數類型的立即數;
n:I/O,表示使用一個帶有已知整數值的立即數;
F:I/O,表示使用一個浮點類型的立即數;
=: O 表示此Output操作表達式是只寫的
+ :O 表示此Output操作表達式是可讀可寫的
&:O 表示此Output操作表達式獨占為其指定的寄存器
%:I 表示此Input操作表達式中的C/C++表達式可以與下一個Input操作表達式中的C/C++表達式互換
一些例子:
例一:
int foo = 10, bar = 15;
__asm__ __volatile__("addl %%ebx,%%eax"
:"=a"(foo)
:"a"(foo), "b"(bar)
);
printf("foo+bar=%d\n", foo);
輸出:foo+bar=25
例二:
int my_var = 10, my_int = 15;
__asm__ __volatile__(
" lock ;\n\t"
" addl %1,%0 ;\n\t"
: "=m" (my_var)
: "ir" (my_int), "m" (my_var)
);
printf("my_int + my_var = %d",my_var);
輸出:my_int + my_var = 25
說明:這個加法是原子的,如果將第一句的‘lock’去掉,可以消除加法的原子性。 代碼中使用‘=m’表明my_var是一個程序的輸出,並存儲在內存中。‘ir’表明my_int是一個整數並存儲在寄存器中。
字符串拷貝函數:
static inline char * strcpy(char * dest,const char *src)
{
int d0, d1, d2;
__asm__ __volatile__( "1:\t lodsb\n\t" //1:只是一個跳轉標志
"stosb \n\t"
"testb %%al,%%al\n\t" //判斷字符串是否復制結束
"jne 1b" //如果字符串未結束,跳轉到1:處
: "=&S" (d0), "=&D" (d1), "=&a" (d2)
: "0" (src),"1" (dest)
: "memory");
return dest;
}
函數將esi寄存器指向的內容拷貝到edi中,到遇到0時停止。使用“&S”,“&D”,“&a”約束,表明寄存器esi,edi,eax的內容當函數執行之后不可用。
lodsb 指令:從esi 指向的源地址中逐一讀取一個字符,送入AL 中; (然后,可以先判斷這個字符是什么字符,如0dh,0ah 之類等,再執行相應的操作);
stosb 指令:一般跟隨在lodsb 指令后面,將AL 中的字符逐一寫入edi 指向的目的地址;
如果是lobsw ,表明要處理的是字,而不是字符;則采用的相應指令是:stosw ;那么要判斷的寄存器是AX,而不是AL 了.
如果是lobsd ,表明要處理的是雙字;則采用的相應指令是: stosd ;這時候,要判斷的寄存器就是EAX 了.
代碼中:
: "=&S" (d0), "=&D" (d1), "=&a" (d2)
表明esi內容來源於參數0,esi內容來源於參數1
而:
: "0" (src),"1" (dest)
表明了參數0即是函數參數列表中的src,參數1即是函數參數列表中的dest。
清除方向標志,在字符串的比較,賦值,讀取等一系列和rep連用的操作中,di或si是可以自動增減的而不需要人來加減它的值,cld即告訴程序si,di向前移動,std指令為設置方向,告訴程序si,di向后移動
#define mov_blk(src, dest, numwords) \
__asm__ __volatile__ ( \
"cld\n\t" \
"rep\n\t" \
"movsl" \
: \
: "S" (src), "D" (dest), "c" (numwords) \
: "%ecx", "%esi", "%edi" \
)
先說搬移字串。搬移字串指令有兩種,分別是 MOVSB 和 MOVSW,先說 MOVSB。MOVSB 的英文是 move string byte,意思是搬移一個字節,它是把 DS:SI 所指地址的一個字節搬移到 ES:DI 所指的地址上,搬移后原來的內容不變,但是原來 ES:DI 所指的內容會被覆蓋而且在搬移之后 SI 和 DI 會自動地址向下一個要搬移的地址。
一般而言,通常程序設計師一般並不會只搬一個字節,通常都會重復許多次,如果要重復的話,就得把重復次數 ( 也就是字串長度 ) 先記錄在 CX 寄存器,並且在 MOVSB 之前加上 REP 指令,REP 是重復 (repeat) 的意思。這種寫法很是奇怪,一般而言匯編語言源文件的每一行都只有一個指令,但 REP MOVSB 卻可以在同一行寫兩個指令,當然分開寫也是一樣的。
對於cld 和 movsl 的使用可以參考:
http://www.cnblogs.com/cykun/archive/2010/10/27/1862940.html