GNU C内联汇编(AT&T语法)

时间:2022-05-15 13:47:45

注:以下内容为学习笔记,多数是从书本、资料中得来,只为加深印象,及日后参考。然而本人表达能力较差,写的不好。因非翻译、非转载,只好选原创,但多数乃摘抄,实为惭愧。但若能帮助一二访客,幸甚!


内联汇编提供了可以在C或C++代码中创建汇编语言代码,不必连接额外的库或程序。这种方法对最终程序在汇编语言级别如何实现特定的函数,给予程序员更多的控制权。


1.基本的内联汇编

1)asm格式

GNU的C编译器使用asm关键字指出使用汇编语言编写的源代码段落。基本格式:
asm("assembly code");
括号中的汇编格式:指令必须在引号里;指令超过一条,必须使用新行字符分隔。如:
asm ( "movl	$1,	%eax\n\t"
	  "movl	$0,	%ebx\n\t"
	  "int	$0x80" );

2)使用全局C变量

如何将数据传递和传出汇编语言呢?一种方法是使用C语言的全局变量,并且只有全局的变量才能在基本的内联汇编代码内使用。
示例:
/*************************************************************************
	> File:		use_global_var.c
	> Author:	孤舟钓客
	> Mail:		guzhoudiaoke@126.com 
	> Time:		2012年12月23日 星期日 11时33分25秒
 ************************************************************************/

#include<stdio.h>

int a = 11;
int b = 22;
int result;

int main()
{
	asm ( "pusha\n\t"
		  "movl		a,		%eax\n\t"
		  "movl		b,		%ebx\n\t"
		  "imull	%ebx,	%eax\n\t"
		  "movl		%eax,	result\n\t"
		  "popa" );
	printf ("The answer is %d\n", result);
	return 0;
}

运行结果:
liury@liury-laptop:~/program/asm/inline_assembly/use_global_var$ ls
use_global_var.c
liury@liury-laptop:~/program/asm/inline_assembly/use_global_var$ gcc -o use_global_var use_global_var.c 
liury@liury-laptop:~/program/asm/inline_assembly/use_global_var$ ./use_global_var 
The answer is 242

注释:
反汇编:

可以发现a和b在.data段中,并且类型、对齐方式等的设置。result没有初始化,故声明为.comm值。
注意开头和结尾的PUSHA,POPA。因为后面的C代码可能用到寄存器,而内联汇编中可能改变了它们,会发生不可预料的后果,故要在开始的位置保存它们,最后恢复它们。


3)volatile修饰符

编译器会试图优化生成的汇编代码以提高性能。但对内联汇编来说,优化有时并不是好事。如果不希望编译器处理内联汇编代码,可以明确地说明。用volatile修饰符可以完成这个请求:
asm volatile ("assembly code");


4)__asm__替换关键字

ANSI C 规范把关键字asm用于其他用途,不能将它用于内联汇编语句。如果希望使用ANSI C 约定编写代码,必须使用关键字__asm__替换一般的关键字asm。汇编代码段则与asm一样。__asm__可以使用__volatile__进行修饰。


2.扩展的asm

基本的asm格式简单,但有局限:所有输入输出必须使用全局C变量;必须注意不改变任何寄存器的值。
扩展格式提供附加选项。


1)扩展asm格式

扩展asm提供附加的特性,格式:
asm ("assembly code" : output locations : input operands : changed registers);
assembly code:汇编代码,同基本的asm
output locations:输出位置,包含内联汇编代码的输出值的寄存器和内存位置的列表
input operands: 输入操作数,包含内联汇编代码的输入值的寄存器和内存位置的列表
changed registers:改动的寄存器,内联代码改变的任何其他寄存器列表
若不生成输出值:asm ("assembly code" : : input operands : changed registers);
若不改动任何寄存器: asm ("assembly code" : output locations : input operands);
.file	"use_global_var.c"
.globl a
	.data
	.align 4
	.type	a, @object
	.size	a, 4
a:
	.long	11
.globl b
	.align 4
	.type	b, @object
	.size	b, 4
b:
	.long	22
	.comm	result,4,4
	.section	.rodata
.LC0:
	.string	"The answer is %d\n"
	.text
.globl main
	.type	main, @function
main:
	pushl	%ebp
	movl	%esp, %ebp
	andl	$-16, %esp
	subl	$16, %esp
#APP
# 16 "use_global_var.c" 1
	pusha
	movl		a,		%eax
	movl		b,		%ebx
	imull	%ebx,	%eax
	movl		%eax,	result
	popa
# 0 "" 2
#NO_APP
	movl	result, %edx
	movl	$.LC0, %eax
	movl	%edx, 4(%esp)
	movl	%eax, (%esp)
	call	printf
	movl	$0, %eax
	leave
	ret
	.size	main, .-main
	.ident	"GCC: (Ubuntu 4.4.3-4ubuntu5.1) 4.4.3"
	.section	.note.GNU-stack,"",@progbits

