1.前言
绝大多数情况下,我们不会在编程中使用汇编,但是在一些特殊的情况下,汇编还是能完成一些平常难以实现的功能。
以下代码在 Ubuntu2004
,g++ 10.2 上测试通过。
2. 内联汇编基本格式
asm ( " asm code "
: output operands /* optional */
: input operands /* optional */
: list of clobbered registers /* optional */
);
内联汇编以 asm
关键字声明,括号中的内容由汇编指令,输出,输入和被破坏的寄存器列表。
3. 汇编示例
3.1 标准内联汇编
// main.cc
int main(int argc, char const *argv[])
{
asm("mov $60, %rax;" // Linux 上的“退出”的系统调用序号
"mov $2, %rdi\n\t" // 此程序返回 2
"syscall");
}
因为内联汇编都是文本字符串,我们必须显式使用换行符或分号说明一条指令的结束。上面的代码和下面的一样:
// main.cc
#include <iostream>
int main(int argc, char const *argv[])
{
exit(2);
}
编译运行之后,可以看到程序的返回值:
$ g++ main.cc -o main && ./main
$ echo $?
2
3.2 拓展内联汇编
// main.cc
#include <iostream>
using namespace std;
int main(int argc, char const *argv[])
{
int a = 1;
int b;
asm("movl %1, %%eax;"
"movl %%eax, %0;"
: "=r"(b) /* output */
: "r"(a) /* input */
: "%eax" /* clobbered register */
);
cout << "b=" << b << endl;
return 0;
}
上面的汇编基本上包含了内联汇编的所有要素。程序完成的功能是把a的值复制给b。
%0
和 %1
表示我们声明的第一个和第二个变量,并以此类推,在上面的程序中分别是输出值b,输入值a。冒号和输入输出之间的是声明约束,r
表明让编译器自动选择可用的寄存器。内联汇编的最后部分是被破坏的寄存器列表,由于我们的运算需要使用EAX寄存器做中间变量,我们需要告诉编译器帮我们保存和在事后恢复EAX的值。
执行上面的代码,输出如下:
$ g++ main.cc -o main && ./main
b=1
指定输入输出别名:
#include <iostream>
using namespace std;
int main()
{
int a = 1, b;
asm("movl %[in], %%eax;"
"movl %%eax, %[out];"
: [ out ] "=r"(b) /* output */
: [ in ] "r"(a) /* input */
: "%eax" /* clobbered register */
);
cout << "b:" << b << endl;
}
上面的程序中,指定输出的别名为out,输入的别名为a,然后就可以在汇编指令直接引用这两个名字(称为占位符),使代码更加易读。
其他例子:
// main.cc
#include <iostream>
using namespace std;
int main(int argc, char const *argv[])
{
int b = 1;
asm("leal (%0,%0,4), %0"
: "=r"(b)
: "0"(b)
);
cout << "b=" << b << endl;
return 0;
}
"0"
表示使用和第一个操作数一样的寄存器,上面的程序计算 b = b * 5
;在没有显示使用其他寄存器的情况下,不需要声明被破坏的寄存器,声明后反而会导致编译错误。
$ g++ main.cc -o main && ./main
b=5
3.3 指定寄存器
// main.cc
#include <iostream>
using namespace std;
int main(int argc, char const *argv[])
{
int b = 1, a = 2;
// res = a + b
int res;
asm("add %%rbx, %%rax;"
: "=a"(res)
: "a"(a), "b"(b)
);
cout << "res=" << res << endl;
return 0;
}
在上面的程序中,我们通过约束声明指定使用EAX寄存器作为输出,同事使用EAX和EBX寄存器作为输入。更多的约束可以看这篇文章中的内联汇编约束说明部分。
$ g++ main.cc -o main && ./main
res=3
3.4 在汇编指令中调用普通函数
// main.cc
#include <iostream>
using namespace std;
extern "C"
int add(int a, int b)
{
return a + b;
}
int main(int argc, char const *argv[])
{
int b = 1, a = 2;
int res;
asm("call add;" : "=a"(res) : "D"(a), "S"(b) :);
cout << "res=" << res << endl;
return 0;
}
在上面的程序中,使用call指令调用add函数。而且根据ABI约定,将第一个参数放在了RDI寄存器,将第二个参数放在了RSI寄存器,函数返回值放在了RAX寄存器。
需要注意的是由于C++支持函数重载,所以就算函数声明一致,函数签名和C语言中的函数签名也不一致,在C++中需要在函数声明中加上 extern "C"
让函数使用C语言的方式编译和链接。
$ g++ main.cc -o main && ./main
res=3
3.5 在代码中调用汇编函数
更常用的一种情况是在代码中调用使用汇编语言实现的函数。
我们用汇编实现一个add函数
// add.s
.section .text
.type add, @function
.globl add
add:
push %rbp
mov %rsp, %rbp
mov %rdi, %rax
add %rsi, %rax
mov %rbp, %rsp
pop %rbp
ret
在C++代码或单独的头文件中声明add函数,在main函数中调用它。
#include <iostream>
using namespace std;
extern "C" int add(int a, int b);
int main()
{
int a = 1;
int b = 2;
cout << "add = " << add(a, b) << endl;
}
编译时加上汇编函数的代码文件,就可以在C++代码中调用汇编实现的函数。
$ g++ main.cc add.s -o main && ./main
add = 3
3.6 运算溢出检测
正常情况下我们很难知道一个运算是否溢出,但是借助汇编就比较容易。
#include <iostream>
void test1()
{
int a = INT32_MAX - 1;
int b = 2;
int c = a + b;
uint64_t OF = 0;
asm("pushf;"
"pop %0"
: "=r"(OF));
std::cout << "overflow=" << bool((1 << 11) & OF) << std::endl;
std::cout << "a+b=" << c << std::endl;
// 还可以使用and和test指令,只是test指令不返回结果。
}
int main(int argc, char const* argv[])
{
test1();
return 0;
}
CPU的溢出标志位于 EFLAGS
寄存器的第11位,我们只要将 EFLAGS
寄存器的值复制出来,就可以方便获取运算的状态。
$ g++ overflow_detect.cc -o overflow_detect && ./overflow_detect
overflow=1
a+b=-2147483648
4. 参考
《汇编语言程序设计》
《汇编语言程序设计》代码