C语言内联汇编

内联汇编

内联汇编是指代在C/C++中嵌入的汇编代码,不需要额外的外部调用的操作,可以直接嵌入使用。

常见的两种内联汇编格式有:GCC内联汇编、VC内联汇编。

gcc内联汇编

gcc内联汇编的通用格式如下

c 复制代码
asm ( 
	汇编语句
    : 输出操作数     // 非必需
    : 输入操作数     // 非必需
    : 其他被污染的寄存器 // 非必需
    );

我们可以通过下面的示例来具体了解其使用方式:

c 复制代码
#include <stdio.h>
 
int main()
{
    int a=1, b=2, c=0;
 
    // add 操作
    asm(
        "addl %2, %0"       // 1
        : "=g"(c)           // 2
        : "0"(a), "g"(b)    // 3
        : "memory");        // 4
    printf("c:%d\n", c);
    return 0;
}

上述内联汇编代码实现了一个相加操作,将变量a和b进行相加并且存到c中。

我们可以看到上述代码出现了很多汇编中本来没有的新元素,诸如%0 "=g"等。因为通常我们的内联汇编代码都很简单,相对于真实的汇编语言而言缺失了很多必要的操作,这些东西就是内联汇编为了减少这些"失误丢失"而做的信息补充。

我们依次来分析上述语段:

//1

第一行是汇编语句,这里只有一行,如果涉及多行可以用 ; 或者\t \n来分割

//2

第二行输出语句是输出操作数。

通用格式为"=?"(val)

##val是C/C++语言中的存储使用的变量名称

##?是标识符,告诉汇编语言用什么模式来代理这个操作数
标识符参数

a,b,c,d,S,D 分别代表 eax,ebx,ecx,edx,esi,edi 寄存器

r 上面的寄存器的任意一个(谁闲着就用谁)

m 内存

i 立即数(常量,只用于输入操作数)

g 寄存器、内存、立即数 都行(gcc你看着办)

^9ebb80

//3

第3行是输入操作数

通用格式 "?"(var) 的形式

? 除了可以是上面的那些标识符,还可以是输出操作数的序号,表示用 var 来初始化该输出操作数

复制代码
int i, j; 
asm( 
"mov %1, %0;" 
	: "=r"(j) 
	: "r"(i) : 
);
关于标号,内联汇编会按照顺序进行排布。比如此时%0就是对应输出列表的j。%1对应输入列表的i.

//4

第4行标出那些在汇编代码中修改了的、又没有在输入/输出列表中列出的寄存器,这样 gcc 就不会擅自使用这些"危险的"寄存器。

还可以用 "memory" 表示在内联汇编中修改了内存,之前缓存在寄存器中的内存变量需要重新读取。
**在汇编中用 %序号 来代表这些输入/输出操作数,序号从 0 开始。为了与操作数区分开来,寄存器用两个%引出,如:%%eax

VC内联汇编

VC内联汇编的操作比GCC的内联汇编简洁、简单很多。

其通用格式如下:

c 复制代码
__asm{
    汇编语句
}

我们可以看到外面省略了很多信息的补充说明。

c 复制代码
#include <stdio.h>
 
int main()
{
    int a=1, b=2, c=0;
    //  add 操作
    __asm{
        push eax    // 保护 eax
        mov eax, a  // eax = a;
        add eax, b  // eax = eax + b;
        mov c, eax  // c = eax;
        pop eax     // 恢复 eax
    }
    printf("现在c是:%d\n", c);
    return 0;
}

VC汇编的内容上,更加贴近我们理解的汇编上,我们可以直接用C/C++中的变量名当作寄存器或者说是一个地址来使用。但是要注意有些变量名如果和汇编中的保留字相冲突,使用过程中将会报错。

虽然VC内联汇编更加简洁,但是它没有输入/输出操作数列表,它无法知道我们进行了哪些操作,需要保存哪些数据,这些信息都是需要我们自己考虑的(包括环境保护),VC内联只是简单的讲我们写入的语句放进环境中跑而已。

限制符(gcc)

我们直观的理解gcc内联汇编以及VC内联汇编的区别无非在于两点。1、输入/输出的提示指定(gcc);2、直接用变量名(vc)。

那么假如我们在gcc中不使用提示指定会产生什么效果?

c 复制代码
#include <stdio.h>  
#include <stdlib.h>  
int add(int i, int j) 
{ 
	asm (
	 "mov %rdi, %rax;"
	  "add %rsi, %rax;" 
	  );
} 
int main(int argc, char *argv[])
{ 
	int i = atoi(argv[1]); 
	int j = atoi(argv[2]);
	int res = add(i, j); 
	printf("%s + %s is %d\n", argv[1], argv[2], res);
	return res; 
}

