最牛X的GCC 内联汇编

1. 简介linux

1.1 版权许可编程

Copyright (C) 2003 Sandeep S.缓存

本文档自由共享;你能够从新发布它,而且/或者在遵循自由软件基金会发布的 GNU 通用公共许可证下修改它;也能够是该许可证的版本 2 或者(按照你的需求)更晚的版本。函数

发布这篇文档是但愿它可以帮助别人,可是没有任何担保;甚至不包括可售性和适用于任何特定目的的担保。关于更详细的信息,能够查看 GNU 通用许可证。学习

1.2 反馈校订优化

请将反馈和批评一块儿提交给 Sandeep.S。我将感谢任何一个指出本文档中错误和不许确之处的人;一被告知,我会立刻改正它们。编码

1.3 致谢.net

我对提供如此棒的特性的 GNU 人们表示真诚的感谢。感谢 Mr.Pramode C E 所作的全部帮助。感谢在 Govt Engineering College 和 Trichur 的朋友们的精神支持和合做,尤为是 Nisha Kurur 和 Sakeeb S 。 感谢在 Gvot Engineering College 和 Trichur 的老师们的合做。code

另外,感谢 Phillip , Brennan Underwood 和 colin@nyx.net ;这里的许多东西都厚颜地直接取自他们的工做成果。
最牛X的GCC 内联汇编最牛X的GCC 内联汇编教程

2. 概览

在这里,咱们将学习 GCC 内联汇编。这里内联表示的是什么呢?

咱们能够要求编译器将一个函数的代码插入到调用者代码中函数被实际调用的地方。这样的函数就是内联函数。这听起来和宏差很少?这二者确实有类似之处。

内联函数的优势是什么呢?

这种内联方法能够减小函数调用开销。同时若是全部实参的值为常量,它们的已知值能够在编译期容许简化,所以并不是全部的内联函数代码都须要被包含进去。代码大小的影响是不可预测的,这取决于特定的状况。为了声明一个内联函数,咱们必须在函数声明中使用 "inline" 关键字。

如今咱们正处于一个猜想内联汇编究竟是什么的点上。它只不过是一些写为内联函数的汇编程序。在系统编程上,它们方便、快速而且极其有用。咱们主要集中学习(GCC)内联汇编函数的基本格式和用法。为了声明内联汇编函数,咱们使用 "asm" 关键词。

内联汇编之因此重要,主要是由于它能够操做而且使其输出经过 C 变量显示出来。正是由于此能力, "asm" 能够用做汇编指令和包含它的 C 程序之间的接口。

3. GCC 汇编语法

最牛X的GCC 内联汇编最牛X的GCC 内联汇编

Linux上的 GNU C 编译器 GCC ,使用 AT&T / UNIX 汇编语法。在这里,咱们将使用 AT&T 语法 进行汇编编码。若是你对 AT&T 语法不熟悉的话,请没关系张,我会教你的。AT&T 语法和 Intel 语法的差异很大。我会给出主要的区别。

1.源操做数和目的操做数顺序

AT&T 语法的操做数方向和 Intel 语法的恰好相反。在Intel 语法中,第一操做数为目的操做数,第二操做数为源操做数,然而在 AT&T 语法中,第一操做数为源操做数,第二操做数为目的操做数。也就是说,

Intel 语法中的 "Op-code dst src" 变为 AT&T 语法中的 "Op-code src dst"。

2.寄存器命名

寄存器名称有 "%" 前缀,即若是必须使用 "eax",它应该用做 "%eax"。

3.当即数

AT&T 当即数以 "$" 为前缀。静态 "C" 变量也使用 "$" 前缀。在 Intel 语法中,十六进制常量以 "h" 为后缀,然而 AT&T 不使用这种语法,这里咱们给常量添加前缀 "0x"。因此,对于十六进制,咱们首先看到一个 "$",而后是 "0x",最后才是常量。

4.操做数大小

在 AT&T 语法中,存储器操做数的大小取决于操做码名字的最后一个字符。操做码后缀 ’b’ 、’w’、’l’ 分别指明了字节(8位)、字(16位)、长型(32位)存储器引用。Intel 语法经过给存储器操做数添加 "byte ptr"、 "word ptr" 和 "dword ptr" 前缀来实现这一功能。

