目录
一、概述
二、内联汇编语法
1、内联汇编常规语法说明
①、 asm
② 、asm-qualifiers
③ 、AssemblerTemplate
④、 OutputOperands
⑤、 InputOperands
⑥ 、Clobbers
2、内联汇编中的earlyclobber
3、根据语法编写一个简单的add函数
总结
一、概述
C语言在线编译工具:https://gcc.godbolt.org/
内联汇编是一种在高级编程语言(如C或C++)中直接嵌入汇编代码的技术。这种方法允许程序员在源代码中直接插入汇编指令,从而实现对底层硬件的更直接控制和优化。
-
内联汇编通常使用
asm
关键字将汇编代码嵌入到C/C++源代码中。 -
在GCC编译器中,内联汇编遵循AT&T语法,其中操作数的顺序是源操作数在前,目的操作数在后。
-
寄存器名称前有
%
前缀,如%eax
。 -
可以使用C/C++变量和函数,并能通过汇编指令直接操作这些变量。
二、内联汇编语法
1、内联汇编常规语法说明
如下图
①、 asm
也可以写作“__asm__”,表示这是一段内联汇编。
② 、asm-qualifiers
有3个取值:volatile、inline、goto。
volatile的意思是易变的、不稳定的,用来告诉编译器不要随便优化这段代码,否则可能出问题。比如汇编指令“mov r0, r0”,它把r0的值复制到r0,并没有实际做什么事情,你的本意可能是用这条指令来延时。编译器看到这指令后,可能就把它去掉了。加上volatile的话,编译器就不会擅自优化。
③ 、AssemblerTemplate
汇编指令,用双引号包含起来,每条指令用“\n”分开,比如:
“mov %0, %1\n” “add %0, %1, %2\n”
④、 OutputOperands
输出操作数,内联汇编执行时,输出的结果保存在哪里。
格式如下,当有多个变量时,用逗号隔开:
[ [asmSymbolicName] ] constraint (cvariablename)
asmSymbolicName是符号名,随便取,也可以不写。
constraint表示约束,有如下常用取值:
constraint | 描述 |
m | memory operand,表示要传入有效的地址,只要CPU能支持该地址,就可以传入 |
r | register operand,寄存器操作数,使用寄存器来保存这些操作数 |
i | immediate integer operand,表示可以传入一个立即数 |
constraint前还可以加上一些修饰字符,比如“=r”、“+r”、“=&r”,含义如下:
constraint Modifier Characters | 描述 |
= | 表示内联汇编会修改这个操作数,即:写 |
+ | 这个操作数即被读,也被写 |
& | 它是一个earlyclobber操作数 |
cvariablename:C语言的变量名。
示例1如下:
[result] "=r" (sum)
它的意思是汇编代码中会通过某个寄存器把结果写入sum变量。在汇编代码中可以使用“%[result]”来引用它。
示例2如下:
"=r" (sum)
在汇编代码中可以使用“%0”、“%1”等来引用它。
⑤、 InputOperands
输入操作数,内联汇编执行前,输入的数据保存在哪里。
格式如下,当有多个变量时,用逗号隔开:
[ [asmSymbolicName] ] constraint (cexpression)
asmSymbolicName是符号名,随便取,也可以不写。
constraint表示约束,参考上一小节,跟OutputOperands类似。
cexpression:C语言的表达式。
示例1如下:
[a_val]"r"(a), [b_val]"r"(b)
它的意思变量a、b的值会放入某些寄存器。在汇编代码中可以使用%[a_val]、%[b_val]使用它们。
示例2如下:
"r"(a), "r"(b)
它的意思变量a、b的值会放入某些寄存器。在汇编代码中可以使用%0、%1等使用它们。
⑥ 、Clobbers
在汇编代码中,对于“OutputOperands”所涉及的寄存器、内存,肯定是做了修改。但是汇编代码中,也许要修改的寄存器、内存会更多。比如在计算过程中可能要用到r3保存临时结果,我们必须在“Clobbers”中声明r3会被修改。
下面是一个例子:
: "r0", "r1", "r2", "r3", "r4", "r5", "memory"
我们常用的是有“cc”、“memory”,意义如下:
Clobbers | 描述 |
cc | 表示汇编代码会修改“flags register” |
memory | 表示汇编代码中,除了“InputOperands”和“OutputOperands”中指定的之外, 还会会读、写更多的内存 |
2、内联汇编中的earlyclobber
OutputOperands的约束中经常可以看到“=&r”,其中的“&”表示earlyclobber,它是最难理解的。有一些输出操作数在汇编代码中早早就被写入了新值A,在这之后,汇编代码才去读取某个输入操作数,这个输出操作数就被称为earlyclobber(早早就被改了)。
这可能会有问题:假设早早写入的新值A,写到了r0寄存器;后面读输入操作数时得到数值B,也可能写入r0寄存器,这新值A就被破坏了。
核心原因就在于输出操作数、输入操作数都用了同一个r0寄存器。为什么要用同一个?因为编译器不知道你是earlyclobber的,它以为是先读入了所有输入操作数,都处理完了,才去写输出操作数的。按这流程,没人来覆盖新值A。
所以,如果汇编代码中某个输出操作数是earlyclobber的,它的constraint就要加上“&”,这就是告诉编译器:给我分配一个单独的寄存器,跟输入操作数用同一个寄存器。
比如现在有这段代码
反汇编如下:
为了避免这种问题,此时只需要在输出结果出加上"&"即可。这是告诉编译器,对于%0操作数它是earlyclobber的,不能跟其他操作数共用寄存器。
3、根据语法编写一个简单的add函数
int add_asm1(int a, int b)
{int res;__asm__ volatile ("add %0, %1, %2":"=&r"(res):"r"(a), "r"(b):"cc");return res;
}
代码实现的就是把第1、2个操作数相加,存入第0个操作数。也就是把a、b相加,存入res。还可以使用另一种写法,在Linux内核中这种用法比较少见。
int add_asm2(int a, int b)
{int res;__asm__ volatile ("add %[result], %[v1], %[v2]":[result]"=&r"(res):[v1]"r"(a), [v2]"r"(b):"cc");return res;
}
总结
文章将C语言中的内联汇编的基本语法做了介绍,并说明一些参数的应用场景和注意事项。最后添加了一个函数例子来理解语法。