GNU 内联汇编

2021/11/14 22:13:04

本文主要是介绍GNU 内联汇编,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

1. asm格式

GNU的C编译器使用asm关键字:asm段格式如下:
asm ("assembly code");
一些汇编器使用制表符字符缩进指令以便区分和标签。GNU编译器不需要这样做,单为保持一致使用这样方式。
asm("mov $1, %eax\n\tmov $0, %ebx\n\tint $0x80");
这样格式有些混乱,下面这样方式书写:

asm("mov $1, %eax\n\t"
    "mov $0, %ebx\n\t"
    "int $0x80");

将asm放入到代码中例如:

#include <stdio.h>
int main()
{
    int a = 10;
    int b = 20, result;
    result = a * b;
    asm ("nop");
    printf("The result is %d\n", result);
    return 0;
}

C源代码中生成的汇编代码,其中#APP#NO_APP符号表示是asm生成的段落 内容。

1.1 使用全局变量

基本内联汇编代码,可以使用程序定义的全局C变量

#include <stdio.h>
int a = 10;
int b = 20;
int result
int main()
{
    asm ("pusha\n\t"
         "movl a, %eax\n\t"
         "movl b, %ebx\n\t"
         "imull %ebx, %eax\n\t"
         "movl %eax, result\n\t"
         "popa");
   printf("the answer is %d\n", result);
   return 0;
}

注意:数据变量必须被声明为全局的,不能在asm段中使用局部变量。

1.2 使用volatile修饰符

使用volatile修饰符的添加不希望优化这个代码段。
asm volatile("assembly code");

1.3 使用替换的关键字

ANSI C规范把关键字asm用于其他用途,不能用于内联汇编。ANSI C想使用内联汇编,则使用__asm__关键字替代asm
__asm__("assembly code");

2.扩展asm

基本asm有其局限性:

  • 1.输入和输出都必须是全局变量
  • 2.内联汇编不能改变任何寄存器的值。

GNU汇编器提供asm段的扩展格式来帮助解决这些问题。asm扩展版的格式如下:

asm("assembly code"
  :ouput locations
  :input operands
  :changed registers);

有4个部分组成,使用冒号组成:

  • 汇编代码:使用基本asm格式相同的语法内联汇编代码
  • 输出位置:包含内联汇编输出值的寄存器和内存位置的列表
  • 输入操作数:包含内联汇编代码的输入值的寄存器和内存位置的列表
  • 改动的寄存器:内联代码改变的任何其他寄存器的列表

其中,输入和输出可以为空,只有最后一个冒号可以省略。

2.1 指定输入和输出值

扩展格式,可以从寄存器内存位置输入值和输出值赋值。输入值和输出值列表的格式是:
"constraint" (variable)
其中:

  • variable是程序中C变量。注意:扩展asm格式中,局部和全局变量都可以使用。
  • constraint定义把变量存放到哪里(输入来说)或从哪里传送变量(输出来说)。
    • 使用它定义变量存放在寄存器中还是在内存位置中。
      约束constraint是单一字符的代码,约束代码中:
约束 描述
a 使用%eax,%ax或者%al寄存器
b 使用%ebx,%bx或者%bl寄存器
c 使用%ecx,%cx或者%cl寄存器
d 使用%edx,%dx或者%dl寄存器
S 使用%esi或者%si寄存器
D 使用%edi或者%di寄存器
r 使用任何通用的寄存器
q 使用%eax,%ebx,%ecx,或者%edx寄存器之一
A 对于64位值使用%eax和%ebx寄存器
f 使用浮点寄存器
t 使用第一个(顶部的)浮点寄存器
u 使用第二个浮点寄存器
m 使用变量的内存位置
o 使用偏移内存位置
V 只是用直接内存位置
i 使用立即整数值
n 使用值已知的立即整数值
g 使用任何可用的寄存器或者内存位置

除了约束之外,输出值包含约束修饰符,指示编译器如何处理输出值,可以使用如下来修饰输出值:

输出修饰符 描述
+ 可以读取和写入操作数
= 只能写入操作数
% 如果必要,操作数可以和下一个操作数切换
& 在内联函数完成之前,可以删除或者重新使用操作数

例如:asm("assembly code" : "=a"(result) :"d"(data1), "c"(data2));

  • 把C变量data1放入到EDX寄存器中
  • 把data2存放到ECX寄存器中
  • 内联汇编的结果放入到EAX中,然后传递给变量result

注意:输入是C变量给寄存器,而输出是寄存器给C变量。

2.2 使用寄存器

如果输入和输出都给寄存器,几乎和平常一样使用寄存器,需注意的是扩展asm中,在汇编代码中引用寄存器,必须使用两个百分号符号

#include <stdio.h>
int main()
{
    int data1 = 10;
    int data2 = 20;
    int result;
    // edx为data1,ecx为data2
    // eax赋值给result
    asm ("imull %%edx, %%ecx\n\t"
         "movl %%ecx, %%eax"
         : "=a"(result)
         : "d"(data1), "c"(data2));
    printf("The result is %d\n", result);
    return 0;
}

MOVS指令输入值包含输出位置,volatile很重要,编译器因为没有输出值,认为这个asm没有必要而删除asm段:

#include <stdio.h>
int main()
{
    char input[30] = {"This is a test message.\n"};
    char output[30];
    int length = 25;
    asm volatile (
      "cld\n\t"
      "rep movsb"
      :
      :"S"(input), "D"(output), "c"(length)  
    );
    printf("%s", output)
    return 0;
}

2.3 使用占位符

很多输入和输出的情况,可以使用占位符(placeholder)。占位符是前面加上百分号的数字。按照内联汇编代码中列出的每个输入值和输出值在列表中的位置。每个值被赋予一个从开始的数字。然后可以在汇编代码中使用占位符表示值。

asm("assembly code"
    :"=r"(result)
    :"r"(data1), "r"(data2));

其中:

  • %0将表示包含变量值result的寄存器
  • %1将表示包含变量值data1的寄存器
  • %2将表示包含变量值data2的寄存器

注意:占位符提供在内联汇编代码中利用寄存器和内存位置的方法。汇编代码中使用占位符只作为原始的数据:

imull %1, %2
movl %2, %0

示例如下:

#include <stdio.h>
int main()
{
    int data1 = 10;
    int data2 = 20;
    int result;
    asm ("imull %1, %2\n\t"
         "movl %2, %0"
         :"=r"(result)
         :"r"(data1),"r"(data2));
   printf("The result is %d\n", result)
   return 0;
}

gdb调试:info reg
整个asm被看作为一条语句,使用stepi来调试asm汇编

2.4 引用占位符

如果内联汇编代码中的输入值和输出值共享程序中相同的C变量,可以指定使用占位符作为约束值。

修改上面个汇编代码如下:

asm("imull %1, %0"
    :"=r"(data2)
    :"r"(data1),"0"(data2));

2.5 替换占位符

如果很多占位符,就会混乱。从GNU3.1开始允许声明替换名称作为占位符。格式如下:%[name]"constraint"(variable)

定义的值name成为内联汇编代码中的变量新的占位符标识符,下面例子:

asm("imull %[value1], %[value2]"
    :[value2]"=r"(data2)
    :[value1]"r"(data1),"0"(data2));

使用占位替换占位符名称的方式和使用普通的占位符相同。

2.6 改动的寄存器列表

改动寄存器列表:如果改动了不在输入和输出列表中的寄存器,要进行申明这些寄存器。

#include <stdio.h>
int main()
{
    int data1 = 10;
    int result;
    asm ("imull %1, %2\n\t"
         "movl %2, %0"
         :"=r"(result)
         :"r"(data1),"0"(result)
        :"%eax");
   printf("The result is %d\n", result)
   return 0;
}

让编译器正确避免使用eax寄存器,因为在内联汇编代码中声明了要使用它。

在改动寄存器列表中使用memory,通知编译器这个内存位置在内联汇编代码中被改动。

2.7 使用内存位置

虽然内联汇编代码使用寄存器比较快,但也可以使用C变量的内存位置。约束m用于引用输入和输出值中的内存位置。要求使用寄存器的汇编指令,仍然必须使用寄存器,所以不许不得不定义保存数据的中间寄存器。

#include <stdio.h>
int main()
{
    int dividend = 20;
    int divisor = 5;
    int result;
    asm("divb %2\n\t"
        "movl %%eax, %0"
        :"m"(result)
        :"a"(dividend), "m"(divisor));
   printf("The result is %d\n", result);
   return 0;
}

2.8 处理跳转

内联汇编代码中是使用标签有两个限制。

  • 只能跳转到相同的asm段内的标签。
  • 不同asm不适用相同的标签,不适用C关键字。(局部标签解决)
#include <stdio.h>
int main()
{
    int a= 10;
    int b= 20;
    int result;
    asm ("cmp %1, %2\n\t"
         "jge greater\n\t"
         "movl %1, %0\n\t"
         "jmp end\n"
  "greater:\n\t"
         "movl %2, %0\n"
         "end:"
         :"=r"(result)
         :"r"(a),"r"(b));
       
   printf("The larger value is %d\n", result)
   return 0;
}

使用局部标签方法:条件分支和无条件分支都允许指定一个数字加上方向标志作为标签,方向标志指出处理器应该向那个方向查找数字型标签。第一个遇到的标签会被采用。

#include <stdio.h>
int main()
{
    int a= 10;
    int b= 20;
    int result;
    asm ("cmp %1, %2\n\t"
        "jge 0f\n\t"
        "movl %1, %0\n\t"
        "jmp 1f\n"
       "0:\n\t"
        "movl %2, %0\n"
       "1:"
        :"=r"(result)
        :"r"(a),"r"(b));
      
   printf("The larger value is %d\n", result)
   return 0;
}

backward和forward

3.使用内联汇编代码

和对待C宏函数一样,可以声明包含内联汇编的宏函数。内联汇编代码必须使用扩展asm格式,便于输入正确的输入值和输出值。

定义一个内联汇编宏函数的一个例子:

#define GREATER(a, b, result) ({\
    asm("cmp %1, %2\n\t" \
        "jge 0f\n\t" \
        "movl %1, %0\n\t" \
        "jmp 1f\n" \
        "0:\n\t" \
        "movl %2, %0\n" \
        "1:" \
        :"=r"(result) \
        :"r"(a), "r"(b));})


这篇关于GNU 内联汇编的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程