[翻译] GCC 内联汇编 HOWTO


GCC 内联汇编 HOWTO

v0.1, 01 March 2003.
* * *linux

本 HOWTO 文档将讲解 GCC 提供的内联汇编特性的用途和用法。对于阅读这篇文章,这里只有两个前提要求,很明显,就是 x86 汇编语言和 C 语言的基本认识。编程


原文连接与说明

  1. http://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html
  2. 本翻译文档原文选题自 Linux中国 ,翻译文档版权归属 Linux中国 全部

1. 简介

1.1 版权许可

Copyright (C)2003 Sandeep S.缓存

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

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

1.2 反馈校订

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

1.3 致谢

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

另外,感谢 Phillip , Brennan Underwood 和 colin@nyx.net ;这里的许多东西都厚颜地直接取自他们的工做成果。ui


2. 概览

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

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

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

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

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

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


3. GCC 汇编语法

GCC , Linux上的 GNU C 编译器,使用 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 语法中,存储器操做数的大小取决于操做码名字的最后一个字符。操做码后缀 bwl 分别指明了字节(byte)(8位)、字(word)(16位)、长型(long)(32位)存储器引用。Intel 语法经过给存储器操做数添加 byte ptrword ptrdword 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)( 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不会假设寄存器装入的值是有效的,当退出改变了寄存器值的内联汇编后,寄存器的值不会保存到相应的变量或内存空间 ——译者注)。咱们所能够作的是使用这些没有反作用的指令,或者当咱们退出时固定这些寄存器,或者等待程序崩溃。这是为何咱们须要一些扩展功能。扩展汇编正好给咱们提供了那样的功能。


5. 扩展汇编

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

asm ( 汇编程序模板 
            : 输出操做数                 /* 可选的 */
            : 输入操做数                   /* 可选的 */
            : 修饰寄存器列表               /* 可选的 */
            );

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

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

示例:

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

如今,这段代码是干什么的?以上的内联汇编是将 fill_value 值 连续 count 次 拷贝到 寄存器 edi 所指位置(每执行stosl一次,寄存器 edi 的值会递增或递减,这取决因而否设置了 direction 标志,所以以上代码实则初始化一个内存块 ——译者注)。 它也告诉 gcc 寄存器 ecxedi 一直无效(原文为 eax ,但代码修饰寄存器列表中为 ecx,所以这可能为做者的纰漏 ——译者注)。为了使扩展汇编更加清晰,让咱们再看一个示例。

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

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

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

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

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

5.1 汇编程序模板

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

5.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 添加到修饰寄存器列表(原文修饰寄存器列表拼写有错,这里已修正 ——译者注), gcc 知道它表示x。所以,由于它能够知道 ecx 的值,它就不被看成修饰的(寄存器)了。

5.3 修饰寄存器列表

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

若是咱们的指令能够修改状态寄存器,咱们必须将 cc 添加进修饰寄存器列表。

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

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

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

5.4 Volatile ...?

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

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

asm volatile ( ... : ... : ... : ...);

当咱们必须很是谨慎时,请使用 __volatile__

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

Some Useful Recipes 一节中,我提供了多个内联汇编函数的例子。这儿咱们详细查看修饰寄存器列表。


6. 更多关于约束

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

6.1 经常使用约束

在许多约束中,只有小部分是经常使用的。咱们将看看这些约束。

  1. 寄存器操做数约束(r)

    当使用这种约束指定操做数时,它们存储在通用寄存器(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
  1. 内存操做数约束(m)

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

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

  2. 匹配(数字)约束

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

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

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

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

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

其余一些约束:

  1. m : 容许一个内存操做数使用机器广泛支持的任一种地址。
  2. o : 容许一个内存操做数,但只有当地址是可偏移的。即,该地址加上一个小的偏移量能够获得一个地址。
  3. V : A memory operand that is not offsettable. In other words, anything that would fit the m constraint but not the o constraint.
  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, 1, 2, or 3 (lea 指令的移位)。
  8. N : 范围从 0 到 255 的常量(对于 out 指令)。
  9. f : 浮点寄存器
  10. t : 第一个(栈顶)浮点寄存器
  11. u : 第二个浮点寄存器
  12. A : 指定 ad 寄存器。这主要用于想要返回 64 位整形数,使用 d 寄存器保存最高有效位和 a 寄存器保存最低有效位。

6.2 约束修饰符

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

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

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


7. 一些实用的诀窍

如今咱们已经介绍了关于 GCC 内联汇编的基础理论,如今咱们将专一于一些简单的例子。将内联汇编函数写成宏的形式老是很是方便的。咱们能够在内核代码里看到许多汇编函数。(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 代表 my_var 是一个输出且位于内存。相似地,ir 代表 my_int 是一个整型,并应该存在于其余寄存器(回想咱们上面看到的表格)。没有寄存器位于修饰寄存器列表中。

  1. 如今咱们将在一些寄存器/变量上展现一些操做,并比较值。
__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 位于修饰寄存器列表中。也就是说,代码将改变内存中的内容。

  1. 如何置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 添加进修饰寄存器列表。

  1. 如今咱们看看一些更为复杂而有用的函数。字符串拷贝。
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 的内容发生改变,这是块移动的反作用。所以咱们必须将它们添加进修饰寄存器列表。

  1. 在 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 内联。所以咱们能够在内核源码中发现许多各类各样的例子。它们能够帮助咱们不少。

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


9. 参考

  1. Brennan’s Guide to Inline Assembly
  2. Using Assembly Language in Linux
  3. Using as, The GNU Assembler
  4. Using and Porting the GNU Compiler Collection (GCC)
  5. Linux Kernel Source

via: http://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html

相关文章
相关标签/搜索