所以,Intel的 "mov al, byte ptr foo" 在 AT&T 语法中为 "movb foo, %al"。

5.存储器操做数

在 Intel 语法中,基址寄存器包含在 "[" 和 "]" 中,然而在 AT&T 中,它们变为 "(" 和 ")"。另外,在 Intel 语法中, 间接内存引用为

"section:[base + index*scale + disp]",在 AT&T中变为 "section:disp(base, index, scale)"。

须要牢记的一点是,当一个常量用于 disp 或 scale,不能添加 "$" 前缀。

如今咱们看到了 Intel 语法和 AT&T 语法之间的一些主要差异。我仅仅写了它们差异的一部分而已。关于更完整的信息,请参考 GNU 汇编文档。如今为了更好地理解,咱们能够看一些示例。

+------------------------------+------------------------------------+
|       Intel Code             |      AT&T Code                     |
+------------------------------+------------------------------------+
| mov     eax,1                |  movl    $1,%eax                   |   
| mov     ebx,0ffh             |  movl    $0xff,%ebx                |   
| int     80h                  |  int     $0x80                     |   
| mov     ebx, eax             |  movl    %eax, %ebx                |
| mov     eax,[ecx]            |  movl    (%ecx),%eax               |
| mov     eax,[ebx+3]          |  movl    3(%ebx),%eax              | 
| mov     eax,[ebx+20h]        |  movl    0x20(%ebx),%eax           |
| add     eax,[ebx+ecx*2h]     |  addl    (%ebx,%ecx,0x2),%eax      |
| lea     eax,[ebx+ecx]        |  leal    (%ebx,%ecx),%eax          |
| sub     eax,[ebx+ecx*4h-20h] |  subl    -0x20(%ebx,%ecx,0x4),%eax |
+------------------------------+------------------------------------+

4. 基本内联

基本内联汇编的格式很是直接了当。它的基本格式为:
asm("汇编代码");
示例

asm("movl %ecx %eax"); /* 将 ecx 寄存器的内容移至 eax  */
__asm__("movb %bh (%eax)"); /* 将 bh 的一个字节数据 移至 eax 寄存器指向的内存 */

你可能注意到了这里我使用了 "asm" 和 "__asm__"。这二者都是有效的。若是关键词 "asm" 和咱们程序的一些标识符冲突了,咱们可使用 "__asm__"。若是咱们的指令多于一条,咱们能够每一个一行,并用双引号圈起,同时为每条指令添加 ’/n’ 和 ’/t’ 后缀。这是由于 gcc 将每一条看成字符串发送给as(GAS)(LCTT 译注: GAS 即 GNU 汇编器),而且经过使用换行符/制表符发送正确格式化后的行给汇编器。

示例

__asm__ ("movl %eax, %ebx/n/t"
         "movl $56, %esi/n/t"
         "movl %ecx, $label(%edx,%ebx,$4)/n/t"
         "movb %ah, (%ebx)");

若是在代码中,咱们涉及到一些寄存器(即改变其内容),但在没有恢复这些变化的状况下从汇编中返回,这将会致使一些意想不到的事情。这是由于 GCC 并不知道寄存器内容的变化,这会致使问题,特别是当编译器作了某些优化。在没有告知 GCC 的状况下,它将会假设一些寄存器存储了一些值——而咱们可能已经改变却没有告知 GCC——它会像什么事都没发生同样继续运行(LCTT 译注:什么事都没发生同样是指GCC不会假设寄存器装入的值是有效的,当退出改变了寄存器值的内联汇编后,寄存器的值不会保存到相应的变量或内存空间)。咱们所能够作的是使用那些没有反作用的指令,或者当咱们退出时恢复这些寄存器,要不就等着程序崩溃吧。这是为何咱们须要一些扩展功能,扩展汇编给咱们提供了那些功能。

5. 扩展汇编

在基本内联汇编中,咱们只有指令。然而在扩展汇编中,咱们能够同时指定操做数。它容许咱们指定输入寄存器、输出寄存器以及修饰寄存器列表。GCC 不强制用户必须指定使用的寄存器。咱们能够把头疼的事留给 GCC ,这可能能够更好地适应 GCC 的优化。无论怎么说,基本格式为:
asm ( 汇编程序模板
: 输出操做数 /* 可选的 */
: 输入操做数 /* 可选的 */
: 修饰寄存器列表 /* 可选的 */
);

