【ATT 与 Intel】汇编与C语言相互调用及内联汇编

一、ATT 与 Intel

x86架构的处理器的汇编指令一般使用有两种:

  • ATT 汇编
  • Intel 汇编

常用的汇编器:

  • MS VC 编译器:只支持 Intel 格式
  • GNU CC 编译器:支持 ATT 格式和 Intel 格式,一般从 gcc 的上层开始调用像cc、ar 等工具。

ATT 与 Intel 汇编代码格式区别如下:

  • Intel 代码省略了指示大小的后缀。我们看到指令 push 和 mov,而不是 pushq 和 movq
  • Intel 代码省略了寄存器名字前面的 ‘ % ’ 符号,用的是 rbx,而不是 %rbx
  • Intel 代码用不同的方式来描述内存中的位置,例如是 ‘ QWORD PTR [rbx] ’ 而不是 ‘ (%rbx) ’
  • 在带有多个操作数的指令情况下,列出操作数的顺序相反。例如,ATT格式:mov 源操作数, 目的操作数;Intel格式:mov 目的操作数, 源操作数
  • ATT 注释使用 ’ # ',Intel 注释使用 ’ ; ’

详细的区别参考:AT&T与Intel格式的汇编语法 🚀

思考: 既然 gcc 可以编译 ATT 和 Intel 编程风格,它是如何区分的?
回答: 默认的情况下,gcc 按照 ATT 的格式进行编译的,如果在代码前面加上 .intel_syntax noprefix 那么 gcc 会按照 Intel 的格式进行编译,直到出现 .att_syntax 时,汇编代码会切换为 ATT 格式。
参考: GCC Inline ASM 🚀

GCC 将 .c 文件编译成汇编代码,默认是编译成ATT 格式的,如果产生 Intel 格式可以使用如下命令:gcc -Og -S -masm=intel main.c

在这里插入图片描述

参考:windows x64 GCC AT&T汇编程序分析 - 编译器设计前的铺垫 🚀

二、函数调用的约定

  1. 在x86架构下,C语言的实参是要入栈的(ARM 架构下是放到寄存器中,当实参数量比较多的时候,其余的入栈),C编译器使用cdecl约定,入栈的顺序是从右往左
  2. 函数的返回值是存放在 %eax 寄存器中。
  3. 被调用者创建栈空间,调用者回收栈空间。
  • call add 语句:将下一条指令地址入栈,然后将add函数的地址加载如EIP寄存器中。 (等价于 push %eip + movl <add>, %eip)
  • ret 语句:将ESP指向的栈空间数据恢复到EIP寄存器中,然后跳转到EIP指向的地址,同时ESP指针上移。(等价于 pop %eip)
  • leave 主要恢复栈空间,相当于:
    movl %ebp, %esp 释放被调函数栈空间
    popl %ebp 恢复ebp为调用函数基址

三、C语言调用汇编程序

C程序:

#include <stdio.h>
int add(int, int);

int main(int argc, const char *argv[])
{
	int ret = 0;
	ret = add(5, 11);
	printf("The return value is %d. \n", ret);
	return 0;
}

汇编代码:

.type add, @function
.global add              # 设置 add 函数为全局可见    
add:
	pushl %ebp
	movl %esp, %ebp
	movl 8(%ebp), %eax
	addl 12(%ebp), %eax  # 参数的返回值是放在 %eax 中
	popl %ebp
	ret                  # 这个语句和 call 是搭配使用的

编译命令:

gcc -o app main.c add.s

C语言函数编译成汇编代码后的一般结构为:

pushl %ebp            ; 调用者栈底寄存器入栈保存
movl %esp, %ebp       ; 针对于被调用者,赋值新的栈底寄存器
sub $0x8, %esp        ; 为被调用者申请一段栈空间
...                   ; 被调用者主体执行块
leave                 ; 释放栈空间
ret                   ; 同调用者的call 搭配使用,恢复EIP寄存器,返回调用者

(调用者和被调用者的结构是基本相似的:保存调用者的状态、申请栈空间、从上一个调用者栈段中获取参数、执行函数功能函数、往eax寄存器中赋值作为参数输出、清空栈空间、返回调用者程序)

