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

 

你可能感兴趣的:(gcc)