汇编程序模板由汇编指令组成。每个操做数由一个操做数约束字符串所描述,其后紧接一个括弧括起的 C 表达式。冒号用于将汇编程序模板和第一个输出操做数分开,另外一个(冒号)用于将最后一个输出操做数和第一个输入操做数分开(若是存在的话)。逗号用于分离每个组内的操做数。总操做数的数目限制在 10 个,或者机器描述中的任何指令格式中的最大操做数数目,以较大者为准。

最牛X的GCC 内联汇编最牛X的GCC 内联汇编

若是没有输出操做数但存在输入操做数,你必须将两个连续的冒号放置于输出操做数本来会放置的地方周围。

示例:

asm ("cld/n/t"
     "rep/n/t"
     "stosl"
     : /* 无输出寄存器 */
     : "c" (count), "a" (fill_value), "D" (dest)
     : "%ecx", "%edi" 
     );

如今来看看这段代码是干什么的?以上的内联汇编是将 "fill_value" 值连续 "count" 次拷贝到寄存器 "edi" 所指位置(LCTT 译注:每执行 stosl 一次,寄存器 edi 的值会递增或递减,这取决因而否设置了 direction 标志,所以以上代码实则初始化一个内存块)。 它也告诉 gcc 寄存器 "ecx" 和 "edi" 一直无效。为了更加清晰地说明,让咱们再看一个示例。

int a=10, b;
asm ("movl %1, %%eax; 
      movl %%eax, %0;"
     :"=r"(b)        /* 输出 */
     :"r"(a)         /* 输入 */
     :"%eax"         /* 修饰寄存器 */
     );

这里咱们所作的是使用汇编指令使 ’b’ 变量的值等于 ’a’ 变量的值。一些有意思的地方是:

  1. "b" 为输出操做数,用 %0 引用,而且 "a" 为输入操做数,用 %1 引用。
  2. "r" 为操做数约束。以后咱们会更详细地了解约束(字符串)。目前,"r" 告诉 GCC 可使用任一寄存器存储操做数。输出操做数约束应该有一个约束修饰符 "=" 。这修饰符代表它是一个只读的输出操做数。
  3. 寄存器名字以两个 % 为前缀。这有利于 GCC 区分操做数和寄存器。操做数以一个 % 为前缀。
  4. 第三个冒号以后的修饰寄存器 %eax 用于告诉 GCC %eax 的值将会在 "asm" 内部被修改,因此 GCC 将不会使用此寄存器存储任何其余值。

当 “asm” 执行完毕, "b" 变量会映射到更新的值,由于它被指定为输出操做数。换句话说, “asm” 内 "b" 变量的修改应该会被映射到 “asm” 外部。

如今,咱们能够更详细地看看每个域。

1.汇编程序模板

汇编程序模板包含了被插入到 C 程序的汇编指令集。其格式为:每条指令用双引号圈起,或者整个指令组用双引号圈起。同时每条指令应以分界符结尾。有效的分界符有换行符("/n")和分号(";")。"/n" 能够紧随一个制表符("/t")。咱们应该都明白使用换行符或制表符的缘由了吧(LCTT 译注:就是为了排版和分隔)?和 C 表达式对应的操做数使用 %0、%1 ... 等等表示。

2.操做数

C 表达式用做 “asm” 内的汇编指令操做数。每一个操做数前面是以双引号圈起的操做数约束。对于输出操做数,在引号内还有一个约束修饰符,其后紧随一个用于表示操做数的 C 表达式。即,“操做数约束”(C 表达式)是一个通用格式。对于输出操做数,还有一个额外的修饰符。约束字符串主要用于决定操做数的寻址方式,同时也用于指定使用的寄存器。

若是咱们使用的操做数多于一个,那么每个操做数用逗号隔开。

在汇编程序模板中,每一个操做数用数字引用。编号方式以下。若是总共有 n 个操做数(包括输入和输出操做数),那么第一个输出操做数编号为 0 ,逐项递增,而且最后一个输入操做数编号为 n - 1 。操做数的最大数目在前一节咱们讲过。