四、汇编程序调用C语言

C程序:

#include <stdio.h>

int add(int a, int b)
{
	return (a+b);
}

int main(int argc, const char *argv[])
{
	int ret = 0;
	ret = func(1, 10);
	printf("The return value is %d. \n", ret);
	return 0;
}

汇编代码:

.type func, @function
.global func              ; 设置 func 函数为全局可见    
func:
	pushl %ebp
	movl %esp, %ebp
	sub $0x08, %esp       ; 开辟一段栈空间
	movl 8(%ebp), %eax
	movl %eax, (%esp)
	movl 12(%ebp), %eax
	movl %eax, 4(%esp)
	call add
	add $0x08, %esp       ; 清除栈空间
	popl %ebp
	ret                  ; 这个语句和 call 是搭配使用的

编译命令:

gcc -o app main.c func.s

在这里插入图片描述

汇编语句调用C语句主要需要考虑,如果有参数的情况下,如何提供为C语言提供参数。当前例子的情况是C调用汇编,再由汇编调用C,根据编译的约定,栈空间的布局是约定好的,依次为参数 —> 返回地址 —> 调用者栈段的栈底指针 —> 新的栈段,C函数编译成汇编代码,编译器不会这么智能的知道参数在什么位置,只是按约定往上偏移一定的距离取参数,所以这里汇编多了④⑤4条语句,目的就是把参数放到C函数待会取参数的位置。

因此,汇编调用C函数的时候需要在栈空间中布置好传递给C函数的参数。

五、内联汇编

5.1、基本asm格式

;基本 asm 格式
asm [volatile]("汇编指令");

说明:

  1. 可以在一对双引号中全部写出,也可将一条指令放在一对双引号中;
  2. 当一对双引号内有多条指令时,必须用\n分隔符进行分割,为了排版,一般会加上\t
  3. volatile 关键字之后,告诉编译器不要优化手写的内联汇编代码。
;举例1:一个引号中多个指令
asm (“nop\n\tnop\n\t”);

;举例2:基本内联汇编中,寄存器名称前只有一个百分号,注意区别扩展asm
asm volatile ("movl a, %eax\n\t"
        	  "addl b, %eax\n\t"
              "movl %eax, c");

示例:

#include <stdio.h>
int result = 10;
int main(int argc, const char *argv[])
{
	asm volatile("addl $0x01, result\n\t"
		         "subl $0x02, result\n\t");
	printf("The result is %d. \n", result);
	return 0;
}

思考: 为什么在汇编代码中,可以使用变量result?
答案: 变量 result 被 .global 修饰,相当于是把它们导出为全局的,所以可以在汇编代码中使用。如果是一个局部变量,在汇编代代码中就不会用 .global 导出,此时在内联汇编指令中,还可以直接使用吗?【解决方法:扩展asm格式】

5.2、扩展asm格式

;扩展 asm 格式:
asm [volatile]("汇编指令":”输出操作数列表“:”输入操作数列表“:“改动的寄存器”);

说明:

  1. 扩展asm格式中的寄存器名称前面必须使用两个百分号(%%),基本内联汇编中的寄存器名称前面只有一个百分号(%);
  2. 输出操作数列表:汇编代码如何把处理结果传递到 C 代码中;
  3. 输入操作数列表:C 代码如何把数据传递给内联汇编代码;
  4. 改动的寄存器:告诉编译器,在内联汇编代码中,我们使用了哪些寄存器,这样的话 gcc 就会避免在其它地方使用这些寄存器;
  5. “改动的寄存器”可以省略,此时最后一个冒号可以不要,但是前面的冒号必须保留,即使输出/输入操作数列表为空。
;输出/输入操作数列表格式
“[输出修饰符]约束”(寄存器或内存地址)

输出修饰符:(这里的操作数是指寄存器或内存地址)
+:被修饰的操作数可以读取,可以写入;
=:被修饰的操作数只能写入;
%:被修饰的操作数可以和下一个操作数互换;
&: 在内联函数完成之前,可以删除或者重新使用被修饰的操作数;

