GCC内联汇编基础

gcc 内联汇编用法介绍 前言大部分内容翻译提取自某国外HOW-TO文档,原地址: http://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html AT&T汇编语法gcc内联汇编语法采用AT&T格式,主要不同有五个方面 1. 源操作数和目标操作数的位置与intel格式不同 2. 寄存器命名,AT&T在寄存器前加百分号(%) 3. 字面值前加美元符号($ 阅读详情

英文原文作者 Sandeep.S

英文原文来自 https://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html

本文将介绍GCC编译环境下,在C语言代码中嵌入汇编代码的基本方法。阅读本文需要您具备80X86汇编语言和C语言的基础知识。为了使中文描述更加清楚自然,翻译过程中加入了稍许解释和意译部分。

简介

版权/反馈/勘误/感谢等信息。1

概要

在讨论GCC内联汇编之前,我们先来搞搞清楚,到底什么是内联汇编?

先看在C语言中,我们可以指定编译器将一个函数代码直接复制到调用其代码的地方执行。这种函数调用方式和默认压栈调用方式不同,我们称这种函数为内联函数。内联函数看起来很像宏?两者确实有许多共同之处。

那么,内联函数有哪些优点呢?

很明显,内联函数降低了函数的调用开销:如果多次被调用的某个函数实参相同,那么它的返回值一定是相同的,这就给编译器留下了优化空间。此时编译器完全可以直接用这个返回值替代这个函数,而不必把该函数的代码插入到调用者的代码中再去计算结果了。如此一来,不但减少了代码量,还节省了计算资源。指定编译器将一个函数处理为内联函数,我们只要在函数申明前加上inline关键字即可。

基于对上述内联函数的认知,我们大概可以想象出内联汇编到底是怎么一回事了。内联汇编相当于用汇编语句写成的内联函数。它方便,快速,对系统编程有着举足轻重的作用。本文主要就GCC内联函数的格式和使用方法展开讨论。在GCC中声明一个内联汇编函数,我们要用asm这个关键字。

之所以内联汇编如此有用,主要是因为它可以操作C语言变量,比如可以输出值到C语言变量。这个特性使内联汇编成为汇编代码和调用其C程序之间的桥梁。

GCC汇编格式。

GCC (GNU Compiler for Linux) 使用AT&T/UNIX汇编语法。所以这篇文章将会用AT&T汇编格式来写汇编代码。如果你不熟悉AT&T汇编语法也没有关系,下面会有些简单的介绍。AT&T和Intel汇编语法差别比较大,二者主要不同之处如下:

  1. 源操作数和目的操作数的方向
    AT&T和Intel汇编语法源操作数和目的操作数的方向正好相反。Intel中第一个操作数作为目的操作数,第二个操作数作为源操作数。而在AT&T中,第一个操作数是源操作数,第二个是目的操作数:
    "OP-code dst src" //Intel语法
  2. "Op-code src dst" //AT&T语法
  3. 寄存器命名
    在AT&T汇编中, 寄存器名前有%前缀。例如,如果要使用eax,得写作: %eax。
  4. 立即数 (Immediate Operand)
    在AT&T语法中,立即数(Immediate Operand)都有'$'前缀。引用的C语言静态变量 (static C variables) 也必须放上'$'前缀;
    此外,在Intel语法中, 16进制的常数是以’h’作为后缀的,但是在AT&T语法中, 是以'0x’作为前缀的。因此,在AT&T语法中,一个16进制常数的写法是:首先以$开头接着是0x,最后是常数本身。
  5. 操作数大小
    在AT&T语法中,操作符的最后一个字符决定着操作数访问内存的长度:以’b’, 'w'和 'l'为后缀指明内存访问长度是 byte(8-bit), word(16-bit)还是long(32-bit)。而Intel语法在操作数前加上'byte ptr', 'word ptr'和'dword ptr'的内存操作符来达到相同目的。
    因此 Intel汇编写法:
    mov al, byte ptr foo

  6. 用AT&T语法写就是:
    movb foo, %al
  7. 内存操作数
    在Intel语法中,基址寄存器是放在方括号‘[]’中的,但AT&T是放在小括弧’()’内的。
    因此,在Intel语法中,一个间接内存寻址是这么写的:
    section:[base + index * scale + disp]。

  8. 而在AT&T中则应该写成这样:
    section:disp(base, index, scale)

  9. 此外对于AT&T汇编,当一个常数被用作disp或者scale时,不需要'$'前缀。这点需要记住。

以上就是AT&T和Intel汇编语法的一些主要不同点。这只是一小部分,具体内容需要参考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+ecx2h | addl (%ebx,%ecx,0x2),%eax |

| lea eax,ebx+ecx | leal (%ebx,%ecx),%eax |

| sub eax,ebx+ecx4h-20h | subl -0x20(%ebx,%ecx,0x4),%eax |

基本内联汇编 (Basic Inline)

