素槿
Published on 2025-08-22 / 2 Visits
0

C++内联汇编

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. 参考

《汇编语言程序设计》

《汇编语言程序设计》代码