比如上面的例子,感觉上用VC和gcc的效果是差不多的,确实它符合汇编上的理解(默认函数返回值放置在rax)。

上述例子不开启优化编译的时候,是可以正常使用的,但是如果开启了优化编译就会出现问题。

复制代码
gcc test.c -o test #不开启优化
gcc test.c -o test -O2 #开启优化

0000000000001060 <main>:
    1060:       41 54                   push   %r12
    1062:       ba 0a 00 00 00          mov    $0xa,%edx
    1067:       53                      push   %rbx
    1068:       48 89 f3                mov    %rsi,%rbx
    106b:       48 83 ec 08             sub    $0x8,%rsp
    106f:       48 8b 7e 08             mov    0x8(%rsi),%rdi
    1073:       31 f6                   xor    %esi,%esi
    1075:       e8 c6 ff ff ff          callq  1040 <strtol@plt>
    107a:       48 8b 7b 10             mov    0x10(%rbx),%rdi
    107e:       ba 0a 00 00 00          mov    $0xa,%edx
    1083:       31 f6                   xor    %esi,%esi
    1085:       e8 b6 ff ff ff          callq  1040 <strtol@plt>
    108a:       48 89 f8                mov    %rdi,%rax               # add()
    108d:       48 01 f0                add    %rsi,%rax
    1090:       45 31 e4                xor    %r12d,%r12d
    1093:       48 8b 53 10             mov    0x10(%rbx),%rdx
    1097:       48 8b 73 08             mov    0x8(%rbx),%rsi
    109b:       31 c0                   xor    %eax,%eax
    109d:       44 89 e1                mov    %r12d,%ecx
    10a0:       48 8d 3d 5d 0f 00 00    lea    0xf5d(%rip),%rdi        # 2004 <_IO_stdin_used+0x4>
    10a7:       e8 84 ff ff ff          callq  1030 <printf@plt>
    10ac:       48 83 c4 08             add    $0x8,%rsp
    10b0:       44 89 e0                mov    %r12d,%eax
    10b3:       5b                      pop    %rbx
    10b4:       41 5c                   pop    %r12
    10b6:       c3                      retq
    10b7:       66 0f 1f 84 00 00 00    nopw   0x0(%rax,%rax,1)
    10be:       00 00

可以看到,108a处其实是我们写入的内联汇编指令,优化之后,由于我们没有指定其输入、输出的提示,所以程序不知道有哪些信息是应该作为输入,哪些又应该是作为输出的(包括rax,因为rax是我们认为汇编默认的函数返回值,但是此刻程序并不知道我们想要rax,进行优化后不会特意保留)

我们可以对上述内容的内联汇编部分进行一个修改。

c 复制代码
 int res;
 asm (
 		  "mov %%rdi, %%rax;"
 		  "add %%rsi, %%rax;"
 		  : "=a"(res)
 		  : "D"(i), "S"(j)
 		  :
 	);

这段代码指定了输出的值是通过rax存储,并且放置在变量res上。输入的值是变量i(用rdi代理)和j(用rsi代理)。

复制代码
0000000000001060 <main>:
    1060:       41 54                   push   %r12
    1062:       ba 0a 00 00 00          mov    $0xa,%edx
    1067:       53                      push   %rbx
    1068:       48 89 f3                mov    %rsi,%rbx
    106b:       48 83 ec 08             sub    $0x8,%rsp
    106f:       48 8b 7e 08             mov    0x8(%rsi),%rdi
    1073:       31 f6                   xor    %esi,%esi
    1075:       e8 c6 ff ff ff          callq  1040 <strtol@plt>
    107a:       48 8b 7b 10             mov    0x10(%rbx),%rdi
    107e:       ba 0a 00 00 00          mov    $0xa,%edx
    1083:       31 f6                   xor    %esi,%esi
    1085:       49 89 c4                mov    %rax,%r12
    1088:       e8 b3 ff ff ff          callq  1040 <strtol@plt>
    108d:       44 89 e7                mov    %r12d,%edi
    1090:       48 8b 53 10             mov    0x10(%rbx),%rdx
    1094:       89 c6                   mov    %eax,%esi
    1096:       48 89 f8                mov    %rdi,%rax
    1099:       48 01 f0                add    %rsi,%rax
    109c:       48 8b 73 08             mov    0x8(%rbx),%rsi
    10a0:       41 89 c4                mov    %eax,%r12d
    10a3:       89 c1                   mov    %eax,%ecx
    10a5:       48 8d 3d 58 0f 00 00    lea    0xf58(%rip),%rdi        # 2004 <_IO_stdin_used+0x4>
    10ac:       31 c0                   xor    %eax,%eax
    10ae:       e8 7d ff ff ff          callq  1030 <printf@plt>
    10b3:       48 83 c4 08             add    $0x8,%rsp
    10b7:       44 89 e0                mov    %r12d,%eax
    10ba:       5b                      pop    %rbx
    10bb:       41 5c                   pop    %r12
    10bd:       c3                      retq
    10be:       66 90                   xchg   %ax,%ax