2)指定输入和输出

扩展格式中,可从寄存器和内存位置给输入、输出赋值,输入、输出列表的格式:
"constraint" (variable)
variable 是C变量。扩展asm中,局部和全局变量都可以用。约束(constraint)定义把变量存放在哪里(对于输入值)或者从哪里传送变量(对于输出值)。使用它定义把变量存放在寄存器还是内存位置中。
约束是单一字符的代码,定义如下:
------------------------------------------------------------------------
约束 描述
--------------------------------------------------------------------
a Use the %eax, %ax, or %al registers.
b Use the %ebx, %bx, or %bl registers.
c Use the %ecx, %cx, or %cl registers.
d Use the %edx, %dx, or $dl registers.
S Use the %esi or %si registers.
D Use the %edi or %di registers.
r Use any available general-purpose register.
q Use either the %eax, %ebx, %ecx, or %edx register.
A Use the %eax and the %edx registers for a 64-bit value.
m Use the variable\u2019s memory location.
o Use an offset memory location.
V Use only a direct memory location.
i Use an immediate integer value.
n Use an immediate integer value with a known value.
g Use any register or memory location available.
-------------------------------------------------------------------------
除了这些约束外,输出值还包含一个约束修饰符,它指示编译器如何处理输出值:
---------------------------------------------------------------------
输出修饰符 描述
---------------------------------------------------------------
+ 可以读取和写入操作数
= 只能写入操作数
% 如果必要,操作数可以和下一个操作数切换
& 在内联函数完成前,可以删除或者重新使用操作数
----------------------------------------------------------------------
示例:
asm ("assembly code" : "=a"(result) : "d"(data1) : "c"(data2));
把C语言变量data1放到EDX中,data2放到ECX中,结果存放到EAX中然后传送给result。

3)使用寄存器

如果输入值和输出变量被赋值给寄存器,那么在内联汇编中几乎可以像平常一样使用寄存器。
示例:
/*************************************************************************
	> File:		use_registers.c
	> Author:	孤舟钓客
	> Mail:		guzhoudiaoke@126.com 
	> Time:		2012年12月23日 星期日 13时56分38秒
 ************************************************************************/

#include<stdio.h>

int main()
{
	int data1 = 11;
	int data2 = 22;
	int result;

	__asm__ ("imull	%%edx,	%%ecx\n\t"
			 "movl	%%ecx,	%%eax"
			 : "=a"(result)
			 : "d"(data1), "c"(data2));


	printf("The result is %d\n", result);
}

运行:
liury@liury-laptop:~/program/asm/inline_assembly/use_registers$ gcc -o use_registers use_registers.c
liury@liury-laptop:~/program/asm/inline_assembly/use_registers$ ls
use_registers  use_registers.c  use_registers.s
liury@liury-laptop:~/program/asm/inline_assembly/use_registers$ ./use_registers 
The result is 242

注释:
为了使用占位符见下面,使用寄存器时要写两个%
"=a" 使用等号符号修饰输出寄存器表明汇编代码只能写入它,这是对内联汇编代码中所有输出值的要求。
反汇编:
	movl	$11, 28(%esp)
	movl	$22, 24(%esp)
	movl	28(%esp), %eax
	movl	24(%esp), %ecx
	movl	%eax, %edx
#APP
# 16 "use_registers.c" 1
	imull	%edx,	%ecx
	movl	%ecx,	%eax
# 0 "" 2
#NO_APP
	movl	%eax, 20(%esp)

可见,编译器把C局部变量栈上的值加载到了寄存器中,并通过把EAX中的结果输出给栈上的变量result。

不一定要在内联汇编中指定输出值,一些汇编指令已经假设输入值包含输出值。比如MOVS指令输入值包含输出位置。
示例:
/*************************************************************************
	> File:		only_input.c
	> Author:	孤舟钓客
	> Mail:		guzhoudiaoke@126.com 
	> Time:		2012年12月23日 星期日 14时15分12秒
 ************************************************************************/

#include<stdio.h>

int main()
{
	char input[30] = "Hello inline assembly.\n";
	char output[30];
	int len = 24;

	__asm__ __volatile__ (
			"cld\n\t"
			"rep	movsb"
			:
			: "S"(input), "D"(output), "c"(len));


	printf("%s", output);
	return 0;
}

运行:
liury@liury-laptop:~/program/asm/inline_assembly/use_registers$ gcc -o only_input only_input.c 
liury@liury-laptop:~/program/asm/inline_assembly/use_registers$ ./only_input 
Hello inline assembly.

注释:
程序把MOVS 需要的三个输入值作为输入,要复制的字符串的位置存放在ESI中,目标位置存放在EDI中,要复制的字符串长度存放在ECX中,
输出值已被定义为输入值之一,所以在扩展格式中没有专门定义输出值。
此时volatile很重要,否则编译器或许会认为这个asm段是不必要的而删除它,因为它不生成输出。