输出操做数表达式必须为左值。输入操做数的要求不像这样严格。它们能够为表达式。扩展汇编特性经常用于编译器所不知道的机器指令 ;-)。若是输出表达式没法直接寻址(即,它是一个位域),咱们的约束字符串必须给定一个寄存器。在这种状况下,GCC 将会使用该寄存器做为汇编的输出,而后存储该寄存器的内容到输出。

正如前面所陈述的同样,普通的输出操做数必须为只写的; GCC 将会假设指令前的操做数值是死的,而且不须要被(提早)生成。扩展汇编也支持输入-输出或者读-写操做数。

因此如今咱们来关注一些示例。咱们想要求一个数的5次方结果。为了计算该值,咱们使用 "lea" 指令。

asm ("leal (%1,%1,4), %0"
     : "=r" (five_times_x)
     : "r" (x) 
     );

这里咱们的输入为 x。咱们不指定使用的寄存器。 GCC 将会选择一些输入寄存器,一个输出寄存器,来作咱们预期的工做。若是咱们想要输入和输出放在同一个寄存器里,咱们也能够要求 GCC 这样作。这里咱们使用那些读-写操做数类型。这里咱们经过指定合适的约束来实现它。

asm ("leal (%0,%0,4), %0"
     : "=r" (five_times_x)
     : "0" (x) 
     );

如今输出和输出操做数位于同一个寄存器。可是咱们没法得知是哪个寄存器。如今假如咱们也想要指定操做数所在的寄存器,这里有一种方法。

asm ("leal (%%ecx,%%ecx,4), %%ecx"
     : "=c" (x)
     : "c" (x) 
     );

在以上三个示例中,咱们并无在修饰寄存器列表里添加任何寄存器,为何?在头两个示例, GCC 决定了寄存器而且它知道发生了什么改变。在最后一个示例,咱们没必要将 'ecx' 添加到修饰寄存器列表(LCTT 译注: 原文修饰寄存器列表这个单词拼写有错,这里已修正),gcc 知道它表示 x。所以,由于它能够知道 "ecx" 的值,它就不被看成修饰的(寄存器)了。

3.修饰寄存器列表

一些指令会破坏一些硬件寄存器内容。咱们不得不在修饰寄存器中列出这些寄存器,即汇编函数内第三个 ’:’ 以后的域。这能够通知 gcc 咱们将会本身使用和修改这些寄存器,这样 gcc 就不会假设存入这些寄存器的值是有效的。咱们不用在这个列表里列出输入、输出寄存器。由于 gcc 知道 “asm” 使用了它们(由于它们被显式地指定为约束了)。若是指令隐式或显式地使用了任何其余寄存器,(而且寄存器没有出如今输出或者输出约束列表里),那么就须要在修饰寄存器列表中指定这些寄存器。

若是咱们的指令能够修改条件码寄存器(cc),咱们必须将 "cc" 添加进修饰寄存器列表。

若是咱们的指令以不可预测的方式修改了内存,那么须要将 "memory" 添加进修饰寄存器列表。这可使 GCC 不会在汇编指令间保持缓存于寄存器的内存值。若是被影响的内存不在汇编的输入或输出列表中,咱们也必须添加 "volatile" 关键词。

咱们能够按咱们的需求屡次读写修饰寄存器。参考一下模板内的多指令示例;它假设子例程 _foo 接受寄存器 "eax" 和 "ecx" 里的参数。

asm ("movl %0,%%eax;
      movl %1,%%ecx;
      call _foo"
     : /* no outputs */
     : "g" (from), "g" (to)
     : "eax", "ecx"
     );

4.Volatile ...?

若是你熟悉内核源码或者相似漂亮的代码,你必定见过许多声明为 "volatile" 或者 "__volatile__"的函数,其跟着一个 "asm" 或者 "__asm__"。我以前提到过关键词 "asm" 和 "__asm__"。那么什么是 "volatile" 呢?

若是咱们的汇编语句必须在咱们放置它的地方执行(例如,不能为了优化而被移出循环语句),将关键词 "volatile" 放置在 asm 后面、()的前面。以防止它被移动、删除或者其余操做,咱们将其声明为 "asm volatile ( ... : ... : ... : ...);"

若是担忧发生冲突,请使用 "__volatile__"。

