目录
引言
在编程的广阔世界里,汇编语言以其独特的地位和强大的功能,为开发者提供了深入硬件底层、优化性能的有力手段。今天,让我们一起通过具体的代码例子,深入了解汇编使用的一些特性。
一、调用约定的奥秘
如下代码示例均可在vs软件断点调试时,在代码处右键弹出反汇编选项,点击后即可看到对应的汇编代码。
1.1 . C调用约定(_cdecl)
// 01调用约定.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
//1.c调用方式 :从右到左入栈,函数外部平衡堆栈
int _cdecl fun_cdecl(int a,int b)
{
return a + b;
}
在C调用约定中,参数从右到左依次压入栈中,而堆栈的平衡是在函数外部进行的。这意味着调用函数的代码需要负责清理堆栈,这样的设计使得可变参数函数(如printf)成为可能,因为调用者知道参数的个数,可以准确地清理堆栈。
1.2 stdcall调用约定(_stdcall)
//2.stdcall windowsAPI调用约定 :从右到左入栈,函数内部平衡堆栈 : ret 8
int _stdcall fun_stdcall(int a, int b)
{
return a + b;
}
stdcall调用约定常用于Windows API函数。同样,参数从右到左入栈,但与_cdecl不同的是,堆栈的平衡是在函数内部完成的。函数返回时使用“ret 8”指令,这里的8表示参数占用的字节数,自动清理堆栈,减轻了调用者的负担,提高了代码的执行效率。
1.3 fastcall快速调用约定(_fastcall)
//3.fastcall快速调用约定 :从右到左入栈,函数内部平衡堆栈
int _fastcall fun_fastcall(int a, int b,int c,int d )
{
return a + b+c+d;
}
fastcall快速调用约定为了进一步提高性能,它会尽可能地将参数放入寄存器中传递,减少栈操作的开销。参数同样从右到左入栈,并且在函数内部平衡堆栈。对于一些对性能要求极高的函数,这种调用约定能显著提升运行速度。
1.4 thiscall调用约定(C++类成员函数)
//4.this c++调用约定 从右到左入栈,函数内部平衡堆栈
class OBJ {
public:
int fun_thiscall(int a, int b, int c, int d)
{
return a + b + c + d;
}
int m_number;
};
在C++类的成员函数中,使用thiscall调用约定。除了参数从右到左入栈和函数内部平衡堆栈外,它还有一个特殊之处,即通过this指针来访问对象的成员变量。this指针隐含地作为第一个参数传递,让成员函数能够知道它是被哪个对象调用的。
代码整合:
// 01调用约定.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
//1.c调用方式 :从右到左入栈,函数外部平衡堆栈
int _cdecl fun_cdecl(int a,int b)
{
return a + b;
}
//2.stdcall windowsAPI调用约定 :从右到左入栈,函数内部平衡堆栈 : ret 8
int _stdcall fun_stdcall(int a, int b)
{
return a + b;
}
//3.fastcall快速调用约定 :从右到左入栈,函数内部平衡堆栈
int _fastcall fun_fastcall(int a, int b,int c,int d )
{
return a + b+c+d;
}
//4.this c++调用约定 从右到左入栈,函数内部平衡堆栈
class OBJ {
public:
int fun_thiscall(int a, int b, int c, int d)
{
return a + b + c + d;
}
int m_number;
};
int main()
{
//1.c调用方式
//fun_cdecl(1, 2);
//2.stdcall windowsAPI调用约定
//fun_stdcall(1, 2);
//3.fastcall快速调用约定
//fun_fastcall(1, 2,3,4);
//4.this c++调用约定
OBJ obj;
obj.fun_thiscall(1, 2, 3, 4);
return 0;
}
二、X64汇编的独特魅力
X64函数调用
// 02x64汇编.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
//x64函数调用
long long fun(long long a, long long b, long long c, long long d, long long x)
{
return a + b + c + d + x;
}
int main()
{
//64位调用
fun(0x1, 0x2, 0x3, 0x4, 0x5);
return 0;
}
在64位环境下,X64汇编有着自己独特的函数调用规范。例如,在这个函数中,参数通过特定的寄存器传递,这与32位环境下主要通过栈传递参数有所不同。这种方式减少了栈操作,提高了函数调用的效率,对于处理大量数据和高性能计算的场景尤为重要。
三、裸函数的特殊用途
裸函数与普通函数的区别
// 03裸函数.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
//裸函数 编译器不做任何优化
void _declspec(naked) fun1()
{
_asm mov eax,99
_asm ret
}
//普通函数
void fun2()
{
//会添加开辟栈帧
}
int main()
{
//1.裸函数函数调用
fun1();
//2.普通属性
fun2();
return 0;
}
裸函数通过`_declspec(naked)`声明,它告诉编译器不对函数进行任何常规的优化和代码生成,比如不会自动生成开辟和清理栈帧的代码。在`fun1`函数中,我们直接使用汇编指令`mov eax, 99`和`ret`,完全掌控函数的执行流程。而普通函数`fun2`,编译器会自动添加开辟栈帧等常规操作,虽然方便但在某些对性能和代码体积要求苛刻的场景下可能不够灵活。
四、X64混合编程的实践
4.1 环境设置与项目搭建
(1)要进行X64混合编程,首先需要将VS的编译环境设置为X64,然后添加一个function.asm的文件用于编写汇编代码。接着要对项目属性和文件属性进行一系列设置。
(2)设置项目属性:如图所示,需要进行特定的配置,确保项目能够正确处理汇编代码。
(3)设置文件属性:对function.asm文件的属性也需要调整,以便编译器能够正确识别和处理汇编代码。
4.2 代码编写与调用
在function.asm文件中编写汇编代码,比如实现一个简单的两数取最大值的方法:
.code ; 代码段开始
; 函数功能:比较两个整数并返回较大值
; 参数:
; rcx - 第一个整数参数
; rdx - 第二个整数参数
; 返回值:rax 寄存器存储两个参数中的较大值
Max proc
mov rax, rcx ; 将第一个参数的值存入 rax 寄存器
cmp rax, rdx ; 比较 rax 和 rdx 的值
jge _ge ; 如果 rax 大于等于 rdx,跳转到 _ge 标签处
_le:
mov rax, rdx ; 如果 rax 小于 rdx,将第二个参数的值存入 rax 寄存器
_ge:
ret ; 返回 rax 中的值(即较大值)
Max endp
END ; 汇编源文件结束
在源文件中,通过`extern "C"`声明外部函数,防止C++的名称粉碎机制导致链接错误:因为被c++名称粉碎机制糟蹋后,链接器就会报无法解析的外部符号的错误。.asm文件的函数名没有经过名称粉碎机制的。
#include "stdafx.h"
//声明函数
extern"C" int Max( int left , int right );
int main()
{
//调用汇编函数
int a =Max(5, 6);
return 0;
}
通过这样的方式,我们实现了C++与汇编语言的混合编程,充分发挥了两种语言的优势,既利用了汇编语言的高效性和对硬件的直接控制能力,又结合了C++的高级特性和便捷的编程方式。
汇编语言的这些特性为我们提供了丰富的编程手段,无论是优化性能、深入底层开发,还是进行混合编程,都能帮助我们更好地完成各种复杂的任务,在不同的编程场景中发挥出独特的价值,值得开发者们深入学习和探索。