gcc内联汇编与AT&T语法

参考:
1. AT&T标准语法
https://developer.ibm.com/articles/l-gas-nasm/?mhsrc=ibmsearch_a&mhq=gas

2. GCC内联汇编
http://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html


本篇主要说明NASM(Intel)GAS(AT&T)的区别 以及 内联汇编的使用
注释: 用中括号括起来的表示该部分是可以省略的

1. NASM和AT&T的指令格式是相反的
Intel: mov eax, 4
AT&T:  mov $4, %eax


2. AT&T中的立即数是以$开头的
Intel: push 4
AT&T:  pushl $4
补充:16进制的支持:0x开头,示例: $0x04


3. AT&T的寄存器是以%开头的
Intel: push eax
AT&T:  pushl %eax


4. AT&T的操作大小是根据操作码后面的单位指定的
Intel: mov al, byte ptr foo
AT&T:  movb foo, %al
相关操作数: b(byte-8bits)w(word-16bits)l(dword-32bits)q(64bits)

5. 远转移指令
Intel: call/far section:offset
AT&T:  lcall/lfar $section, $offset

Intel: retf [stack-adjust]
AT&T:  lret [$stack-adjust]


6. 注释
* 在Intel中,注释使用的是 # 或者是 ;
* 在AT&T中,支持使用#		//		/**/ 	;: 同时支持c中的注释方法和Intel中的方法


7. 伪指令
* 在Intel中,伪指令不需要以.开头,例如: section、equ、times等等
* 在AT&T中,所有的伪指令都以.开头
以下列出了AT&T的伪指令说明:
.text	声明下面开始是代码段
.data	声明下面开始是数据段
.globl	声明某各地址(标号(: 函数或者变量))是全局的
		此外: 写成global 和 .global 也是一样的意思

	对于globl的使用,可能你还不太了解,这里给出了两个示例:
	示例1:
	-----------------------------
	.LC0:
		.string	"Hello"	# .string你可能还不太明白是什么,没关系,在后面我们将会解释它
		.text
		.globl	test	# 这里的test直接对应了下方的test函数
	test:				# 因为定义了globl,所以该方法变成了全局的,也就是说其它程序是可以调用到test函数的
		pushq	%rbp
		movq	%rsp, %rbp
		leaq	.LC0(%rip), %rdi
		call	puts@PLT
		nop
		popq	%rbp
		ret

	.LC1:
		// .globl te   # 这句话被我注释掉了,这样一来,其它文件将看不到te函数,即: te函数前加了static修饰符
		// 补充: .globl放哪里声明都是一样的,但是为了代码的舒适度,我们习惯用这样的格式放一起
	te:
		pushq	%rbp
		movq	%rsp, %rbp
		leaq	.LC0(%rip), %rdi
		call	puts@PLT
		nop
		popq	%rbp
		ret
	-----------------------------

	示例2:
	-----------------------------
		.globl	a
		.data
	a:
		.long	5
	----------------------------
	说明: 利用globl说明了a是全局的,这样一来其它函数就也能使用到变量a了
	.long是用于说明要多大空间的内存存储a的,在后面会解释到它


* 在Intel中,提供了伪指令dd、dw、db来说明空出32bits、16bits、8bits的空间来存储某各数据
  而在AT&T中,对应的则是.long.int.byte
	Intel: dd/dw/db 5
	AT&T:  .long/.int/.byte 5

	特别注意: 这里的立即数不能加$
* 除了上述的3种外,AT&T对于字符串做了特殊的支持
  .ascii、.asciz、.string
  Intel: db "Hello", 0x00
  AT&T: .string "Hello"		# 结尾会自动帮我们补上0x00:
  AT&T: .byte 'H', 'e', 'l', 'l', 'o', 0x00: .ascii和.asciz与.string相同,这里就不再做演示
  注意: 在AT&T中,""''是严格区分的,''只能存放字符,而""可以存放字符串,即: 结尾是0x00

 .locmm size 		分配一块指定大小的内存,单位字节
 Intel: [varname] resb size
 AT&T:  .locmm varname, size
 注: 在Intel中,varname可以省略,但在AT&T中不可以


 .rept <expression> nop
 .endr
 相当于NASM中的times <expression> nop
 expression: 常量表达式
 nop: 		 汇编指令(包含伪指令)
 注意: 可以看到我把.rept和.endr写在了不同行,因为他们.endr必须另起一行,并且该行必须以.endr开头
   	   否则编译器将找不到.endr

   	   示例: 
   	   Intel: times 5 mov eax, 0
   	   AT&T:
   	   .rept 5
   	   		mov $0, %eax
   	   .endr

   	   示例2:
   	   Intel: times 512-12 db 0
   	   AT&T:
   	   .rept 512-12 	.byte 0
   	   .endr


 .set varname, value  	设置某变量的值为value
 Intel: varname equ value
 	示例:
 	.set num, 10
	movl	$num, -4(%rbp)