这时程序可以正常运行。

纯函数优化

但是此时要注意纯函数的优化问题。(本质上就是注意限制符代理的使用)

纯函数是指一个函数的运算结果只依赖于传入的参数,而且除了运算结果没有其他的副作用,比如上面的add函数。

当编译器遇到纯函数需要多次运行的时候,那么根据纯函数的特性编译器会认为纯函数多次调用的返回结果都是相同的,而且没有副作用,则会将多次调用优化成一次。

c 复制代码
#include <stdio.h> 
#include <stdlib.h> 

int add(int i, int j) {
    int res;
    asm (
            "mov %%rdi, %%rax;"
            "add %%rsi, %%rax;"
            : "=a"(res)
            : "D"(i), "S"(j)
            : 
       );
    return res;
}

int main(int argc, char *argv[]) {
    int i = atoi(argv[1]);
    int j = atoi(argv[2]);
    while(1) {
        int res = add(i, j);
        printf("%s + %s is %d\n", argv[1], argv[2], res);
    }
    return 0;
}

使用优化编译。

复制代码
gcc test.c -o test -O2 #开启优化

0000000000001060 <main>:
    1060:       55                      push   %rbp
    1061:       ba 0a 00 00 00          mov    $0xa,%edx
    1066:       53                      push   %rbx
    1067:       48 89 f3                mov    %rsi,%rbx
    106a:       48 83 ec 08             sub    $0x8,%rsp
    106e:       48 8b 7e 08             mov    0x8(%rsi),%rdi
    1072:       31 f6                   xor    %esi,%esi
    1074:       e8 c7 ff ff ff          callq  1040 <strtol@plt>
    1079:       48 8b 7b 10             mov    0x10(%rbx),%rdi
    107d:       31 f6                   xor    %esi,%esi
    107f:       ba 0a 00 00 00          mov    $0xa,%edx
    1084:       89 c5                   mov    %eax,%ebp
    1086:       e8 b5 ff ff ff          callq  1040 <strtol@plt>
    108b:       89 ef                   mov    %ebp,%edi
    108d:       89 c6                   mov    %eax,%esi
    108f:       48 89 f8                mov    %rdi,%rax
    1092:       48 01 f0                add    %rsi,%rax
    1095:       89 c5                   mov    %eax,%ebp
    1097:       66 0f 1f 84 00 00 00    nopw   0x0(%rax,%rax,1)
    109e:       00 00
    10a0:       48 8b 53 10             mov    0x10(%rbx),%rdx
    10a4:       48 8b 73 08             mov    0x8(%rbx),%rsi
    10a8:       89 e9                   mov    %ebp,%ecx
    10aa:       31 c0                   xor    %eax,%eax
    10ac:       48 8d 3d 51 0f 00 00    lea    0xf51(%rip),%rdi        # 2004 <_IO_stdin_used+0x4>
    10b3:       e8 78 ff ff ff          callq  1030 <printf@plt>
    10b8:       eb e6                   jmp    10a0 <main+0x40>
    10ba:       66 0f 1f 44 00 00       nopw   0x0(%rax,%rax,1)

10b8处是一个无条件跳转,回到10a0处,构成了while循环。而纯函数add()只在108f处有调用过一次,并没有在while循环的内部每次调用。这正是编译器把add()作为纯函数来优化的结果。

假如在add函数中的内联汇编代码并没有构成纯函数输出,那么我们就要注意限制符的设置。

c 复制代码
#include <stdio.h> 
#include <stdlib.h> 

int add(int *i, int j) {
    int res;
    asm (
            "mov 0(%%rdi), %%edx;"
            "mov %%edx, %%eax;"
            "inc %%edx;"
            "mov %%edx, 0(%%rdi);"
            "add %%esi, %%eax;"
            : "=a"(res)
            : "D"(i), "S"(j)
            : "%rdx"
       );
    return res;
}

int main(int argc, char *argv[]) {
    int i = atoi(argv[1]);
    int j = atoi(argv[2]);
    while(i < 5) {
        int res = add(&i, j);
        printf("%s + %s is %d\n", argv[1], argv[2], res);
    }
    return 0;
}

对于上面代码,很明显其不属于纯函数,因为其i的值会随着程序的进行发生改变。但是如果我们按照之前内联汇编的写法,它依旧会被程序认为是纯函数,而按照纯函数优化方式进行优化。

