内联汇编
内联汇编是指代在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++相对难以实现,所以我们才需要去用内联汇编。比如对栈帧的一些操作,以及信息的获取等。此外,如果某些情况特别需要注重效率,我们也可以进行内联汇编,例如某些功能特定的函数需要执行特别多次,在要求效率的情况下,对这些函数进行汇编化的操作,能让性能得到可观的提升。