GCC内联汇编教程

 原文链接:https://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html ,此文为我的中文翻译,转载请标明出处!


GCC-Inline-Assembly-HOWTO

Sandeep.S

 


本HOWTO解释了GCC提供的内联汇编功能的使用方法。阅读本文只有两个先决条件,是x86汇编语言和C的基本知识。


 

1. 简介

2. 概述

3. GCC汇编语法

4. 基本内联

5. 扩展的Asm

6. 更多关于约束

7. 一些有用的方法

8. 结束语

9. 参考文献


1. 简介

1.1版权和许可

版权所有(C)2003 Sandeep S.

这份文件是免费的; 您可以根据自由软件基金会发布的GNU通用公共许可证条款重新分发和/或修改此内容; 许可证的第2版,或(根据您的选择)任何更高版本。

本文件的分发是希望它有用,但没有任何担保; 甚至没有适销性或特定用途适用性的暗示保证。有关更多详细信息,请参阅GNU通用公共许可证。

 

1.2反馈和更正

请向Sandeep.S提出反馈和批评 。我将感谢任何指出本文件中的错误和不准确之处的人; 我得到通知后,我会尽快纠正他们。

 

1.3致谢

我衷心感谢GNU人员提供了这样一个伟大的功能。感谢Mr.Pramode CE所做的一切帮助。感谢Govt Engineering College的朋友,Trichur的道义支持与合作,特别是对Nisha Kurur和Sakeeb S.感谢我在Govt Engineering College的亲爱的老师,Trichur的合作。

另外,感谢Phillip,Brennan Underwood和colin@nyx.net; 这里的许多东西都是从他们的作品中无耻地偷走的。

 


2. 概述

我们在这里了解GCC内联汇编。这个内联代表什么?

我们可以指示编译器将函数的代码插入其调用者的代码中,直到实际调用的位置。这些函数是内联函数。听起来像宏?确实有相似之处。

内联函数有什么好处?

这种内联方法减少了函数调用开销。如果任何实际参数值是常量,则它们的已知值可能允许在编译时进行简化,因此不需要包含所有内联函数的代码。对代码大小的影响不太可预测,这取决于具体情况。要声明内联函数,我们必须inline在其声明中使用关键字 。

现在我们可以猜猜什么是内联汇编。它只是一些编写为内联函数的汇编程序。它们在系统编程中非常方便,快速且非常有用。我们的主要重点是研究(GCC)内联汇编函数的基本格式和用法。要声明内联汇编函数,我们使用关键字asm

内联汇编很重要,主要是因为它能够操作并使其输出在C变量上可见。由于这种能力,“asm”作为汇编指令和包含它的“C”程序之间的接口。

 


3. GCC汇编语法

GCC是用于Linux的GNU C编译器,它使用AT&T / UNIX 汇编语法。这里我们将使用AT&T语法进行汇编编码。如果您不熟悉AT&T语法,请不要担心,我会教您。这与Intel语法完全不同。我将给出重大分歧。

  1. Source-Destination顺序。

    AT&T语法中操作数的方向与英特尔的方向相反。在Intel语法中,第一个操作数是目标,第二个操作数是源,而在AT&T语法中,第一个操作数是源,第二个操作数是目标。即

    Intel语法中的“Op-code dst src”更改为

    AT&T语法中的“Op-code src dst”。

  2. 注册命名。

    寄存器名称以%为前缀,即,如果要使用eax,则写入%eax。

  3. 即时操作数。

    AT&T的即时操作数前面是'$'。对于静态“C”变量,前缀为'$'。在Intel语法中,对于十六进制常量,'h'是后缀,这里我们将'0x'加到常量前面。因此,对于十六进制,我们首先看到'$',然后是'0x',最后是常量。

  4. 操作数大小。

    在AT&T语法中,存储器操作数的大小由操作码名称的最后一个字符确定。“b”,“w”和“l”的操作码后缀指定字节(8位),字(16位)和长(32位)存储器引用。Intel语法通过在内存操作数(不是操作码)前加上'byte ptr','word ptr'和'dword ptr'来实现这一点。

    因此,英特尔“mov al,byte ptr foo”在AT&T语法中是“movb foo,%al”。

  5. 内存操作数。

    在Intel语法中,基址寄存器包含在'['和']'中,而在AT&T中它们更改为'('和')'。此外,在Intel语法中,间接内存引用就像

    section:[base + index * scale + disp],改为

    section:AT&T中的disp(基数,指数,比例)。

    需要记住的一点是,当一个常量用于disp / scale时,'$'不应该是前缀。

 