若是咱们的汇编只是用于一些计算而且没有任何反作用,不使用 "volatile" 关键词会更好。不使用 "volatile" 能够帮助 gcc 优化代码并使代码更漂亮。

在“一些实用的诀窍”一节中,我提供了多个内联汇编函数的例子。那里咱们能够了解到修饰寄存器列表的细节。

6. 更多关于约束

到这个时候,你可能已经了解到约束和内联汇编有很大的关联。但咱们对约束讲的还很少。约束用于代表一个操做数是否能够位于寄存器和位于哪一种寄存器;操做数是否能够为一个内存引用和哪一种地址;操做数是否能够为一个当即数和它可能的取值范围(即值的范围),等等。

6.1 经常使用约束/strong>

在许多约束中,只有小部分是经常使用的。咱们来看看这些约束。
1. 寄存器操做数约束

当使用这种约束指定操做数时,它们存储在通用寄存器(GPR)中。请看下面示例:

asm ("movl %%eax, %0/n" :"=r"(myval));

这里,变量 myval 保存在寄存器中,寄存器 eax 的值被复制到该寄存器中,而且 myval 的值从寄存器更新到了内存。当指定 "r" 约束时, gcc 能够将变量保存在任何可用的 GPR 中。要指定寄存器,你必须使用特定寄存器约束直接地指定寄存器的名字。它们为:

+---+--------------------+
| r |    Register(s)     |
+---+--------------------+
| a |   %eax, %ax, %al   |
| b |   %ebx, %bx, %bl   |
| c |   %ecx, %cx, %cl   |
| d |   %edx, %dx, %dl   |
| S |   %esi, %si        |
| D |   %edi, %di        |
+---+--------------------+

2. 内存操做数约束

当操做数位于内存时,任何对它们的操做将直接发生在内存位置,这与寄存器约束相反,后者首先将值存储在要修改的寄存器中,而后将它写回到内存位置。但寄存器约束一般用于一个指令必须使用它们或者它们能够大大提升处理速度的地方。当须要在 “asm” 内更新一个 C 变量,而又不想使用寄存器去保存它的值,使用内存最为有效。例如,IDTR 寄存器的值存储于内存位置 loc 处:

asm("sidt %0/n" : :"m"(loc));

3. 匹配(数字)约束

在某些状况下,一个变量可能既充当输入操做数,也充当输出操做数。能够经过使用匹配约束在 "asm" 中指定这种状况。

asm ("incl %0" :"=a"(var):"0"(var));

在操做数那一节中,咱们也看到了一些相似的示例。在这个匹配约束的示例中,寄存器 "%eax" 既用做输入变量,也用做输出变量。 var 输入被读进 %eax,而且等递增后更新的 %eax 再次被存储进 var。这里的 "0" 用于指定与第 0 个输出变量相同的约束。也就是,它指定 var 输出实例应只被存储在 "%eax" 中。该约束可用于:

  • 在输入从变量读取或变量修改后且修改被写回同一变量的状况
  • 在不须要将输入操做数实例和输出操做数实例分开的状况

使用匹配约束最重要的意义在于它们能够有效地使用可用寄存器。

其余一些约束:

  1. "m" : 容许一个内存操做数,可使用机器广泛支持的任一种地址。
  2. "o" : 容许一个内存操做数,但只有当地址是可偏移的。即,该地址加上一个小的偏移量能够获得一个有效地址。
  3. "V" : 一个不容许偏移的内存操做数。换言之,任何适合 "m" 约束而不适合 "o" 约束的操做数。
  4. "i" : 容许一个(带有常量)的当即整形操做数。这包括其值仅在汇编时期知道的符号常量。
  5. "n" : 容许一个带有已知数字的当即整形操做数。许多系统不支持汇编时期的常量,由于操做数少于一个字宽。对于此种操做数,约束应该使用 'n' 而不是'i'。
  6. "g" : 容许任一寄存器、内存或者当即整形操做数,不包括通用寄存器以外的寄存器。

