汇编的用途
- 掌握编程语言,代码的本质
- 破解
- …
汇编
汇编语言与机器语言
一一对应
,每一条机器语言指令都有与之对应
的汇编指令
高级语言可以通过编译得到汇编语言/机器语言,但汇编语言/机器语言
几乎不
可能还原成高级语言
Visual studio 在调试下右键转汇编
9: int arrray[] = { 1,2,3 }; //C++语言
//汇编- 地址 机器指令 汇编代码
00007FF7BA87191C C7 45 08 01 00 00 00 mov dword ptr [arrray],1
00007FF7BA871923 C7 45 0C 02 00 00 00 mov dword ptr [rbp+0Ch],2
00007FF7BA87192A C7 45 10 03 00 00 00 mov dword ptr [rbp+10h],3
- 编译型语言(不依赖虚拟机)
C\C++\OC\Swift
- 脚本语言
Python\JS\PHP
- 编译型语言(依赖虚拟机)
Java\Ruby
Visual studio 2022常用快捷键
为什么C++支持函数重载
采用
name mangling
或者name decoration
技术
- C++编译器默认会对
符号名
(比如函数名)进行改变,修饰 - 重载时会生成多个不用的函数名,不同编译器(
MSVC,g++
)有不同的生成规则
默认参数
- 默认参数必须从右边开始
- 如果同时有声明,实现,默认参数只能放在函数声明中
指针存储函数名
void func(int v1,void(*p)(int)) //函数指针
{
p(v1);
}
void test(int a)
{
cout << "test(int) - "<< a <<endl;
}
int main(void)
{
void(*p)(int) = test; //指针存储函数名
p(10);
func(20,test);
}
extern “C”
被extern "C"修饰的代码会按照
C
语言的方式去编译,用在C和C++混合开发
#include <iostream>
using namespace std;
extern "C" void func() //使用C编译函数重载
{
}
extern "C" void func(int v)
{
}
//或者
extern "C" //使用大括号C编译内部
{
void func()
{
}
void func(int v)
{
}
}
int main()
{
return 0;
}
//“func”: 无法重载具有外部 "C" 链接的
如果函数同时有声明和实现,要让函数声明被
extern "C"
修饰,函数实现可以不修饰(声明和实现都放,或者放声明处)
cpp
使用C语言开源库
//math.c文件
int sum(int v1, int v2)
{
return v1 + v2;
}
int delta(int v1, int v2)
{
return v1 - v2;
}
#include <iostream>
using namespace std;
//或者
extern "C" //使用c来编译c函数
{
int sum(int v1, int v2);
int delta(int v1, int v2);
}
int main()
{
cout << sum(10, 20) << endl;
cout << delta(30, 20) << endl;
return 0;
}
/*
输出
30
10
*/
C
文件写入头文件
//math.h
#pragma once
int sum(int v1, int v2)
{
return v1 + v2;
}
int delta(int v1, int v2)
{
return v1 - v2;
}
#include <iostream>
using namespace std;
//或者
extern "C" //使用c来编译头文件
{
#include "math.h"
}
int main()
{
cout << sum(10, 20) << endl;
cout << delta(30, 20) << endl;
return 0;
}
/*
输出
30
10
*/
宏定义
- C++默认会定义宏
#define _cplusplus
,将此应用到都文件中判断是否为C++环境来决定是否用C编译
#pragma once
#ifdef _cplusplus
extern "C"
{
#endif //_cplusplus
int sum(int v1, int v2)
{
return v1 + v2;
}
int delta(int v1, int v2)
{
return v1 - v2;
}
#ifdef _cplusplus
}
#endif //_cplusplus
#pragma once
- 我们经常使用
#ifndef,#define,#endif
来防止头文件的内容被重复包含 #pragma once
可以防止整个文件的内容被重复包含
内联函数与宏
- 内联函数和宏,都可以减少函数调用的开销
- 对比宏,内联函数多了语法检测和函数特性
C++有些表达式可以被赋值
int a = 1;
int b = 2;
//赋值给了a
(a = b) = 3;
//赋值给了 b
(a < b ? a : b) = 4;
const
//const的含义,const修饰的是右边的内容
int age = 10;
const int *p1 = &age;
//修饰*p1是常量,而p1地址可以改
int const *p2 = &age; //同p1,const可以和类型互换位置
int * const p3 = &age;
//const修饰右边的内容p3是地址常量,不变,而*p3可以修改
const int * const p4 = &age;
int const * const p5 = &age;
//p4和p5等价,地址和*p4,*p5都是常量不能变
引用
- 引用的本质就是弱化了的指针
- 一个引用占用一个指针的大小
- 在C语言中,使用指针(Pointer)可以间接获取,修改某个变量的值
- 在C++中,使用引用(Reference)可以起到指针类似的功能
注意: - 引用相当于变量的别名(基本数据类型,枚举,结构体,类,数组等,都可以有引用)
- 对引用做计算,就是对引用所指向的变量做计算
- 在定义的事后就必须初始化,一旦指向了某个变量,就不可以再改变,“从一而终”
- 可以利用引用初始化另一个引用,相当于某个变量的多个别名
- 不存在(引用的引用,指向引用的指针,引用数组)
- 引用存在的价值:比指针更安全,函数返回值可以被赋值
- const必须卸载&符号的左边,才能算常引用
const
引用的特点
- 可以指向临时数据(常量,表达式,函数返回值等)
- 可以指向不同类型的数据
- 作为函数参数时(此规则也适用于
const指针
)
- 可以接受
const
和非cons
实参(非const
引用,只能接受非const
实参) - 可以跟非const引用构成重载
- 当常引用指向了
不同类型
的数据时,会产生临时变量
,即引用指向的并不是初始化时的那个变量
int age = 10;
const long& aAge = age;
age = 30;
00007FF71556184D C7 45 04 0A 00 00 00 mov dword ptr [rbp+4],0Ah
00007FF715561854 8B 45 04 mov eax,dword ptr [rbp+4]
00007FF715561857 89 45 44 mov dword ptr [rbp+44h],eax
00007FF71556185A 48 8D 45 44 lea rax,[rbp+44h]
00007FF71556185E 48 89 45 28 mov qword ptr [rbp+28h],rax
00007FF715561862 C7 45 04 1E 00 00 00 mov dword ptr [rbp+4],1Eh
#include <iostream>
#include "math.h"
using namespace std;
/*
两者引用:机器码相同,汇编相同
*/
int main()
{
int age = 22;
int* p = &age;
*p = 25;
cout << age << endl;
int& ref = age;
ref = 30;
cout << age << endl;
return 0;
}
9: int& ref = age;
00007FF759091914 48 8D 45 04 lea rax,[rbp+4]
00007FF759091918 48 89 45 28 mov qword ptr [rbp+28h],rax
10: ref = 30;
00007FF75909191C 48 8B 45 28 mov rax,qword ptr [rbp+28h]
00007FF759091920 C7 00 1E 00 00 00 mov dword ptr [rax],1Eh
11: int* p = &age;
00007FF6CD505214 48 8D 45 04 lea rax,[rbp+4]
00007FF6CD505218 48 89 45 28 mov qword ptr [rbp+28h],rax
12: *p = 25;
00007FF6CD50521C 48 8B 45 28 mov rax,qword ptr [rbp+28h]
00007FF6CD505220 C7 00 19 00 00 00 mov dword ptr [rax],19h
常引用数组
//数组名arr起始就是数组的地址,也是数组首元素的地址
// 数组名arr可以看做是指向数组首元素的指针(int *)
int arr[] = {1,2,3};
// int (&ref)[3] = arr;
int * const &ref = arr; //常量要用常引用
汇编语言
汇编语言的种类
8086
汇编(16bit
)x86
汇编(32bit
)x64汇编(64bit)
- ARM回避那(嵌入式,移动设备)
- …
x64
汇编根据编译器不同,两种书写格式
Intel
:
AT&T
AT&T汇编 VS Intel汇编
寄存器与内存
假设内存中有块红色内存空间的值是3,现在想把它的值加1,并将结果存储到蓝色内存空间
cpu
首先会将红色内存空间的值放到EAX
寄存器中:mov eax
, 红色内存空间- 然后让
eax
寄存器与1
相加:add eax,1
- 最后将值赋值给内存空间:
mov
蓝色内存空间,eax
int a = 5;
a = a + 1;
10: int a = 5;
00007FF6E7DA520D C7 45 04 05 00 00 00 mov dword ptr [rbp+4],5
11: a = a + 1;
00007FF6E7DA5214 8B 45 04 mov eax,dword ptr [rbp+4]
00007FF6E7DA5217 FF C0 inc eax
00007FF6E7DA5219 89 45 04 mov dword ptr [rbp+4],eax
x64寄存器Register
RAX(包含EAX)\REX\RCX\RDX
:通用寄存器32bit:EAX\EBX\ECX\EDX
:通用寄存器16bit:AX\BX\CX\DX
:通用寄存器x64
一个寄存器8
个字节R
开头的寄存器是64bit
的,占8
字节E
开头的寄存器是32bit
的,占4
个字节
C++内联汇编
#include <iostream>
using namespace std;
int main()
{
int a = 10;
//双下划,C++嵌入汇编执行
__asm
{
mov eax, 10
}
}
x86
汇编重要指令
mov
dest,src
将src的内容赋值给dest,类似于dest = src
[地址值]
- 中括号[]里面放的都是内存地址
* `word`是2字节,`dword`是4字节(double word),`qword`是八字节(quad word)
call
函数地址
调用函数,并不是真实地址
CPU大小端模式,大部分都是小端模式
小端模式:高高低低,高字节放高地址,低字节放低地址
一个变量的地址值,是它所在字节地址中的最小值
lea
dest
,[地址值]
将地址赋值给
dest
,类似dest = 地址值
ret
函数返回
xor
op1
,op2
将
op1
和op2``异或
的结果赋值给op1
,类似于op1
=op1
^op2
add
op1
,op2
类似于
op1 = op1 + op2
sub
op1
,op2
类似于
op1
=op1
-op2
inc
op
自增,类似于 op = op + 1
dec
op
自减,类似于op = op - 1
cmp
是``compare的简称,比较
eax和
b`的值是否相等
跳转指令表格
JE, JZ | 结果为零则跳转(相等时跳转) | ZF=1 | ||
---|---|---|---|---|
equal | zero | |||
JNE, JNZ | 结果不为零则跳转(不相等时跳转) | ZF=0 | ||
not equal | not zero | |||
JS | 结果为负则跳转 | SF=1 | ||
sign(有符号\有负号) | ||||
JNS | 结果为非负则跳转 | SF=0 | ||
not sign(无符号\无负号) | ||||
JP, JPE | 结果中1的个数为偶数则跳转 | PF=1 | ||
parity even | ||||
JNP, JPO | 结果中1的个数为偶数则跳转 | PF=0 | ||
parity odd | ||||
JO | 结果溢出了则跳转 | OF=1 | ||
overflow | ||||
JNO | 结果没有溢出则跳转 | OF=0 | ||
not overflow | ||||
JB, JNAE | 小于则跳转 (无符号数) | CF=1 | ||
below | not above equal | < | ||
JNB, JAE | 大于等于则跳转 (无符号数) | CF=0 | ||
not below | above equal | >= | ||
JBE, JNA | 小于等于则跳转 (无符号数) | CF=1 or ZF=1 | ||
below equal | not above | <= | ||
JNBE, JA | 大于则跳转(无符号数) | CF=0 and ZF=0 | ||
not below equal | above | > | ||
JL, JNGE | 小于则跳转 (有符号数) | SF≠ OF | ||
little | not great equal | < | ||
JNL, JGE | 大于等于则跳转 (有符号数) | SF=OF | ||
not little | great equal | >= | ||
JLE, JNG | 小于等于则跳转 (有符号数) | ZF=1 or SF≠ OF | ||
little equal | not great | <= | ||
JNLE, JG | 大于则跳转(有符号数) | ZF=0 and SF=OF | ||
not little equal | great | > |
汇编符号说明
参考的C++变量名规范
- 全局变量:
g_
- 成员变量:
m_
- 静态变量:
s_
- 常量:
c_
类补充
struct
默认public
class
默认private
如何利用指针间接访问所指向量的成员变量
- 从指针中取出对象的地址
- 利用对象的地址
+
成员变量的偏移量计算出成员变量的地址 - 根据成员变量的地址访问成员变量的存储空间
- 类函数默认会被赋值为
ccccccc
, cc -> int3
:起到断点的作用- 中断:interrupt
内存空间的布局
每个应用都有自己独立的内存空间
- 栈空间
每调用一个函数就会给它分配一段连续的栈空间,等函数调用完毕后会自动回收这段栈空间
自动分配和回收
- 堆空间
需要主动去申请和释放,在程序运行过程,为了能够自由控制内存的声明周期,大小,会经常使用堆空间的内存
- 代码区(段)
用于存放代码
- 全局区(数据段)
用于存放全局变量等
对初始内存初始化
memset
函数是将较大的数据结构(比如对象,数组等)内存清零的比较快的方法
int *p = (int *)malloc(sizeof(int)*10);
//*p=0;//并不能全部清零,仅前四个字节
memset(p,0,40);//从p地址开始,连续个字节,都设置为0
Person person;
person.m_id = 1;
person.m_age = 20;
person.m_height = 180;
memset(&person,0,sizeof(person)); //赋值person为零
new
的初始化
int *p1 = new int; //未被初始化
int *p2 = new int(); //被初始化为0
int *p3 = new int(5); //被初始化为5
int *p4 = new int[3]; //数组元素未被初始化
int *p5 = new int[3](); //3个数组元素都被初始化为0
int *p6 = new int[3]{}; //3个数组元素都被初始化为0
int *p7 = new int[3]{ 5 } //数组首元素被初始化为5,其它元素被初始化为零
malloc
不会调用类的构造&析构函数
#include <iostream>
using namespace std;
struct Person
{
int m_age;
Person()
{
cout << "Person()" << endl;
}
~Person()
{
cout << "释放Person()" << endl;
}
};
int main(void)
{
Person person; //会调用默认构造函数
Person* p = new Person; //申请空间也会调用默认构造函数
delete p;
//malloc并不调用默认构造函数,
Person* q = (Person*)malloc(sizeof(Person));
free(q); //也不会调用析构函数
return 0;
}
/*
Person()
Person()
释放Person()
释放Person()
*/
成员变量的初始化
- 全局区默认初始化为0
- 如果自定义了构造函数,除了全局区,其他内存空间的成员变量默认都不会被初始化,需要开发人员手动初始化。
#include <iostream>
using namespace std;
struct Person
{
int m_age;
};
Person g_person; //全局变量初始化为0
int g_age; //全局变量默认初始化为 0
int main(void)
{
//Person person; //堆空间:没有初始化成员变量
//堆空间:没有初始化成员变量
Person* p0 = new Person;
//堆空间:成员变量初始化为0,//如果有构造函数则不进行初始化
Person* p1 = new Person();
cout << g_age << endl;
cout << g_person.m_age << endl;
//cout << person.m_age << endl;
cout << p0->m_age << endl;
cout << p1->m_age << endl;
return 0;
}
/*输出:
0
0
-842150451
0
*/
构造函数的相互调用
struct Person
{
int m_age;
int m_height;
Person() :Person(0,0)
{
//Person(0,0)//不可,此命令汇编申请临时对象,而非调用
}
Person(int age,int height):m_age(age),m_height(height)
{
}
}
call汇编机械码
- call汇编机械码
E8
直接调用,写死 - call汇编机械码
FF
间接调用,可变
调用多态性虚表的汇编过程
//调用run
Animal *cat = new Cat();
cat->run();
//ebp-8是指针变量cat的地址
//eax存储的是cat对象的地址
move eax,dword ptr [ebp-8]
//取出cat对象的最前面4个字节(虚表地址)给edx
mov edx, dword ptr [eax]
//跳出虚表的最前面4个字节,取出4个字节赋值给eax
//取出cat::run的函数调用地址给eax
mov eax ,dword ptr [edx+4]
//call cat::run
call eax
单例模式(简)
设计模式的一种,保证某个类永远只创建一个对象
- 构造函数私有化
- 定义一个私有的static成员变量指向唯一的那个单例对象
- 提供一个公共的访问单例对象的接口
/* 安全位置小火箭*/
class Rocket
{
private:
Rocket()
{
}
~Rocket()
{
}
static Rocket *ms_rocket;
public:
static Rocket *sharedRocket()
{
//这里要考虑多线程的安全
if(ms_recket == NULL)
{
ms_rocket = new Rocket();
}
return ms_rocket;
}
static void deleteRocket()
{
if(ms_rocket != NULL)
{
delete ms_rocket;
ms_rocket = NULL;
}
}
}
const成员
- const成员:被const修饰的成员变量,非静态成员函数
- const成员变量
- 必须初始化(类内部初始化)可以在声明的时候直接初始化赋值
- 非static的const成员变量还可以再初始化列表中初始化
- const成员函数(非静态)
- const关键字写在参数列表后面,函数的声明和实现必须带const
- 内部不能修改非static成员函数
- 内部只能调用const成员函数,static成员函数
- 非const成员函数可以调用const成员函数
- const成员函数和非const成员函数构成重载
- 非const对象(指针)优先调用非const成员函数
- const对象(指针)只能调用const成员函数,static成员函数
拷贝函数
- 利用已经存在的car3对象创建一个car3新对象
- car4初始化会调用拷贝构造函数
class Car
{
public:
int m_price;
int m_length;
car(int price = 0, int length = 0):m_price(price),m_length(length)
{
cout<< "Car(int price = 0,int length = 0)"<<endl;
}
//拷贝构造函数(会默认生成浅拷贝构造函数)
Car(const Car &car):m_price(car.m_price),m_length(car.m_length)
{
cout <<"Car(const Car &car)"<<endl;
// m_price = car.m_price;
// m_length = car.m_length;
}
void display()
{
cout<< "price = " << m_price << " , length="<<m_length << endl;
}
}
Car car3(100,5);
Car car4(car3);
car4.display();
浅拷贝(shallow copy):指针类型的变量只会拷贝地址值
深拷贝(deep copy):将指针指向的内容拷贝到新的存储空间
class Car
{
int m_price;
char *m_name;
void copy(const char*name = NULL)
{
if(name == NULL)
return ;
}
//申请新的存储空间
m_name = new char[strlen(name)+1] {};
strcpy(m_name,name);
}
浅,深拷贝
- 编译器默认的提供的拷贝是
浅拷贝
(shallow copy) - 将一个对象中所有成员变量的值拷贝到另一个对象
- 如果某个成员变量是个指针,只会拷贝指针中存储的地址值,并不会拷贝指针指向的内存空间
- 可能会导致堆空间多次free的问题
- 如果需要实现
深拷贝
(deep copy),就需要自定义拷贝构造函数 - 将指针类型的成员变量所指向的内存空间,拷贝到新的内存空间
对象类型的参数和返回值
使用对象类型作为函数的参数或者返回值,可能会产生一些不必要的中间对象,返回引用可避免中间值
匿名对象
匿名对象:没有变量名,没有被指针指向的对象,用完后马上调用析构
car();
//一次性对象,调用完之后立即释放空间
隐式构造/转换构造
Person p1 = 20;
//等同于
Person p2(20);
//都会调用单构造函数
//隐式构造
Person test2()
{
return 40; //隐式构造,会将40传递给Person单函数对象
}
可以通过关键字explicit在定义构造函数时禁止掉隐式构造
编译器何时自动生成构造函数
virtual function table在汇编显示:vftable
- 成员变量在声明的同时进行了初始化
- 有定义虚函数
- 虚继承其他类
- 包含了对象类型的成员,且这个成员有构造函数(编译器生成或自定义)
- 父类有构造函数(编译器生成或自定义)
- 对象创建后,需要做一些额外操作时(比如内存操作,函数调用),编译器一般都会为期生成构造函数
内部类
如果将类A定义在类C的内部,那么类A就是一个内部类(嵌套类)
- 内部类的特点
- 支持public,protected,private权限
- 成员函数可以直接访问其外部类对象的所有成员(反过来不行)
- 成员函数可以直接不带类名,对象名访问其外部类的static成员
- 不会影响外部类的内存布局
局部类
在一个
函数内部
定义的类,称为局部类
- 局部域仅限于所在的函数内部
- 其所有的成员必须定义在类内部,不允许定义static成员变量
- 成员函数不能直接访问函数的局部变量(static变量除外)
- 在函数内部定义的类如果未调用类和函数,并不会每次调用函数都调用类
类型转换
- C语言风格类型转换符
- (type)expression
- type(expression)
int a = 10;
double d = (double) a;
double d2 = double(a);
- C++中有
4
个类型转换符- static_cast
- dynamic_cast
- reinterpret_cast
- const_cast
- 使用格式:xx_cast(expression)
int a = 10;
double d = static_cast<double>(a);
const_cast
一般用于
去
除const
属性,将const
转换为非const
#include <iostream>
using namespace std;
class Person
{
};
int main(void)
{
const Person* p1 = new Person();
//实现去除const属性的强制类型转换
Person* p2 = const_cast<Person*>(p1);
Person* p3 = (Person*)p1;
//汇编语言相同
return 0;
}
/*
12: Person* p2 = const_cast<Person*>(p1);
00007FF773E2197C 48 8B 45 08 mov rax,qword ptr [rbp+8]
00007FF773E21980 48 89 45 28 mov qword ptr [rbp+28h],rax
13: Person* p3 = (Person*)p1;
00007FF773E21984 48 8B 45 08 mov rax,qword ptr [rbp+8]
00007FF773E21988 48 89 45 48 mov qword ptr [rbp+48h],rax
*/
dynamic_cast
一般用于
多态
类型的转换,有运行
时安全检测
#include <iostream>
using namespace std;
class Person
{
virtual void run()
{
}
};
class Student : public Person
{
};
class Car
{
};
int main(void)
{
Person* p1 = new Person();
Person* p2 = new Student();
cout << "P1 = " << p1 << endl;
cout << "P2 = " << p2 << endl;
//用于多态类型转换student和Person没有多态没有虚函数会报错
//不太安全,Person赋值给student,子类赋值给父类,不安全
Student* stu1 = dynamic_cast<Student *>(p1); //不安全
Student* stu2 = dynamic_cast<Student *>(p2); //安全
//其它方式会把地址完全转过来
cout << "stu1 = " << stu1 << endl; //不安全会清空,安全检测
cout << "stu2 = " << stu2 << endl;
return 0;
}
/*
P1 = 0000019F7CD6C8B0
P2 = 0000019F7CD6C2C0
stu1 = 0000000000000000 //p1赋值失败
stu2 = 0000019F7CD6C2C0 //p2成功
*/
static_cast
- 对比dynamic_cast,缺乏运行时安全检测
不能交叉转换
(不是同一继承体系的,无法转换)- 常用语
基本数据类型
的转换,非const转成const
- 适用较广
Person* p1 = new Person();
Car* c1 = dynamic_cast<Car*>(p1);
//报错,交叉转换不允许car和Person没有任何关系
//Car* c2 = static_cast<Car*>(p1);
//基本类型转换
int a = 10;
double d = static_cast<double>(a);
return 0;
reinterpret_cast
- 属于比较
底层的强制转换
,没有任何类型检查和格式转换,仅仅是简单的二进制数据拷贝
可以交叉转换
可以将指针和整数相互转换
#include <iostream>
using namespace std;
int main(void)
{
int a = 10;
//小端模式从后面开始读
//二进制 0000 1010 0000 0000 0000 0000
//十六进制: 0A 00 00 00
//int a = 10;
//double八个字节,double和int并不能简单的二进制copy
// 00 00 00 00 00 00 24 40
double d = reinterpret_cast<double&>(a);
//二进制纯copy
cout << "a= " << a << endl;
cout << "d= " << d << endl;
return 0;
}
/*
a= 10
d= 8.33713e+80
*/
参考资料:《30小时快速精通C++和外挂》
不是同一继承体系的,无法转换)
- 常用语
基本数据类型
的转换,非const转成const
- 适用较广
Person* p1 = new Person();
Car* c1 = dynamic_cast<Car*>(p1);
//报错,交叉转换不允许car和Person没有任何关系
//Car* c2 = static_cast<Car*>(p1);
//基本类型转换
int a = 10;
double d = static_cast<double>(a);
return 0;
reinterpret_cast
- 属于比较
底层的强制转换
,没有任何类型检查和格式转换,仅仅是简单的二进制数据拷贝
可以交叉转换
可以将指针和整数相互转换
#include <iostream>
using namespace std;
int main(void)
{
int a = 10;
//小端模式从后面开始读
//二进制 0000 1010 0000 0000 0000 0000
//十六进制: 0A 00 00 00
//int a = 10;
//double八个字节,double和int并不能简单的二进制copy
// 00 00 00 00 00 00 24 40
double d = reinterpret_cast<double&>(a);
//二进制纯copy
cout << "a= " << a << endl;
cout << "d= " << d << endl;
return 0;
}
/*
a= 10
d= 8.33713e+80
*/
参考资料:《30小时快速精通C++和外挂》
https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html ↩︎