AT&T 汇编-12: 内联汇编

在涉及到一些对硬件的操作或者对性能有极致要求的时候,除了使用汇编函数之外还可以直接在 c 语言里编写汇编代码片段。

基本格式

一个最简单的什么都不做的 c 语言程序:

/* inline.c */

int main()
{
   asm ("nop");

   return 0;
}

内联汇编使用关键字 asm 表示在 c 语言中使用汇编代码:

asm ("assembly code");

使用命令

gcc -S inline.c

转换成汇编语言

    .file   "inline.c"
    .text
.globl main
    .type   main, @function
main:
    pushl   %ebp
    movl    %esp, %ebp
#APP
# 3 "inline.c" 1
    nop
# 0 "" 2
#NO_APP
    movl    $0, %eax
    popl    %ebp
    ret
    .size   main, .-main
    .ident  "GCC: (Debian 4.4.5-8) 4.4.5"
    .section    .note.GNU-stack,"",@progbits

其中“#APP”和“#NO_APP”之间就是 c 程序中使用的内联汇编部分。

汇编语言是每行一条指令,所以使用多条指令时需要换行符来分隔,有些汇编器可能还要求使用制表符。为了方便阅读,可以把每条语句单独写在一行:

asm ("asm code1\n\t"
     "asm code2\n\t"
     "asm code3\n\t");

一般来说内联汇编都需要和主程序进行数据交换完成特定的功能,这就需要使用主程序中定义的变量。但是基本的内联汇编只能使用全局变量(各个函数的局部变量存放在栈中):

/* inline-2.c */

#include <stdio.h>

int a = 2, b = 3, result;

int main()
{
   asm ("pushl %eax\n\t"
        "movl  a, %eax\n\t"
        "addl  b, %eax\n\t"
        "movl  %eax, result\n\t"
        "popl  %eax");

   printf("result = %d\n", result);

   return 0;
}

编译器有时候可能会对代码进行优化,这包括消除不使用的变量,调整代码执行顺序等操作,这可能会对内联汇编有一定影响。如果不希望编译器对内联汇编进行优化可以加上关键字“volatile”:

asm volatile ("asm code");

扩展格式

由于基本的内联汇编格式提供的功能有限,gcc 提供了扩展的格式:

asm ("assembly code" : output locations : input operands : changed registers);

这种格式分为 4 个部分,部分之间使用冒号分隔:第一部分是要执行的汇编代码;第二部分是输出位置;第三部分是输入内容;第四部分是没有出现在输入或输出部分中而且在第一部分中被修改的寄存器列表。同一部分如果有多个值用逗号分隔。

输入和输出部分

输入和输出部分的格式是:

"constraint" (variable)

其中“variable”是主程序中的全局或局部变量,“constraint”是对变量的约束,它的值是单个字符(摘自《汇编语言程序设计》):

使用寄存器

+------------+------------------------------------------+
| constraint |                  含义                    |
+------------+------------------------------------------+
|     a      | 使用 %eax,%ax 或 %al 寄存器             |
+------------+------------------------------------------+
|     b      | 使用%ebx,%bx 或 %bl 寄存器              |
+------------+------------------------------------------+
|     c      | 使用%ecx,%cx 或 %cl 寄存器              |
+------------+------------------------------------------+
|     d      | 使用%edx,%dx 或 %dl 寄存器              |
+------------+------------------------------------------+
|     S      | 使用 %esi 或 %si 寄存器                  |
+------------+------------------------------------------+
|     D      | 使用 %edi 或 %di 寄存器                  |
+------------+------------------------------------------+
|     r      | 使用任何可用的通用寄存器                 |
+------------+------------------------------------------+
|     q      | 使用 %eax,%ebx,%ecx 或 %edx 寄存器之一 |
+------------+------------------------------------------+
|     A      | 对于 64 位值使用 %eax 和 %edx 寄存器     |
+------------+------------------------------------------+
|     f      | 使用浮点寄存器                           |
+------------+------------------------------------------+
|     t      | 使用第一个(顶部的)浮点寄存器           |
+------------+------------------------------------------+
|     u      | 使用第二个浮点寄存器                     |
+------------+------------------------------------------+
|     m      | 使用变量的内存位置                       |
+------------+------------------------------------------+
|     o      | 使用偏移内存位置                         |
+------------+------------------------------------------+
|     V      | 只使用直接内存位置                       |
+------------+------------------------------------------+
|     i      | 使用立即整数值                           |
+------------+------------------------------------------+
|     n      | 使用值已知的立即整数值                   |
+------------+------------------------------------------+
|     g      | 使用任何可用的寄存器或内存位置           |
+------------+------------------------------------------+

除了上面列出的值外,对于输出位置还需要加上以下值中的一个:

+------------+------------------------------------------------+
| constraint |                     含义                       |
+------------+------------------------------------------------+
|     +      | 可以读取和写入操作数                           |
+------------+------------------------------------------------+
|     =      | 只能写入操作数                                 |
+------------+------------------------------------------------+
|     %      | 如果必要,操作数可以和下一个操作数切换         |
+------------+------------------------------------------------+
|     &      | 在内联函数完成之前,可以删除或者重新使用操作数 |
+------------+------------------------------------------------+
/* inline-3.c */

#include <stdio.h>

int main()
{
   int a = 6, b = 3, result;

   asm ("addl  %%ebx, %%eax"
        : "=a"(result)
        : "a"(a), "b"(b));

   printf("result = %d\n", result);

   return 0;
}

