Linux内联汇编

什么是内联?

指定编译器将函数的代码插入到调用者的代码张,直到实际调用的位置,这些函数是内联函数。与宏有一些相似之处,它的好处是可以减少函数调用的开销,如果任何实际参数是常量,则他们的已知值可能允许在编译时进行简化,因此不需要包含所有内联函数的代码

内联汇编其实就是编写为内联函数的汇编程序,为什么使用它呢,因为它能够操作并使其输出在C变量的上可见,,调用asm关键字,声明其为内联函数

 在Linux中汇编的语法

在Linux中GCC使用AT & T语法进行汇编编码

1.source-Destination顺序 :第一个操作数是园,第二个操作数是目标

2.注册命名:寄存器名称以%为浅醉,即:如果要使用eax寄存器,则写入:%eax

3.即时操作数:AT & T 语法中,即时操作数前面是$,对于静态“C”变量,前缀为’$‘,对于16     进制,我们首先看到的是 '$',然后是’0x‘,最后是常量。

4.操作数大小:在AT & T 语法中,存储器操作数的大小由操作数名称到最后一个字符确定。     “b”,"w"和"l"的操作数后缀指定字节(8位),字(16位),长(32位)存储器引用

5.内存操作数:在AT &T  语法中,基址寄存器包含在’(‘和’)‘中

基本内联

 内联汇编的基本格式是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 */

如果由多个指令,则在指令后边机上\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

在基本的内联汇编中,我们只有指令,在拓展汇编中,我们可以指定操作数,,它允许我们指定输入寄存器,输出寄存器,破坏寄存器列表,指定要使用的寄存器并不是强制性的,

基本格式为:

       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的是。寄存器的内容eax和edi不再有效

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

这里使用汇编指令使‘b’的值等于‘a’的值

1.‘b’是输出操作数,有%0引用,‘a’是输入操作数,由%1引用

2.“r”是堆操作数的约束,“r”项GCC表示使用任何寄存器来存储操作数。输出操作数约束应该      有一个约束修饰符“=”。而这个修饰符表示它是输出操作数并且是只写的。

3.寄存器名称前面有两个%的前缀,着有助于GCC区分操作数和寄存器,操作数具有单个%     作为前缀

4.第三个冒号后的修改的寄存器%eax告诉GCC%eax的值将在“asm”中修改,因此GCC不会     使用该寄存器来存储任何其他值

当”asm“执行完成后,“b”将反映更新的值,

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

因此它被指定为输出操作数

汇编模板 

汇编程序模板包含插入C程序的汇编指令集,格式如下:

1.要么每条指令都用双引号括起来,妖媚整个指令组都在双引号内

2.每条指令应以分隔符结束。有效分隔符为换行符(\n)和分号(;).‘\n’后边可能更一个标签(\t)对于C表达式的操作数由%0,%1........表示

操作数 

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

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

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

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

这是一个将数字乘以5的内联汇编

        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的值,所以它不被认为是破坏的.

Volatile

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

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

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

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

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

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

常用约束

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”寄存器返回。

约束修饰符

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

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

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

 一些有用的方法

        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)执行。这就是退出的方式。

  • 13
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值