如下约束为 x86 特有。

  1. "r" : 寄存器操做数约束,查看上面给定的表格。
  2. "q" : 寄存器 a、b、c 或者 d。
  3. "I" : 范围从 0 到 31 的常量(对于 32 位移位)。
  4. "J" : 范围从 0 到 63 的常量(对于 64 位移位)。
  5. "K" : 0xff。
  6. "L" : 0xffff。
  7. "M" : 0、一、2 或 3 (lea 指令的移位)。
  8. "N" : 范围从 0 到 255 的常量(对于 out 指令)。
  9. "f" : 浮点寄存器
  10. "t" : 第一个(栈顶)浮点寄存器
  11. "u" : 第二个浮点寄存器
  12. "A" : 指定 "a" 或 "d" 寄存器。这主要用于想要返回 64 位整形数,使用 "d" 寄存器保存最高有效位和 "a" 寄存器保存最低有效位。

6.2 约束修饰符

当使用约束时,对于更精确的控制超过了对约束做用的需求,GCC 给咱们提供了约束修饰符。最经常使用的约束修饰符为:

  1. "=" : 意味着对于这条指令,操做数为只写的;旧值会被忽略并被输出数据所替换。
  2. "&" : 意味着这个操做数为一个早期改动的操做数,其在该指令完成前经过使用输入操做数被修改了。所以,这个操做数不能够位于一个被用做输出操做数或任何内存地址部分的寄存器。若是在旧值被写入以前它仅用做输入而已,一个输入操做数能够为一个早期改动操做数。

上述的约束列表和解释并不完整。示例可让咱们对内联汇编的用途和用法更好的理解。在下一节,咱们会看到一些示例,在那里咱们会发现更多关于修饰寄存器列表的东西。

7. 一些实用的诀窍