基本内联汇编格式比较直观,可以直接这样写:

asm("assembly code");

例如:

asm("movl %ecx, %eax");             /* 把 ecx 内容移动到 eax */

__asm__("movb %bh , (%eax)");   /* 把bh中一个字节的内容移动到eax指向的内存 */

你可能注意到了这里使用了两个不同的关键字 asm 和 __asm__。这两个关键字都可以使用。不过当遇到asm关键字与程序其他变量有冲突的时候就必须用__asm__了。如果内联汇编有多条指令,则每行要加上双引号,并且该行要以\n\t结尾。这是因为GCC会将每行指令作为一个字符串传给as(GAS),使用换行和TAB可以将正确且格式良好的代码行传递给汇编器。

举个例子:

__asm__ ( "movl %eax, %ebx\n\t"

                "movl $56, %esi\n\t"

                "movl %ecx, $label(%edx,%ebx,$4)\n\t"

                "movb %ah, (%ebx)");

如果在内联代码中操作了一些寄存器,比如你修改了寄存器内容(而之后也没有进行还原操作),程序很可能会产生一些难以预料的情况。因为此时GCC并不知道你已经将寄存器内容修改了。这点尤其是在编译器对代码进行了一些优化的情况下而导致问题。因为编译器注意不到寄存器内容已经被改掉,程序将当作它没有被修改过而继续执行。所以此时我们尽量不要使用这些会产生附加影响的操作,或者当我们退出的时候还原这些操作。否则很可能会造成程序崩溃。可是如果我们必须要这样操作该怎么办呢?我们可以通过下面的讨论的扩展内联汇编进行。

扩展内联汇编 (Extended Asm)

前面讨论的基本内联汇编只涉及到嵌入汇编指令,而在扩展形式中,我们还可以指定操作数,并且可以选择输入输出寄存器,以及指明要修改的寄存器列表。对于要访问的寄存器,并不一定要要显式指明,也可以留给GCC自己去选择,这可能让GCC更好去优化代码。扩展内联汇编格式如下:

asm ( assembler template

       : output operands                /* optional */

       : input operands                   /* optional */

       : list of clobbered registers   /* optional */

);

其中assembler template为汇编指令部分。括号内的操作数都是C语言表达式中常量字符串。不同部分之间使用冒号分隔。相同部分语句中的每个小部分用逗号分隔。最多可以指定10个操作数,不过可能有的计算机平台有额外的文档说明可以使用超过10个操作数。

此外,如果没有输出部分但是有输入部分,我们还得保留输出部分前面的冒号。就像下面这样: 

asm ( "cld\n\t"

         "rep\n\t"

         "stosl"

        : /* no output registers */

        : "c" (count), "a" (fill_value), "D" (dest)

        : "%ecx", "%edi"

     );

上述代码做了些什么呢?它主要是循环count次把fill_value的值到填充到edi寄存器指定的内存位置。并且告诉GCC,寄存器ecx2和edi中的内容可能已经被改变了。 为了有一个更清晰的理解,我们再来看一个例子:

int a=10, b;

asm ( "movl %1, %%eax;

          movl %%eax, %0;"

         :"=r"(b)           /* output */

         :"r"(a)              /* input */

         :"%eax"         /* clobbered register */

);

上面代码实现的功能就是用汇编代码把a的值赋给b。值得注意的几点有:

  • “b”是输出操作数,用%0来访问,”a”是输入操作数,用%1来访问。
  • “r” 是一个constraint, 关于constraint后面有详细的介绍。这里我们只要记住这里”r”的意思就是让GCC自己去选择一个寄存器去存储变量a。输出部分constraint前必须要有个 ”=”修饰,用来说明是一个这是一个输出操作数,并且是只写(write only)的。
  • 你可能已经注意到,有的寄存器名字前面用了”%%”,这是用来让GCC区分操作数和寄存器的:操作数已经用了一个%作为前缀,寄存器只能用“%%”做前缀了。
  • 第三个冒号后面的clobbered register部分有个%eax,意思是内联汇编代码中会改变寄存器eax的内容,如此一来GCC在调用内联汇编前就不会依赖保存在寄存器eax中的内容了。

当这段代码执行结束后,变量”b”的值将会被改写,因为它是被指定作为输出操作数的。这里可以看出在“asm”内部对b的改动将影响到asm外了,正如之前所说的内联汇编起到桥梁作用。

下面我们将对扩展内联汇编各个部分分别进行详细的讨论。

汇编模板

汇编模板部分就是嵌入在C程序中的汇编指令,格式如下:

  • 每条指令放在一个双引号内,或者将所有的指令都放着一个双引号内。
  • 每条指令都要包含一个分隔符。合法的分隔符是换行符(\n)或者分号。用换行符的时候通常后面放一个制表符\t。对此前文已经有所说明。
  • 访问C语言变量用%0,%1…等等。

