0%

Cpp基础知识+底层逆向

cpp 对象

C++ 的每一个成员函数在 class 中声明,但是却不出现在每个对象中:

  • 每一个非内联的成员函数,只会诞生一个函数实例
  • 每个内联函数,会在其每一个使用者身上产生一个函数实例

C++ 的类就相当于一个数据结构体加上多个函数:

1
Class = data structure + code (methods)

This 指针

This 指针就是指向实例对象自己的指针

案例一:This 的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include<iostream>
#include<string.h>
using namespace std;

class Base{
public:
void fun(){
cout<<name<<endl;
}
char name[8];
};

class A : public Base{
public:
void foo(){
strcpy(this->name,"A");
this->fun(); // 相当于fun
}
};

class B : public Base{
public:
void foo(){
strcpy(this->name,"B");
this->fun();
}
};

int main(void){
A *a = new A();
B *b = new B();
a->foo();
b->foo();
}
  • 在调用类函数时,会将其 new 出来的堆内存当做第一个参数传入(相当于传入了该对象的数据结构体)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int __cdecl main(int argc, const char **argv, const char **envp)
{
A *v3; // rax
B *v4; // rax
A *a; // [rsp+0h] [rbp-10h]
B *b; // [rsp+8h] [rbp-8h]

v3 = (A *)operator new(8uLL);
*v3 = 0LL;
a = v3;
v4 = (B *)operator new(8uLL);
*v4 = 0LL;
b = v4;
A::foo(a);
B::foo(b);
return 0;
}
  • 使用 This 指针,可以快速操控本实例对象的各个成员:
1
2
3
4
5
void __cdecl A::foo(A *const this)
{
*(_WORD *)this->name = 65; // 编译器优化了
Base::fun(this);
}

重载

C++ 允许在同一作用域中的某个函数和运算符指定多个定义,分别称为:

  • 函数重载
  • 运算符重载

函数重载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
using namespace std;

class Test{
public:
void print(int i) {
cout<<"int:"<<i<<endl;
}
void print(double f) {
cout<<"double:"<<f<<endl;
}
};

int main(void)
{
Test *t = new Test;
t->print(1);
t->print(1.1);
}
  • 对于类函数来说,编译器会把 [类名称,函数名称,参数列表] 放入哈希函数转化为一个哈希值,并用这个哈希值来当做函数的名称:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
; int __cdecl main(int argc, const char **argv, const char **envp)
public main
main proc near

f= qword ptr -18h
t= qword ptr -8

; __unwind {
push rbp
mov rbp, rsp
sub rsp, 20h
mov edi, 1 ; unsigned __int64
call __Znwm ; operator new(ulong)
mov [rbp+t], rax
mov rax, [rbp+t]
mov esi, 1 ; i
mov rdi, rax ; this
call _ZN4Test5printEi ; Test::print(int)
mov rdx, cs:qword_2018
mov rax, [rbp+t]
mov [rbp+f], rdx
movsd xmm0, [rbp+f] ; f
mov rdi, rax ; this
call _ZN4Test5printEd ; Test::print(double)
mov eax, 0
leave
retn
; } // starts at 11BA
main endp
  • 在汇编代码中,程序调用的函数已经确定
  • 那么编译器可能是在语法分析时,就通过参数列表确定了应该调用的函数

运算符重载(内部):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <iostream>
using namespace std;

class Box
{
public:
double getVolume(void){
return length * breadth * height;
}
void setAll(double len,double bre,double hei){
length = len;
breadth = bre;
height = hei;
}
Box operator + (const Box& b){ /* 重载运算符'+'(只有'+'两边都是Box类型时,才会触发该函数) */
Box box;
box.length = this->length + b.length;
box.breadth = this->breadth + b.breadth;
box.height = this->height + b.height;
return box;
}
double length;
double breadth;
double height;
};

int main(){
Box Box1;
Box Box2;
Box Box3;
double volume = 0.0;

Box1.setAll(1.0,1.0,1.0);
Box2.setAll(2.0,2.0,2.0);

volume = Box1.getVolume();
volume = Box2.getVolume();
Box3 = Box1 + Box2;
volume = Box3.getVolume();

return 0;
}
  • 在一个类中重载运算符过后,其作用范围为整个文件,以及引入该类的其他文件
  • 本质上重载运算符就是调用对应的函数(其参数和返回值必须符合条件)
1
2
3
4
5
6
7
Box::setAll(&Box1, 1.0, 1.0, 1.0);
Box::setAll(&Box2, 2.0, 2.0, 2.0);
Box::getVolume(&Box1);
volume = Box::getVolume(&Box2);
Box::operator+(&v4, &Box1, &Box2);
Box3 = v4;
Box::getVolume(&Box3);
1
2
3
4
5
6
7
Box *__cdecl Box::operator+(Box *retstr, Box *const this, const Box *b)
{
retstr->length = b->length + this->length;
retstr->breadth = b->breadth + this->breadth;
retstr->height = b->height + this->height;
return retstr;
}
  • PS:Cpp 库中也有许多运算符重载的案例:new<<>>

运算符重载(外部):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <iostream>
using namespace std;

class Box
{
public:
double getVolume(void){
return length * breadth * height;
}
void setAll(double len,double bre,double hei){
length = len;
breadth = bre;
height = hei;
}
double length;
double breadth;
double height;
};

Box operator + (const Box& a,const Box& b){
Box box;
box.length = a.length + b.length;
box.breadth = a.breadth + b.breadth;
box.height = a.height + b.height;
return box;
}

int main(){
Box Box1;
Box Box2;
Box Box3;
double volume = 0.0;

Box1.setAll(1.0,1.0,1.0);
Box2.setAll(2.0,2.0,2.0);

volume = Box1.getVolume();
volume = Box2.getVolume();
Box3 = Box1 + Box2;
volume = Box3.getVolume();

return 0;
}
  • 在全局重载运算符过后,其作用范围就是全局(但可以被命名空间限制)
1
2
3
4
5
6
7
Box::setAll(&Box1, 1.0, 1.0, 1.0);
Box::setAll(&Box2, 2.0, 2.0, 2.0);
Box::getVolume(&Box1);
volume = Box::getVolume(&Box2);
operator+(&v4, &Box1, &Box2);
Box3 = v4;
Box::getVolume(&Box3);
1
2
3
4
5
6
7
Box *__cdecl operator+(Box *retstr, const Box *a, const Box *b)
{
retstr->length = b->length + a->length;
retstr->breadth = b->breadth + a->breadth;
retstr->height = b->height + a->height;
return retstr;
}

内联函数

在类的声明内部声明和定义的函数叫做内联成员函数

  • 内联函数类似于宏函数,但内联函数是在编译时展开,而宏在预编译时展开
  • 内联函数的定义和使用必须在同一文件,因此最好将内联函数定义放在头文件中