.section sectionName 		 声明一个段

.align N 			 		指示接下来应以N字节对齐


.file filename			 声明该文件的文件名
.size 			 		一般由编译器生成,以在符号表中加入辅助调试信息。
				 		或者说: 是用于辅助编译器的,生成可执行文件时,该参数会被遗弃
.ident 			 		标识,用于说明支持哪些编译器
.abort 			 		立刻终止编译
.def/.endef name 		定义一个标号用于调试,注意: Linux下已遗弃

* .cfi开头的伪指令
  这系列指令是帮助操作系统为了维护程序执行而添加的,
  例如所有程序会在开头 pushq	 %rbp 来辅助操作系统获取调用关系
  而: .cfi_def_cfa 就可以帮助其了解 rbp 的压栈偏移从而得知调用关系



7. 内存寻址
在Intel中,我们使用[]来取出某各内存中的数据
而在AT&T中,我们使用的是()
Intel: segment:[base  + index*multiplier + offset]
AT&T:  segment:offset(base, index, multiplier)

示例:
AT&T 						Intel
-4(%eax) 					[eax - 4]
(, %eax, 4) 				[eax*4]
(%eax, %ebx, 4)				[eax + ebx*4]
%fs:(%eax) 					[fs:eax]	# 或者可以写作fs:[eax],不过该种方式有的Intel编译器偶尔会不支持




8. 内联汇编
8.1 从一个简单的示例开始
asm("movl %%ecx, %%eax");  // 将寄存器ecx的值赋给eax
注意: 我们可以看到寄存器使用了两个%%,而在我们前面的AT&T语法中,只有一个%
	  这是因为在内联汇编中,一个%被数字约束使用了,为了防止混淆在内联汇编中使用%%
	  关于数字约束,我们后面会讲到

8.2 完整语法格式
asm (assembler template 
    : output operands
    : input operands
    : list of clobbered registers
);

assembler template: 内联汇编指令
output operands:	输出操作数,所有的输出操作数需要以=开头
input operands:		输入操作数
list of clobbered registers: 要保护的寄存器列表,我们的汇编指令中可能会涉及到一些寄存器的修改,而外部的其它程序可能也会使用到
							 这些寄存器,因此,所有你在内联汇编中使用到的寄存器最好都写在这里

示例:
    int a = 0;
    int b = 5;
    __asm__ __volatile__(
    // asm code
    "movl %1, %%eax \n\t"
    "movl %%eax, %0 \n\t"

    :"=r"(a)    // output
    :"r"(b)     // input
    :"eax" 		// clobbered registers
    );
说明: volatile 用于说明不要优化这段代码,一般来说我们对于内联汇编的完整写法都是__asm__ __volatile__打头
执行结果:
	a = 5
	b = 5
"=r"(a)   表示输出为变量a
		  r表示可以使用任意寄存器来存放a(r称为限定符,关于其它限定符可以参考后面的限定符表(见附录))
		  =表示这是一个输出操作数
"r"(b) 	  输入操作数
		  输入变量为b,可以使用任何寄存器存储b的值
"eax"	  因为这里我们使用到了eax寄存器,所以将其写在clobbered registers中
除此之外,我们还看到了%0%1出现带了asm code中,这些就是所谓的数字约束,或者说是变量的引用
%0 		第一个变量,即存储变量a的寄存器
%1 		第二个变量,即存储变量b的寄存器

示例2:
	int a = 0;
    int b = 5;
    int c = 0;
    __asm__ __volatile__(
    // asm code
    "movl %2, %%eax \n\t"
    "movl %%eax, %0 \n\t"
    "movl %%eax, %1 \n\t"
    :"=r"(a), "=r"(c)     // output
    :"r"(b)     // input
    : "eax"
    );
执行结果:
	a = 5
	b = 5
	c = 5
