这篇介绍一下c内联汇编的使用,准确的说,内联汇编不算是汇编的语法,而是c/c++的语法,虽然c/c++是有标准参考的,但是不同编译器使用的 汇编不同,导致内联汇编是编译器相关的。这里介绍vc和gcc下c的内联汇编使用,vc使用masm语法,gcc自然是AT&T语法。
语法规则
一般来讲,MASM的语法规则较多,也比较复杂,不过内联汇编却相当的方便,相反,AT&T的内联汇编语法规非常复杂,这里先介绍简单的MASM内联。
vc环境下内联汇编语法规则如下:
__asm statement
__asm {
statement-1
statement-2
...
statement-n
}
这里的指令即MASM程序,但是有一些限制如下:
- 不能使用数据定义伪指令
- 不能定义结构等复杂数据类型
- 不能使用宏,或宏操作符
- 不能引用段名
当然这里只是简单的介绍一下,其中的细节只能在实践中体会了,接下来举一个简单的例子来说明内联汇编的使用:
#include <stdio.h>
/** 计算两个参数的积 */
int testAssembler(int p1, int p2)
{
__asm {
mov eax, p1
mul p2
}
}
int main()
{
int result;
testAssembler(3, 5);
__asm mov result, eax;
printf("result is %d\n", result);
return 0;
}
这里 testAssembler 函数中使用内联汇编计算了参数的积,调用点并未接受其返回值,因为函数返回值是由eax寄存器返回,则再次使用内联汇编可以取到返回值。
可以看到MASM内联汇编是十分简单的,接下来看gcc的内联语法:
asm (
语句模板
: 输出部分
: 输入部分
: 破坏描述
)
先来说语句模板,这里的语句即为AT&T的基本语法,与MASM不同的是这里不能直接使用局部变量,倒是可以引用全局变量,指 令之间使用;
, \n
, \n\t
分割,后面的部分是可选的,如果只介绍到这里,可如vc一样的方式去使用内联,不过这便损失了gcc更为强大的功能。
在指令部分除了使用 AT&T标准语法之外,还可以有最多10个占位符:%0
, %1
, …
, %9
,他们出现的次序与操作数相对应,另外,可在%和数字之间插入字符w, h, b分别表示访问操作数中的低字,低字中的高字节和低字中的低字节。这里可能说的比较糊涂,不过没关系,介绍之后的部分自然会明白。
输出部分指示此内嵌汇编输出结果保存到什么变量中,这些变量通过占位符作为汇编语句中的操作数。当输出列表多于一个变量时,变量之间用逗号隔开,这些变量之间的修饰字符串中必须包含’=’,来表示它是一个输出操作数,修饰字符稍微会做详细说明。
输入部分看名字就知道是做输入之用,语法格式也与输出部分相同,唯一缺少的就是那个修饰的符号=
。
修饰符
到这里详细说一下修饰字符,修饰字符很多,这里仅仅介绍常用修饰符。
寄存器绑定:这类修饰符修饰的变量跟某中寄存器绑定,随后的操作中,可直接操作相应的寄存器。
修饰符 | 绑定寄存器 |
---|---|
a | %eax |
b | %ebx |
c | %ecx |
d | %edx |
S | %esi |
D | %edi |
q | eax, ebx, ecx, edx 任意一个 |
r | eax, ebx, ecx, edx, esi, edi 任意一个 |
A | 被修饰是64位变量,要与%eax,%edx联合绑定 |
m | 不绑定 |
另外,如果该寄存器已经被绑定,编译器会在合适的地方push到堆栈,使用之后再回复此寄存器。
输出变量修饰
除了以上修饰符,还有一些只用于修饰输出变量,简单介绍如下:
符号 | 说明 |
---|---|
‘&’ | 输出变量不能和输入变量共用同一个寄存器 |
‘=’ | 输出变量只写 |
‘+’ | 先将输出变量预先读入到对应寄存器 |
gcc 内联汇编规范最后一个部分是破坏描述,不论做什么操作,基本都需要寄存器的参与,对于只有c代码的程序,编译器保护寄存器不会被不同模块写乱,但是对于内 联汇编,我们可能修改某些寄存器而破坏了gcc原本保护的寄存器,所以内联汇编时我们需要告诉编译器哪些寄存器使我们所需要改变的,这里破坏描述符由逗号 隔开的字符串组成,每个字符串描述一种情况,一般是寄存器名,另外还有”memory”-内存破坏描述符,内存描述符为了将寄存器保存的变量内容及时写回 到内存中去,避免在寄存器中存有多份不同拷贝,与c++关键字volatile类似。
到这里简要介绍了gcc内联汇编的部分语法,接下来举例说明此用法:
#include <stdio.h>
int main()
{
int sum = 0;
int var1 = 20;
int var2 = 15;
asm volatile( "movl %1, %0\n\t"
"addl %2, %0\n\t"
:"+a"(sum)
:"r"(var1), "r"(var2));
printf("result is %d\n", sum);
return 0;
}
可以看到,在语句模板部分出现了占位符,其中 0,1,2 根据变量出现的次数依次为sum, var1, var2,所以,movl %1, %0 表示把var1存入sum,addl %2, %0表示把var2与sum相加结果存入sum,后面描述部分都比较简单,因为addl语句的关系,需要对%0先读其值后计算,所以需要使用描述 符”+”。
当然,内联汇编只是汇编跟c结合的一种方式,还有一种就是直接把汇编编译成目标文件,再跟c直接链接到一起,这里并没有什么新的东西,仅仅写一个AT&T汇编的例子:
#file Assembler.s
.section .text
.globl _mulTwo
_mulTwo:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %eax
mull 12(%ebp)
movl %ebp, %esp
popl %ebp
ret
这里汇编仅仅实现一个函数,并通过.globl导出符号,这里加下划线的原因是因为Windows的符号规则,之前已有说明。
/* file testAssembler.c*/
#include <stdio.h>
/** 由于c必须声明,这里仅仅对汇编函数做一个c声明 */
int mulTwo(int, int);
int main()
{
printf("%d\n", mulTwo(3, 5));
return 0;
}
测试的c代码只需要定义一个汇编的函数声明即可使用。编译命令可以分别编译成目标文件再链接,也可直接使用 gcc testAssembler.c Assembler.s 编译之,对于反过来回调,汇编代码调用c代码,因为之前在汇编结尾已多次使用exit函数,这里便不再举例,同时,MASM语法的原理也相同,仅换了一套 编译命令而已。