操作数

”asm”内部使用C语言字符串作为操作数。操作数都要放在双引号中。对于输出操作数,还要用“=”修饰。constraint和修饰都放在双引号内。之后是C表达式了。就像下面这样:

"constraint" (C expression)   //"=r"(result) 

对于输出操作数一定要用 “=“修饰。 constraint主要用来指定操作数的寻址类型 (内存寻址或寄存器寻址),也用来指明使用哪个寄存器。

如果有多个操作数,使用逗号隔开。

在汇编模板部分,我们按顺序用数字去引用操作数,引用规则如下:

如果总共有n个操作数(包括输入输出操作数),那么第一个输出操作引用数字为0,依次递增,然后最后一个操作数是n-1。关于操作数数量限制参见前面的章节。

输出操作数表达式必须是左值,输入操作数没有这个限制。注意这里可以使表达式,不仅仅指一个变量。当编译器不知道有这个机器指令的时候(比如新CPU指令出来的时候,编译器还没有支持该指令),扩展汇编形式就能发挥其用武之地了。如果输出表达式不能直接寻址(比如是bit-field), constraint就必须指定一个寄存器。这种情况下,GCC将使用寄存器作为asm的输出。然后保存这个寄存器的值到输出表达式中。

如前文所描述,一般输出操作数必须是只写 (write only)的;GCC将认为在这条指令之前,保存在这种操作数中的值已经过期和不再需要了。当然也支持输入输出类型或者可读可写类型的操作数。

现在我们来看一些例子:

要求把一个数字乘以5,我们可以使用汇编指令lea来实现,具体方法如下:

asm ( "leal (%1,%1,4), %0"

       : "=r" (five_times_x)

       : "r" (x)

    );

这里输入操作数是 ‘x’,因为没有指定具体要使用那个寄存器,GCC会自己选择合适的输入输出寄存器。我们也可以修改constraint部分内容,让GCC固定使用同一个寄存器,具体方法如下:

asm( "lea (%0,%0,4),%0"

       : "=r" (five_times_x)

       : "0" (x)

   );

上面例子中指定GCC始终使用在相同的寄存器来处理输入输出操作数。当然这时我们也不知道GCC具体使那个寄存器,如果需要的话我们也可以像这样指定一个:

asm ( "leal (%%ecx,%%ecx,4), %%ecx"

       : "=c" (x)

       : "c" (x) 

);

上面的三个例子中,我都没有在clobber list部分指定何寄存器。为什么?前两个例子中,因为指定GCC自己选择合适的寄存器,并且GCC知道会改写什么。第三个例子中我们也没有必要把ecx放在clobber list中是因为GCC知道x将存入其中,GCC完全知道ecx的值。所以我们也不用写在clobber list中。

Clobber List

如果某个指令改变了某个寄存器的值,我们就必须在asm中第三个冒号后的Clobber List中标示出该寄存器。为的是通知GCC,让其不再假定之前存入这些寄存器中的值依然合法。输入输出寄存器不用放Clobber List中(看上面就是个例子),因为GCC能知道asm将使用这些寄存器。(因为它们已经显式被指定输入输出标出在输入输出部分) 。其他使用到的寄存器,无论是显示还是隐式的使用,必须在clobbered list中标明。

如果指令中以无法预料的形式修改了内存值,需要在clobbered list中加上”memory”。从而使得GCC不去缓存在这些内存值。此外,如果要改变没有被列在输入和出部分的内存内容时,需要加上volatile关键字说明。clobbered list中列出的寄存器可以被多次读写。

来看一个内联汇编实现乘法的例子,这里内联汇编调用函数_foo,并且接受存在eax和ecx值作为参数:

asm( "movl %0,%%eax;

         movl %1,%%ecx;

         call _foo"

       : /*no outputs*/

       : "g" (from), "g" (to)

       : "eax", "ecx"

   );

Volatile

如果你熟悉内核代码或者一些类似优秀的代码,你一定见过很多在asm或者__asm__后的函数声明前加了volatile 或者__volatile__。前面已经讨论了asm和__asm__的用途,那volatile有什么用途呢?

如果我们要求汇编代码必须在被放置的位置执行(例如不能被循环优化而移出循环),我们就要在asm之后的“()”前,放一个volatile关键字。 这样可以禁止这些代码被移动或删除,像这样声明:

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

同样,如果担心volatile有变量冲突,可以使用__volatile__关键字。

如果汇编代码只是做一些运算而没有什么附加影响的时候最好不要使用volatile修饰。不用volatile能给GCC留下优化代码的空间。

在“常用技巧”章节中的代码示例里有更多的关于volatile的使用详情。3

constraints详解

你可能已经感到我们之前经常提到的constraint是个很重要的内容了。不过之前我们并没有过多的讨论。constraint中可以指明一个操作数是否在寄存器中,在哪个寄存器中;可以指明操作数是否是内存引用,如何寻址;可以说明操作数是否是立即数常量,和其可能是的值(或值范围)。