现在我们看到了英特尔语法和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("assembly code");

例如。


asm("movl %ecx %eax"); /* moves the contents of ecx to eax */
__asm__("movb %bh (%eax)"); /*moves the byte from bh to the memory pointed by eax */

您可能已经注意到我在这里使用过asm__asm__。两者都有效。如果关键字asm与我们程序中的某些内容冲突,我们可以使用__asm__。如果我们有多个指令,我们用双引号每行写一个,并在指令后面加上'\ n'和'\ t'。这是因为gcc将每个指令作为一个字符串发送至汇编器。

例如。


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

如果在我们的代码中,我们触碰(即,更改内容)一些寄存器并从asm返回而不修复这些更改,则会发生一些不好的事情。这是因为GCC不知道寄存器内容的变化,这导致我们遇到麻烦,特别是当编译器进行一些优化时。它会假设某些寄存器包含某些变量的值,我们可能在没有通知GCC的情况下对其进行了更改,并且它仍然没有发生任何事情。我们可以做的是使用那些没有副作用的指令或在我们退出或等待某些事情崩溃时解决问题。这是我们想要一些扩展功能的地方。扩展的asm为我们提供了该功能。

 


5. 扩展的Asm

在基本的内联汇编中,我们只有指令。在扩展汇编中,我们还可以指定操作数。它允许我们指定输入寄存器,输出寄存器和破坏寄存器列表。指定要使用的寄存器并不是强制性的,我们可以将这个问题留给GCC,这可能更适合GCC的优化方案。无论如何,基本格式是:


       asm ( assembler template 
           : output operands                  /* optional */
           : input operands                   /* optional */
           : list of clobbered registers      /* optional */
           );

汇编程序模板由汇编指令组成。每个操作数由操作数约束字符串描述,后跟括号中的C表达式。冒号将汇编程序模板与第一个输出操作数分隔开,另一个冒号将最后一个输出操作数与第一个输入分开,如果有的话。逗号分隔每个组中的操作数。操作数的总数限制为10或机器描述中任何指令模式中的最大操作数数量,以较大者为准。

如果没有输出操作数但是有输入操作数,则必须在输出操作数所在的位置周围放置两个连续的冒号。

例如:


        asm ("cld\n\t"
             "rep\n\t"
             "stosl"
             : /* no output registers */
             : "c" (count), "a" (fill_value), "D" (dest)
             : "%ecx", "%edi" 
             );

现在,这段代码做了什么?上面的内联将fill_value的count倍填充到edi寄存器指向的位置。它还告诉gcc的是,寄存器的内容eaxedi不再有效。让我们再看一个例子,让事情更清晰。


        
        int a=10, b;
        asm ("movl %1, %%eax; 
              movl %%eax, %0;"
             :"=r"(b)        /* output */
             :"r"(a)         /* input */
             :"%eax"         /* clobbered register */
             );       

这里我们做的是使用汇编指令使'b'的值等于'a'的值。一些兴趣点是:

  • “b”是输出操作数,由%0引用,“a”是输入操作数,由%1引用。
  • “r”是对操作数的约束。我们稍后会详细介绍约束。目前,“r”向GCC表示使用任何寄存器来存储操作数。输出操作数约束应该有一个约束修饰符“=”。而这个修饰符表示它是输出操作数并且是只写的。
  • 寄存器名称前面有两个%的前缀。这有助于GCC区分操作数和寄存器。操作数具有单个%作为前缀。
  • 第三个冒号后的修改后的寄存器%eax告诉GCC%eax的值将在“asm”内修改,因此GCC不会使用该寄存器来存储任何其他值。

当“asm”的执行完成时,“b”将反映更新的值,因为它被指定为输出操作数。换句话说,“asm”中对“b”的改变应该反映在“asm”之外。

现在我们可以详细查看每个字段。

 

5.1汇编模板

汇编程序模板包含插入C程序内的汇编指令集。格式如下:要么每条指令都用双引号括起来,要么整个指令组都在双引号内。每条指令也应以分隔符结束。有效分隔符是换行符(\ n)和分号(;)。'\ n'后面可能跟一个标签(\ t)。对应于C表达式的操作数由%0,%1 ......等表示。

 