与上一个程序的区别:
	因为我们输入操作数多了一个变量c,所以对应的变量引用变成了
	%0  a
	%1  c
	%2  b


&约束
在某些情况下,我们希望输数操作数使用的寄存器不要和输入操作数使用的撞上(即: 使用同一种寄存器)
此时就可以使用&约束
示例:
	int a = 0;
    int b = 5;
    __asm__ __volatile__(
    // asm code
    "movl %1, %0 \n\t
    :"=&r"(a)    // output
    :"r"(b)      // input
    :"eax"       // clobbered registers
    );
这样一来我们就能保证变量a和变量b不会使用同一个寄存器
Notice: 关于&的官方完整定义参考附录


数字约束
在某些情况下,我们希望输入操作数和输出操作数是一样的
例如: inc 指令
对于这种情况,我们就可以使用数字约束
	int a = 0;
    __asm__ __volatile__(
    // asm code
    "incl %0 \n\t"
    :"=r"(a)    // output
    :"0r"(a)      // input
    :"eax"       // clobbered registers
    );
说明:
"0r"(a) 0表示从这里开始,后面所有的变量都以0开始计算,也就是说我们的输入变量和输出变量都是 %0
		在之前我们没有数字约束的时候,我们只能从输出操作数中开头的那个变量开始计算,而有了数字约束
		我们就可以指定从这里开始,往后的变量都是以这个数字开始计算的
		但是需要注意的是: 1. 输入操作数不允许使用数字约束
						2. 并且输入操作数中的数字应该在输出操作数存在
						  即: 我们这里不可以写成1r,因为输出操作数中没有1号变量

示例2:
	int a = 0;
    int b = 1;
    __asm__ __volatile__(
    // asm code
    "xchgl %0, %1"
    :"=r"(a), "=r"(b)    // output
    :"0r"(a), "r"(b)      // input
    :"eax"       // clobbered registers
    );
输出结果:
	a = 1
	b = 0





附录:
限定符表(我这里没有全列出来,更多的限定符参考开头的那个网址->2. GCC内联汇编)
限定符 				含义
m 					内存操作数,意味着对变量的操作是直接操作内存单元的
					即: m是包含o和V的含义
o 					内存操作数,即对变量的操作数直接操作内存单元的
					但前提是该内存操作数是可加上offset的,例如数组
V 					内存操作数,即对变量的操作数直接操作内存单元的
					与o相反,该内存操作数不可以加上偏移的
i、n 				i和n都是立即数操作数,即: i和n后面跟的是立即数,Eg. "i"(5)
					唯一的区别在于n所表示的大小是小于16位的,因为在一些很老的机器中是不支持大于16位的操作数的
g 					任意操作数(即: 包含r含义、m含义、i或者是n的含义)
					但是注意: 任意操作数是不包含特殊寄存器的,例如CS、SS、CR0等等
r 					任意通用寄存器
q 					可以是寄存器ax, bx, cx, dx中的任意一个
					注1: 我这里没有写成eax或者rax或者al,是因为在不同位数的CPU中是不一样的,而不是意味着真的是ax
						 其它bx、cx、dx也相同
I 					0-32位大小的立即数,Eg. "I"(5)
J 					0-64位大小的立即数
M 					大小范围在[0, 3]的立即数
N 					大小范围在[0, 255]的立即数
a、b、c、d 			分别对应ax、bx、cx、dx
						我写成ax的原因同q操作数,参考->1
S、D 				分别对应si、di
						我没有写成esi或者rsi的原因同q操作数,参考->1
= 					输出约束符
& 					意味着该寄存器不能是输入操作数中使用过的寄存器
					官方定义: &约束表示该寄存器在输入操作数初始化完成前就被使用了,因此该约束可以告诉编译器在初始化
					输入操作数时(例如: 将变量a的值赋给eax时),该寄存器是不能使用的。
					注意: 我这里为了方便说明,写了&可以声明哪些寄存器是不能使用的,其实内存也是可以用&声明的,它们道理是一样的
					详情参考 -> &约束
012... 			数字约束
					这个可能比较难以理解,但是上方有数字约束的说明,你可以参考那里 -> 数字约束

注: 限定符可以进行组合
	例如我们前面最常用的"=r",虽然是这样,但是需要注意的是我们的输入操作数和输出操作数是不可以交换顺序的
	例如: "ir" 表示用任意一个寄存器存储立即数



  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值