4)使用占位符

当有很多输入值时,上面的方法有点麻烦,于是提供了占位符(placeholder),可以在内联汇编中使用它引入输入和输出。这样可以在对于编译器方便的任何寄存器或者内存位置中声明输入和输出。
占位符是前面加%的数字。按照内联汇编中列出的每个输入值和输出值在列表中的顺序,每个值被赋予一个从0开始的数字,然后可以在汇编代码中使用占位符表示值。如:
asm ("assembly code"
	 : "=r"(result)
	 : "r"(data1), "r"(data2));

将生成如下的占位符:
%0: 表示包含变量值result的寄存器
%1: 表示包含变量值data1的寄存器
%2: 表示包含变量值data2的寄存器
使用占位符:
imull	%1,	%2
movl	%2,	%0

5)引用占位符

如果内联汇编代码中的输入和输出共享C变量,可以指定占位符作为约束值,可减少代码中需要的寄存器数量:
asm ("imull	%1,	%0"
	 : "=r"(data2)
	 : "r"(data1), "0"(data2));
0标记通知编译器使用第一个命名的寄存器存放输出值data2.

6)替换占位符

当输入输出很多时,数字型的占位符会很混乱,新的(3.1开始)GNU编译器允许声明替换的名称作为占位符,格式:
%[name] "constraint" (variable)
示例:
asm ("imull	%[val1], %[val2]"
	 : [val2] "=r"(data2)
	 : [val1] "r"(data1), "0"(data2));

7)改动的寄存器列表

前面的例子中没有指定改动的寄存器,为何? 编译器默认输入值和输出值使用的寄存器都会被改动,并做了相应处理,所以不需要指定这些是改动了的寄存器,而若指定了,会产生错误信息
正确方法:如果内联汇编代码使用了没有被初始地声明为输入输出的任何其他寄存器,则要通知编译器。编译器必须知道这些寄存器,以便避免使用它们。
示例:
asm ("movl	%1,		%%eax\n\t"
	 "addl	%%eax,	%0"
	 : "=r"(result)
	 : "r"(data1), "0"(result)
	 : "%eax" );

在改变的寄存器中指明要使用%eax,则当用"r"指定要使用一个寄存器时就不会选%eax了。
如果在内联汇编中使用了没有在输入输出中定义的任何内存位置,必须标记为被破坏的。在改动的寄存器列表中使用”memory“通知编译器这个内存位置在内联汇编中被改动。

8)使用内存位置

在内联汇编代码中使用寄存器比较快,但也可以直接使用C变量的内存位置。约束m用于引用输入输出的内存位置。
示例:
asm ("divb	%2\n\t"
	 "movl	%eax,	%0"
	 : "=m"(result)
	 : "a"(dividend), "m"(divisor));

9)跳转

内联汇编代码中也可以包含定义位置标签,实现跳转。
示例:
int a = 11;
int b = 22;
int result;

asm ("cmp	%1,	%2\n\t"
	 "jge	greater\n\t"
	 "movl	%1,	%0\n\t"
	 "jmp	end\n"
	 "greater:\n\t"
	 "movl	%2,	%0\n"
	 "end:"
	 : "=r"(result)
	 : "r"(a), "r"(b) );

内联汇编中使用标签的两个限制:
只能跳转到相同的asm段内的标签;
内联汇编也被编码到最终的汇编代码中,如果有另一个asm段,就不能再次使用相同的标签,否则会出错。另外如果试图整合使用C关键字(如函数名称或全局变量)的标签,也会出错。
解决办法:
在不同的asm段中也不用用过的标签;
使用局部标签。
条件分支和无条件分支都运行指定一个数字加上方向标志作为标签,方向标志指出处理器应该向哪个方向查找数字型标签,第一个遇到的标签会被采用。
示例:
asm ("cmp	%1,	%2\n\t"
	 "jge	0f\n\t"
	 "movl	%1,	%0\n\t"
	 "jmp	1f\n"
	 "0:\n\t"
	 "movl	%2,	%0\n"
	 "1:"
	 : "=r"(result)
	 : "r"(a), "r"(b) );

其中f(forward)指出从跳转指令向前(即到后面的代码)查找标签,b(backword)则相反,到向后(到前面的代码)找标签。

3.内联汇编用作宏函数

1)C宏函数

#define NAME	expression
示例:
#define SUM(a, b, result) \
	((result) = (a) + (b))

2)内联汇编宏函数

示例:
#define GREATER(a, b, result) ( { asm ( \
			"cmp	%1,		%2\n\t"	\
			"jge	0f\n\t"			\
			"movl	%1,		%0\n\t"	\
			"jmp	1f\n\t"			\
			"0:\n\t"				\
			"movl	%2,		%0\n\t"	\
			"1:\n\t"				\
			: "=r"(result)			\
			: "r"(a), "r"(b) ); })