定义在类中的成员函数默认都是内联的,类外定义则要加上 inline(类的成员函数是指那些把定义和原型写在类定义内部的函数)

1
2
3
4
5
6
7
class Test{
public:
void setA(int _a); // 普通函数
void setB(int _b){b = _b;} // 隐式的内联函数
inline void setC(int _c); // 显式的内联函数
int a,b,c;
};
  • 在 IDA 中分析或者在 GDB 中调试,都发现函数没有内联成功(还是当成普通文件来调用),可能是编译器的原因

构造函数

以下几种情况下,会合成有用的构造函数:

  • 带有默认构造函数的成员对象(例如:string 类)
  • 一个派生类的父类带有构造函数(或者父类的成员对象带有默认构造函数),那么子类:
    • 如果没有定义构造函数,则会合成默认构造函数
    • 如果有构造函数,但是没有调用父类的构造函数,则编译器会插入一些代码调用父类的默认构造函数
  • 带有一个虚函数的类
    • 类声明(或继承)一个虚函数
    • 类派生自一个继承串链,其中有一个或更多的虚基类

案例一:带有默认构造函数的成员对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>  
#include <string>

using namespace std;

class Test
{
public:
string name;
};

int main(int argc, char* argv[])
{
Test t;
return 0;
}
  • IDA 分析:
1
2
3
4
5
6
7
8
9
10
int __cdecl main(int argc, const char **argv, const char **envp)
{
Test t; // [rsp+10h] [rbp-40h] BYREF
unsigned __int64 v5; // [rsp+38h] [rbp-18h]

v5 = __readfsqword(0x28u);
Test::Test(&t);
Test::~Test(&t);
return 0;
}
  • 由于 Test 类中的 string 类带有默认构造函数,因此 Test 类会合成一个构造函数:
1
2
3
4
void __cdecl Test::Test(Test *const this)
{
std::string::basic_string(this);
}

案例二:没有定义构造函数,则会合成默认构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>  
#include <string>

using namespace std;

class Test1{
public:
Test1();
};

class Test2 : public Test1{

};

Test1::Test1(void){
cout << "Test1" << endl;
}

int main(int argc, char* argv[]){
Test2 t;
return 0;
}
  • IDA 分析:
1
2
3
4
5
6
7
8
9
int __cdecl main(int argc, const char **argv, const char **envp)
{
Test2 t; // [rsp+17h] [rbp-9h] BYREF
unsigned __int64 v5; // [rsp+18h] [rbp-8h]

v5 = __readfsqword(0x28u);
Test2::Test2(&t);
return 0;
}
  • 由于 Test2 没有构造函数,但父类 Test1 有构造函数,因此在 Test2 的构造函数中会调用 Test1 的构造函数:
1
2
3
4
void __cdecl Test2::Test2(Test2 *const this)
{
Test1::Test1(this);
}

案例三:如果有构造函数,但是没有调用父类的构造函数,则编译器会插入一些代码调用父类的默认构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>  
#include <string>

using namespace std;

class Test1{
public:
Test1();
};

class Test2 : public Test1{
public:
Test2();
};

Test1::Test1(void){
cout << "Test1" << endl;
}

Test2::Test2(void){
cout << "Test2" << endl;
}

int main(int argc, char* argv[]){
Test2 t;
return 0;
}
  • IDA 分析:
1
2
3
4
5
6
7
8
9
int __cdecl main(int argc, const char **argv, const char **envp)
{
Test2 t; // [rsp+17h] [rbp-9h] BYREF
unsigned __int64 v5; // [rsp+18h] [rbp-8h]

v5 = __readfsqword(0x28u);
Test2::Test2(&t);
return 0;
}
  • 由于在 Test2 的构造函数中没有调用父类 Test1 的构造函数,因此编译器会自动加上 Test2 的构造函数:
1
2
3
4
5
6
7
8
void __cdecl Test2::Test2(Test2 *const this)
{
__int64 v1; // rax

Test1::Test1(this);
v1 = std::operator<<<std::char_traits<char>>(&std::cout, "Test2");
std::ostream::operator<<(v1, &std::endl<char,std::char_traits<char>>);
}

案例四:带有一个虚函数的类

  • 源代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<iostream>
using namespace std;

class Test{
public:
virtual void foo(){
cout<<"Test::foo() is called"<<endl;
}
};

int main(void){
Test *t = new Test();
t->foo();
return 0;
}
  • IDA 分析:
1
2
3
4
5
6
7
8
9
10
int __cdecl main(int argc, const char **argv, const char **envp)
{
Test *v3; // rbx

v3 = (Test *)operator new(8uLL);
v3->_vptr_Test = 0LL;
Test::Test(v3);
(*v3->_vptr_Test)(v3, argv);
return 0;
}
  • 由于 Test 中有虚函数,因此编译器会自动为其生成构造函数:
1
2
3
4
void __cdecl Test::Test(Test *const this)
{
this->_vptr_Test = (int (**)(...))&off_3D70;
}

虚函数

面向对象的语言有三大特性:继承、封装、多态,虚函数就是 cpp 实现多态的方式

  • 多态:指用相同的接口去表示不同的实现

案例一:使用虚函数实现多态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include<iostream>
using namespace std;

class Base{
public:
virtual void foo(){
cout<<"Base::foo() is called"<<endl;
}
};

class A:public Base{
public:
void foo(){
cout<<"A::foo() is called"<<endl;
}
};

class B:public Base{
public:
void foo(){
cout<<"B::foo() is called"<<endl;
}
};

class C:public Base{
public:
void foo(){
cout<<"C::foo() is called"<<endl;
}
};

int main(void){
Base *a = new B();
a->foo(); // B::foo() is called
((A *)a)->foo(); // B::foo() is called
return 0;
}
  • 当使用类的指针调用成员函数时:
    • 普通函数由指针类型决定
    • 虚函数由指针指向的实际类型决定
1
2
3
4
5
6
7
8
9
10
11
int __cdecl main(int argc, const char **argv, const char **envp)
{
B *v3; // rbx

v3 = (B *)operator new(8uLL);
v3->_vptr_Base = 0LL;
B::B(v3);
(*v3->_vptr_Base)(v3, argv);
(*v3->_vptr_Base)(v3);
return 0;
}
  • 这个 _vptr_Base 就是虚指针基址,它将会在对应的构造函数中进行初始化
1
2
3
4
5
void __cdecl B::B(B *const this)
{
Base::Base(this);
this->_vptr_Base = (int (**)(...))&off_3D40;
}
1
2
.data.rel.ro:0000000000003D40 8C 12 00 00 00 00 00 00       off_3D40 dq offset _ZN1B3fooEv          ; DATA XREF: B::B(void)+18↑o
.data.rel.ro:0000000000003D40 ; B::foo(void)