和 inline-2.c 相比,这里并没有明显地给 %eax 和 %ebx 赋值,而是在扩展格式的输入部分指定了变量和寄存器的关联。把上面的 c 程序转换成汇编代码:

    .file   "inline-3.c"
    .section    .rodata
.LC0:
    .string "result = %d\n"
    .text
.globl main
    .type   main, @function
main:
    pushl   %ebp
    movl    %esp, %ebp
    andl    $-16, %esp
    pushl   %ebx
    subl    $44, %esp
    movl    $6, 20(%esp)
    movl    $3, 24(%esp)
    movl    20(%esp), %eax
    movl    24(%esp), %edx
    movl    %edx, %ebx
#APP
# 9 "inline-3.c" 1
    addl  %ebx, %eax
# 0 "" 2
#NO_APP
    movl    %eax, 28(%esp)
    movl    $.LC0, %eax
    movl    28(%esp), %edx
    movl    %edx, 4(%esp)
    movl    %eax, (%esp)
    call    printf
    movl    $0, %eax
    addl    $44, %esp
    popl    %ebx
    movl    %ebp, %esp
    popl    %ebp
    ret
    .size   main, .-main
    .ident  "GCC: (Debian 4.4.5-8) 4.4.5"
    .section    .note.GNU-stack,"",@progbits

栈的情况如下:

              +----------------+
              |      ...       |
              +----------------+
              | return address |
              +----------------+
              |      ebp       |
              +----------------+ <- 9. pushl %ebp
              |      ...       |
              +----------------+ <- 11. andl $-16, %esp
              |      eax       |
              +----------------+ <- 12. pushl %eax
              |      ...       |
              +----------------+ <- 24. movl %eax, 28(%esp)
              |     result     |
              +----------------+ <- 15. movl $3, 24(%esp)
              |       b        |
              +----------------+ <- 14. movl $6, 20(%esp)
     ^        |       a        |
     |        +----------------+
   memory     |      ...       |
 addresses    +----------------+ <- 13. subl $44, %esp

第 11 行的作用是字节对齐。可以发现第 14,15 两行就是实现内联汇编中输入部分的代码,第 21 行是实际的内联代码,第 24 行是输出部分的代码。

如果输出值已经包含在输入值中则可以省略输出部分,但是这样最好加上关键字“volatile”,因为编译器可能会觉得这段代码不重要而把它消除掉,因为它没有输出。

使用占位符

上面的输入输出都是通过输入部分和输出部分把变量和寄存器进行绑定,然后在第一部分中使用对应的寄存器进行操作,这样的做法会影响编译器对寄存器使用的优化。gcc 提供了一种按照变量出现的顺序来引用变量的方法:

/* inline-4.c */

#include <stdio.h>

int main()
{
   int a = 2, b = 3, result;

   asm ("movl %1, %0\n\t"
        "addl %2, %0"
        : "=r"(result)
        : "r"(a), "r"(b));

   printf("result = %d\n", result);

   return 0;
}

对于数据交换使用了约束符号“r”,表示由编译器来选择要使用的寄存器。其中 %0 表示列表中出现的第一个元素,即用来保存 result 值的寄存器,以此类推,%1 表示 a 对应的寄存器,%2 表示 b 对应的寄存器。

当元素多了之后引用开始变得混乱,从 gcc3.1 开始,允许使用名称来指定变量:

/* inline-5.c */

#include <stdio.h>

int main()
{
   int a = 2, b = 3, result;

   asm ("movl %[a], %[result]\n\t"
        "addl %[b], %[result]"
        : [result] "=r"(result)
        : [a] "r"(a), [b] "r"(b));

   printf("result = %d\n", result);

   return 0;
}

在输入和输出部分变量前使用中括号指定了替换的变量名,这些变量名可以作为替代符在第一部分的汇编语言中使用。

使用内存位置

前面程序的输出都是先保存在寄存器中,然后再从寄存器写入对应的内存位置,这比直接往内存里写结果多了一步操作。在 constraint 中有“m”标记,表示直接使用内存位置,当然这需要在符合指令规定的前提下使用:

/* inline-6.c */

#include <stdio.h>

int main()
{
   int a = 2, b = 3, result;


   asm ("movl %1, %0\n\t"
        "addl %2, %0"
        : "=m"(result)
        : "r"(a), "r"(b));

   printf("result = %d\n", result);

   return 0;
}

通过汇编的结果可以看出,在指令 movl 中直接使用了 result 所在的内存位置。

改动的寄存器列表

上面的程序中输入部分修改了 %eax 和 %ebx 寄存器,但是在第四部分中却没有出现,因为这两个寄存器已经在输入部分出现过。如果把代码改为:

/* inline-7.c */

#include <stdio.h>

int main()
{
   int a = 6, b = 3, result;

   asm ("addl  %%ebx, %%eax"
        : "=a"(result)
        : "a"(a), "b"(b)
        : "%eax", "%ebx");

   printf("result = %d\n", result);

   return 0;
}

编译会报错。第四部分中的寄存器名称要使用完整的名称而不能只使用上面提到的 constraint 代表字符,不过“%”是可选的。像下面的程序:

/* inline-8.c */

#include <stdio.h>

int main()
{
   int a = 5;

   asm ("movl %%eax, %%ebx"
        :
        : "a"(a)
        : "%ebx");

   return 0;
}

因为第一部分中修改了寄存器 %ebx,而 %ebx 并没有出现在输入或输出部分中,所以需要在第四部分列出被修改的寄存器。

发表于 2011年7月19日
  1. 2011年9月6日 11:57 | #1

    欧哥仲用到AT&T指令???大牛啊!!!!膜拜!!!!!

发表评论

XHTML: 您可以使用这些标签: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>