常用constraints

虽然constraints有很多,但常用的并不多。下面我们就来看看这些常用的constraints。

  1. 寄存器操作数constraints: r
    如果操作数指定了这个constraints,操作数将被存储在通用寄存器中。看下面的例子:
    asm ( "movl %%eax, %0" : "=r" (myval));

  2. 上面变量myval会被被保存在一个由GCC自己选择的寄存器中,eax中的值被拷贝到这个寄存器中去,并且在内存中的myval的值也会按这个寄存器值被更新。当constraints ”r” 被指定时,GCC可能会在任何一个可用的通用寄存器中保存这个值。当然,你也可以指定具体使用那个寄存器,用下表所列出的constraints:
    |r|Register(s)|
    |:-|:-|
    |a|%eax, %ax, %al|
    |b|%ebx, %bx, %bl|
    |c|%ecx, %cx, %cl|
    |d|%edx, %dx, %adl|
    |S|%esi, %si|
    |D|%edi, %di|
  3. 内存操作数constraint: m
    当操作数在内存中时,任何对其操作会直接在内存中进行。与寄存器constraint不同的是:指定寄存器constraint时,内存操作时先把值存在一个寄存器中,修改后再将该值写回到该内存中去。寄存器constraint通常只用于必要的汇编指令,或者用于能明显加快操作速度的情况,因为内存constraint能提升C语言变量更新效率,完全没必要通过一个寄存器来中转。下面这个例子中,sidt的值会被直接存储到loc所指向的内存:
    asm (“sidt” %0” : : “m”(loc) );
  4. 匹配constraint
    在某些情况下,一个变量可能被用来传递输入也用来保存输出。这种情况下我们需要用到匹配constraint。
    asm (“incl %0” :”=a”(var) : “0”(var) );

  5. 在之前章节中我们已经看过类似的例子。上面的例子中,%eax被用来传递输入也用来保存输出。输入变量先被读入eax中,incl执行之后,%eax被更新并且保存到变量var中。这里的constraint ”0”就是指定使用和第一个输出相同的寄存器,即输入变量指定放在eax中。这种constraint可以使用在如下场景:
    • 输入值从一个变量读入, 这个变量将被修改并且修改过的值要写回同一个变量;
    • 没有必要把输入和输出操作数分开。
  6. 使用匹配constraint最重要的好处是可以更高效地使用变量寄存器。

其他可能用到的constraint有:

  1. “m”: 使用一个内存操作数,内存地址可以是机器支持的范围内。
  2. “o”: 使用一个内存操作数,但是要求内存地址范围在在同一段内。例如,加上一个小的偏移量来形成一个可用的地址。
  3. “V”: 内存操作数,但是不在同一个段内。换句话说,就是使用除了”o” 以外的”m”的所有的情况。
  4. “i”: 使用一个立即整数操作数(值固定);也包含仅在编译时才能确定其值的符号常量。
  5. “n”: 一个确定值的立即数。很多系统不支持汇编常数操作数小于一个字(word)的长度的情况。这时候使用n就比使用i好。
  6. “g”: 除了通用寄存器以外的任何寄存器,内存和立即整数。

这里是一些x86特有的constraint:

  1. ”r” : Register operand constraint, look table given above.
  2. ”q” : Registers a, b, c or d.
  3. ”I” : Constant in range 0 to 31 (for 32-bit shifts).
  4. ”J” : Constant in range 0 to 63 (for 64-bit shifts).
  5. ”K” : 0xff.
  6. ”L” : 0xffff.
  7. ”M” : 0, 1, 2, or 3 (shifts for lea instruction).
  8. ”N” : Constant in range 0 to 255 (for out instruction).
  9. ”f” : Floating point register
  10. ”t” : First (top of stack) floating point register
  11. ”u” : Second floating point register
  12. ”A” : Specifies the “a” or “d” registers. This is primarily useful for 64-bit integer values intended to be returned with the “d” register holding the most significant bits and the “a” register holding the least significant bits.

constraint修饰符(Constraint Modifiers)

在使用constraint的时候,为了更精确的控制约束,GCC提供了一些修饰符,常用的修饰符有:

  1. “=” 指明这个操作数是只写的;之前保存在其中的值将被废弃而被输出值所代替。
  2. “&” 指明这个操作事数是一个会在使用之前被修改的操作数,这个操作数将在输入指令用过输入操作数之前被修改。因此,该操作数不能被放在一个被用作输入操作数的寄存器或者内存处。只有在该操作数被写入之前完成输入指令的情况下,可以被绑定在该操作数上。4

对于cosntraint的解释还远远没完。代码本身是理解内联汇编最好的老师。下一小结中我们就来看一些代码示例。通过这些示例我们能学到更多关于clobber-list和constraint使用情况。

常用代码示例