5.2操作数

C表达式用作“asm”中的汇编指令的操作数。每个操作数都被写为双引号中的第一个操作数约束。对于输出操作数,在引号内也会有一个约束修饰符,然后是C表达式,它代表操作数。即

“约束”(C表达式)是一般形式。对于输出操作数,将有一个额外的修饰符。约束主要用于决定操作数的寻址模式。它们还用于指定要使用的寄存器。

如果我们使用多个操作数,则用逗号分隔。

在汇编程序模板中,每个操作数都由数字引用。编号如下进行。如果总共有n个操作数(包括输入和输出),则第一个输出操作数编号为0,按递增顺序继续,最后一个输入操作数编号为n-1。最大操作数是我们在上一节中看到的。

输出操作数表达式必须是左值。输入操作数不受此限制。他们可能是表达式。扩展的asm功能最常用于编译器本身不知道存在的机器指令;-)。如果无法直接寻址输出表达式(例如,它是位字段),则我们的约束必须允许寄存器。在这种情况下,GCC将使用寄存器作为asm的输出,然后将该寄存器内容存储到输出中。

如上所述,普通输出操作数必须是只写的; GCC将假设在指令之前这些操作数中的值已经死亡且无需生成。扩展的asm还支持输入输出或读写操作数。

所以现在我们专注于一些例子。我们想要将数字乘以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) 
             );

在上面的三个例子中,我们没有将任何寄存器放入clobber列表。为什么?在前两个例子中,GCC决定寄存器,它知道发生了什么变化。在最后一个,我们没有必要把ecx加入clobber-list,gcc知道它进入了x。因此,因为它可以知道ecx的值,所以它不被认为是破坏的。

 

5.3 Clobber列表

一些指令破坏了一些硬件寄存器。我们必须在clobber-list中列出这些寄存器,即asm函数中第三个'  ' 之后的字段。这是为了告知gcc我们将自己使用和修改它们。所以gcc不会假设它加载到这些寄存器中的值是有效的。我们不应该在此列表中列出输入和输出寄存器。因为,gcc知道“asm”使用它们(因为它们被明确指定为约束)。如果指令隐式或显式地使用任何其他寄存器(并且输入或输出约束列表中不存在寄存器),则必须在破坏列表中指定这些寄存器。

如果我们的指令可以改变条件代码寄存器,我们必须将“cc”添加到破坏寄存器列表中。

如果我们的指令以不可预测的方式修改内存,请将“memory”添加到修饰寄存器列表中。这将导致GCC不在汇编器指令的寄存器中保持缓存的内存值。如果受影响的内存未在asm的输入或输出中列出,我们还必须添加volatile关键字。

我们可以根据需要多次读取和编写被破坏的寄存器。考虑模板中多个指令的示例; 它假定子程序_foo接收在寄存器eaxecx参数的参数。


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

 

5.4Volatile......?

如果您熟悉内核源代码或类似的一些漂亮的代码,您必须已经看到许多函数声明为volatile或 __volatile__。我之前提到过关键字asm__asm__。那这 volatile是什么?

如果我们的汇编语句必须在我们放置的地方执行,(即不能作为优化移出循环),请将关键字volatile放在asm之后和()之前。我们将其声明为

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

使用__volatile__的时候,我们必须非常小心。

如果我们的程序集只是用于进行一些计算并且没有任何副作用,那么最好不要使用关键字volatile。避免gcc无法优化代码。

在 一些有用的方法 中,我提供了许多内联asm函数的示例。在那里我们可以看到详细的clobber列表。

 


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        |
    +---+--------------------+
    

     

  2. 内存操作数约束(m)

    当操作数在存储器中时,对它们执行的任何操作将直接发生在存储器位置,而不是寄存器约束,寄存器约束首先将值存储在要修改的寄存器中,然后将其写回存储器位置。但是寄存器约束通常仅在它们对于指令绝对必要时才使用,或者它们显著加速了该过程。在需要在“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,1,2或3(lea指令的移位)。
  8. “N”:范围为0到255的常量(用于输出指令)。
  9. “f”:浮点寄存器
  10. “t”:第一个(堆栈顶部)浮点寄存器
  11. “u”:第二个浮点寄存器
  12. “A”:指定“a”或“d”寄存器。这对于64位整数值非常有用,这些值旨在通过保存最高有效位的“d”寄存器和保存最低有效位的“a”寄存器返回。

 