对于拥有虚函数的类,其每个对象均具有一个指向本类虚函数表的指针 _vptr_Base(可以将其理解为虚函数的函数指针)

案例二:虚函数的调用与虚表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include<iostream>
using namespace std;

class Base{
public:
virtual void foo1(){
cout<<"Base::foo1() is called"<<endl;
}
virtual void foo2(){
cout<<"Base::foo2() is called"<<endl;
}
virtual void foo3(){
cout<<"Base::foo3() is called"<<endl;
}
virtual void foo4(){
cout<<"Base::foo4() is called"<<endl;
}
virtual void foo5(){
cout<<"Base::foo5() is called"<<endl;
}
};

int main(void){
Base *a = new Base();
a->foo1();
a->foo2();
a->foo3();
a->foo4();
a->foo5();
}
  • cpp 底层处理虚表的方式就是强转并调用 _vptr_Base + offset
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int __cdecl main(int argc, const char **argv, const char **envp)
{
Base *v3; // rbx
Base *a; // [rsp+8h] [rbp-18h]

v3 = (Base *)operator new(8uLL);
v3->_vptr_Base = 0LL;
Base::Base(v3);
a = v3;
(*v3->_vptr_Base)(v3, argv);
(*((void (__fastcall **)(Base *))a->_vptr_Base + 1))(a);
(*((void (__fastcall **)(Base *))a->_vptr_Base + 2))(a);
(*((void (__fastcall **)(Base *))a->_vptr_Base + 3))(a);
(*((void (__fastcall **)(Base *))a->_vptr_Base + 4))(a);
return 0;
}
  • 再看一个去符号的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
__int64 __fastcall main(int a1, char **a2, char **a3)
{
_QWORD *v3; // rbx
_QWORD *v5; // [rsp+8h] [rbp-18h]

v3 = (_QWORD *)operator new(8uLL);
*v3 = 0LL;
sub_13B4(v3, a2);
v5 = v3;
(*(void (__fastcall **)(_QWORD *))*v3)(v3);
(*(void (__fastcall **)(_QWORD *))(*v5 + 8LL))(v5);
(*(void (__fastcall **)(_QWORD *))(*v5 + 16LL))(v5);
(*(void (__fastcall **)(_QWORD *))(*v5 + 24LL))(v5);
(*(void (__fastcall **)(_QWORD *))(*v5 + 32LL))(v5);
return 0LL;
}
  • 查看虚表,里面装有各个虚函数的首地址:
1
2
3
4
5
6
.data.rel.ro:0000000000003D50 9C 12 00 00 00 00 00 00       off_3D50 dq offset _ZN4Base4foo1Ev      ; DATA XREF: Base::Base(void)+8↑o
.data.rel.ro:0000000000003D50 ; Base::foo1(void)
.data.rel.ro:0000000000003D58 D4 12 00 00 00 00 00 00 dq offset _ZN4Base4foo2Ev ; Base::foo2(void)
.data.rel.ro:0000000000003D60 0C 13 00 00 00 00 00 00 dq offset _ZN4Base4foo3Ev ; Base::foo3(void)
.data.rel.ro:0000000000003D68 44 13 00 00 00 00 00 00 dq offset _ZN4Base4foo4Ev ; Base::foo4(void)
.data.rel.ro:0000000000003D70 7C 13 00 00 00 00 00 00 dq offset _ZN4Base4foo5Ev ; Base::foo5(void)
  • 用 GDB 进行调试:
1
2
3
4
5
6
pwndbg> telescope 0x55883ca88000+0x3D50
00:0000│ rdx 0x55883ca8bd50 —▸ 0x55883ca8929c (Base::foo1()) ◂— push rbp
01:00080x55883ca8bd58 —▸ 0x55883ca892d4 (Base::foo2()) ◂— push rbp
02:00100x55883ca8bd60 —▸ 0x55883ca8930c (Base::foo3()) ◂— push rbp
03:00180x55883ca8bd68 —▸ 0x55883ca89344 (Base::foo4()) ◂— push rbp
04:00200x55883ca8bd70 —▸ 0x55883ca8937c (Base::foo5()) ◂— push rbp

案例三:虚函数的继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include<iostream>
using namespace std;

class Base{
public:
virtual void foo1(){
cout<<"Base::foo1() is called"<<endl;
}
virtual void foo2(){
cout<<"Base::foo2() is called"<<endl;
}
virtual void foo3(){
cout<<"Base::foo3() is called"<<endl;
}
};

class A : public Base{
public:
virtual void fooa(){
cout<<"Base::fooa() is called"<<endl;
}
virtual void foob(){
cout<<"Base::fooa() is called"<<endl;
}
};

class B : public Base{
public:
virtual void fooa(){
cout<<"Base::fooa() is called"<<endl;
}
virtual void foob(){
cout<<"Base::fooa() is called"<<endl;
}
};

int main(void){
A *a = new A();
B *b = new B();
a->foo1();
a->foo2();
b->fooa();
b->foob();
a->fooa();
a->foob();
}
  • 当一个类继承带有虚函数的类时,它会先将父类的虚表复制一份,然后将自己的虚表添加到后面:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int __cdecl main(int argc, const char **argv, const char **envp)
{
A *v3; // rbx
B *v4; // rbx
A *a; // [rsp+0h] [rbp-20h]
B *b; // [rsp+8h] [rbp-18h]

v3 = (A *)operator new(8uLL);
v3->_vptr_Base = 0LL;
A::A(v3);
a = v3;
v4 = (B *)operator new(8uLL);
v4->_vptr_Base = 0LL;
B::B(v4);
b = v4;
(*a->_vptr_Base)(a, argv);
(*((void (__fastcall **)(A *))a->_vptr_Base + 1))(a);
(*((void (__fastcall **)(B *))b->_vptr_Base + 3))(b);
(*((void (__fastcall **)(B *))b->_vptr_Base + 4))(b);
(*((void (__fastcall **)(A *))a->_vptr_Base + 3))(a);
(*((void (__fastcall **)(A *))a->_vptr_Base + 4))(a);
return 0;
}
  • 查看虚表,里面装有各个虚函数的首地址:
1
2
3
4
.data.rel.ro:0000000000003D30 D4 12 00 00 00 00 00 00       off_3D30 dq offset _ZN4Base4foo1Ev      ; DATA XREF: Base::Base(void)+8↑o
.data.rel.ro:0000000000003D30 ; Base::foo1(void)
.data.rel.ro:0000000000003D38 0C 13 00 00 00 00 00 00 dq offset _ZN4Base4foo2Ev ; Base::foo2(void)
.data.rel.ro:0000000000003D40 44 13 00 00 00 00 00 00 dq offset _ZN4Base4foo3Ev ; Base::foo3(void)
1
2
3
4
5
6
.data.rel.ro:0000000000003CF8 D4 12 00 00 00 00 00 00       off_3CF8 dq offset _ZN4Base4foo1Ev      ; DATA XREF: A::A(void)+18↑o
.data.rel.ro:0000000000003CF8 ; Base::foo1(void)
.data.rel.ro:0000000000003D00 0C 13 00 00 00 00 00 00 dq offset _ZN4Base4foo2Ev ; Base::foo2(void)
.data.rel.ro:0000000000003D08 44 13 00 00 00 00 00 00 dq offset _ZN4Base4foo3Ev ; Base::foo3(void)
.data.rel.ro:0000000000003D10 7C 13 00 00 00 00 00 00 dq offset _ZN1A4fooaEv ; A::fooa(void)
.data.rel.ro:0000000000003D18 B4 13 00 00 00 00 00 00 dq offset _ZN1A4foobEv ; A::foob(void)
1
2
3
4
5
6
.data.rel.ro:0000000000003CC0 D4 12 00 00 00 00 00 00       off_3CC0 dq offset _ZN4Base4foo1Ev      ; DATA XREF: B::B(void)+18↑o
.data.rel.ro:0000000000003CC0 ; Base::foo1(void)
.data.rel.ro:0000000000003CC8 0C 13 00 00 00 00 00 00 dq offset _ZN4Base4foo2Ev ; Base::foo2(void)
.data.rel.ro:0000000000003CD0 44 13 00 00 00 00 00 00 dq offset _ZN4Base4foo3Ev ; Base::foo3(void)
.data.rel.ro:0000000000003CD8 EC 13 00 00 00 00 00 00 dq offset _ZN1B4fooaEv ; B::fooa(void)
.data.rel.ro:0000000000003CE0 24 14 00 00 00 00 00 00 dq offset _ZN1B4foobEv ; B::foob(void)

调用约定

C/C++ 函数调用约定,主要是对以下两个方面进行了约定:

  • 当参数个数多于一个时,按照什么顺序把参数压入堆栈(参数的入栈顺序)
  • 函数调用后,由谁来把堆栈恢复原状

常见的调用方式有:

  • C 语言:__cdecl __stdcall __fastcall naked __pascal
  • C++ 语言:__cdecl __stdcall __fastcall naked __pascal __thiscall

下面就分别介绍这几种调用方式:

__stdcall:StandardCall 的缩写,是C++的标准调用方式

  • 使用 PASCAL 宏,WINAPI 宏和 CALLBACK 宏来指定函数的调用方式为 stdcall
1
int _stdcall function(int a, int b); // 明确指定用stdcall
  • 参数从右向左依次压入堆栈
  • 由被调用函数自己来恢复堆栈,称为自动清栈
  • 函数名自动加前导下划线,后面紧跟着一个@,其后紧跟着参数的大小

__cdecl:C Declaration 的缩写,cdecl 调用方式又称为C调用方式,是32位C程序默认的调用方式

1
2
int function(int a, int b) // 不加修饰符就是cdecl
int _cdecl function(int a, int b) // 明确指定用cdecl
  • 参数从右向左依次压入堆栈
  • 由调用者恢复堆栈,称为手动清栈
  • 函数名自动加前导下划线

__fastcall:一种快速调用方式(通过 CPU 寄存器来传递参数),是64位C程序默认的调用方式

1
int fastcall function(int a, int b); // 明确指定用fastcall
  • 前6个参数分别放入 RDI,RSI,RDX,RCX,R8,R9
  • 其余参数从右向左依次压入堆栈
  • 如果需要用栈传参,则使用手动清栈

__thiscall:唯一一个不能明确指明的函数修饰,因为 thiscall 不是关键字,是C++类成员函数默认的调用约定

  • 由于成员函数调用还有一个 this 指针,因此必须特殊处理
  • this 指针将作为第一个参数传入,其余参数处理和 __cdecl/__thiscall 一样(取决于程序是32位还是64位)

__stdcall__cdecl 的不同之处:

  • stdcall 方式是在函数返回时利用 retn x 指令清除栈中的参数
  • cdecl 方式是函数返回后,由调用函数者修改 esp 的值来清除栈中的参数

多重继承

一个派生类如果只继承一个基类,称作单继承,那么如果继承了多个基类,就称作多继承

1
class C:public A,public B{};

1685892862095

  • 优点:派生类通过多重继承,可以得到多个基类的数据和方法,更大程度的实现了代码复用
  • 缺点:可能会导致某个类被重复构造,可能得到重复的基类数据

案例:多重继承导致重复基类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include<iostream>
using namespace std;

class A{
public:
A(int data):ma(data){ cout << "A()" << endl; }
~A(){ cout << "~A()" << endl; }
protected:
int ma;
};
class B :public A{
public:
B(int data):A(data),mb(data+1) { cout << "B()" << endl; }
~B(){ cout << "~B()" << endl; }
protected:
int mb;
};
class C :public A{
public:
C(int data):A(data),mc(data+2) { cout << "C()" << endl; }
~C(){ cout << "~C()" << endl; }
protected:
int mc;
};
class D :public B, public C{
public:
D(int data):B(data),C(data),md(data+3) { cout << "D()" << endl; }
~D(){ cout << "~D()" << endl; }
protected:
int md;
};

int main(){
D* a = new D(10);
return 0;
}
1
2
3
4
5
A()
B()
A()
C()
D()
  • A被构造了两次

虚继承

在继承方式前面加上 virtual 关键字就是虚继承

1
class B:virtual public A{};
  • 如果虚继承类拥有派生类,则构造虚基类的任务将会交给派生类完成

案例:虚继承解决重复基类的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include<iostream>
using namespace std;

class A{
public:
A(int data):ma(data){ cout << "A()" << endl; }
~A(){ cout << "~A()" << endl; }
protected:
int ma;
};
class B :virtual public A{
public:
B(int data):A(data),mb(data+1) { cout << "B()" << endl; }
~B(){ cout << "~B()" << endl; }
protected:
int mb;
};
class C :virtual public A{
public:
C(int data):A(data),mc(data+2) { cout << "C()" << endl; }
~C(){ cout << "~C()" << endl; }
protected:
int mc;
};
class D :public B, public C{
public:
D(int data):A(data),B(data),C(data),md(data+3) { cout << "D()" << endl; }
~D(){ cout << "~D()" << endl; }
protected:
int md;
};

int main(){
D* a = new D(10);
return 0;
}
  • PS:由于B虚继承A,因此派生类D需要完成虚基类A的构造
1
2
3
4
A()
B()
C()
D()

案例:虚基类的构造问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include<iostream>
using namespace std;