约束: 通过不同的字符,来告诉编译器使用哪些寄存器,或者内存地址
a:使用 eax/ax/al 寄存器;
b: 使用 ebx/bx/bl 寄存器;
c: 使用 ecx/cx/cl 寄存器;
d: 使用 edx/dx/dl 寄存器;
r: 使用任何可用的通用寄存器;(占位符用法)
m: 使用变量的内存位置;
其他的约束选项还有:D, S, q, A, f, t, u等等

示例:

#include <stdio.h>

int main(int argc, const char *argv[])
{
	int data1 = 1;
	int data2 = 2;
	int data3 = 0;
	asm("addl %%ebx, %%ecx\n\t"
		"movl %%ecx, %%eax\n\t"
		: "=a"(data3)
		: "b"(data1), "c"(data2));
	printf("The data3 is %d. \n", data3);
	return 0;
}

/*
运行结果:
The data3 is 3.
*/

gcc -S main.c -o main.s 编译成汇编代码:

在这里插入图片描述

由此可见,输入/输出操作数列表中,在内联汇编开始前,将输入操作数放到指定的寄存器中,在内联汇编结束后,将指定寄存器输出到输出操作数中。

编译成汇编代码中,内联汇编开始用#APP,结束的时候用#NO_APP

5.3、使用占位符来替代寄存器名称

5.3.1、使用占位符来替代寄存器名称

在上面的示例中,只使用了 2 个寄存器来操作 2 个局部变量,如果操作数有很多,那么在内联汇编代码中去写每个寄存器的名称,就显得很不方便。就让编译器帮我们指定寄存器。

规定: 内联汇编代码中的占位符,从输出操作数列表中的寄存器开始从 0 编号,一直编号到输入操作数列表中的所有寄存器。

示例:

#include <stdio.h>

int main(int argc, const char *argv[])
{
	int data1 = 1;
	int data2 = 2;
	int data3 = 0;
	asm("addl %1, %2\n\t"
		"movl %2, %0\n\t"
		: "=r"(data3)
		: "r"(data1), "r"(data2));
	printf("The data3 is %d. \n", data3);
	return 0;
}

/*
运行结果:
The data3 is 3.
*/

5.3.2、给占位符重命名

给这些占位符重命名,也就是给每一个寄存器起一个别名,然后在内联汇编代码中使用别名来操作寄存器。

示例:

#include <stdio.h>

int main(int argc, const char *argv[])
{
	int data1 = 1;
	int data2 = 2;
	int data3 = 0;
	asm("addl %[v1], %[v2]\n\t"
		"movl %[v2], %[v3]\n\t"
		: [v3]"=r"(data3)
		: [v1]"r"(data1), [v2]"r"(data2));
	printf("The data3 is %d. \n", data3);
	return 0;
}

/*
运行结果:
The data3 is 3.
*/

5.4、使用内存地址

我们可以指定使用哪个寄存器,也可以交给编译器来选择使用哪些寄存器,也可以直接使用变量的内存地址来操作变量。

#include <stdio.h>

int main(int argc, const char *argv[])
{
	int data1 = 1;
	int data2 = 2;
	int data3 = 0;
	asm("movl %1, %%eax\n\t"
		"addl %2, %%eax\n\t"
		"movl %%eax, %0\n\t"
		: "=m"(data3)
		: "m"(data1), "m"(data2));
	printf("The data3 is %d. \n", data3);
	return 0;
}

/*
运行结果:
The data3 is 3.
*/

1、输出操作数列表 “=m”(data3):直接使用变量 data3 的内存地址;
2、输入操作数列表 “m”(data1),“m”(data2):直接使用变量 data1, data2 的内存地址;

规定: 内联汇编代码中,从输出操作数列表中的寄存器开始从 0 编号,一直编号到输入操作数列表中的所有寄存器。所以变量 data3 在内联汇编中用%0表示,变量data1和变量data2用%1和%2表示。

🔍 如理解有误,望不吝指正。

参考文献:
[1]: 内联汇编很可怕吗?看完这篇文章,终结它! 🚀
[2]: linux平台学x86汇编(十八):内联汇编 🚀

评论 19
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值