@TOC
学习视频: 你了解每一行代码的本质么?利用汇编挖掘本质
Visual Studio 查看反汇编
在要执行的程序中打个断点(F9),点击开始调试(F5),右键 > 转到反汇编(CTRL + K, G)
程序的本质
通常,CPU 会先将内存中的数据存储到寄存器中,然后再对寄存器中的数据进行运算
假设内存中有块红色内存空间的值是 3,现在想把它的值加 1,并将结果存储到蓝色内存空间
- CPU 首先会将红色内存空间的值放到 EAX 寄存器中:
mov eax, 红色内存空间
- 然后让 EAX 寄存器与 1 相加:
add eax, 1
- 最后将值赋值给内存空间:
mov 蓝色内存空间, eax
总结:程序中任何操作都是在 CPU 中进行的,哪怕内存中的两个变量进行运算,也会先拿到 CPU 中进行计算,然后再放回内存空间。
编程语言的发展
编程语言的发展史:
- 机器语言:由 0 和 1 组成
- 汇编语言:用符号代替了 0 和 1,比机器语言便于阅读和记忆
- 高级语言: C \ C++ \ Java \ JavaScript \ Python 等,更接近人类自然语言
例如对于这个操作:将寄存器 BX 的内容送入寄存器 AX
- 机器语言:1000100111011000
- 汇编语言:
MOV AX, BX
- 高级语言:
AX = BX;
高级语言不允许直接操作寄存器,以上仅仅是举个例子
高级语言的运行步骤:
- 汇编语言与机器语言一一对应,每一条机器指令都有与之对应的汇编指令
- 汇编语言可以通过编译得到机器语言,机器语言可以通过反汇编得到汇编语言
- 高级语言可以通过编译得到汇编语言 / 机器语言,但汇编语言 / 机器语言几乎不可能还原成高级语言
验证汇编语言 / 机器语言不能还原成高级语言
对于以下两段 C++ 代码,分别查看它们生成的汇编
struct Date { int year; int month; int day; }; Date d = { 1, 2, 3 };
int array[] = { 1, 2, 3 };
可以发现,两段不同的 C++ 代码,生成的汇编一模一样,因此汇编语言几乎不可能准确的还原成高级语言。
有一些软件可以实现汇编转高级语言,只能展现一些基本的逻辑代码。
CPU 架构不同,生成的汇编也是不同的(之前有说过,程序其实都是通过 CPU 执行的)
一些编程语言的本质区别
C++:可以轻易的反汇编
JavaScript:脚本语言,由浏览器进行解析
PHP:脚本语言,由 Zend Engine (ZE) 进行解析
Java:由 JVM 进行装载字节码
以上介绍可以将编程语言总结为三类:
- 编译型的语言(不依赖虚拟机):C \ C++ \ OC \ Swift
- 编译型的语言(依赖于虚拟机):Java \ Ruby
- 脚本语言:Python \ JavaScript
代码的执行效率
if-else
和 switch
,谁的效率高?
int no = 4;
if (no == 1) {
printf("no is 2");
} else if (no == 2) {
printf("no is 2");
} else if (no == 3) {
printf("no is 3");
} else if (no == 4) {
printf("no is 4");
} else if (no == 5) {
printf("no is 5");
} else {
printf("other no");
}
int no = 4;
switch (no) {
case 1:
printf("no is 1");
break;
case 2:
printf("no is 2");
break;
case 3:
printf("no is 3");
break;
case 4:
printf("no is 4");
break;
case 5:
printf("no is 5");
break;
default:
printf("other no");
break;
}
定义一个变量,来计算代码的运行时间,这种方法是 “事后统计法”,它的缺点很显著:
- 严重依赖机器的性能
- 需要编写额外的测试代码
汇编代码简单知识:
cmp
:compare,比较jne
:jump not equal,不相等才跳转jmp
:jump,无条件跳转
if-else 反汇编
if-else 反汇编的情况是固定的,不会随着条件多少变化:
- 每个 if 语句都会经历
cmp
比较操作,然后进行jne
,不相等才跳转 可见越后面满足条件的 if 语句,代码执行效率越低
将可能性大的 if 条件提前,可以优化代码效率
int no = 4;
00007FF793D95FAB mov dword ptr [no],4
if (no == 1) {
00007FF793D95FB2 cmp dword ptr [no],1
// jne 后面的地址,代表跳转到下个执行的地方,即 else if (no == 2) 处地址
00007FF793D95FB6 jne test0+36h (`07FF793D95FC6h`)
printf("no is 2");
00007FF793D95FB8 lea rcx,[string "no is 2" (07FF793D9AC28h)]
00007FF793D95FBF call printf (07FF793D91190h)
// 07FF793D96022 这个地址已经跳出 if-else 语句
00007FF793D95FC4 jmp test0+92h (07FF793D96022h)
} else if (no == 2) {
`00007FF793D95FC6` cmp dword ptr [no],2
00007FF793D95FCA jne test0+4Ah (07FF793D95FDAh)
printf("no is 2");
00007FF793D95FCC lea rcx,[string "no is 2" (07FF793D9AC28h)]
00007FF793D95FD3 call printf (07FF793D91190h)
00007FF793D95FD8 jmp test0+92h (07FF793D96022h)
} else if (no == 3) {
00007FF793D95FDA cmp dword ptr [no],3
00007FF793D95FDE jne test0+5Eh (07FF793D95FEEh)
printf("no is 3");
00007FF793D95FE0 lea rcx,[string "no is 3" (07FF793D9AC38h)]
00007FF793D95FE7 call printf (07FF793D91190h)
00007FF793D95FEC jmp test0+92h (07FF793D96022h)
} else if (no == 4) {
00007FF793D95FEE cmp dword ptr [no],4
00007FF793D95FF2 jne test0+72h (07FF793D96002h)
printf("no is 4");
00007FF793D95FF4 lea rcx,[string "no is 4" (07FF793D9AC48h)]
00007FF793D95FFB call printf (07FF793D91190h)
00007FF793D96000 jmp test0+92h (07FF793D96022h)
} else if (no == 5) {
00007FF793D96002 cmp dword ptr [no],5
00007FF793D96006 jne test0+86h (07FF793D96016h)
printf("no is 5");
00007FF793D96008 lea rcx,[string "no is 5" (07FF793D9AC58h)]
00007FF793D9600F call printf (07FF793D91190h)
} else {
00007FF793D96014 jmp test0+92h (07FF793D96022h)
printf("other no");
00007FF793D96016 lea rcx,[string "other no" (07FF793D9AC68h)]
00007FF793D9601D call printf (07FF793D91190h)
}
switch 反汇编
条件比较少的情况
int no = 4;
switch (no) {
case 1:
printf("no is 1");
break;
case 2:
printf("no is 2");
break;
default:
printf("other no");
break;
}
int age = 4;
反汇编分析:
- 条件比较少的情况,编译器不会生成优化代码,代码和 if-else 一样,与每个 case 的值进行比较
- 此时 switch 和 if-else 效率差不多
switch (no) {
00BB1A3C mov eax,dword ptr [no]
00BB1A3F mov dword ptr [ebp-0DCh],eax
00BB1A45 cmp dword ptr [ebp-0DCh],1
00BB1A4C je _$EncStackInitStart+3Dh (0BB1A59h)
00BB1A4E cmp dword ptr [ebp-0DCh],2
00BB1A55 je _$EncStackInitStart+4Ch (0BB1A68h)
00BB1A57 jmp _$EncStackInitStart+5Bh (0BB1A77h)
case 1:
printf("no is 1");
00BB1A59 push offset string "no is 1" (0BB7B6Ch)
00BB1A5E call _printf (0BB10CDh)
00BB1A63 add esp,4
break;
00BB1A66 jmp _$EncStackInitStart+68h (0BB1A84h)
case 2:
printf("no is 2");
00BB1A68 push offset string "no is 2" (0BB7B30h)
00BB1A6D call _printf (0BB10CDh)
00BB1A72 add esp,4
break;
00BB1A75 jmp _$EncStackInitStart+68h (0BB1A84h)
default:
printf("other no");
00BB1A77 push offset string "other no" (0BB7B60h)
00BB1A7C call _printf (0BB10CDh)
00BB1A81 add esp,4
break;
}
条件比较多的情况
case 值连续
反汇编分析:
- switch 在一开始执行了多句汇编,用于计算
jmp
的地址 - 不存在越后面满足条件的 case 语句效率越低的情况
int no = 5;
00451AF5 mov dword ptr [no],5
switch (no) {
00451AFC mov eax,dword ptr [no]
00451AFF mov dword ptr [ebp-0DCh],eax
00451B05 mov ecx,dword ptr [ebp-0DCh]
00451B0B sub ecx,1
00451B0E mov dword ptr [ebp-0DCh],ecx
00451B14 cmp dword ptr [ebp-0DCh],4
00451B1B ja $LN8+0Fh (0451B75h)
00451B1D mov edx,dword ptr [ebp-0DCh]
00451B23 jmp dword ptr [edx*4+451BA0h]
case 1:
printf("no is 1");
00451B2A push offset string "no is 1" (0457B6Ch)
00451B2F call _printf (04510CDh)
00451B34 add esp,4
break;
00451B37 jmp $LN8+1Ch (0451B82h)
case 2:
printf("no is 2");
00451B39 push offset string "no is 2" (0457B30h)
00451B3E call _printf (04510CDh)
00451B43 add esp,4
break;
00451B46 jmp $LN8+1Ch (0451B82h)
case 3:
printf("no is 3");
00451B48 push offset string "no is 3" (0457B3Ch)
00451B4D call _printf (04510CDh)
00451B52 add esp,4
break;
00451B55 jmp $LN8+1Ch (0451B82h)
case 4:
printf("no is 4");
00451B57 push offset string "no is 4" (0457B48h)
00451B5C call _printf (04510CDh)
00451B61 add esp,4
break;
00451B64 jmp $LN8+1Ch (0451B82h)
case 5:
printf("no is 5");
00451B66 push offset string "no is 5" (0457B54h)
00451B6B call _printf (04510CDh)
00451B70 add esp,4
break;
00451B73 jmp $LN8+1Ch (0451B82h)
default:
printf("other no");
00451B75 push offset string "other no" (0457B60h)
00451B7A call _printf (04510CDh)
00451B7F add esp,4
break;
}
细节研究: 这种情况下是如何计算并跳转的
jmp 451BB0h
:直接跳转到 451BB0h 这个地址所对应的代码jmp [451BB0h]
:去 451BB0h 这个地址的内存空间取出一个地址值,再跳转到取出的地址对应的代码
int no = 5;
00451AF5 mov dword ptr [no],5
switch (no) {
00451AFC mov eax,dword ptr [no]
00451AFF mov dword ptr [ebp-0DCh],eax
00451B05 mov ecx,dword ptr [ebp-0DCh]
// ecx == no == 5
// ecx = ecx - 1
00451B0B sub ecx,1
00451B0E mov dword ptr [ebp-0DCh],ecx
// no不在case的范围中的情况
00451B14 cmp dword ptr [ebp-0DCh],4
00451B1B ja $LN8+0Fh (0451B75h)
// edx == ecx == 4
00451B1D mov edx,dword ptr [ebp-0DCh]
// edx * 4 + 451BA0h == 4 * 4 + 451BA0h == 451BB0h
// jmp [451BB0h]
// jmp 451BB0h 这个地址存储的地址值
00451B23 jmp dword ptr [edx*4+451BA0h]
- switch 跳转前其实已经将每个 case 代码的首地址算好并存储,并且其之间相差 4 字节
- no 是否在 case 范围的计算:比较
no - min
与max - min
,min
和max
是 case 的最小最大值 以上汇编使用的公式:
jmp [(no-x) * 4 + 某个地址]
,x
是 switch 中最小的 case 的值所以 switch 中 case 乱序对效率没有影响!这点和 if-else 不同
总结:case 值连续的情况下,每个 case 代码的地址值已经在内存中提前存好,利用公式 (no-x) * 4 + 某地址
直接跳转。哪怕有 100 个 case,只要连续,也是以上流程。相比 if-else 最坏可能要算 100 次,switch 效率更高。
case 值不连续
case 值不连续的情况,其实算法和连续是一样的。
区别是:会把最小的 case 到最大的 case 跳转的地址先算出来(4 个字节)。
例如给的是 case 1、3、5、6,其实提前算好值是 case 1、2、3、4、5、6,其中 2、4 对应跳转到 default。
case 值跨度很大
提前将 case1、case2 ... case12 的要加的值存储在内存中(用 1 个字节存储)
no 是否在 case 范围的计算, 与之前一样:比较no - min
与max - min
,min
和max
是 case 的最小最大值。因此如果输入 100,经过判断不在 case 范围内,直接跳转 default
特殊情况:跨度极其大,case 1,2,3,4,10000 这种,那么 switch 就无法做优化,其底层和 if-else 是一样的,每个条件进行比较再判断是否跳转。
switch 底层本质上是空间换时间的优化,跨度极其大情况下的空间换时间是很亏的事情
总结
对比 if-else,编译器会对 switch 做一定的优化,提高执行效率。
a++ 和 ++a
++a 反汇编
int a = 5;
int b = ++a + 2;
反汇编分析:
int a = 5;
00291F55 mov dword ptr [a],5
int b = ++a + 2;
// eax = a, eax == 5
00291F5C mov eax,dword ptr [a]
// eax = eax + 1, eax == 6
00291F5F add eax,1
// a = eax, a == 6
00291F62 mov dword ptr [a],eax
// ecx = a, ecx == 6
00291F65 mov ecx,dword ptr [a]
// ecx = ecx + 2, ecx == 8
00291F68 add ecx,2
// b = ecx, b == 8
00291F6B mov dword ptr [b],ecx
a++ 反汇编
int a = 5;
int b = a++ + 2;
反汇编分析:
int a = 5;
00241F55 mov dword ptr [a],5
int b = a++ + 2;
// eax = a, eax == 5
00241F5C mov eax,dword ptr [a]
// eax = eax + 2, eax == 7
00241F5F add eax,2
// b = eax, b == 7
00241F62 mov dword ptr [b],eax
// ecx = a, ecx == 5
00241F65 mov ecx,dword ptr [a]
// ecx = ecx + 1, exc == 6
00241F68 add ecx,1
// a = ecx, a == 6
00241F6B mov dword ptr [a],ecx
构造函数
构造函数(也叫构造器),在对象创建的时候自动调用,一般用于完成对象的初始化工作
简单使用:
class Person {
public:
int m_age;
void run() {
cout << "age is " << m_age << ", run()------" << endl;
}
// 构造函数
Person() {
m_age = 0;
cout << "Person()------" << endl;
}
Person(int age) {
m_age = age;
cout << "Person(int age)------" << endl;
}
};
Person *p = new Person();
p->run();
Person *p1 = new Person();
p1->run();
Person *p2 = new Person();
p2->run();
构造函数反汇编
这种情况下,我们手动书写了 Car()
的无参构造函数。
class Car {
public:
int m_price;
Car() {
cout << "Car()" << endl;
}
};
int main() {
Car *car = new Car();
cout << car->m_price << endl;
return 0;
}
反汇编:可以发现代码中有 call Car::Car (07FF7E5D0123Fh)
,确实调用了汇编
Car *car = new Car();
00007FF7E5D027FB mov ecx,4
00007FF7E5D02800 call operator new (07FF7E5D0104Bh)
00007FF7E5D02805 mov qword ptr [rbp+108h],rax
00007FF7E5D0280C cmp qword ptr [rbp+108h],0
00007FF7E5D02814 je main+4Bh (07FF7E5D0282Bh)
00007FF7E5D02816 mov rcx,qword ptr [rbp+108h]
// 此处调用了构造函数
00007FF7E5D0281D call Car::Car (07FF7E5D0123Fh)
00007FF7E5D02822 mov qword ptr [rbp+118h],rax
00007FF7E5D02829 jmp main+56h (07FF7E5D02836h)
00007FF7E5D0282B mov qword ptr [rbp+118h],0
00007FF7E5D02836 mov rax,qword ptr [rbp+118h]
00007FF7E5D0283D mov qword ptr [rbp+0E8h],rax
00007FF7E5D02844 mov rax,qword ptr [rbp+0E8h]
00007FF7E5D0284B mov qword ptr [car],rax
下面这种情况,我们没有手动写构造函数:
class Car {
public:
int m_price;
};
int main() {
Car *car = new Car();
return 0;
}
反汇编:可以发现没有调用call Car::Car
,显然没有生成无参构造函数
Car *car = new Car();
00512495 push 4
00512497 call operator new (0511140h)
0051249C add esp,4
0051249F mov dword ptr [ebp-0D4h],eax
005124A5 cmp dword ptr [ebp-0D4h],0
005124AC je __$EncStackInitStart+4Ah (05124C6h)
005124AE xor eax,eax
005124B0 mov ecx,dword ptr [ebp-0D4h]
005124B6 mov dword ptr [ecx],eax
005124B8 mov edx,dword ptr [ebp-0D4h]
005124BE mov dword ptr [ebp-0DCh],edx
005124C4 jmp __$EncStackInitStart+54h (05124D0h)
005124C6 mov dword ptr [ebp-0DCh],0
005124D0 mov eax,dword ptr [ebp-0DCh]
005124D6 mov dword ptr [car],eax
结论:
可以理解为,要做一些初始化操作的时候才会生成构造函数,没有任何操作就无需生成
在 Java 中,默认会给成员变量赋初值,默认应该是会生成构造函数的
以下情况下会生成构造函数,可以反汇编验证(这里不放汇编代码了):
- 成员变量在声明的同时进行了初始化,会生成构造函数
class Car {
public:
int m_price = 5;
};
int main() {
Car* c = new Car();
return 0;
}
- 包含了对象类型的成员,且这个成员有构造函数,会生成构造函数
class Car {
public:
int m_price = 5;
Car() {
cout << "Car()---" << endl;
}
};
class Person {
Car car;
};
int main() {
Person* p = new Person();
return 0;
}
- 父类有构造函数,子类没有构造函数,子类会生成构造函数,在里面调用父类的构造函数
父子都有构造函数时,创建子类对象 会先调用父类的构造函数,再调用自己的构造函数
class Person {
public:
Person() {}
};
class Student : public Person {
public:
};
int main() {
Student* stu = new Student();
}
函数和方法
很多开发者都会这样去定义
- 方法是定义在类里面的
- 函数是定义在类外面的
在汇编层面看来,函数和方法没有任何区别,都存储在代码区,都是 call
一个内存地址
函数的内存布局
调用一个函数,会开辟一段栈空间给函数
寄存器:
- esp:永远指向栈顶,push 和 pop 操作会自动控制其指向
push 操作会往栈顶新增数据,同时 esp 指向其内存地址
pop 会弹出栈顶数据,同时 esp 指针指向上一个地址,被弹出数据的地址中数据仍在,相当于垃圾数据,等待以后 push 的数据将其覆盖。
初始
--------------------
esp -> 0x2009 |
--------------------
执行 push 4
--------------------
esp -> 0x2005 4 |
0x2009 |
--------------------
再执行 push 5
--------------------
esp -> 0x2001 5 |
0x2005 4 |
0x2009 |
--------------------
执行 pop eax, 此时相当于 eax = 5
栈顶的内存空间不再被指向,相当于垃圾数据,等待被覆盖
--------------------
0x2001 5(此时这段内存相当于垃圾数据)
esp -> 0x2005 4 |
0x2009 |
--------------------
执行 pop ebx, 此时相当于 ebx = 4
--------------------
0x2001 5(此时这段内存相当于垃圾数据)
0x2005 4(此时这段内存相当于垃圾数据)
esp -> 0x2009 |
--------------------
执行 push 3, 把之前等待被覆盖的垃圾数据覆盖了
--------------------
0x2001 5(此时这段内存相当于垃圾数据)
esp -> 0x2005 3 |
0x2009 |
--------------------
- ebp:永远指向栈底
- 栈指针寄存器
栈平衡:函数调用前后其栈顶指针指向是相同的,即调用完后栈 esp 指针会回到原来位置
栈空间是系统不断覆盖读写的,不存在释放操作
void test1(int v1, int v2) {
}
递归函数
递归的层次太深会导致栈空间不够用,栈空间溢出