关于阅读本篇内容的一些汇编预备知识,可以参考:
x86汇编指南之寄存器与内存寻址模式 (opens in a new tab)
GCC汇编语法与Intel汇编语法的几个差异点 (opens in a new tab)
通过使用asm关键字,允许在C代码中嵌入汇编指令。
GCC提供了两种形式的内联asm语句:
- 一个是基本内联汇编,不支持操作数;
- 另一个是扩展内联汇编,可以包含一到多个操作数。
使用扩展内联汇编,可以在代码中混合使用C代码和汇编语句。
GCC的内联汇编,采用了AT&T的汇编语法。
基本内联汇编
首先,来看看最简单的内联汇编格式:
asm("汇编指令");
格式说明:先是关键词asm,然后是一对括号,括号内是用双引号括起来的汇编指令。
例如:
asm("movl %ecx,%eax"); /* 将寄存器ecx的值传入寄存器eax */
另一个等价的写法是:
__asm__("movl %ecx,%eax");
可以看到,我们在asm的前面和后面分别添加了两个下划线'_'。
当关键字asm在程序中可能出现冲突时,可以使用
__asm__
来代替。
上面的示例,是对于单条汇编指令的用法。
如果有多条汇编指令,需要每条指令作为一个字符串(被双引号包含)并独立写成一行,然后在指令的末尾添加\n\t(最后一条指令可以不用添加)。 这样,GCC才能够正确的解析并将指令发送给汇编器执行。
例如:
asm ("pushl %eax\n\t"
"movl $0, %eax\n\t"
"popl %eax");
扩展内联汇编
基本的内联汇编,仅支持简单的指令代码。
如果想支持更复杂的应用场景,如支持输入和输出参数,那么可以使用扩展内联汇编。
扩展内联汇编的基本格式为:
asm [ volatile ] ( /* 在这里编写汇编指令 */
[: /* 输出操作数列表 - 以英文逗号分隔的0到多个输出操作数 */
[: /* 输入操作数列表 - 以英文逗号分隔的0到多个输入操作数 */
[: /* clobbered寄存器列表,多个寄存器以英文逗号分隔 */
]]]);
其中,中括号[ ]表示里面的内容是可选的。
最简单的格式就是基本内联汇编的写法,即括号内只有汇编指令代码。
简化一下格式。
asm ( 汇编指令
: 输出操作数列表(可选)
: 输入操作数列表(可选)
: clobbered寄存器列表(可选)
);
再把括号内的参数写成一行来看看。
asm (汇编指令 : 输出操作数列表 : 输入操作数列表 : clobbered寄存器列表)
括号内的参数,使用英文冒号':'分隔为四个部分。
第1部分是汇编指令,和本文最开始介绍的相同。采用AT&T汇编语法,每条指令写成一行。
第2部分是输出操作数列表,为可选参数,多个输出操作数以英文逗号','分隔。
第3部分是输入操作数列表,为可选参数,多个输出操作数以英文逗号','分隔。
第4部分是clobbered寄存器列表,为可选参数,多个寄存器以英文逗号','分隔。
除了汇编指令之外,其他各部分都是可选的。
对于输入和输出操作数,其格式都是:"约束符"(C表达式)
如果没有输出操作数,只有输入操作数,需要在汇编指令和输入操作数之间连续写2个冒号':'。
也就是说,输出操作数对应的冒号':'不能省略。例如:
asm (汇编指令 : : 输入操作数列表 : clobbered寄存器列表)
实例
使用汇编指令,将b的值设置为a对应的值。
int a=10, b;
asm ("movl %1, %%eax;
movl %%eax, %0;"
:"=r"(b) /* 输出操作数 */
:"r"(a) /* 输入操作数 */
:"%eax" /* clobbered寄存器 */
);
其中,b作为输出操作数,在汇编指令中使用%0来引用。a作为输入操作数,在汇编指令中使用%1来引用。
r用来约束操作数,告诉GCC可以使用任意寄存器来存储被约束的操作数。
对于输出操作数,双引号内的约束符还需要添加一个修饰符'=',这个等号用来表示被修饰的是输出操作数,并且是只写的。
对于寄存器,使用两个百分号'%'作为前缀(不是一个);而对于普通操作数,则使用一个百分号'%'作为前缀。这样便于GCC区分解析寄存器和普通操作数。
最后一个参数%eax,用于通知GCC,寄存器eax将会在汇编指令中被使用和修改,让GCC不要使用该寄存器来存储任何值。
第1条指令,表示将输入操作数a传入寄存器eax。
第2条指令,表示将寄存器eax的值传入输出操作数b中。
下面具体说明一下asm括号内各个部分的含义及用法。
汇编指令
第1部分是汇编指令,可以是一条或多条指令。
如果是多条指令,可以使用英文分号';'分隔,或者使用\n\t进行分隔。可以是每条指令使用一个双引号,或者多条指令使用一个双引号。
在汇编指令中,对输出操作数和输入操作数的引用,使用百分号加数字编号的格式,编号从0开始。
先对输出操作数进行编号,比如有2个输出操作数,则分别表示为%0、%1。 再对输入操作数进行编号,比如也有2个输入操作数,则继续编号为%2、%3。 以此类推。
输入和输出操作数
第2部分和第3部分,分别是输出操作数列表和输入操作数列表,两者之间以英文冒号':'分隔。
输入和输出操作数列表在许多方面是相同的:
- 操作数的格式都是:"约束符"(C表达式)
- 多个操作数之间都以英文逗号','分隔
- 两者均为可选参数
对于输出操作数,双引号内的约束符还需要添加一个修饰符'=',这个等号用来表示被修饰的是输出操作数,并且是只写的。
如果没有输出操作数,只有输入操作数,需要在汇编指令和输入操作数之间连续写2个冒号':'。 也就是说,输出操作数对应的冒号':'不能省略。
下面是一些例子:
1)输入操作数和输出操作数使用任意寄存器来存储。
asm ("leal (%1,%1,4), %0"
: "=r" (x_times_5)
: "r" (x) );
2)输入操作数和输出操作数使用同一个寄存器来存储。将输入操作数的约束设置为0,表示和位置0(第1个)的输出操作数使用相同的寄存器。
asm ("leal (%0,%0,4), %0"
: "=r" (x)
: "0" (x) );
3)在约束符中,a、b、c、d分别表示寄存器eax、ebx、ecx、edx。下面输入操作数和输出操作数使用同一个ebx寄存器。
asm ("leal (%%ebx,%%ebx,4), %%ebx"
: "=b" (x)
: "b" (x) );
clobber寄存器列表
第4部分是clobber寄存器列表。用于通知GCC,指定的寄存器将会在汇编指令中被使用和修改,让GCC不要使用该寄存器来存储任何值。
在输入操作数和输出操作数中使用的寄存器,不需要在clobber列表中列出。因为这个是显式指定的约束,GCC知道在汇编指令中会使用到它们。
多个clobber寄存器之间以英文逗号','分隔。
volatile
volatile关键字,用于让GCC优化器禁用特定的优化。
例如:当优化器发现输出操作数没有用时,可能会丢弃这些语句。当优化器判定被执行的代码总是返回相同的结果时,则会将代码从循环逻辑中移出来。
如果使用了volatile关键字,那么优化器将会禁用上面的优化。
换句话说,使用volatile可以让汇编指令在我们所编写的代码位置中执行,而不会被删除、移动或者执行其他优化。
为了避免造成冲突,volatile同样可以写成
__volatile__
常用约束符
寄存器操作数约束
输入和输出操作数,比较常见的是使用寄存器来进行约束。
寄存器约束有很多,常见的约束符及其含义如下。
r:任意一个寄存器(动态分配)
a:EAX寄存器
b:EBX寄存器
c:ECX寄存器
d:EDX寄存器
f:浮点寄存器
D:EDI寄存器
S:ESI寄存器
在指令执行前,会先将输入或输出操作数的值存入寄存器中,然后再使用相应的寄存器来完成指令操作。
对于输出操作数,在指令执行完成后,会将寄存器的值回写到输出操作数对应的变量中。
内存操作数约束
当操作数在内存中时,对其进行任何操作将直接反映到内存中。
使用约束符m,来代替寄存器约束,可以实现直接对内存中的值进行修改。
#include <stdio.h>
int main (void) {
int x = 3;
asm volatile("incl %0" : : "m"(x));
printf("%d", x);
}
程序执行后输出结果:
4
如果使用寄存器约束,那么内存值会先存储到寄存器中,在指令执行结束后再将寄存器的值回写到内存。
匹配约束
当一个变量既作为输入操作数,也作为输出操作数时,可以使用匹配约束,使得输入和输出操作数使用同一个寄存器。
例如:
asm ("incl %0" : "=a"(var) : "0"(var));
其中,输入操作数的约束为0,表示该输入操作数和第0个输出操作数使用相同的约束。
在该例子中,输出操作数的约束为a,因此输入操作数和输出操作数都使用寄存器eax来存储。
更多实例
1)实例1
使用汇编指令add,实现将两个数值相加。
#include <stdio.h>
int main(void)
{
int foo = 10, bar = 15;
__asm__ __volatile__("addl %%ebx,%%eax" // 汇编指令
:"=a"(foo) // 输出操作数:a表示eax,用于存放计算结果,最后保存到foo
:"a"(foo), "b"(bar) // 输入操作数:第一个参数是foo,用eax来存放;第二个参数是bar,用ebx来存放
);
printf("foo+bar=%d\n", foo);
return 0;
}
执行程序后,foo的值为25。
程序执行后输出结果:
foo+bar=25
2)实例2
#include <stdio.h>
#include <math.h>
#include <stdlib.h>
#include <time.h>
int f( int );
int main (void) {
int x;
asm volatile("movl $3,%0" : "=g"(x) : : "memory"); // x = 3;
printf("%d -> %d\n", x, f(x));
}
int f( int x ) {
asm volatile ("movl %0,%%eax;" // eax=3
"imull $3, %%eax;" // eax=eax*3=9
"addl $4,%%eax;" // eax=eax+4=13
:
:"a" (x)
: "memory"
); // 返回 (3*x + 4);
}
第一段指令代码没有输入操作数,指令的作用是将常量值3传入x。
第二段指令代码没有输出操作数,其中
- 第1条指令表示将x的值传入eax;
- 第2条指令表示将eax和3相乘,并将结果存入eax;
- 第3条指令表示将eax和4相加,并将结果存入eax。
函数执行完成后,将eax的值作为结果返回。
程序执行后输出结果:
3 -> 13
参考
GCC-Inline-Assembly-HOWTO (opens in a new tab)