class A{
public:
A(int data):ma(data){ cout << "A()" << endl; }
~A(){ cout << "~A()" << endl; }
protected:
int ma;
};
class B :virtual public A{
public:
B(int data):A(data),mb(data+1) { cout << "B()" << endl; }
~B(){ cout << "~B()" << endl; }
protected:
int mb;
};
class C :public B{
public:
C(int data):A(data),B(data),mc(data+2) { cout << "C()" << endl; }
~C(){ cout << "~C()" << endl; }
protected:
int mc;
};
class D :public C{
public:
D(int data):A(data),C(data),md(data+3) { cout << "D()" << endl; }
~D(){ cout << "~D()" << endl; }
protected:
int md;
};

int main(){
D* a = new D(10);
return 0;
}
  • 由于B虚继承A,因此派生类CD都有可能完成虚基类A的构造
  • 通常由最后一级的派生类通常负责构造虚基类

虚基类的内存布局:

1
2
3
4
5
00:00000x55555556aea0 ◂— 0x0
01:00080x55555556aea8 ◂— 0x21 /* '!' */
02:0010│ rbx 0x55555556aeb0 —▸ 0x555555557cb8 ◂— 0x555555557cb8
03:00180x55555556aeb8 ◂— 0xc0000000b /* '\x0b' */
04:00200x55555556aec0 ◂— 0xa0000000d /* '\r' */
  • 虚继承的子类都有一个虚基类指针,其指向虚基类表(虚基类指针和虚函数的虚指针不是同一个东西)
  • 虚继承底层实现原理与编译器相关,一般通过虚基类指针和虚基类表实现
1
2
3
4
5
6
7
8
pwndbg> telescope 0x55555556aea0
00:00000x55555556aea0 ◂— 0x0
01:00080x55555556aea8 ◂— 0x31 /* '1' */
02:0010│ rbx 0x55555556aeb0 —▸ 0x555555557c28 ◂— 0x0
03:00180x55555556aeb8 ◂— 0xc0000000b /* '\x0b' */
04:00200x55555556aec0 ◂— 0xd /* '\r' */
05:00280x55555556aec8 —▸ 0x555555557c40 —▸ 0x55555555536c (A::fun()) ◂— endbr64
06:00300x55555556aed0 ◂— 0xa /* '\n' */
  • 这里的 0x555555557c40 就是A类的虚指针,而 0x555555557c28 是D类的虚基类指针

匿名函数 Lambda

lambda 函数是一种匿名函数,它表示一个接受参数并返回一个值的函数

lambda 函数的语法如下:

1
[capture](parameters) -> return_type { function_body }

测试样例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>

typedef int (*lambda_ta)(int);
typedef int (*lambda_tb)(int,int);
typedef double (*lambda_tc)(double);

lambda_ta lambdaA = [](int a) -> int {
return a * 2;
};

lambda_tb lambdaB = [](int a,int b) -> int {
return a * b;
};

int main() {
int x0 = 5;
int x1 = lambdaA(x0);
int x2 = lambdaB(x0,x1);
double y0 = x1 + x2;
double y1 = [](double a) -> double {
return a * 2;
}(y0);

return 0;
}
  • 在底层,局部匿名函数和全局匿名函数的实现不同
