专栏首页RainMark 的文章GNU C 内联汇编介绍

GNU C 内联汇编介绍

GNU C 内联汇编介绍

简介

1、很早之前就听说 C 语言能够直接内嵌汇编指令。但是之前始终没有去详细了解过。最近由于某种需求,看到了相关的 C 语言代码。也就自然去简单的学习了一下如何在 C 代码中内嵌汇编指令。


asm/__asm__ 关键字

1、总的来说在 C 代码中我们通过 asm/__asm__ 关键字来告诉编译器将指定的内容当汇编指令处理。废话不多说,先看个例子:

#include <stdio.h>

int main(int argc, char *argv[])
{
    int x = 3, y = 4;

    __asm__("addl %%ebx, %%eax"
        : "=a" (y)
        : "b" (x), "a" (y));
    
    printf("x + y = %d\n", y);
    return 0;
}

2、这个例子,求两数之和。将 x 的值加到 y 中,并输出 y 值。首先来看一下在 C 代码中插入汇编指令的框架代码:

__asm__("汇编指令1\n\t"
        "汇编指令2\n\t"
        "汇编指令3\n\t"
        "汇编指令n"
        : 输出变量列表
        : 输入变量列表
        : 被破坏的寄存器列表);

汇编指令

1、在 __asm__(); 的“”中,便是编写汇编指令的地方。利用 C 语言自动连接双引号的特性,我们可以像框架那样每一行只写一条指令,当然你也可以全部写在一行,那么需要用 ';' 将不同的指令分开。

2、\n 用于指令换行,\t使 GCC 编译的时候产生的汇编指令格式保持规范。 GCC 默认使用 AT&T 格式的汇编语法 它与 intel 的汇编语法之间稍有不同。简单说两点不同的地方:

  • AT&T 汇编在操作寄存器时需要在前面加一个 '%' 符号,而 intel 的不用。由于在 C 代码中嵌入汇编时,写在字符串中,由于 '%' 在 C 语言中是特殊字符,所以为什么在第一个例子中寄存器前加了两个 '%'.
  • AT&T 在操作立即数时,需要在立即数前面加 '$',而 intel 却是 '#'.
  • AT&T 的源与目的与 intel 相反。例如: intel:mov eax, #1 AT&T:movl $1, %eax.

3、这里只是提到了本文中会见到的一部分差异,更多具体关于 AT&T 汇编的知识,这里就不再赘述。可参见相关描述 AT&T 汇编的书籍。


输出变量列表

1、输出变量列表是描述,在内嵌的汇编指令中将哪些值输出到 C 代码环境中的哪个变量中。比如第一个例子中我们指定在执行完了所写的汇编指令后将 eax 寄存器的值输出到变量 y 中。 其中 "=a" 指明使用 eax 寄存器为输出寄存器,输出到紧跟的变量 (y) 中。

  • = 代表输出变量用作输出,原来的值会被新值替换。
  • + 代表即可用作输入,也可用作输出。

2、输出变量列表可以写多个变量,每个之间使用逗号隔开。例如:: “=a” (x), "=b" (y), "=r" (z)。其中用到的 a, b 等代表相应的寄存器。如下是一部分对应关系。


代码

含义

a

使用寄存器 eax

b

使用寄存器 ebx

c

使用寄存器 ecx

d

使用寄存器 edx

S

使用 esi

D

使用 edi

q

使用动态分配字节可寻址寄存器

r

使用任意动态分配的寄存器

A

使用寄存器 eax 与 edx 联合

m

使用内存地址

o

使用内存地址并可以加偏移量

I

使用常数 0-31

J

使用常数 0-63

K

使用常数 0-255

M

使用常数 0-3

N

使用一字节常数 0-255


3、这里仅仅列出了一部分常用到的代码,更多详细请参考 GNU C 的 GCC 使用手册。 这里讲一下 "=r" 的用法,像 a, b 这些代码都是指定使用的寄存器。但是 r 是让编译器随机给一个,那么我怎么知道是那个呢? 不用担心,编译器为使用的随机寄存器遍了一个号。规则是:从输出列表开始,一直到输入列表结束,从左到右,从上到下一次为 %0, %1, %2....所以我们可以这样改写第一个代码例子:

#include <stdio.h>

int main(int argc, char *argv[])
{
    int x = 3, y = 4;

    __asm__("addl %1, %0"
        : "=r" (y)
        : "r" (x), "0" (y));
    
    printf("x + y = %d\n", y);
    return 0;
}

输入变量列表

