【ATT 与 Intel】汇编与C语言相互调用及内联汇编 您所在的位置:网站首页 三种常见汇编语言及区别 【ATT 与 Intel】汇编与C语言相互调用及内联汇编

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

2024-06-09 13:53| 来源: 网络整理| 查看: 265

目录 一、ATT 与 Intel二、函数调用的约定三、C语言调用汇编程序四、汇编程序调用C语言五、内联汇编5.1、基本asm格式5.2、扩展asm格式5.3、使用占位符来替代寄存器名称5.3.1、使用占位符来替代寄存器名称5.3.2、给占位符重命名 5.4、使用内存地址

一、ATT 与 Intel

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

ATT 汇编Intel 汇编

常用的汇编器:

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

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

Intel 代码省略了指示大小的后缀。我们看到指令 push 和 mov,而不是 pushq 和 movqIntel 代码省略了寄存器名字前面的 ‘ % ’ 符号,用的是 rbx,而不是 %rbxIntel 代码用不同的方式来描述内存中的位置,例如是 ‘ 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汇编程序分析 - 编译器设计前的铺垫 🚀

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

C程序:

#include 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 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]("汇编指令");

说明:

可以在一对双引号中全部写出,也可将一条指令放在一对双引号中;当一对双引号内有多条指令时,必须用\n分隔符进行分割,为了排版,一般会加上\t;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 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]("汇编指令":”输出操作数列表“:”输入操作数列表“:“改动的寄存器”);

说明:

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

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

约束: 通过不同的字符,来告诉编译器使用哪些寄存器,或者内存地址。 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 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 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 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 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汇编(十八):内联汇编 🚀



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有