GCC 内嵌汇编之扩展的行内汇编
扩展的行内汇编
在扩展的行内汇编中,可以将 C 语言表达式指定为汇编指令的操作数,而且不用去管如何将 C 语言表达式的值读入寄存器,以及如何将计算结果写回 C 变量,你只要告诉程序中 C 语言表达式与汇编指令操作数之间的对应关系即可,GCC 会自动插入代码完成必要的操作。
使用内嵌汇编,要先编写汇编指令模板,然后将 C 语言表达式与指令的操作数相关联,并告诉 GCC 对这些操作有哪些限制条件。例如在下面的汇编语句:
__asm__ __volatile__ ("movl %1,%0" : "=r" (result) : "r" (input));
“movl %1,%0”是指令模板;“%0”和“%1”代表指令的操作数,称为占位符,内嵌汇编靠它们将 C 语言表达式与指令操作数相对应。指令模板后面用小括号括起来的是 C 语言表达式,本例中只有两个:“result”和“input”,他们按照出现的顺序分别与指令操作数“%0”,“%1,”对应; 注意对应顺序: 第一个 C 表达式对应“%0”; 第二个表达式对应“%1”,依次类推,操作数至多有 10 个,分别用“%0”,“%1”….“%9,”表示。
在每个操作数前面有一个用引号括起来的字符串,字符串的内容是对该操作数的限制或者说要求。“result”前面的限制字符串是“=r”,其中“=”表示“result”是输出操作数,“r”表示需要将“result”与某个通用寄存器相关联,先将操作数的值读入寄存器,然后在指令中使用相应寄存器,而不是“result”本身,当然指令执行完后需要将寄存器中的值存入变量“result”,从表面上看好像是指令直接对“result”进行操作,实际上 GCC 做了隐式处理,这样我们可以少写一些指令。“input”前面的“r”表示该表达式需要先放入某个寄存器,然后在指令中使用该寄存器参加运算。
限制字符必须与指令对操作数的要求相匹配,否则产生的汇编代码将会有错,读者可以将上例中的两个“r”,都改为“m” (m,表示操作数放在内存,而不是寄存器中),编译后得到的结果是:
movl input, result
很明显这是一条非法指令,因此限制字符串必须与指令对操作数的要求匹配。例如指令movl 允许寄存器到寄存器,立即数到寄存器等,但是不允许内存到内存的操作,因此两个操作数不能同时使用“m”作为限定字符。
扩展的行内汇编的语法
内嵌汇编语法如下:
__asm__(
汇编语句模板:
输出部分:
输入部分:
破坏描述部分);
即格式为 asm ( "statements" : output_regs : input_regs : clobbered_regs);
共四个部分:汇编语句模板,输出部分,输入部分,破坏描述部分,各部分使用“:”格开,汇编语句模板必不可少,其他三部分可选,如果使用了后面的部分,而前面部分为空,也需要用“:”格开,相应部分内容为空。
下面是一个简单的例子:
int main(void)
{
int dest;
int value=1;
asm(
"movl %1, %0"
: "=a"(dest)
: "c" (value)
: "%ebx");
printf("%d\n", dest);
return 0;
}
在这段内嵌汇编的意思是将 value 变量的值复制到变量 dest 中,并指定在汇编中使用 eax
与 ecx 寄存器,同时在最后标识这两个寄存器的值有被改变。
1) 汇编语句模板
汇编语句模板由汇编语句序列组成,语句之间使用“;”、“\n”或“\n\t”分开。指令中的操作数可以使用占位符引用 C 语言变量, 操作数占位符最多 10 个, 名称如下: %0, %1…,%9。指令中使用占位符表示的操作数,总被视为 long 型(4,个字节),但对其施加的操作根据指令可以是字或者字节,当把操作数当作字或者字节使用时,默认为低字或者低字节。对字节操作可以显式的指明是低字节还是次字节。方法是在%和序号之间插入一个字母,“b”
代表低字节,“h”代表高字节,例如: %h1。
2) 输出部分
输出部分描述输出操作数,不同的操作数描述符之间用逗号格开,每个操作数描述符由
限定字符串和 C 语言变量组成。每个输出操作数的限定字符串必须包含“=”表示它是一个
输出操作数。例:
__asm__ __volatile__ ("pushfl ; popl %0 ; cli":"=g" (x) )
在这里“x”便是最终存放输出结果的 C 程序变量,而“=g”则是限定字符串,限定字符串表示了对它之后的变量的限制条件,这样 GCC 就可以根据这些条件决定如何分配寄存器,如何产生必要的代码处理指令,以及如何处理操作数与 C 表达式或 C 变量之间的关系。
3) 输入部分
输入部分描述输入操作数,不同的操作数描述符之间使用逗号格开,每个操作数描述符
同样也由限定字符串和 C 语言表达式或者 C 语言变量组成。例:
__asm__ __volatile__ ("lidt %0" : : "m" (real_mode_idt));
4) 限定字符
可以看到,限制字符有很多种,有些是与特定体系结构相关。此处仅列出一些常用的限定字符和 i386 中可能用到的一些常用的限定符。它们的作用是指示编译器如何处理其后的C 语言变量与指令操作数之间的关系,例如是将变量放在寄存器中还是放在内存中等,下表
列出了常用的限定字母。
|
限定符 |
限定符 |
具体的一 个寄存器 |
“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” |
放入 eax 和 edx,把 eax 和 edx,合成一个 64 位的寄存器(uselong longs) |
|
内存 |
“m” |
内存变量 |
“o” |
操作数为内存变量,但是其寻址方式是偏移量类型 |
|
“V” |
操作数为内存变量,但寻址方式不是偏移量类型 |
|
“,” |
操作数为内存变量,但寻址方式为自动增量 |
|
“p” |
操作数是一个合法的内存地址(指针) |
|
寄存器或内存 |
“g” |
将输入变量放入 eax, ebx, ecx , edx 中的一个或者作为内存变量 |
立即数 |
“X” |
操作数可以是任何类型 |
“I” |
0-31 之间的立即数(用于 32 位移位指令) |
|
“J” |
0-63 之间的立即数(用于 64 位移位指令) |
|
“N” |
0-255 ,之间的立即数(用于 out 指令) |
|
“i” |
立即数 |
|
|
“n” |
立即数,有些系统不支持除字以外的立即数,这些系统应该 使用“n” |
操作数类型 |
“=” |
操作数在指令中是只写的(输出操作数) |
“+” |
操作数在指令中是读写类型的(输入输出操作数) |
|
浮点数 |
“=” |
操作数在指令中是只写的(输出操作数) |
“+” |
操作数在指令中是读写类型的(输入输出操作数) |
|
“f” |
浮点数 |
|
“t” |
第一个浮点寄存器 |
|
“u” |
第二个浮点寄存器 |
|
“G” |
标准的 80387 |
|
% |
该操作数可以和下一个操作数交换位置 |
另外“0”,“1”, ..., “9”表示用它限制的操作数与某个指定的操作数匹配,也即该操作数就是指定的那个操作数,例如用“0”去描述“%1”操作数,那么“%1”引用的其实就是“%0”操作数,注意作为限定符字母的 0-9 ,与指令中的“%0”-“%9”的区别,前者描述操作数,后者代表操作数。
5) 破坏描述部分
修改描述部分可以防止内嵌汇编在使用某些寄存器时导致错误。修改描述符是由逗号隔开的字符串组成的,每个字符串描述一种情况,一般是寄存器,有时也会有“memory”。例如:“%eax”、“%ebx”、“memory”等。具体的意思就是告诉编译器在编译内嵌汇编的时候不能使用某个寄存器或者不能使用内存的空间。
下面用一些具体的示例来讲述 GCC 如何把内嵌汇编转换成标准的 AT&T 汇编的。
首先看一个简单的例子,这个例子:
int main(void)
{
int result = 2;
int input = 1;
__asm__ __volatile__ ("addl %1, %0": "=r"(result): "r"(input));
printf("%d\n", result);
return 0;
}
这段内嵌汇编原本的目的是输出 1+2=3 的结果,也就是将 input 变量的值与 result 变量的值相加之后再存入 result 中。可以看到在汇编语句模板中的%1 与%0 分别代表 input 与 result变量,而“=r”与“r”则表示两个变量在汇编中应该对应两个寄存器, “=”表示 result 是输出变量。然而实际运行后发现结果实际上是 2。这是为什么呢?我们用(objdump -j .text –S 可执行文件名)这样的命令来查看编译生成后的代码发现这段内嵌汇编经 GCC 翻译后所对应的AT&T 汇编是:
movl $0x2,0xfffffffc(%ebp)
movl $0x1,0xfffffff8(%ebp)
movl 0xfffffff8(%ebp),%eax
addl %eax,%eax
movl %eax,0xfffffffc(%ebp)
前两句汇编分别是为 result 和 input 变量赋值。input 为输入型变量,而且需要放在寄存
器中, GCC 给它分配的寄存器是%eax,在执行 addl 之前%eax 的内容已经是 input 的值。可见对于使用“r”限制的输入型变量或者表达式,在使用之前 GCC 会插入必要的代码将他们的值读到寄存器; “m”型变量则不需要这一步。读入 input 后执行 addl,显然 addl %eax,%eax的值不对。再往后看: movl %eax,0xfffffffc(%ebp)的作用是将结果存回 result,分配给 result的寄存器与分配给 input 的一样,都是%eax。
综上可以总结出如下几点:
1. 使用“r”限制的输入变量, GCC 先分配一个寄存器,然后将值读入寄存器,最后用该寄存器替换占位符;
2. 使用“r”限制的输出变量, GCC 会分配一个寄存器,然后用该寄存器替换占位符,但是在使用该寄存器之前并不将变量值先读入寄存器, GCC 认为所有输出变量以前的值都没有用处,不读入寄存器,最后 GCC 插入代码,将寄存器的值写回变量;因为第二条,上面的内嵌汇编指令不能奏效,因此需要在执行 addl 之前把 result 的值读入寄存器,也许再将 result 放入输入部分就可以了(因为第一条会保证将 result 先读入寄存器)。修改后的指令如下(为了更容易说明问题将 input 限制符由“r, ”改为“m”):
int main(void)
{
int result = 2;
int input = 1;
__asm__ __volatile__ ("addl %2,%0":"=r"(result):"r"(result),"m"(input));
printf("%d\n", result);
return 0;
}
这段内嵌汇编所对应的 AT&T 汇编如下:
movl $0x2,0xfffffffc(%ebp)
movl $0x1,0xfffffff8(%ebp)
movl 0xfffffffc(%ebp),%eax
addl 0xfffffff8(%ebp),%eax
movl %eax,0xfffffffc(%ebp)
看上去上面的代码可以正常工作,因为我们知道%0 和%1 都和 result 相关,应该使用同一个寄存器,而且事实上在实际结果中 GCC 也确实是使用了同一个寄存器 eax,所以可以得到正确的结果 3。但是为了更保险起见,为了确保%0 与%1 与同一个寄存器关联我们可以
使用如下的方法
int main(void)
{
int result = 2;
int input = 1;
__asm__ __volatile__ ("addl %2,%0":"=r"(result):"0"(result),"m"(input));
printf("%d\n", result);
return 0;
}
它所对应的 AT&T 汇编为:
movl $0x2,0xfffffffc(%ebp)
movl $0x1,0xfffffff8(%ebp)
movl 0xfffffffc(%ebp),%eax
addl 0xfffffff8(%ebp),%eax
movl %eax,0xfffffffc(%ebp)
输入部分中的 result 用匹配限制符“0”限制,表示%1 与%0,代表同一个变量, 输入部分
说明该变量的输入功能,输出部分说明该变量的输出功能,两者结合表示 result 是读写型。%0和%1,表示同一个 C 变量,所以放在相同的位置,无论是寄存器还是内存。
至此读者应该明白了匹配限制符的意义和用法。在新版本的 GCC 中增加了一个限制字符“+”,它表示操作数是读写型的, GCC 知道应将变量值先读入寄存器,然后计算,最后写回变量,而无需在输入部分再去描述该变量。
int main(void)
{
int result = 2;
int input = 1;
__asm__ __volatile__ ("addl %1, %0": "+r"(result): "r"(input));
printf("%d\n", result);
return 0;
}
这段内嵌汇编所对应的 AT&T 汇编为:
movl $0x2,0xfffffffc(%ebp)
movl $0x1,0xfffffff8(%ebp)
mov 0xfffffffc(%ebp),%eax
mov 0xfffffff8(%ebp),%edx
add %edx,%eax
mov %eax,0xfffffffc(%ebp)
通过这段内嵌汇编所对应的 AT&T 汇编我们可以看出系统首先将 result 变量的值读入了eax 寄存器,并为 input 变量分配了 edx 寄存器,然后将 eax 与 edx 的值相加后将结果写入内存。
接下来的一个示例要较为复杂一些:
int main(void)
{
int count=3;
int value=1;
int buf[10];
asm(
"cld \n\t"
"rep \n\t"
"stosl"
:
: "c" (count), "a" (value) , "D" (buf) );
printf("%d %d %d\n", buf[0],buf[1],buf[2]);
}
经 GCC 翻译后所对应的 AT&T 汇编是:
movl 0xfffffff4(%ebp),%ecx
movl 0xfffffff0(%ebp),%eax
lea 0xffffffb8(%ebp),%edi
cld
repz stos %eax,%es:(%edi)
在这里 count、 value 和 buf 是三个输入变量,它们都是 C 程序中的变量,“c”、“a”和
“D”表示这三个输入值分别被存放入寄存器 ECX、 EAX 与 EDI;“cld rep stosl”是需要
执行的汇编指令;而“%ecx、 %edi”表示这两个寄存器在指令中被改变了。这段内嵌汇编
要做的就是向 buf 中写 count 个 value 值。
最后我们给出一个比较综合一点的例子:
int main(void)
{
int input, output, temp;
input = 1;
__asm__ __volatile__ ("movl $0, %%eax;\n\t"
"movl %%eax, %1;\n\t"
"movl %2, %%eax;\n\t"
"movl %%eax, %0;\n\t"
: "=m" (output), "=m"(temp)
: "r" (input)
:"eax");
printf("%d %d\n", temp,output);
return 0;
}
这段内嵌汇编经由 GCC 转化成的汇编代码如下:
movl $0x1,0xfffffffc(%ebp)
mov 0xfffffffc(%ebp),%edx
mov $0x0,%eax
mov %eax,0xfffffff4(%ebp)
mov %edx,%eax
mov %eax,0xfffffff8(%ebp)
可以看到,由于 input、 output、 temp 都是程序局部整型数变量,于是它们实际上是存放在堆栈中的,也就是内存中的某个部分。其中 output 和 temp 是输出变量,而且“=m”表
明它们应该在内存中, input 是输入变量,“r”表明它应存放在寄存器中,于是首先把 1 存入 input 变量,然后将变量的值复制给了 edx 寄存器,在这里我们可以看到内嵌汇编中使用了破环描述符“eax”,这是告诉编译器在程序中 eax 寄存器已被使用,这样编译器为了避免冲突会将输入变量存放在除 eax 以外别的寄存器中,如像我们最后看到的 edx 寄存器。看看内嵌汇编的代码编译生成的 AT&T 汇编,我们可以发现第二句内嵌汇编中的%1 转化成了0xfffffff4 (%ebp),对应的就是 temp 变量;第三句中的%2 则对应到了%edx,即 input 变量所存放的寄存器;而%0 就对应到 output 变量所存放的内存位置 0xfffffff8 (%ebp)。