1
2
3
4
x0 = 5;
x1 = lambdaA(5);
y0 = (double)(x1 + lambdaB(5, x1));
main::{lambda(double)#1}::operator()(&__closure, y0);
  • 匿名函数和普通函数本质上的区别就是:匿名函数不能通过函数名来调用
  • 调用全局匿名函数其实是调用全局变量上对应的函数指针:
1
2
3
4
5
6
7
8
.data:0000000000004010                               public lambdaA
.data:0000000000004010 ; lambda_ta lambdaA
.data:0000000000004010 1A 12 00 00 00 00 00 00 lambdaA dq offset _ZN7lambdaAMUliE_4_FUNEi
.data:0000000000004010 ; DATA XREF: main+22↑r
.data:0000000000004018 public lambdaB
.data:0000000000004018 ; lambda_tb lambdaB
.data:0000000000004018 55 12 00 00 00 00 00 00 lambdaB dq offset _ZN7lambdaBMUliiE_4_FUNEii
.data:0000000000004018 ; DATA XREF: main+33↑r
  • 而局部匿名函数本质上是对 () 的重载
  • 调用局部匿名函数其实是调用对应的重载函数:
1
2
3
4
5
6
double __cdecl main::{lambda(double)#1}::operator()(
const main::$256B327EE357AF9FCD813216707B60D5 *const __closure,
double a)
{
return a + a; /* PS:编译器对这里进行了优化 */
}

泛型编程

模板是泛型编程的基础,泛型编程即以一种独立于任何特定类型的方式编写代码

模板有两类:函数模板,类模板

函数模板

不规定某个函数的传参类型或者返回值类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <string>

using namespace std;

template <class T>
void Swap(T &num1, T& num2){
T tmp = num1;
num1 = num2;
num2 = tmp;
}

int main() {
int a = 10, b = 20;
Swap(a,b); /* 函数模板实例化 */
cout << "a :" << a << "b :" << b << endl;

float c = 10.55f, d = 3.14f;
Swap(c, d); /* 函数模板实例化 */
cout << "c :" << c << "d :" << d << endl;
}
  • 函数模板不规定参数类型,返回值类型
  • 在编译时,编译器会根据实际传参来创建不同类型的副本,这个过程被称为函数模板实例化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
a = 10;
b = 20;
Swap<int>(&a, &b); /* 函数模板实例化(创建int类型的副本) */
v3 = std::operator<<<std::char_traits<char>>(&std::cout, &unk_2005);
v4 = std::ostream::operator<<(v3, (unsigned int)a);
v5 = std::operator<<<std::char_traits<char>>(v4, &unk_2009);
v6 = std::ostream::operator<<(v5, (unsigned int)b);
std::ostream::operator<<(v6, &std::endl<char,std::char_traits<char>>);
c = 10.55;
d = 3.1400001;
Swap<float>(&c, &d); /* 函数模板实例化(创建float类型的副本) */
v7 = std::operator<<<std::char_traits<char>>(&std::cout, &unk_200D);
v8 = std::ostream::operator<<(v7, *(double *)_mm_cvtsi32_si128(LODWORD(c)).m128i_i64);
v9 = std::operator<<<std::char_traits<char>>(v8, &unk_2011);
v10 = std::ostream::operator<<(v9, *(double *)_mm_cvtsi32_si128(LODWORD(d)).m128i_i64);
std::ostream::operator<<(v10, &std::endl<char,std::char_traits<char>>);

1686123247822

下面是一个特殊的案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
#include <cassert>
using namespace std;

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

typedef int (*lambda_t)(int,int);
lambda_t sub = [](int a,int b) -> int {
return a-b;
};

template <typename Fn>
void func(Fn const &fn){
int a = 10;
int b = 5;
cout << fn(a,b) << endl;
}

int main(){
func(add);
func([](int a,int b) -> int {
return a * b;
});
func(sub);
}
  • 函数模板和类模板都可以将 “函数指针” 当做类型
1
2
3
func<int ()(int,int)>((int (*)(int, int))add); /* 普通函数 */
func<main::{lambda(int,int)#1}>(&fn); /* 局部匿名函数 */
func<int (*)(int,int)>(&sub); /* 全局匿名函数 */

类模板

不规定该类中某个函数的传参类型或返回值类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <iostream>
#include <string>
#include <cassert>

namespace mzt {
template<class T>
class vector {
public:
vector() : _a(nullptr),_size(0),_capacity(0){}
void push_back(const T& data) {
if (_capacity == _size) {
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
T* tmp = new T[newcapacity];
assert(tmp);
_a = tmp;
_capacity = newcapacity;
}
_a[_size++] = data;
}
T& operator[](size_t pos) {
assert(pos < _size);
return _a[pos];
}
size_t getsize() {
return _size;
}
private:
T* _a;
size_t _size;
size_t _capacity;
};
}

using namespace std;

int main() {
mzt::vector<int> a; /* 类模板实例化 */
a.push_back(1);
a.push_back(2);
mzt::vector<double> b; /* 类模板实例化 */
b.push_back(3.0);
b.push_back(4.0);
}
  • 类模板实例化与函数模板实例化不同
  • 类模板实例化需要在类模板名字后跟 <> 指定类型(函数模板实例化也可以用 <> 指定类型,即使没有,编译器也会自动识别类型)
1
2
3
4
5
6
7
8
9
10
mzt::vector<int>::vector(&a); /* 类模板实例化 */
LODWORD(b._a) = 1;
mzt::vector<int>::push_back(&a, (const int *)&b);
LODWORD(b._a) = 2;
mzt::vector<int>::push_back(&a, (const int *)&b);
mzt::vector<double>::vector(&b); /* 类模板实例化 */
data = 3.0;
mzt::vector<double>::push_back(&b, &data);
data = 4.0;
mzt::vector<double>::push_back(&b, &data);

模板特例化

在原模板类的基础上,针对特殊类型所进行特殊化的实现方式

模板特化中分为:函数模板特化,类模板特化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>
#include <cstring>

using namespace std;

bool IsEqual(int left, int right) {
return left == right;
}

template<class T>
bool IsEqual(const T& left, const T& right) {
return left == right;
}

template< >
bool IsEqual<const char *>(const char* const& left,
const char* const& right) {
return strcmp(left, right) == 0;
}

int main(){
int a = 0;
int b = 1;
const char* p1 = "hello";
const char* p2 = "hello";
bool ret;

ret = IsEqual(a, b);
ret = IsEqual<int>(a, b);
ret = IsEqual<const char*>(p1, p2);
}
1
2
3
4
5
6
7
v4 = 0;
v5 = 1;
v6 = "hello";
v7[0] = "hello";
IsEqual(0, 1); /* 调用普通函数(最优先) */
IsEqual<int>(&v4, &v5); /* 调用模板函数 */
IsEqual<char const*>(&v6, v7); /* 调用特例化的模板函数(次优先) */
  • 如果遇到相同普通函数(函数名和类型都相同),编译器则会优先调用普通函数(最优先)
  • 如果指定类型匹配,特例化的模板函数会比普通的模板函数优先调用(次优先)

类型形参 & 非类型形参

模板参数分类:类型形参、非类型形参

  • 类型形参:出现在模板参数列表中,跟在 class 或者 typename 之后的参数类型名称
  • 非类型形参:就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <cassert>

namespace mzt {
template<class T = int, size_t N = 10> /* T为类型形参,N为非类型形参 */
class Array {
public:
T& operator[](size_t pos) { /* 重载[] */
return arr[pos];
}
private:
T arr[N];
};
}

int main(){
mzt::Array< > a; /* 不提供模板参数,使用缺省值 */
a[0]=1;
a[1]=2;
return 0;
}
1
2
*mzt::Array<int,10ul>::operator[](&a, 0LL) = 1;
*mzt::Array<int,10ul>::operator[](&a, 1uLL) = 2;

右值引用

左值 & 右值

  • 左值是可以放在赋值号左边可以被赋值的值,左值必须要在内存中有实体
    • 当左值被赋值时,左值本身保留
  • 右值当在赋值号右边取出值赋给其他变量的值,右值可以在内存也可以在CPU寄存器
    • 当右值被赋值时,右值会被释放
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <memory>
using namespace std;

int main(){
int a = 10;
int &b = a; /* 左值引用(b相当于a的别名) */
int &&c = 20; /* 右值引用(c相当于20的别名) */

cout << &a << ":" << a << endl;
cout << &b << ":" << b << endl;
cout << &c << ":" << c << endl;
}
1
2
3
0x7ffc2d216c30:10
0x7ffc2d216c30:10
0x7ffc2d216c34:20

IDA 分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
v13 = 10; /* int a = 10 */
v15 = &v13; /* int &b = a */
v14 = 20; /* int &&c = 20 */
v16 = &v14;
v3 = std::ostream::operator<<(&std::cout, &v13);
v4 = std::operator<<<std::char_traits<char>>(v3, ":");
v5 = std::ostream::operator<<(v4, v13);
std::ostream::operator<<(v5, &std::endl<char,std::char_traits<char>>);
v6 = std::ostream::operator<<(&std::cout, v15);
v7 = std::operator<<<std::char_traits<char>>(v6, ":");
v8 = std::ostream::operator<<(v7, *v15);
std::ostream::operator<<(v8, &std::endl<char,std::char_traits<char>>);
v9 = std::ostream::operator<<(&std::cout, v16);
v10 = std::operator<<<std::char_traits<char>>(v9, ":");
v11 = std::ostream::operator<<(v10, *v16);
std::ostream::operator<<(v11, &std::endl<char,std::char_traits<char>>);
  • 在底层,右值引用相当于是赋值与左值引用的结合

std::move

std::move 唯一的功能是将一个左值强制转化为右值引用

1
2
3
4
template <class _Ty>
inline _CONST_FUN typename remove_reference<_Ty>::type&& move(_Ty&& _Arg) _NOEXCEPT {
return (static_cast<typename remove_reference<_Ty>::type&&>(_Arg));
}
  • 如果是左值,就通过 static_cast 将传进来的参数强转为右值并返回
  • 如果是右值,直接返回
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <utility>
#include <vector>
#include <string>
using namespace std;

int main(){
std::string s = "Hello";
std::string sleft;
std::string sright;

sleft = s; /* 左值赋值 */
std::cout << s << endl;
std::cout << sleft << endl;
sright = std::move(s); /* 右值赋值 */
std::cout << s << endl;
std::cout << sright << endl;
}
1
2
3
4
Hello
Hello

Hello
  • 左值赋值:字符串 s 仍然存在
  • 右值赋值:字符串 s 被置空

IDA 分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
std::allocator<char>::allocator(&v9, argv, envp);
std::string::basic_string<std::allocator<char>>(v10, "Hello", &v9);
std::allocator<char>::~allocator(&v9);
std::string::basic_string(v11);
std::string::basic_string(v12);
std::string::operator=(v11, v10);
v3 = std::operator<<<char>(&std::cout, v10);
std::ostream::operator<<(v3, &std::endl<char,std::char_traits<char>>);
v4 = std::operator<<<char>(&std::cout, v11);
std::ostream::operator<<(v4, &std::endl<char,std::char_traits<char>>);
v5 = std::move<std::string &>(v10);
std::string::operator=(v12, v5);
v6 = std::operator<<<char>(&std::cout, v10);
std::ostream::operator<<(v6, &std::endl<char,std::char_traits<char>>);
v7 = std::operator<<<char>(&std::cout, v12);
std::ostream::operator<<(v7, &std::endl<char,std::char_traits<char>>);
std::string::~string(v12);
std::string::~string(v11);
std::string::~string(v10)
  • std::move 在底层会直接返回参数的原始值,真正置空字符串 s 的操作是 std::string::operator=() 函数
1
2
3
4
__int64 __fastcall std::move<std::string &>(__int64 a1)
{
return a1;
}
  • 仔细分析可以发现左值赋值和右值赋值调用的 std::string::operator=() 函数不一样
1
2
.text:0000000000002509 E8 E2 FC FF FF                call    __ZNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEaSERKS4_ ; std::string::operator=(std::string const&)
.text:0000000000002509
1
2
.text:0000000000002577 E8 84 FD FF FF                call    __ZNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEaSEOS4_ ; std::string::operator=(std::string&&)
.text:0000000000002577

拷贝构造函数 & 移动构造函数

如果想用其它对象初始化一个同类的新对象,只能借助类中的拷贝构造函数

  • 其实现原理是为新对象复制一份和其它对象一模一样的数据
  • 当类中拥有指针类型的成员变量时,拷贝构造函数中需要以深拷贝的方式复制该指针成员

移动构造函数使用右值引用形式的参数

  • 在此构造函数中,num 指针变量采用的是浅拷贝的复制方式
  • 在函数内部置空了 d.num(为了避免 “同一块对空间被释放多次”)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>
using namespace std;

class demo{
public:
demo():num(new int(1)){
cout<<"construct!"<<endl;
}
demo(const demo &d):num(new int(*d.num)){ /* 拷贝构造函数(深拷贝) */
cout<<"copy construct!"<<endl;
}
demo(demo &&d):num(d.num){ /* 移动构造函数 */
d.num = NULL;
cout<<"move construct!"<<endl;
}
~demo(){
cout<<"class destruct!"<<endl;
}
int *num;
};

demo get_demo(){
return demo();
}

int main(){
demo a = get_demo();
demo b = a;
demo c = move(a);
return 0;
}

IDA 分析:

1
2
3
4
5
6
7
get_demo((demo *)v5);
demo::demo((demo *)v6, (const demo *)v5); /* 拷贝构造 */
v3 = std::move<demo &>(v5);
demo::demo(v7, v3); /* 移动构造 */
demo::~demo((demo *)v7);
demo::~demo((demo *)v6);
demo::~demo((demo *)v5);
1
2
3
4
5
6
7
8
9
10
11
void __fastcall demo::demo(demo *this, const demo *a2)
{
_DWORD *v2; // rax
__int64 v3; // rax

v2 = (_DWORD *)operator new(4uLL);
*v2 = **(_DWORD **)a2;
*(_QWORD *)this = v2;
v3 = std::operator<<<std::char_traits<char>>(&std::cout, "copy construct!");
std::ostream::operator<<(v3, &std::endl<char,std::char_traits<char>>);
}
1
2
3
4
5
6
7
8
9
__int64 __fastcall demo::demo(_QWORD *a1, _QWORD *a2)
{
__int64 v2; // rax

*a1 = *a2;
*a2 = 0LL;
v2 = std::operator<<<std::char_traits<char>>(&std::cout, "move construct!");
return std::ostream::operator<<(v2, &std::endl<char,std::char_traits<char>>);
}

智能指针

智能指针 (Smart Pointer) 是一种在 C++ 中用于管理动态分配的内存的类,它提供了一种灵活的方式来管理对象的生命周期,可以避免资源泄漏和内存错误

auto_ptr

当对象过期时,其析构函数将自动使用 delete 来释放内存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <memory>
using namespace std;

class Auto {
public:
Auto() { cout << "Auto create" << endl; }
~Auto() { cout << "Auto delete" << endl; }
int key1 = 10;
int key2 = 20;
};

int main(){
auto_ptr<Auto> test(new Auto);
int key1 = (*test).key1;
int key2 = test->key2;
int key3 = key1 + key2;
cout << key3 << endl;
return 0;
}
  • 栈上的对象会在当前语句块结束时释放,堆上的对象需要使用 delete 释放
  • auto_ptr 属于栈上的对象,在它的析构函数中会尝试 delete 目标对象

IDA 分析如下:

1
2
3
4
5
6
7
8
v5 = (Auto *)operator new(8uLL);
Auto::Auto(v5); /* Auto的构造函数 */
std::auto_ptr<Auto>::auto_ptr(v8, v5); /* auto_ptr的构造函数 */
v6 = *(_DWORD *)std::auto_ptr<Auto>::operator*(v8);
v7 = v6 + *(_DWORD *)(std::auto_ptr<Auto>::operator->(v8) + 4);
v3 = std::ostream::operator<<(&std::cout, v7);
std::ostream::operator<<(v3, &std::endl<char,std::char_traits<char>>);
std::auto_ptr<Auto>::~auto_ptr(v8); /* auto_ptr的析构函数 */
  • auto_ptr 的析构函数中会自动释放 Auto(传入对象):
1
2
3
4
5
6
7
8
9
10
11
void __fastcall std::auto_ptr<Auto>::~auto_ptr(Auto **a1)
{
Auto *v1; // rbx

v1 = *a1;
if ( *a1 )
{
Auto::~Auto(*a1);
operator delete(v1, 8uLL);
}
}
  • auto_ptr 创建的对象仍然具有指针的性质,其原因是 auto_ptr*-> 等符号进行了重构
1
2
3
4
__int64 __fastcall std::auto_ptr<Auto>::operator*(__int64 a1)
{
return *(_QWORD *)a1;
}
1
2
3
4
__int64 __fastcall std::auto_ptr<Auto>::operator->(__int64 a1)
{
return *(_QWORD *)a1;
}
  • PS:对于 *-> 的重构只是实现了解引用而已,不负责具体的偏移

unique_ptr

unique_ptrauto_ptr 的用法几乎一样,另外添加了如下几个特性:

  • 基于排他所有权模式:两个指针不能指向同一个资源
  • 无法进行左值 unique_ptr 复制构造,也无法进行左值复制赋值操作,但允许临时右值赋值构造和赋值
  • 在 STL 容器中使用 unique_ptr,不允许直接赋值
  • 支持对象数组的内存管理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <memory>
using namespace std;

int main(){
unique_ptr<string> p1(new string("11111111"));
unique_ptr<string> p2(new string("22222222"));

//p1 = p2; // 禁止左值赋值
//unique_ptr<string> p3(p2); // 禁止左值赋值构造

cout << "p1:" << p1.get() << endl;
cout << "p2:" << p2.get() << endl;

unique_ptr<string> p3(std::move(p2));
p2 = std::move(p1); // 使用move把左值转成右值就可以赋值了,效果和auto_ptr赋值一样

cout << "-------------------" << endl;
cout << "p1:" << p1.get() << endl;
cout << "p2:" << p2.get() << endl;
cout << "p3:" << p3.get() << endl;
}
1
2
3
4
5
6
p1:0x559625bfdeb0
p2:0x559625bfdee0
-------------------
p1:0
p2:0x559625bfdeb0
p3:0x559625bfdee0

IDA 分析如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 std::allocator<char>::allocator(v27, argv, envp);
v23 = (void *)operator new(0x20uLL);
std::string::basic_string<std::allocator<char>>(v23, "11111111", v27);
std::unique_ptr<std::string>::unique_ptr<std::default_delete<std::string>,void>(v25, v23); /* unique_ptr的构造函数 */
std::allocator<char>::~allocator(v27);
std::allocator<char>::allocator(v27, v23, v3);
v24 = (void *)operator new(0x20uLL);
std::string::basic_string<std::allocator<char>>(v24, "22222222", v27);
std::unique_ptr<std::string>::unique_ptr<std::default_delete<std::string>,void>(v26, v24); /* unique_ptr的构造函数 */
std::allocator<char>::~allocator(v27);

......

v10 = std::move<std::unique_ptr<std::string> &>(v26); /* 把左值转成右值 */
std::unique_ptr<std::string>::unique_ptr(v27, v10); /* unique_ptr的构造函数 */
v11 = std::move<std::unique_ptr<std::string> &>(v25); /* 把左值转成右值 */
std::unique_ptr<std::string>::operator=(v26, v11);

......

std::unique_ptr<std::string>::~unique_ptr(v27);
std::unique_ptr<std::string>::~unique_ptr(v26);
std::unique_ptr<std::string>::~unique_ptr(v25)

shared_ptr

shared_ptr 基于非排他所有权模式:允许多个指针指向同一个资源

  • 当复制或拷贝时,引用计数加 “1”
  • 当智能指针析构时,引用计数减 “1”
  • 如果计数为 “0”,代表已经没有指针指向这块内存,那么就释放它
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
#include <memory>
using namespace std;

void deleteStr(string* p) { /* 允许自定义析构函数 */
cout << *p << endl;
}

int main(){
string* str = new string("11111111");
std::shared_ptr<string> p1(str, deleteStr);
std::shared_ptr<string> p2(p1);
std::shared_ptr<string> p3(p2);

cout << "p1 count = " << p1.use_count() << endl;
cout << "p2 count = " << p2.use_count() << endl;
cout << "p3 count = " << p3.use_count() << endl;

p1 = NULL;
p2 = NULL;

cout << "p1 count = " << p1.use_count() << endl;
cout << "p2 count = " << p2.use_count() << endl;
cout << "p3 count = " << p3.use_count() << endl;

p3 = NULL;
cout << "22222222" << endl;

return 0;
}
1
2
3
4
5
6
7
8
p1 count = 3
p2 count = 3
p3 count = 3
p1 count = 0 /* p1被置空 */
p2 count = 0 /* p2被置空 */
p3 count = 1 /* shared_ptr的计数器为"1" */
11111111 /* 当shared_ptr的计数器为"0"时,调用析构函数 */
22222222

weak_ptr

weak_ptr 是为配合 shared_ptr 而引入的一种智能指针,它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造

weak_ptr 提供的是一个弱引用:

  • 弱引用不更改引用计数,类似普通指针

shared_ptr 提供的是一个强引用:

  • 当对象被创建时,计数为 “1”,每创建一个变量引用该对象时,该对象的计数就增加“1”,当上述变量销毁时,对象的计数减 “1”,当计数为 “0” 时,这个对象也就被析构了
  • 强引用计数在很多种情况下都是可以正常工作的,但是也有不凑效的时候,当出现循环引用时,就会出现严重的问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <iostream>
#include <memory>
using namespace std;

class parent;
class children;
typedef shared_ptr<parent> parent_ptr;
typedef shared_ptr<children> children_ptr;
typedef weak_ptr<parent> parent_ptr2;

class parent{
public:
~parent() { std::cout <<"destroying parent\n"; }
public:
children_ptr children;
};

class children{
public:
~children() { std::cout <<"destroying children\n"; }
public:
parent_ptr parent;
};

void test(){
parent_ptr father(new parent());
children_ptr son(new children());

father->children = son;
son->parent = father;
cout << "father count = " << father.use_count() << endl;
cout << "son count = " << son.use_count() << endl;
}

int main(){
std::cout<<"begin test\n";
test();
std::cout<<"end test\n";
}
  • 由于 parentchildren 对象互相引用,它们的引用计数都是 “2”,不能自动释放
  • 并且此时这两个对象再无法访问到
1
2
3
4
begin test
father count = 2
son count = 2
end test

使用弱引用 weak_ptr 即可打破循环:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <iostream>
#include <memory>
using namespace std;

class parent;
class children;
typedef shared_ptr<parent> parent_ptr;
typedef shared_ptr<children> children_ptr;
typedef weak_ptr<parent> parent_ptr2;

class parent{
public:
~parent() { std::cout <<"destroying parent\n"; }
public:
children_ptr children;
};

class children{
public:
~children() { std::cout <<"destroying children\n"; }
public:
parent_ptr2 parent;
};

void test(){
parent_ptr father(new parent());
children_ptr son(new children());

father->children = son;
son->parent = father;
cout << "father count = " << father.use_count() << endl;
cout << "son count = " << son.use_count() << endl;
}

int main(){
std::cout<<"begin test\n";
test();
std::cout<<"end test\n";
}
1
2
3
4
5
6
begin test
father count = 1 /* 由于children使用弱指针,因此children并不会使father的计数器增加 */
son count = 2
destroying parent
destroying children
end test