如今咱们已经介绍了关于 GCC 内联汇编的基础理论,如今咱们将专一于一些简单的例子。将内联汇编函数写成宏的形式老是很是方便的。咱们能够在 Linux 内核代码里看到许多汇编函数。(usr/src/linux/include/asm/*.h)。

  1. 首先咱们从一个简单的例子入手。咱们将写一个两个数相加的程序。
    int main(void)
    {
            int foo = 10, bar = 15;
            __asm__ __volatile__("addl  %%ebx,%%eax"
                                 :"=a"(foo)
                                 :"a"(foo), "b"(bar)
                                 );
            printf("foo+bar=%d/n", foo);
            return 0;
    }

    这里咱们要求 GCC 将 foo 存放于 %eax,将 bar 存放于 %ebx,同时咱们也想要在 %eax 中存放结果。'=' 符号表示它是一个输出寄存器。如今咱们能够以其余方式将一个整数加到一个变量。

    __asm__ __volatile__(
                         "   lock       ;/n"
                         "   addl %1,%0 ;/n"
                         : "=m"  (my_var)
                         : "ir"  (my_int), "m" (my_var)
                         :                                 /* 无修饰寄存器列表 */
                         );

    这是一个原子加法。为了移除原子性,咱们能够移除指令 'lock'。在输出域中,"=m" 代表 myvar 是一个输出且位于内存。相似地,"ir" 代表 myint 是一个整型,并应该存在于其余寄存器(回想咱们上面看到的表格)。没有寄存器位于修饰寄存器列表中。

    最牛X的GCC 内联汇编最牛X的GCC 内联汇编

  2. 如今咱们将在一些寄存器/变量上展现一些操做,并比较值。
    __asm__ __volatile__(  "decl %0; sete %1"
                         : "=m" (my_var), "=q" (cond)
                         : "m" (my_var) 
                         : "memory"
                         );

    这里,my_var 的值减 1 ,而且若是结果的值为 0,则变量 cond 置 1。咱们能够经过将指令 "lock;/n/t" 添加为汇编模板的第一条指令以增长原子性。

    以相似的方式,为了增长 my_var,咱们可使用 "incl %0" 而不是 "decl %0"。

    这里须要注意的地方是(i)my_var 是一个存储于内存的变量。(ii)cond 位于寄存器 eax、ebx、ecx、edx 中的任何一个。约束 "=q" 保证了这一点。(iii)同时咱们能够看到 memory 位于修饰寄存器列表中。也就是说,代码将改变内存中的内容。

  3. 如何置 1 或清 0 寄存器中的一个比特位。做为下一个诀窍,咱们将会看到它。
    __asm__ __volatile__(   "btsl %1,%0"
                          : "=m" (ADDR)
                          : "Ir" (pos)
                          : "cc"
                          );

    这里,ADDR 变量(一个内存变量)的 'pos' 位置上的比特被设置为 1。咱们可使用 'btrl' 来清除由 'btsl' 设置的比特位。pos 的约束 "Ir" 代表 pos 位于寄存器,而且它的值为 0-31(x86 相关约束)。也就是说,咱们能够设置/清除 ADDR 变量上第 0 到 31 位的任一比特位。由于条件码会被改变,因此咱们将 "cc" 添加进修饰寄存器列表。

  4. 如今咱们看看一些更为复杂而有用的函数。字符串拷贝。
    static inline char * strcpy(char * dest,const char *src)
    {
    int d0, d1, d2;
    __asm__ __volatile__(  "1:/tlodsb/n/t"
                           "stosb/n/t"
                           "testb %%al,%%al/n/t"
                           "jne 1b"
                         : "=&S" (d0), "=&D" (d1), "=&a" (d2)
                         : "0" (src),"1" (dest) 
                         : "memory");
    return dest;
    }

    源地址存放于 esi,目标地址存放于 edi,同时开始拷贝,当咱们到达 0 时,拷贝完成。约束 "&S"、"&D"、"&a" 代表寄存器 esi、edi 和 eax 早期修饰寄存器,也就是说,它们的内容在函数完成前会被改变。这里很明显能够知道为何 "memory" 会放在修饰寄存器列表。

    咱们能够看到一个相似的函数,它能移动双字块数据。注意函数被声明为一个宏。

    #define mov_blk(src, dest, numwords) /
    __asm__ __volatile__ (                                          /
                           "cld/n/t"                                /
                           "rep/n/t"                                /
                           "movsl"                                  /
                           :                                        /
                           : "S" (src), "D" (dest), "c" (numwords)  /
                           : "%ecx", "%esi", "%edi"                 /
                           )

    这里咱们没有输出,寄存器 ecx、esi和 edi 的内容发生了改变,这是块移动的反作用。所以咱们必须将它们添加进修饰寄存器列表。

  5. 在 Linux 中,系统调用使用 GCC 内联汇编实现。让咱们看看如何实现一个系统调用。全部的系统调用被写成宏(linux/unistd.h)。例如,带有三个参数的系统调用被定义为以下所示的宏。
    type name(type1 arg1,type2 arg2,type3 arg3) /
    { /
    long __res; /
    __asm__ volatile (  "int $0x80" /
                      : "=a" (__res) /
                      : "0" (__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2)), /
                        "d" ((long)(arg3))); /
    __syscall_return(type,__res); /
    }

    不管什么时候调用带有三个参数的系统调用,以上展现的宏就会用于执行调用。系统调用号位于 eax 中,每一个参数位于 ebx、ecx、edx 中。最后 "int 0x80" 是一条用于执行系统调用的指令。返回值被存储于 eax 中。

    每一个系统调用都以相似的方式实现。Exit 是一个单一参数的系统调用,让咱们看看它的代码看起来会是怎样。它以下所示。

    {
            asm("movl $1,%%eax;         /* SYS_exit is 1 */
                 xorl %%ebx,%%ebx;      /* Argument is in ebx, it is 0 */
                 int  $0x80"            /* Enter kernel mode */
                );
    }

    Exit 的系统调用号是 1,同时它的参数是 0。所以咱们分配 eax 包含 1,ebx 包含 0,同时经过 "int $0x80" 执行 "exit(0)"。这就是 exit 的工做原理。

8. 结束语

这篇文档已经将 GCC 内联汇编过了一遍。一旦你理解了基本概念,你就能够按照本身的需求去使用它们了。咱们看了许多例子,它们有助于理解 GCC 内联汇编的经常使用特性。

GCC 内联是一个极大的主题,这篇文章是不完整的。更多关于咱们讨论过的语法细节能够在 GNU 汇编器的官方文档上获取。相似地,要获取完整的约束列表,能够参考 GCC 的官方文档。

固然,Linux 内核大量地使用了 GCC 内联。所以咱们能够在内核源码中发现许多各类各样的例子。它们能够帮助咱们不少。

若是你发现任何的错别字,或者本文中的信息已通过时,请告诉咱们。

免费提供最新Linux技术教程书籍,为开源技术爱好者努力作得更多更好:http://www.linuxprobe.com/

相关文章
相关标签/搜索