到目前为止,GCC内联汇编基础知识就已经讲完了。接下来让我们通过一些简单的例子来巩固我们所学到到知识。内联汇编函数可以很方便的用宏的形式来编写,linux内核代码中有很多这样的实例(在/usr/src/linux/asm/*.h)。

  1. 我们从一个简单的例子看起。我们来写一个把两个数字加起来的一个程序。
    int main(void)
  2. {
  3.   int foo = 10, bar = 15;
  4.   __asm__ __volatile__ ( "addl %%ebx, %%eax"
  5.                                       : ”=a”(foo)
  6.                                       : ”a”(foo), “b”(bar)
  7.                                 );
  8.  
  9.   prinft(“foo+bar=%d\n”, foo);
  10.   return 0;
  11. }

  12. 在上面代码中,我们强制让GCC将foo的值存储在%eax中,将bar的值存储在%ebx中,并且让输出值放在%eax中。其中“=”指明这是一个输出寄存器。我们再来看看另外一个把两个数相加的代码段:
    __asm__ __volatile__ (
  13.                     "    lock        \n"
  14.                     "    addl %1,%0; \n"
  15.                   : "=m"    (my_var)
  16.                   : "ir"    (my_int), "m" (my_var)
  17.                   : /* no clobber-list */
  18. );      

  19. 上面代码是一个原子加法操作。要移除该原子操作可以删除lock指令。在输出部分“=m”指出直接输出到内存my_var。类似的,”ir”是指my_int是一个整型数并且要保存到一个寄存器中(可以参考上面关于constraint的列表)。这里clobber list中没有指定任何寄存器。
  20. 我们在一些寄存器或变量上来执行一些操作来对比下它们的值。
    __asm__ __volatile__ ( "decl %0; sete %1"
  21.                     : "=m" (my_var), "=q" (cond)
  22.                     : "m" (my_var)
  23.                     : "memory"
  24.                     );

  25. 上面的程序将my_var减一并且如果减一的最终结果为零就将cond置位。我们可以在汇编语句之前加上”lock;\n\t”让其变成原子操作。
    类似的,我们可以用”incl %0”替换”decl %0”来增加my_var的值。
    这里值得注意的几点有:
    • my_var是存在内存中的变量;
    • cond存在通用寄存器中(eax,ebx,ecx,edx),因为有限制条件”=q”;
    • clobber list中指定了“memory”,说明代码将改变内存值。
  26. 如何设置和清除寄存器中的某一位?来看看下面这个技巧。
    __asm__ __volatile__( “btsl %1, %0”
  27.                     : “=m” (ADDR)
  28.                     : “Ir” (pos)
  29.                     : “cc”
  30.                     );

  31. 上面例子中变量ADDR(一个内存变量)的’pos’位置值被设置成了1。我们可以使用btrl指令来清除由btsl设置的位。pos变量的限定符constraint为”Ir”说明pos放在寄存器中,并且取值范围是0-31(I是一个x86相关constraint)。因此我们可以设置或者清除ADDR变量中从第0到第31位的值。因为这个操作涉会改变相关寄存器的内容,因此我们加上”cc”在clobberlist中。
  32. 现在我再来看一些更加复杂但是有用的函数。字符串拷贝函数:
    static inline char* strcpy (char* dest, const char* src)
  33. {
  34. int d0, d1, d2;
  35. __asm__ __volatile__(  "1:/tlodsb\n\t"
  36.                       "stosb\n\t"
  37.                       "testb %%al,%%al\n\t"
  38.                       "jne 1b"
  39.                     : "=&S" (d0), "=&D" (d1), "=&a" (d2)
  40.                     : "0" (src),"1" (dest)
  41.                     : "memory");
  42. return dest;
  43. }

  44. 上面代码的源地址存在esi寄存器中,目的地址存在EDI中。接着开始复制操作,直到遇到0结束。约束符constraint 为”&S”,”&D”,”&a”,指定了使用的寄存器为esi,edi和eax。很明显这些寄存器是clobber寄存器,因为它们的内容会在函数执行后被改变。此外我们也能看出为什么memory被放在clobber list中,因为d0, d1, d2被更新了。
    我们再来看一个类似的函数。该函数用来移动一块双字(double word)。注意这个函数是用宏来定义的。
    #define mov_blk(src, dest, numwords) \
  45. __asm__ __volatile__ (                                          \
  46.                       "cld\n\t"                                \
  47.                       "rep\n\t"                                \
  48.                       "movsl"                                  \
  49.                       :                                        \
  50.                       : "S" (src), "D" (dest), "c" (numwords)  \
  51.                       : "%ecx", "%esi", "%edi"                 \
  52.                       )

  53. 该函数没有输出,但是块移动过程导致ecx, esi, edi内容被改变,所以我们必须把它们放在clobber list中。
  54. 在Linux中,系统调用是用GCC内联汇编的形式实现的。让我们来看看一个系统调用是如何实现的。所有的系统调用都是用宏来写的 (在linux/unistd.h)。例如,一个带三个参数的系统调用的定义如下:
    #define _syscall3(type,name,type1,arg1,type2,arg2,type3,arg3) \
  55. type name(type1 arg1,type2 arg2,type3 arg3) \
  56. { \
  57. long __res; \
  58. __asm__ volatile (  "int $0x80" \
  59.                 : "=a" (__res) \
  60.                 : "0" (__NR_##name),"b" ((long)(arg1)),"c" ((long) arg2)), \
  61.                   "d" ((long)(arg3))); \
  62. __syscall_return(type,__res); \
  63. }

  64. 所有带三个参数的系统调用都会用上面这个宏来执行。这段代码中,系统调用号放在eax中,参数分别放在ebx,ecx,edx中,最后用”int 0x80”执行系统调用。返回值放在eax中。
    Linux中所有的系统调用都是用上面类似的方式实现的。比如Exit系统调用,它是带单个参数的系统调用。实现的代码如下:
    {
  65.     asm("movl $1,%%eax;         /* SYS_exit is 1 */
  66.               xorl %%ebx,%%ebx;      /* Argument is in ebx, it is 0 */
  67.               int  $0x80"            /* Enter kernel mode */
  68.         );
  69. }

  70. Exit的系统调用号是1,参数为0,所以我们把1放到eax中并且把0放到ebx中,最后通过调用int $0x80,exit(0)就被执行了。这就是exit函数的全部。

结束语

这篇文章讲述了GCC内联汇编的基础知识。一旦你理解了这些基础内容,自己再一步步的看下去就没有什么困难了。通过这些例子可以更好的帮助我们理解内联汇编的常用特性。

GCC内联汇编是一个很大的主题,这片文章的讨论还远远不够。本篇文章我们提到的大多数语法都可以在官方文档GNU Assembler中看到。完整的constraint也可以在GCC官方文档中找到。

Linux内核大范围内使用了GCC内联汇编,我们可以从中找到各种各样的例子来学习。这对我们也很有帮助。

如果你找到任何低级的排版打字错误或者过期的内容,请联系我。

参考文献

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

版权声明

本博客所有文章皆为原创文章,作者保留所有版权。转载必须包含本声明,并保持本文完整,并以超链接形式注明本文原始地址:http://www.macode.net/gcc-inline-assembly-guide/

  1. 这里信息价值不大,没有翻译。具体参加原文:https://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html#s1↩︎
  2. 原文有误,原文是这里是eax。 ↩︎
  3. 原文有误: 原文是clobber-list,这样应该是volatile ↩︎
  4. 因为译者对此处内容不了解,故翻译的不好。本段的英文原文是  “&” Means that this operand is an earlyclobber operand, which is modified before the instruction is finished using the input operands. Therefore, this operand may not lie in a register that is used as an input operand or as part of any memory address. An input operand can be tied to an earlyclobber operand if its only use as an input occurs before the early result is written.↩︎
  5. 原文链接失效 ↩︎
  6. 原文链接失效 ↩︎

GCC 内联汇编语法 阅读详情

相关推荐

DPDK gcc内联汇编

dpdk 内联汇编

fengcai_ke的博客 505

GCC-内联汇编

认识GCC内联汇编,读懂GCC内联汇编

m0_46929878的博客 1367

Spring AI Alibaba Skill 实战:定义、注册与渐进式披露的企业级落地

摘要:很多团队在接入大模型时,第一步做到的是“能对话”,第二步遇到的瓶颈却是“不能可靠执行”。Skill 的本质不是给模型多几个函数,而是把企业系统中的可控能力,以可治理、可审计、可扩展的方式交给 Agent。本文围绕 Spring AI Alibaba Skill 的定义、注册与渐进式披露,结合电商客服与售后场景,系统讲清楚其底层原理、架构设计、生产级实现、高并发治理与工程实践。@Component@Tool(description = "根据订单号查询订单状态、支付状态和物流状态")扫描包含。

一起修行,不孤单。这里有编程语言、开发工具、学习方法论和踩坑笔记。用简单的话说复杂的事,陪你从小白到进阶。周更干货,欢迎一起成长! 623

关于gcc汇编过程的一些事(一)

一、前言   众所周知,C语言中“编译”这个词其实已经被泛化了。很多时候,该词直接取代了整个从c文件到到可执行文件的流程,但为了笔者叙述方便,本人有必要在此先做一些说明。   C语言的整套流程,准确的说是预处理、编译、汇编以及链接四个过程。预处理完成头文件展开与宏替换,得到i文件;编译将i文件转换为汇编指令——s文件;汇编将s文件转换为o文件;链接将多个o文件联合得到最终的可执行文件。详细内容网上其余文章已有详细说明,此处不再赘述。   此外,由于处理器的发展,现在32、64位机C语言的int和long都是

SVRach的博客 2224

用Django框架+爬虫技术实现自动获取可画(Canva)团队会员资格的方法

可画(canva)是一个非常流行的在线平面设计平台,因为它无需专业训练和技能就可以利用其丰富的资源设计出美观、酷炫的作品,因而它受到了极其广泛的设计专业和非专业人群的喜爱本文介绍了一种方法,它采用Django网络框架与爬虫技术相结合,实时监控定时更新可画团队版免费邀请链接的网站页面,一旦链接有更新动作,第一时间获取相关信息,并直接加载这个有效的邀请链接,自动进入可画网站获得团队版使用资格。

hhyyqq的博客 2209

SIwave仿真手册——信号完整性仿真之S参数的提取(二)

上一篇简单了讲述了软件的各个操作界面,本次博客更新关于的仿真,因为第二章完整的内容比较多,所以分成好几篇博客进行分享。

luohuo9844的博客 1万+

GCC源码分析(八) — 语法/语义分析之声明与函数定义的解析

一、概述 由前可知, c_parser_declaration_or_fndef负责对声明和函数定义的解析, 对于声明的解析其整体流程是: 解析声明说明符(c_parser_declspecs) 解析出第一个声明符(c_parser_declarator) 根据声明说明符 +声明符为声明构建声明节点(XXX_DECL),并将新创建的声明绑定到当前scope(start_decl) 若下一个符号是等号(=)则解析此声明符的初值(c_parser_initializer); 不论是否有=,当前声..

ashimida 1656

为何LPDDR4支持on-die ECC,但是DDR4却不支持呢?

DRAM的ECC,目前的编码方式都是128bit data,产生8bit parity或者ECC code. 而且必须要128bit才行。这8bit的ECC code是要被写入到DRAM内存里面的,从而read的时候,可以读取这8bit ECC code来判定read data是否有错,如果是SBF,就可以纠错了,如果是double bit,大部分时候是可以detect到错误,即所谓的SEC(Single Error Correct) + DED(Double Error Detect).

一个DRAM从业者的告白 1556

gcc汇编

在Linux的源代码中,有很多C语言的函数中嵌入一段汇编语言程序段,这就是gcc提供的“asm”功能,例如在include/asm-i386/system.h中定义的,读控制寄存器CR0的一个宏read_cr0():   #define read_cr0() ({ \          unsigned int __dummy; \          __asm__( \

潘无穷 2434

GD32H7 SPI驱动实战:手把手教你用SPI3连接外部Flash(W25Q128)并实现读写

本文详细介绍了基于GD32H7系列MCU的SPI3接口驱动W25Q128外部Flash的全流程开发指南。从硬件设计、引脚配置到SPI3外设深度配置,再到底层通信函数实现和高级功能优化,提供了完整的解决方案。特别针对高速读取、DMA传输和信号完整性等关键环节进行了优化,确保系统在-40℃~85℃范围内稳定工作。

weixin_28729205的博客 381

西门子S7-200 SMART控制步进电机(二)

目录 一、开环运动控制方法 二、运动轴概述 三、配置运动控制向导 一、开环运动控制方法 S7-200 SMART CPU提供三种开环运动控制的方法: 1、脉冲宽度调制(PWM):内置于CPU中,用于速度、位置或占空比的控制 2、脉冲串输出(PTO):内置于CPU中,用于指定长度脉冲或指定距离的控制 3、运动轴:内置于CPU总,用于速度和位置的控制。本文所讲的就是通过运动轴实现步进电机的运动控制。 SR20和ST20 CP...

qq_43134642的博客 2万+

【转贴】GCC内联汇编基础

原文作者 Sandeep.S英文原文 [https://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html] 本文将介绍GCC编译环境下,在C语言代码中嵌入汇编代码的基本方法。阅读本文需要您具备80X86汇编语言和C语言的基础知识。为了使中文描述更加清楚自然,翻译过程中加入了稍许解释和意译部分。 简介 版权/反馈/勘误/...

weixin_30443813的博客 424

基于PINN的一个简单的求解偏微分方程实例

本人是做化工模拟的一名学生,最近在学习PINN(Physics-Informed Neural Networks,物理信息神经网络)对管道动力学进行建模研究,PINN由于其对机理与数据的良好兼顾性非常热门,本文是作者对PINN入门研究过程的一个练手小题目,main code也是作者代码是基于pytorch2.6.0写的,发上来供各位参考,欢迎研究管道动力学和PINN的小伙伴一起讨论~

weixin_64491760的博客 1741

linux gcc 内联汇编入门

目录 2. 概览(Overview of the whole thing.) 3、GCC汇编语法(GCC Assembler Syntax.) 3.1. 源-目标顺序(Source-Destination Ordering) 3.2. 寄存次命名(Registry Naming) 3.3. 立即操作数(Immediate Operands) 3.4. 操作数大小(Operand Siz...

whatday的专栏 866

YOLOv8改进,YOLOv8主干网络替换为ShuffleNetV2

ShuffleNet V2的设计原则:1.减少内存访问成本:通过优化数据传输路径,减少内存访问延迟,提高整体运算速度。2.考虑平台特性:针对不同硬件平台的特性进行优化设计,确保在目标平台上的高效运行。3.结构简洁高效:采用简洁而高效的模块设计,降低计算复杂度,同时保持或提升模型的预测准确性。下图,以四种不同计算复杂度水平下的准确性(在ImageNet验证集上的分类),速度和FLOPs的测量结果。(a, c) 为GPU结果,批量大小为8。(b, d) 为ARM结果,批量大小为1。

在职AI算法工程师,擅长计算机视觉,YOLO目标检测、分割等,擅长web、pyqt界面可视化,好内容持续更新中,来这里跟大家一起学习,共同进步 1567

廖雪峰Python教程[完整版]

廖雪峰的python教程整理的,已经排好版了,而且还是完整的,适合小白

GCC内联汇编教程

 原文链接:https://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html ,此文为我的中文翻译,转载请标明出处! GCC-Inline-Assembly-HOWTO Sandeep.S   本HOWTO解释了GCC提供的内联汇编功能的使用方法。阅读本文只有两个先决条件,是x86汇编语言和C的基本知识。   1. 简介 ...

lwx62的专栏 1万+

LinePhone3.2.7(android端最新版本) sip客户端

SIP客户端For android:LinePhone3.2.7(最新版本),官网地址:http://www.linphone.org/

GCC 内联汇编

LINUX 下GCC 汇编

海鲨数据库架构师 1304

RU DOS版、EFI版32bit/64bit BIOS常用工具

DOS版、EFI版的RU工具,可用来查看IO、MEM、PCI、SMBIOS等资源。亲测在EFI SHELL下可用。

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

riscv下的GCC内联汇编

GCC内联汇编 文章目录GCC内联汇编语法规则输入操作数与输出操作数可能影响的寄存器或存储器Example汇编中调用C/C++函数 语法规则 asm volatile( 汇编指令列表 ∶输出操作数 //非必需 ∶输入操作数 //非必需 ∶可能影响的寄存器或存储器 1/非必需 ); Explanation asm 是GCC的关键字,表示内联汇编操作,注 __asm__ 是GCC中asm的宏定义,也可以使用 volatile 或者 __volatile__ 是可选的,表示不进行任何优化,否则某些

m0_52050582的博客 6163

GCC内联汇编

用内联汇编的主要目的是为了提高效率:假设有一个比较文本差异的程序 diff,它花了 99% 的时间在 strcmp 这个函数上,如果用内联汇编实现的一个高效的 strcmp 比用 C 语言实现的快 1 倍,那么专家花在这个小小函数上的心思就能够将整个程序的效率提高差不多 1 倍,这是很值得去做的"斤斤计较"。还有一个目的就是为了实现 C 语言无法实现的部分,比如说 IO 操作,还有我们上一篇中提到的自主修改 esp 寄存器也是必须用汇编才能实现的。学 gcc 内联汇编最好的导师莫过于 linux 内核,有很

koozxcv的博客 905

gcc 内联汇编

gcc 内联汇编 文章目录gcc 内联汇编前言一、GCC asm 声明小例子二、优化C代码三、MORE内嵌汇编作为预处理宏C 桩函数替换C变量的符号名替换C函数的符号名强制使用特定的寄存器临时使用寄存器寄存器的用途Common pitfalls 前言 gcc 内联汇编介绍,转载 https://blog.youkuaiyun.com/lhf_tiger/article/details/32343851 对于基于ARM的RISC处理器,GNU C编译器提供了在C代码中内嵌汇编的功能。这种非常酷的特性提供了C代码没有的

yanzhongqian的博客 961

译:GCC内联汇编入门

原文: GCC-Inline-Assembly-HOWTO 1. 简介(Introduction.) 1.1 Copyright and License. Copyright (C)2017 桂糊涂 Copyright (C)2003 Sandeep S. This document is free; you can redistribute and/or modify this unde...

phenixyf的专栏 919

gcc内联汇编示例

在某些特殊情况下,通过内联汇编,可以提高程序效率,且更加灵活。

m0_63892652的博客 521

arm下的gcc内联汇编

arm下的gcc内联汇编 __asm__ __volatile__内嵌汇编用法简述 在阅读C/C++原码时经常会遇到内联汇编的情况,下面简要介绍下__asm__ __volatile__内嵌汇编用法。 带有C/C++表达式的内联汇编格式为: __asm__ __volatile__("Instruction List" : Output : Input : Clobber/Modif

格物致知 1865
上一篇: vim的一些设置。
下一篇: 工作以后十不要
fancylea
博客等级 码龄20年 52粉丝 43原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值