复制代码
0000000000001060 <main>:
    1060:       55                      push   %rbp
    1061:       ba 0a 00 00 00          mov    $0xa,%edx
    1066:       53                      push   %rbx
    1067:       48 89 f3                mov    %rsi,%rbx
    106a:       48 83 ec 18             sub    $0x18,%rsp
    106e:       48 8b 7e 08             mov    0x8(%rsi),%rdi
    1072:       31 f6                   xor    %esi,%esi
    1074:       e8 c7 ff ff ff          callq  1040 <strtol@plt>
    1079:       48 8b 7b 10             mov    0x10(%rbx),%rdi
    107d:       31 f6                   xor    %esi,%esi
    107f:       ba 0a 00 00 00          mov    $0xa,%edx
    1084:       89 44 24 0c             mov    %eax,0xc(%rsp)
    1088:       e8 b3 ff ff ff          callq  1040 <strtol@plt>
    108d:       83 7c 24 0c 04          cmpl   $0x4,0xc(%rsp)
    1092:       7f 3b                   jg     10cf <main+0x6f>
    1094:       48 8d 7c 24 0c          lea    0xc(%rsp),%rdi
    1099:       89 c6                   mov    %eax,%esi
    109b:       8b 17                   mov    (%rdi),%edx
    109d:       89 d0                   mov    %edx,%eax
    109f:       ff c2                   inc    %edx
    10a1:       89 17                   mov    %edx,(%rdi)
    10a3:       01 f0                   add    %esi,%eax
    10a5:       89 c5                   mov    %eax,%ebp
    10a7:       66 0f 1f 84 00 00 00    nopw   0x0(%rax,%rax,1)
    10ae:       00 00 
    10b0:       48 8b 53 10             mov    0x10(%rbx),%rdx
    10b4:       48 8b 73 08             mov    0x8(%rbx),%rsi
    10b8:       31 c0                   xor    %eax,%eax
    10ba:       89 e9                   mov    %ebp,%ecx
    10bc:       48 8d 3d 41 0f 00 00    lea    0xf41(%rip),%rdi        # 2004 <_IO_stdin_used+0x4>
    10c3:       e8 68 ff ff ff          callq  1030 <printf@plt>
    10c8:       83 7c 24 0c 04          cmpl   $0x4,0xc(%rsp)
    10cd:       7e e1                   jle    10b0 <main+0x50>
    10cf:       48 83 c4 18             add    $0x18,%rsp
    10d3:       31 c0                   xor    %eax,%eax
    10d5:       5b                      pop    %rbx
    10d6:       5d                      pop    %rbp
    10d7:       c3                      retq   
    10d8:       0f 1f 84 00 00 00 00    nopl   0x0(%rax,%rax,1)
    10df:       00 

那么为什么会产生这种问题呢?

!\[Knowledge Warehouse/200_计算机系统/230_操作系统/内联汇编#\^9ebb80]

我们在使用内联汇编中的"D"(i)说明i是保存在寄存器rdi中,但这里的i实际上是一个int指针,指向了一片内存区域,所以用"D"这种类型字符串,实际上是把i是指针这一信息丢失了的。我们将其修改成"m"。就可以将i指向一片内存区域的信息告诉程序了。

c 复制代码
#include <stdio.h> 
#include <stdlib.h> 

int add(int *i, int j) {
    int res;
    asm (
            "mov %1, %%rdi;"
            "mov 0(%%rdi), %%edx;"
            "mov %%edx, %%eax;"
            "inc %%edx;"
            "mov %%edx, 0(%%rdi);"
            "add %%esi, %%eax;"
            : "=a"(res)
            : "m"(i), "S"(j)
            : "%rdx", "%rdi"
       );
    return res;
}

int main(int argc, char *argv[]) {
    int i = atoi(argv[1]);
    int j = atoi(argv[2]);
    while(i < 5) {
        int res = add(&i, j);
        printf("%s + %s is %d\n", argv[1], argv[2], res);
    }
    return 0;
}

以上编译可不被纯函数优化影响。

使用场景

为什么需要使用内联汇编?

内联汇编通常是在我们不得已的情况下再去使用的,有些功能实现用C/C++相对难以实现,所以我们才需要去用内联汇编。比如对栈帧的一些操作,以及信息的获取等。此外,如果某些情况特别需要注重效率,我们也可以进行内联汇编,例如某些功能特定的函数需要执行特别多次,在要求效率的情况下,对这些函数进行汇编化的操作,能让性能得到可观的提升。

内联汇编、内联汇编例子来源
限制符、限制符例子来源

相关推荐
Scott9999HH2 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
码智社3 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海3 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖4 小时前
JDK 26 新特性详解
java·开发语言
马优晨4 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区6 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大7 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai7 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
এ慕ོ冬℘゜7 小时前
前端基础:什么是时间戳?JS获取时间戳三种方法与实战用途
开发语言·前端·javascript
执明wa7 小时前
LayoutInflater详解: XML是如何变成View的?
android·xml·开发语言·android studio