gcc內嵌匯編


最近在看“程序員的自我修養”,看到了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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM