• 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

    make it simple, make it happen
  • 相关阅读:
    2020软件工程最后一次作业
    常用的10种算法

    赫夫曼编码
    哈希表(散列)
    查找算法
    排序算法
    递归

    软件工程最后一次作业
  • 原文地址:https://www.cnblogs.com/zhuyp1015/p/2478099.html
Copyright © 2020-2023  润新知