1、和输出变量列表一样,使用的寄存器代码依然一样的含义。只是少了 '=' 而已。注意如果一个变量使用 'r' 代码时,既做输出,又做输入的话,在写输入变量对应的寄存器时,就写它在输出列表里对应的编号。如上一个例子中 y 既做输出又做输入,那么刚进入汇编指令时,%0的值便为 y 之前的值 4 ,指令结束后 %0 为 7 , 接着又把 %0 输出到了 y 。


破坏寄存器列表

1、这一行告诉 GCC 在内联的汇编代码中,哪些寄存器可能会被使用到(显式/隐式)。那么 GCC 就会在进入内联汇编之前将这些寄存器保存起来,最后再恢复。避免影响到其他的代码。 早期的 GCC 要求把输入、输出用到的寄存器写到破坏列表里面。但是现在的编译器能够自动保存、恢复在输出、输入列表里面用到的寄存器。因此上述的例子中由于没有影响到其他非输出、非输入的寄存器,所以可以省略破坏列表。

看个栗子:

#include <stdio.h>

char* strcpy(char *dst, const char *src)
{
    __asm__("cld\n"
        "1:\tlodsb\n\t"
        "stosb\n\t"
        "testb %%al, %%al\n\t"
        "jne 1b"
        :
        :"S" (src), "D" (dst)
        :"ax");
    return dst;
}

int main(int argc, char *argv[])
{
    char buf[512];

    strcpy(buf,"Hello,AT&T!");
    printf("%s\n", buf);
    return 0;
}

// 代码中隐式的使用到了 ax 寄存器,因此我们特别的指明了 ax 为被破坏的寄存器。

GCC 的一些新特性

1、新的 GCC 允许我们为随机分配的寄存器命名,这样极大的方便我们编写内联汇编代码。看个例子:

#include <stdio.h>

int main ( int argc , char *argv[] )
{

    int a = 1;
    int b = 2;

    __asm__("addl %[b], %[a]"
        : [a] "=r"(a)
        : [b] "r"(b), "[a]"(a));

    printf ("a = %d\n" , a);
    return 0;
}   

2、其实一看代码,你就明白,只需要在指明 "=r" , "r" 的前面加上 [name] 之后,便可以在汇编指令里面直接通过 %[name] 的方式使用相应分配的寄存器了。 我在阅读 GCC 的使用手册时,发现了这个特性十分方便,因此在这里特别提出。当然还有很多新特性,感兴趣的读者可以自行阅读 GNU GCC 的开发者手册,并寻找有用的特性。记得回来分享哦。

好了,这次就到这里吧!


// 本文属于博主原创,欢迎使用任何形式的转载。
// 但是必须注明出处,否则必究相关责任。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 计算几何 平面最近点对 nlogn分治算法 求平面中距离最近的两点

    int SOLVE(int left,int right)//求解点集中区间[left,right]中的最近点对

    RainMark
  • How to create and apply a patch with Git

    Creating a patch file with git is quite easy to do, you just need to see how it’...

    RainMark
  • 浅谈 Make 命令

      代码变成可执行文件,叫做编译(compile);先编译这个,还是先编译那个(即编译的安排),叫做构建(build)。

    RainMark
  • 深入理解计算机系统(3.3)------操作数指示符和数据传送指令

      在上一篇博客 程序编码以及数据格式 中我们给出了一个简单的C程序,然后编译成了汇编代码。大家看不懂没关系,后面的博客我们将逐渐揭开一些汇编指令的神秘面纱。本...

    IT可乐
  • go语言调度器源代码情景分析之二:CPU寄存器

    寄存器是CPU内部的存储单元,用于存放从内存读取而来的数据(包括指令)和CPU运算的中间结果,之所以要使用寄存器来临时存放数据而不是直接操作内存,一是因为CPU...

    阿波张
  • 45:十进制到八进制

    45:十进制到八进制 总时间限制: 1000ms 内存限制: 65536kB描述 把一个十进制正整数转化成八进制。 输入一行,仅含一个十进制表示的整数a(...

    attack
  • 【python数据挖掘实战】之一:异常检测

    等,有着重要的作用。由于在以上场景中,异常的数据量都是很少的一部分,因此诸如:SVM、逻辑回归等分类算法,都不适用,因为:

    统计学家
  • js导json数据到excel接口

    用户1220053
  • 倒着输出整数

    版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 ...

    喜欢ctrl的cxk
  • 汇编语言 手记5

    段寄存器就是提供段地址的。 8086CPU有4个段寄存器 CS DS SS ES 当8086CPU要访问内存时,由这4个段寄存器提供内存单元的段地址。 CS(代...

    用户1154259

扫码关注云+社区

领取腾讯云代金券