6.2约束修饰符

在使用约束时,为了更精确地控制约束的影响,GCC为我们提供了约束修饰符。最常用的约束修饰符是

  1. “=”:表示该操作数对该指令是只写的; 先前的值被丢弃并由输出数据替换。
  2. “&”:表示此操作数是一个earlyclobber操作数,在使用输入操作数完成指令之前修改该操作数。因此,该操作数可能不在于用作输入操作数的寄存器或任何存储器地址的一部分。如果输入操作数仅用作输入,则在写入早期结果之前,它可以绑定到earlyclobber操作数。

    约束的列表和解释绝不是完整的。示例可以更好地理解内联asm的使用和使用。在下一节中,我们将看到一些示例,我们将在其中找到有关clobber-lists和约束的更多信息。

     


7. 一些有用的方法。

现在我们已经介绍了关于GCC内联汇编的基本理论,现在我们将集中讨论一些简单的例子。将内联asm函数编写为MACRO总是很方便。我们可以在内核代码中看到许多asm函数。(/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在%eax中存储foo,bar存储在%ebx中,我们也希望结果保存在%eax中。'='符号表示它是输出寄存器。现在我们可以用其他方式实现变量和整数相加。


     __asm__ __volatile__(
                          "   lock       ;\n"
                          "   addl %1,%0 ;\n"
                          : "=m"  (my_var)
                          : "ir"  (my_int), "m" (my_var)
                          :                                 /* no clobber-list */
                          );
    

    这是一个原子加法。我们可以删除指令'lock'来删除原子性。在输出字段中,“= m”表示my_var是输出,它在内存中。类似地,“ir”表示,my_int是一个整数,应该驻留在某个寄存器中(回想一下我们上面看到的表)。clobber列表中没有寄存器。

     

  2. 现在我们将对一些寄存器/变量执行一些操作并比较该值。


     __asm__ __volatile__(  "decl %0; sete %1"
                          : "=m" (my_var), "=q" (cond)
                          : "m" (my_var) 
                          : "memory"
                          );
    

    这里,my_var的值减1,如果结果值是0 ,则设置变量cond。我们可以通过添加指令“lock; \ n \ t”作为汇编程序模板中的第一条指令来添加原子性。

    以类似的方式,我们可以使用“incl%0”而不是“decl%0”,以便增加my_var。

    这里要注意的是(i)my_var是驻留在内存中的变量。(ii)约束“= q”保证了cond在eax,ebx,ecx和edx寄存器其中之一中。(iii)我们可以看到内存在clobber列表中。即,代码正在改变内存的内容。

     

  3. 如何设置/清除寄存器中的位?作为下一个方法,我们将会看到它。


    __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”添加到clobberlist。

     

  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是early clobber寄存器,即它们的内容将在函数完成之前改变。这里也很清楚为什么memory在clobberlist中。

     

    我们可以看到一个类似的函数移动一个double words。请注意,该函数声明为宏。


    #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的内容发生的变化是块移动的副作用。所以我们必须将它们添加到clobber列表中。

     

  5. 在Linux中,使用GCC内联汇编实现系统调用。让我们看看如何实现系统调用。所有系统调用都写成宏(linux / unistd.h)。例如,具有三个参数的系统调用被定义为宏,如下所示。


    #define _syscall3(type,name,type1,arg1,type2,arg2,type3,arg3) \
    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收集返回值。

     

    每个系统调用都以类似的方式实现。退出是一个单个参数系统调用,让我们看看它的代码是什么样的。它如下所示。


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

    退出的数量是“1”,这里,它的参数是0。所以我们安排eax包含1和ebx包含0和by int $0x80exit(0)执行。这就是退出的方式。

 

 


8. 结束语

本文档介绍了GCC内联汇编的基础知识。一旦理解了基本概念,就不难采取自己的步骤。我们看到了一些有助于理解GCC内联汇编常用功能的示例。

GCC内联是一个广泛的主题,本文绝不是完整的。关于我们讨论的语法的更多细节可以在GNU Assembler的官方文档中找到。同样,有关约束的完整列表,请参阅GCC的官方文档。

当然,Linux内核大规模使用GCC Inline。所以我们可以在内核源代码中找到各种各样的例子。他们可以帮助我们很多。

如果您在本文档中发现任何明显错别字或过时信息,请告知我们。

 


9. 参考文献

  1. Brennan的内联汇编指南
  2. 在Linux中使用汇编语言
  3. 使用as,GNU汇编程序
  4. 使用和移植GNU编译器集合(GCC)
  5. Linux内核源码

 


  • 21
    点赞
  • 72
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
GCC内联汇编asm格式是一种将汇编代码嵌入到C或C++源代码中的方法。它允许开发人员直接使用汇编语言来访问底层硬件或执行高性能算法。以下是GCC内联汇编asm格式的详细说明。 基本格式 GCC内联汇编asm格式基本格式如下: ```c asm (assembly code : output operands : input operands : clobbered registers); ``` - assembly code:汇编代码,可以是单行或多行代码。 - output operands:用于存储计算结果的变量,可以有多个,用逗号分隔。输出操作数是可选的,可以省略。 - input operands:用于传递参数的变量,可以有多个,用逗号分隔。输入操作数是必需的。 - clobbered registers:代码执行期间会被修改的寄存器,用于通知编译器。可以有多个,用逗号分隔。clobbered registers是可选的,可以省略。 示例 以下是一个简单的GCC内联汇编asm格式示例,将eax寄存器中的值加1,并将结果存储在eax中。 ```c int value = 10; asm ("addl $1, %%eax" : "=a" (value) : "a" (value)); ``` - "addl $1, %%eax":汇编代码,将eax加1。 - "=a" (value):输出操作数,将eax中的值存储在value变量中。 - "a" (value):输入操作数,将value的值传递给eax。 - 没有clobbered registers。 输出操作数 输出操作数用于将汇编代码的结果存储在变量中。输出操作数有两种类型:普通输出(通道约束)和跨约束输出。 普通输出 普通输出使用“=约束”语法表示,其中约束指定了输出操作数应存储在哪个寄存器或内存位置中。约束可以是以下之一: - "=r"(任意寄存器) - "=m"(任意内存位置) - "=a"(eax寄存器) - "=d"(edx寄存器) - "=q"(eax或edx寄存器) 示例 以下是一个使用普通输出的示例,将eax寄存器中的值加1,并将结果存储在value变量中。 ```c int value; asm ("addl $1, %%eax" : "=a" (value) : "a" (value)); ``` 跨约束输出 跨约束输出是一种将结果存储在多个输出变量中的方法。它使用“+约束”语法表示,其中约束指定了输出操作数应存储在哪个寄存器或内存位置中。多个约束可以用逗号分隔。 示例 以下是一个使用跨约束输出的示例,将eax寄存器中的值加1,并将结果存储在value1和value2变量中。 ```c int value1, value2; asm ("addl $1, %%eax" : "+a" (value1), "=r" (value2)); ``` 输入操作数 输入操作数用于将变量的值传递给汇编代码。输入操作数使用“约束”语法表示,其中约束指定了变量应该存储在哪个寄存器或内存位置中。约束可以是以下之一: - "r"(任意寄存器) - "m"(任意内存位置) - "a"(eax寄存器) - "d"(edx寄存器) - "q"(eax或edx寄存器) 示例 以下是一个使用输入操作数的示例,将value变量的值传递给eax寄存器中。 ```c int value = 10; asm ("movl %0, %%eax" : : "r" (value)); ``` clobbered registers clobbered registers是在汇编代码执行期间会被修改的寄存器列表。它用于通知编译器哪些寄存器应该被保存和恢复。clobbered registers使用“%约束”语法表示,其中约束指定了被修改的寄存器名称。多个寄存器可以用逗号分隔。 示例 以下是一个使用clobbered registers的示例,将eax寄存器中的值加1,并告诉编译器edx寄存器也被修改了。 ```c asm ("addl $1, %%eax" : : "a" (value) : "%edx"); ``` 总结 GCC内联汇编asm格式是一种将汇编代码嵌入到C或C++源代码中的方法。它允许开发人员直接使用汇编语言来访问底层硬件或执行高性能算法。通过输出操作数、输入操作数和clobbered registers,开发人员可以管理汇编代码与C或C++代码之间的数据流和寄存器使用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值