diff --git "a/2023/04/23/CPP\346\231\272\350\203\275\346\214\207\351\222\210/index.html" "b/2023/04/23/CPP\346\231\272\350\203\275\346\214\207\351\222\210/index.html" index 8023afd..acba6d6 100644 --- "a/2023/04/23/CPP\346\231\272\350\203\275\346\214\207\351\222\210/index.html" +++ "b/2023/04/23/CPP\346\231\272\350\203\275\346\214\207\351\222\210/index.html" @@ -5,24 +5,24 @@ - - C++ 智能指针:shared_ptr 和 weak_ptr - 千里之行始于足下 + + C++ 智能指针:shared_ptr 和 weak_ptr - 千里之行,始于足下 - - + + - - + + - + @@ -74,7 +74,7 @@ href="/" aria-label="" > - 千里之行始于足下 + 千里之行,始于足下 @@ -245,15 +245,14 @@

-

在循环引用的场景下探讨 C++ 中 shard_ptr 和 weak_ptr 原理

- + -

引言

C++ 中经常需要 new 一个对象,开辟一个内存空间,返回一个指针来操作这个内存。使用完毕之后,需要通过 delete 来释放内存空间。如果内存没有释放,那这块内存将无法再利用,导致内存泄漏。为降低人为疏忽,C++ 11 的新特性中引入了三种智能指针,来自动化地管理内存资源:

+

引言

C++ 中经常需要 new 一个对象,开辟一个内存空间,返回一个指针来操作这个内存。使用完毕之后,需要通过 delete 来释放内存空间。如果内存没有释放,那这块内存将无法再利用,导致内存泄漏。为降低人为疏忽,C++ 11 的新特性中引入了三种智能指针,来自动化地管理内存资源:

unique_ptr: 管理的资源唯一的属于一个对象,但是支持将资源移动给其他 unique_ptr 对象。当拥有所有权的 unique_ptr 对象析构时,资源即被释放。

shared_ptr: 管理的资源被多个对象共享,内部采用引用计数跟踪所有者的个数。当最后一个所有者被析构时,资源即被释放。

weak_ptr: 与 shared_ptr 配合使用,虽然能访问资源但却不享有资源的所有权,不影响资源的引用计数。有可能资源已被释放,但 weak_ptr 仍然存在。因此每次访问资源时都需要判断资源是否有效。

本文主要在循环引用的场景下探讨 shard_ptr 和 weak_ptr 原理。

-

循环引用

shared_ptr 通过引用计数的方式管理内存,当进行拷贝或赋值操作时,每个 shared_ptr 都会记录有多少个其他的 shared_ptr 指向相同的对象,当引用计数为 0 时,内存将被自动释放。

+

循环引用

shared_ptr 通过引用计数的方式管理内存,当进行拷贝或赋值操作时,每个 shared_ptr 都会记录有多少个其他的 shared_ptr 指向相同的对象,当引用计数为 0 时,内存将被自动释放。

1
2
auto p = make_shared<int>(10); // 创建一个名为 p 的 shared_ptr,指向一个取值为 10 的 int 型对象,这个数值 10 的引用计数为 1,只有 p
auto q(p); // 创建一个名为 q 的 shared_ptr,并用 p 初始化,此时 p 和 q 指向同一个对象,此时数值 10 的引用计数为 2

当对 shared_ptr 赋予新值,或被销毁时,引用计数会递减。

1
2
auto r = make_shared<int>(20); // 创建一个名为 r 的 shared_ptr,指向一个取值为 20 的 int 型对象,这个数值 20 的引用计数为 1,只有 r
r = q; // 对 r 赋值,让 r 指向数值 10。此时数值 10 的引用计数加 1 为 3,数值 20 的引用计数减 1 位 0,数值 20 的内存将被自动释放
@@ -269,7 +268,7 @@

shared_ptr 原理

shared_ptr 实际上是对裸指针进行了一层封装,成员变量除了裸指针外,还有一个引用计数,它记录裸指针被引用的次数(有多少个 shared_ptr 指向这同一个裸指针),当引用计数为 0 时,自动释放裸指针指向的资源。影响引用次数的场景包括:构造、赋值、析构。基于三个最简单的场景,实现一个 demo 版 shared_ptr 如下(实现既不严谨也不安全,仅用于阐述原理):

+

shared_ptr 原理

shared_ptr 实际上是对裸指针进行了一层封装,成员变量除了裸指针外,还有一个引用计数,它记录裸指针被引用的次数(有多少个 shared_ptr 指向这同一个裸指针),当引用计数为 0 时,自动释放裸指针指向的资源。影响引用次数的场景包括:构造、赋值、析构。基于三个最简单的场景,实现一个 demo 版 shared_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
40
41
42
43
44
45
46
47
48
49
#include <iostream>
#include <memory>
using namespace std;

template<typename T>
class SharedPtr {
public:
int* counter; // 引用计数,用指针表示,多个 SharedPtr 之间可以同步修改
T* resource; // 裸指针

SharedPtr(T* resc = nullptr) { // 构造函数
cout << __PRETTY_FUNCTION__ << endl;
counter = new int(1);
resource = resc;
}

SharedPtr(const SharedPtr& rhs) { // 拷贝构造函数
cout << __PRETTY_FUNCTION__ << endl;
resource = rhs.resource;
counter = rhs.counter;
++*counter;
}

SharedPtr& operator=(const SharedPtr& rhs) { // 拷贝赋值函数
cout << __PRETTY_FUNCTION__ << endl;
--*counter; // 原来指向的资源的引用计数减 1
if (*counter == 0) {
delete counter;
delete resource;
}

resource = rhs.resource;
counter = rhs.counter;
++*counter; // 新指向的资源的引用计数加 1
}

~SharedPtr() { // 析构函数
cout << __PRETTY_FUNCTION__ << endl;
--*counter;
if (*counter == 0) {
delete counter;
delete resource;
}
}

int use_count() {
return *counter;
}
};

在循环引用示例中,用到了 make_shared 函数:

@@ -282,43 +281,42 @@

1
2
3
4
auto son = make_shared<Son>();  // 调用构造函数,son.counter=1
auto father = make_shared<Father>(); // 调用构造函数,father.counter=1
son->father_ = father; // 调用赋值函数,son.counter=2
father->son_ = son; // 调用赋值函数,father.counter=2

当 main 函数执行完时,执行析构函数,此时由于 son.counter=1,father.couter=1,不满足 if 条件,不会实行 delete 命令完成资源释放,导致内存泄漏。

-

weak_ptr 原理

为解决循环引用的问题,仅使用 shared_ptr 是无法实现的。堡垒无法从内部攻破的时候,需要借助外力,于是有了 weak_ptr,字面意思是弱指针。为啥叫弱呢?shared_ptr A 被赋值给 shared_ptr B 时,A 的引用计数加 1;shared_ptr A 被赋值给 weak_ptr C 时,A 的引用计数不变。引用力度不够强,不足以改变引用计数,所以就弱了(个人理解,有误请指正)。

+

weak_ptr 原理

为解决循环引用的问题,仅使用 shared_ptr 是无法实现的。堡垒无法从内部攻破的时候,需要借助外力,于是有了 weak_ptr,字面意思是弱指针。为啥叫弱呢?shared_ptr A 被赋值给 shared_ptr B 时,A 的引用计数加 1;shared_ptr A 被赋值给 weak_ptr C 时,A 的引用计数不变。引用力度不够强,不足以改变引用计数,所以就弱了(个人理解,有误请指正)。

weak_ptr 在使用时,是与 shared_ptr 绑定的。基于 SharedPtr 实现来实现 demo 版的 WeakPtr,并解决循环引用的问题,全部代码如下:

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
#include <iostream>
#include <memory>
using namespace std;

template<typename T>
class SharedPtr {
public:
int* counter;
int* weakref;
T* resource;

SharedPtr(T* resc = nullptr) {
cout << __PRETTY_FUNCTION__ << endl;
counter = new int(1);
weakref = new int(0);
resource = resc;
}

SharedPtr(const SharedPtr& rhs) {
cout << __PRETTY_FUNCTION__ << endl;
resource = rhs.resource;
counter = rhs.counter;
++*counter;
}

SharedPtr& operator=(const SharedPtr& rhs) {
cout << __PRETTY_FUNCTION__ << endl;
--*counter;
if (*counter == 0) {
delete counter;
delete resource;
}

resource = rhs.resource;
counter = rhs.counter;
++*counter;
}

~SharedPtr() {
cout << __PRETTY_FUNCTION__ << endl;
--*counter;
if (*counter == 0) {
delete counter;
delete resource;
}
}

int use_count() {
return *counter;
}
};

template<typename T>
class WeakPtr {
public:
T* resource;

WeakPtr(T* resc = nullptr) {
cout << __PRETTY_FUNCTION__ << endl;
resource = resc;
}

WeakPtr& operator=(SharedPtr<T>& ptr) {
cout << __PRETTY_FUNCTION__ << endl;
resource = ptr.resource;
++*ptr.weakref; // 赋值时引用计数 counter 不变,改变弱引用计数 weakref
}

~WeakPtr() {
cout << __PRETTY_FUNCTION__ << endl;
}
};

class Son;

class Father {
public:
SharedPtr<Son> son_;
Father() {
cout << __PRETTY_FUNCTION__ << endl;
}
~Father() {
cout << __PRETTY_FUNCTION__ << endl;
}
};

class Son {
public:
WeakPtr<Father> father_; // 将 SharedPtr 改为 WeakPtr
Son() {
cout << __PRETTY_FUNCTION__ << endl;
}
~Son() {
cout << __PRETTY_FUNCTION__ << endl;
}
};

int main()
{
auto son_ = new Son(); // 创建一个 Son 对象,返回指向 Son 对象的指针 son_
auto father_ = new Father(); // 创建一个 Father 对象,返回指向 Father 对象的指针 father_
SharedPtr<Son> son(son_); // 调用 SharedPtr 构造函数:son.counter=1, son.weakref=0
SharedPtr<Father> father(father_); // 调用 SharedPtr 构造函数:father.counter=1, father.weakref=0
son.resource->father_ = father; // 调用 WeakPtr 赋值函数:father.counter=1, father.weakref=1
father.resource->son_ = son; // 调用 SharedPtr 赋值函数:son.counter=2, son.weakref=0
cout << "son: " << son.use_count() << endl;
cout << "father: " << father.use_count() << endl;
return 0;
}

代码执行结果如下:

-

WeakPtr::WeakPtr(T*) [with T = Father]

+

WeakPtr::WeakPtr(T*) [with T = Father]

Son::Son()

-

SharedPtr::SharedPtr(T*) [with T = Son]

+

SharedPtr::SharedPtr(T*) [with T = Son]

Father::Father()

-

SharedPtr::SharedPtr(T*) [with T = Son]

-

SharedPtr::SharedPtr(T*) [with T = Father]

-

WeakPtr& WeakPtr::operator=(SharedPtr&) [with T = Father]

-

SharedPtr& SharedPtr::operator=(const SharedPtr&) [with T = Son]

+

SharedPtr::SharedPtr(T*) [with T = Son]

+

SharedPtr::SharedPtr(T*) [with T = Father]

+

WeakPtr& WeakPtr::operator=(SharedPtr&) [with T = Father]

+

SharedPtr& SharedPtr::operator=(const SharedPtr&) [with T = Son]

son: 2

father: 1

-

SharedPtr::~SharedPtr() [with T = Father]

+

SharedPtr::~SharedPtr() [with T = Father]

Father::~Father()

-

SharedPtr::~SharedPtr() [with T = Son]

-

SharedPtr::~SharedPtr() [with T = Son]

+

SharedPtr::~SharedPtr() [with T = Son]

+

SharedPtr::~SharedPtr() [with T = Son]

Son::~Son()

-

WeakPtr::~WeakPtr() [with T = Father]

+

WeakPtr::~WeakPtr() [with T = Father]

可以看到 Son 对象和 Father 对象均被析构,内存泄漏的问题得到解决。析构过程解读如下:

-

SharedPtr::~SharedPtr() [with T = Father] # 析构 father,由于 father.couter=1,减 1 后执行 delete father_

+

SharedPtr::~SharedPtr() [with T = Father] # 析构 father,由于 father.couter=1,减 1 后执行 delete father_

Father::~Father() # 析构 father_,执行~Father(),进一步析构成员变量

-

SharedPtr::~SharedPtr() [with T = Son] # 析构 SharedPtr,此时 son.couter 减 1,son.counter=1

-

SharedPtr::~SharedPtr() [with T = Son] # 析构 son,由于 son.counter=1,减 1 后执行 delete son_

+

SharedPtr::~SharedPtr() [with T = Son] # 析构 SharedPtr,此时 son.couter 减 1,son.counter=1

+

SharedPtr::~SharedPtr() [with T = Son] # 析构 son,由于 son.counter=1,减 1 后执行 delete son_

Son::~Son() # 析构 son_,执行~Son(),进一步析构成员变量

-

WeakPtr::~WeakPtr() [with T = Father] # 析构 WeakPtr

+

WeakPtr::~WeakPtr() [with T = Father] # 析构 WeakPtr

-

总结

    +

    总结

    1. 尽量使用智能指针管理资源申请与释放,减少人为 new 和 delete 误操作和考虑不周的问题。
    2. 使用 make_shared 来创建 shared_ptr,如果先 new 一个对象,再用这个对象的裸指针构造一个 shared_ptr 指针,可能出现问题。shared_ptr 会自动释放资源,如果再手动 delete,释放两次那就挂了。
    - diff --git "a/2023/04/27/CPP\346\236\204\351\200\240\345\207\275\346\225\260/index.html" "b/2023/04/27/CPP\346\236\204\351\200\240\345\207\275\346\225\260/index.html" index ecb2880..37a58c8 100644 --- "a/2023/04/27/CPP\346\236\204\351\200\240\345\207\275\346\225\260/index.html" +++ "b/2023/04/27/CPP\346\236\204\351\200\240\345\207\275\346\225\260/index.html" @@ -5,24 +5,24 @@ - - C++ 构造函数:默认构造和拷贝构造 - 千里之行始于足下 + + C++ 构造函数:默认构造和拷贝构造 - 千里之行,始于足下 - - + + - - + + - + @@ -74,7 +74,7 @@ href="/" aria-label="" > - 千里之行始于足下 + 千里之行,始于足下
@@ -245,30 +245,29 @@

-

浅析 C++ 的默认构造函数与拷贝构造函数

- + -

定义

C++ 是一种面向对象编程语言,在设计时,将具有共同特征的对象抽象成类,在使用时,通过将类实例化得到不同的对象。创建一个实例对象的时候,就需要用到构造函数。
创建一个实例有两种方法:
(1) 通过初始化成员变量的来构造实例对象,这时候调用普通构造函数。
(2) 通过复制已有的实例化对象来构造新的实例对象,这时候调用拷贝构造函数。

+

定义

C++ 是一种面向对象编程语言,在设计时,将具有共同特征的对象抽象成类,在使用时,通过将类实例化得到不同的对象。创建一个实例对象的时候,就需要用到构造函数。
创建一个实例有两种方法:
(1) 通过初始化成员变量的来构造实例对象,这时候调用普通构造函数。
(2) 通过复制已有的实例化对象来构造新的实例对象,这时候调用拷贝构造函数。

构造函数的函数名与类名相同,没有返回值,可以通过不同的参数值实现重载。定义 Object 类如下:

1
2
3
4
5
6
7
8
class Object
{
public:
int d;
Object() { std::cout << "default constructor" << std::endl; } // 默认构造函数,调用时可不提供参数的构造函数,即 Object obj
Object(int k) { d = k; std::cout << "defined constructor" << std::endl; } // 普通构造函数
Object(const Object& obj) { d = obj.d; std::cout << "copy constructor" << std::endl; } // 拷贝构造函数,定义时需要以当前类对象的 const 引用作为入参,即 const Object& obj
};

定义 Object 对象如下:

1
2
3
Object obj1 = Object();  // default constructor
Object obj2 = Object(3); // defined constructor
Object obj3 = obj1; // copy constructor,注意不是重载 = 的赋值函数
-

合成

当程序中没有显示定义默认和拷贝构造函数时,逻辑上编译器会自己定义默认和拷贝构造函数。编译器自己定义的构造函数可以分为无用的 (trivial) 和有用的(non-trivial)。编译器通过实现优化,仅在有限的几种情况下,编译器才会将定义的有用的构造函数真正合成出来。

-

默认构造函数

C++ 默认构造函数有两个重要作用:

+

合成

当程序中没有显示定义默认和拷贝构造函数时,逻辑上编译器会自己定义默认和拷贝构造函数。编译器自己定义的构造函数可以分为无用的 (trivial) 和有用的(non-trivial)。编译器通过实现优化,仅在有限的几种情况下,编译器才会将定义的有用的构造函数真正合成出来。

+

默认构造函数

C++ 默认构造函数有两个重要作用:

一是提供成员变量初始化的方法,默认构造函数并不会申请成员变量所需的内存空间,也不会主动给成员变量赋初值,而只是提供了给成员变量赋初值的一种途径。

二是在 virtual 场景下,完成虚函数表指针和虚基类指针的设置,使得在运行时可以通过指针或引用访问到正确的函数实现,呈现出多态的特性。

鉴于此,C++ 编译器会在以下场景合成 non-trivial 的默认构造函数:

1、virtual 场景,即 定义了虚函数或虚继承 的情形,此时需要合成默认构造函数,以正确设置虚函数表指针和虚基类指针。

2、组合或继承场景下,被组合的类或被继承的类中显式定义了默认构造函数,需要在合成的默认构造函数中递归调用被组合类或被继承类的构造函数。

注:
非 virtual 场景,C++ 构造函数类似于 Python 的__init__方法,区别在于 C++ 编译器会在构造函数中自动插入父类的构造函数,而 Python 需要通过 super 显式调用父类的构造函数。

-

拷贝构造函数

C 语言中的拷贝是 bitwise 拷贝(内存上存的是啥就拷贝啥),C++ 如果用 bitwise 拷贝,可能会导致程序出问题,例如:

+

拷贝构造函数

C 语言中的拷贝是 bitwise 拷贝(内存上存的是啥就拷贝啥),C++ 如果用 bitwise 拷贝,可能会导致程序出问题,例如:

1、在 virtual 场景下,如果用派生类对象初始化父类对象(Base base_obj = derived_obj 对象模型会发生切割),bitwise 拷贝使得 base_obj 的 vptr 指向 Derived 的虚函数表,导致程序异常。

2、在非 virtual 场景下,如果类中有指针类型的成员对象,bitwise 拷贝使得两个对象的成员变量指向同一处内存(浅拷贝),析构时由于内存被重复释放导致程序崩溃,此时需要定义拷贝构造函数使得拷贝结果位于一处新内存上(深拷贝)。

因此,C++ 编译器会在以下场景合成 non-trivial 的拷贝构造函数:

1、virtual 场景,即 定义了虚函数或虚继承 的情形,此时需要合成拷贝构造函数,保证在复制过程(特别是用派生类对象初始化基类对象)中正确设置虚函数表指针和虚基类指针。

2、组合或继承场景下,被组合的类或被继承的类中显式定义了拷贝构造函数,需要在合成的默认构造函数中递归调用被组合类或被继承类的拷贝构造函数。

-

应用

设计模式中有一种单例模式,意思就是在程序中有且仅有唯一的实例。要实现单例模式,需要解决两个问题:

+

应用

设计模式中有一种单例模式,意思就是在程序中有且仅有唯一的实例。要实现单例模式,需要解决两个问题:

(1) 创建一个实例:解决存在性问题

(2) 禁止多个实例:解决唯一性问题

定义 SingleInstance 类如下:

@@ -293,7 +292,6 @@

应用

default constructor

- diff --git "a/2023/05/07/CPP\345\257\271\350\261\241\346\250\241\345\236\213/index.html" "b/2023/05/07/CPP\345\257\271\350\261\241\346\250\241\345\236\213/index.html" index 26d4280..27abeb3 100644 --- "a/2023/05/07/CPP\345\257\271\350\261\241\346\250\241\345\236\213/index.html" +++ "b/2023/05/07/CPP\345\257\271\350\261\241\346\250\241\345\236\213/index.html" @@ -5,21 +5,21 @@ - - C++ 对象模型:对象内存布局 - 千里之行始于足下 + + C++ 对象模型:对象内存布局 - 千里之行,始于足下 - - + + - - + + @@ -33,7 +33,7 @@ - + @@ -86,7 +86,7 @@ href="/" aria-label="" > - 千里之行始于足下 + 千里之行,始于足下

@@ -257,25 +257,24 @@

-

介绍 C++ 对象在内存中的存储模型

- + -

对象模型基础

C++ 在 C 的基础上引入了面向对象的程序设计思想,类的封装、继承和多态使得 C++ 功能更加强大。C++ 的类包含数据成员和成员函数:

+

对象模型基础

C++ 在 C 的基础上引入了面向对象的程序设计思想,类的封装、继承和多态使得 C++ 功能更加强大。C++ 的类包含数据成员和成员函数:

数据成员:静态数据成员,非静态数据成员

成员函数:非静态成员函数、静态函数、虚函数

C++ 对象模型研究 C++ 类的数据成员和成员函数在内存中如何布局的问题。假设有个 C++ 类定义如下:

1
2
3
4
5
6
7
8
9
10
class Base
{
public:
int member; // 非静态数据成员
static int smember; // 静态数据成员

void func(); // 非静态成员函数
static void sfunc(); // 静态成员函数
virtual void vfunc(); // 虚函数
};
-

简单对象模型

在简单对象模型中,类的每个实例化对象在内存中都会申请一块连续内存,在这块内存上保存着指向成员变量和函数的指针。这样做的好处是,每个对象的内存大小是固定的,不管是数据成员还是成员函数,都只对应一个指针,在访问时可以很快计算出指针偏移量。但这样做的弊端也很明显,当访问对象的数据成员时,需要先找到数据成员的指针,然后通过这个指针获取到真实的数据,多了一次寻址,效率较低。

- +

简单对象模型

在简单对象模型中,类的每个实例化对象在内存中都会申请一块连续内存,在这块内存上保存着指向成员变量和函数的指针。这样做的好处是,每个对象的内存大小是固定的,不管是数据成员还是成员函数,都只对应一个指针,在访问时可以很快计算出指针偏移量。但这样做的弊端也很明显,当访问对象的数据成员时,需要先找到数据成员的指针,然后通过这个指针获取到真实的数据,多了一次寻址,效率较低。

+ -

表驱动对象模型

表格驱动对象模型对类中的数据成员和成员函数进行了区分,每个对象保留两个指针,一个指向数据成员表,其中存放数据成员的取值,一个指向成员函数表,其中存放指向成员函数的指针。这种做法的好处是对数据和函数进行分开管理,不好的地方在于访问成员函数时,需要先找到成员函数表,效率更低。

- +

表驱动对象模型

表格驱动对象模型对类中的数据成员和成员函数进行了区分,每个对象保留两个指针,一个指向数据成员表,其中存放数据成员的取值,一个指向成员函数表,其中存放指向成员函数的指针。这种做法的好处是对数据和函数进行分开管理,不好的地方在于访问成员函数时,需要先找到成员函数表,效率更低。

+ -

C++ 对象模型

介绍 C++ 对象模型之前,先理解 C++ 中虚函数的概念。C++ 的多态是通过虚函数实现的。在基类中用 virtual 关键字定义虚函数,在派生类中重写虚函数,那么在运行时,传入基类指针时,会根据对象的实际类型来调用正确的函数,实现多态。

+

C++ 对象模型

介绍 C++ 对象模型之前,先理解 C++ 中虚函数的概念。C++ 的多态是通过虚函数实现的。在基类中用 virtual 关键字定义虚函数,在派生类中重写虚函数,那么在运行时,传入基类指针时,会根据对象的实际类型来调用正确的函数,实现多态。

不妨试想一下,如果要自己设计 C++ 对象模型,应该怎么做?我是这样想的:C++ 的类实例化为不同的对象,这些对象的静态数据成员是共享的,非静态数据成员是互不影响的,成员函数对各个对象来说是相同的。那么非静态数据成员应该放在每个对象的内存空间中,以保证不同对象相互独立;静态数据成员和成员函数应该放在这个类的公共空间,不用每个对象都存一份,减少存储空间;而虚函数在继承中发挥独特作用,应该与其他成员函数分开。

揭开谜底,以 Base 类为例,C++ 对象模型是长这样的:

- +

可以看出:

    @@ -283,10 +282,10 @@

    普通继承场景下的对象模型

    单一继承场景

    以 Base 为基类,定义派生类 Derived 如下:

    +

    普通继承场景下的对象模型

    单一继承场景

    以 Base 为基类,定义派生类 Derived 如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Derived : public Base
    {
    public:
    int member; // 非静态数据成员
    static int smember_d; // 静态数据成员

    void func(); // 非静态成员函数
    static void sfunc_d(); // 静态成员函数
    virtual void vfunc(); // 虚函数
    virtual void vfunc_d(); // 虚函数
    }

    这种场景下,Derived 的对象模型如下:

    - +

    可以看出:

      @@ -294,28 +293,28 @@

      多重继承场景

      单一继承场景下,派生类在基类的虚函数表上进行修改。多重继承场景,有多个基类的虚函数表。定义基类 Base1 和派生类 Derived 如下:

      +

      多重继承场景

      单一继承场景下,派生类在基类的虚函数表上进行修改。多重继承场景,有多个基类的虚函数表。定义基类 Base1 和派生类 Derived 如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      class Base1
      {
      public:
      int member1; // 非静态数据成员

      virtual void vfunc(); // 虚函数
      virtual void vfunc1(); // 虚函数
      virtual void vfunc2(); // 虚函数
      };

      class Derived : public Base, public Base1
      {
      public:
      int member_d; // 非静态数据成员
      static int smember_d; // 静态数据成员

      void func(); // 非静态成员函数
      static void sfunc_d(); // 静态成员函数
      virtual void vfunc(); // 虚函数
      virtual void vfunc1(); // 虚函数
      virtual void vfunc_d(); // 虚函数
      };

      此时 Derived 的对象模型为:

      - +

      可以看出:

      1. 继承多个父类时,派生类的对象模型中分别存储一个虚函数表的指针。如果派生类重写的基类同名虚函数,那就在虚函数表中进行替换;如果派生类新增了虚函数,那么在第一个父类的虚函数表中进行新增扩展。
      -

      虚继承场景下的对象模型

      使用普通继承的菱形继承

      多重继承场景中,有一种特殊的情况叫菱形继承,它是这样操作的:B1 继承 B,B2 继承 B,D 继承 B1 和 B2,祖父类 B 被子类 D 重复继承,图示如下:

      - +

      虚继承场景下的对象模型

      使用普通继承的菱形继承

      多重继承场景中,有一种特殊的情况叫菱形继承,它是这样操作的:B1 继承 B,B2 继承 B,D 继承 B1 和 B2,祖父类 B 被子类 D 重复继承,图示如下:

      +

      按多重继承的方式推导 D 的对象模型是这样的:

      - +

      可以看到,在菱形继承场景中,访问祖父类成员 B::b 时,出现了二义性。

      为解决菱形继承中的问题,C++ 发明了虚继承。虚继承的对象模型与普通继承不同:普通继承是子类拷贝并修改父类的虚函数表;虚继承是子类和父类的虚函数表分开保存,将指向父类的虚函数表的指针也加入到子类的对象模型中。

      -

      使用虚继承的单一继承

      假定单一虚继承场景如下:

      - +

      使用虚继承的单一继承

      假定单一虚继承场景如下:

      +

      此时派生类 Derived 的对象模型为:

      - +

      可以看到:

        @@ -323,18 +322,17 @@

        使用虚继承的菱形继承

        假定使用虚继承的菱形继承场景如下:

        - +

        使用虚继承的菱形继承

        假定使用虚继承的菱形继承场景如下:

        +

        结合多重继承与单一虚继承,推导派生类 Derived 的对象模型为:

        - +

        可以看到:

        1. 多重继承的布局基本不变,虚基类的信息被追加到内存布局最后,并用 0x00000000 隔开。此时再访问 B::b 不会出现二义性。
          附注:
          推导时,从子类往祖父类逐步推进,子类与父类适用多重继承,父类与祖父类适用单一虚继承,每一步只决定派生类的数据成员的位置,例如 B::b 的布局应该由单一虚继承决定,如果在多重继承时决定,那推出来也是二义的。
        -

        总结

        对象模型揭示了 C++ 对象在内存中的存储布局,结合面向对象的三大特性(继承、封装和多态)来理解对象模型,印象会更深一点。

        - +

        总结

        对象模型揭示了 C++ 对象在内存中的存储布局,结合面向对象的三大特性(继承、封装和多态)来理解对象模型,印象会更深一点。

        diff --git "a/2023/05/10/CPP\344\270\216Python\347\232\204OOP\346\257\224\350\276\203/index.html" "b/2023/05/10/CPP\344\270\216Python\347\232\204OOP\346\257\224\350\276\203/index.html" index 7733d9b..820d8fb 100644 --- "a/2023/05/10/CPP\344\270\216Python\347\232\204OOP\346\257\224\350\276\203/index.html" +++ "b/2023/05/10/CPP\344\270\216Python\347\232\204OOP\346\257\224\350\276\203/index.html" @@ -5,25 +5,25 @@ - - C++ 与 Python 的 OOP 比较 - 千里之行始于足下 + + C++ 与 Python 的 OOP 比较 - 千里之行,始于足下 - - + + - - + + - + @@ -76,7 +76,7 @@ href="/" aria-label="" > - 千里之行始于足下 + 千里之行,始于足下
@@ -247,12 +247,11 @@

-

C++ 和 Python 都是从 C 语言演变出来的面向对象(OOP)的编程语言,本文基于 OOP 三特性,比较 C++ 和 Python 的异同点

- + -

1. OOP 三特性

封装 :将相关数据和操作数据的方法打包成一个类。不同的类相互隔离,也可以自由组合。
继承 :从一个父类衍生出子类,子类可以自然地拥有与父类的相同的属性和行为。
多态 :子类与父类或者兄弟类在某一种行为上有所区别,即同一函数不同实现。
个人理解,继承保持了类之间的共性,多态使得这些具有共性的类之间有各自的特性。

-

2. 封装

类是一组数据以及操这组数据的函数(方法)的集合。类是对象的抽象模板,对象是类的具体实例,给类的数据取不同的值,同一个类就产成了不同的对象。

-

数据

于是数据应该有两种:一种是与 类级别 的,同一个类取值都一样,与实例无关;另一种是 实例级别 的,同一个类的不同实例取值各不相同。

+

1. OOP 三特性

封装 :将相关数据和操作数据的方法打包成一个类。不同的类相互隔离,也可以自由组合。
继承 :从一个父类衍生出子类,子类可以自然地拥有与父类的相同的属性和行为。
多态 :子类与父类或者兄弟类在某一种行为上有所区别,即同一函数不同实现。
个人理解,继承保持了类之间的共性,多态使得这些具有共性的类之间有各自的特性。

+

2. 封装

类是一组数据以及操这组数据的函数(方法)的集合。类是对象的抽象模板,对象是类的具体实例,给类的数据取不同的值,同一个类就产成了不同的对象。

+

数据

于是数据应该有两种:一种是与 类级别 的,同一个类取值都一样,与实例无关;另一种是 实例级别 的,同一个类的不同实例取值各不相同。

@@ -272,7 +271,7 @@

数据

实例变量
-

方法

既然数据有两种,方法至少也应该有两种,一种是类级别的,一种是实例级别的。类级别的数据在实例化之前就存在,在实例化之前操作类级别的数据,是一种方法。实例化之后产生了实例级别的数据,这时候的方法可以同时操作两类数据,是另一种方法。

+

方法

既然数据有两种,方法至少也应该有两种,一种是类级别的,一种是实例级别的。类级别的数据在实例化之前就存在,在实例化之前操作类级别的数据,是一种方法。实例化之后产生了实例级别的数据,这时候的方法可以同时操作两类数据,是另一种方法。

@@ -293,29 +292,27 @@

方法

C++ 中还有一种重要方法是 虚函数,使用虚函数可以实现 C++ 中的多态。
Python 中还有一种方法是静态方法。在 Python 中可以认为,实例方法传入了实例对象的指针,类方法传入了类的指针,而静态方法既不需要传入实例,也不需要传入类。

-

3. 继承

子类继承父类,使子类拥有父类的数据和方法。

-

单一继承

这种情况下,python 和 C++ 的最大区别应该在于继承方式。C++ 继承分为 public、private、protected 三种,Python 都是 public。

-

多重继承

没有虚函数的情况下,区别主要有两点:
(1) 假设函数名为 fun,当多个父类中定义方法 fun,而子类没有定义方法 fun,通过子类调用方法 fun,C++ 会 报错 ,Python 会使用MRO 来确定调用哪个父类的 fun。
(2) 对菱形继承的情况,C++ 要使用 虚继承,Python 要使用super 结合 MRO

-

4. 多态

多态在代码上的表现为一个方法多个实现。C++ 的多态必须建立在继承基础上,现有继承,后有多态。Python 的多态没有继承关系的限制,只要实现了同名方法即可。

-

C++ 多态

前文介绍了 C++ 对象的内存模型,这里只说最简单的单一继承情况。C++ 通过父类的指针或引用调用虚函数,在编译期间无法确定调用的是父类的实现还是子类的实现,只有在执行期间访问内存模型中的虚函数表才能确定。
假设 Derived 类继承 Base 类,Base 类中定义了虚函数 method,Derived 类重写了虚函数 method,此时 Base 类和 Derived 类的对象模型如图:

- +

3. 继承

子类继承父类,使子类拥有父类的数据和方法。

+

单一继承

这种情况下,python 和 C++ 的最大区别应该在于继承方式。C++ 继承分为 public、private、protected 三种,Python 都是 public。

+

多重继承

没有虚函数的情况下,区别主要有两点:
(1) 假设函数名为 fun,当多个父类中定义方法 fun,而子类没有定义方法 fun,通过子类调用方法 fun,C++ 会 报错 ,Python 会使用MRO 来确定调用哪个父类的 fun。
(2) 对菱形继承的情况,C++ 要使用 虚继承,Python 要使用super 结合 MRO

+

4. 多态

多态在代码上的表现为一个方法多个实现。C++ 的多态必须建立在继承基础上,现有继承,后有多态。Python 的多态没有继承关系的限制,只要实现了同名方法即可。

+

C++ 多态

前文介绍了 C++ 对象的内存模型,这里只说最简单的单一继承情况。C++ 通过父类的指针或引用调用虚函数,在编译期间无法确定调用的是父类的实现还是子类的实现,只有在执行期间访问内存模型中的虚函数表才能确定。
假设 Derived 类继承 Base 类,Base 类中定义了虚函数 method,Derived 类重写了虚函数 method,此时 Base 类和 Derived 类的对象模型如图:

+

执行如下代码:

1
2
Base *ptr = new Derived();
ptr->method();

编译器看到 ptr 是 Base 类型,如果 method 不是虚函数,那么执行的应该是 Base::method。现在 method 是虚函数,执行期调用 Base::method 还是 Derived::method,要看赋给 ptr 的是 Base 对象地址还是 Derived 对象地址。上面的代码是创建了一个 Derived 对象,并把地址传给 Base 类指针,但是内存模型中的 vptr 指向的仍然是 Derived 类实现的虚函数,所以最后调用的是 Derived::method。

-

Python 多态

相比 C++ 复杂的内存模型,Python 的鸭子类型让多态更灵活(Python 内存模型跟多态的关系好像不大)。

+

Python 多态

相比 C++ 复杂的内存模型,Python 的鸭子类型让多态更灵活(Python 内存模型跟多态的关系好像不大)。

1
2
3
4
5
6
7
8
9
10
11
12
class Cat:
@classmethod
def say(cls):
print("miao miao")

class Dog:
@classmethod
def say(cls):
print("wang wang")

def func(kls):
kls.say()

这里 Cat 和 Dog 没有继承关系,say 也不是虚函数,调用 func(Cat)和 func(Dog)都能正确执行。要是再定义个 Person 类,只要定义了方法 say,就可以把 Person 传给 func 完成调用。

-

5. 写在最后

虽然都是从 C 语言发展出来的 OOP 语言,C++ 和 Python 的区别还是挺大的,特别是多态的处理,所以对相同逻辑的多态执行结果也是有区别的:

+

5. 写在最后

虽然都是从 C 语言发展出来的 OOP 语言,C++ 和 Python 的区别还是挺大的,特别是多态的处理,所以对相同逻辑的多态执行结果也是有区别的:

C++ 示例

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

class Base
{
public:
int x = 1;
virtual void print() { std::cout << x << std::endl; }
};

class Derived : public Base
{
public:
int x = 2;
};

int main() {
Base *ptr = nullptr;
ptr = new Base();
ptr->print(); // 打印 1
ptr = new Derived();
ptr->print(); // 打印 1
}

Python 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Base:
def __init__(self):
self.x = 1
def print(self):
print(self.x)

class Derived(Base):
def __init__(self):
self.x = 2

def func(Kls):
obj = Kls()
obj.print()

func(Base) // 打印1
func(Derived) // 打印2
- - diff --git "a/2023/05/19/Linux\344\273\216\345\205\245\351\227\250\345\210\260\347\206\237\347\273\203/index.html" "b/2023/05/19/Linux\344\273\216\345\205\245\351\227\250\345\210\260\347\206\237\347\273\203/index.html" index 8146de8..c55e117 100644 --- "a/2023/05/19/Linux\344\273\216\345\205\245\351\227\250\345\210\260\347\206\237\347\273\203/index.html" +++ "b/2023/05/19/Linux\344\273\216\345\205\245\351\227\250\345\210\260\347\206\237\347\273\203/index.html" @@ -5,29 +5,29 @@ - - Linux 从入门到熟练 - 千里之行始于足下 + + Linux 从入门到熟练 - 千里之行,始于足下 - - + + - - + + - + - + @@ -76,7 +76,7 @@ href="/" aria-label="" > - 千里之行始于足下 + 千里之行,始于足下
@@ -247,16 +247,15 @@

-

常用的各类 Linux 操作命令

- + -

一、引言

操作系统有个内核,负责管理系统的进程、内存、设备、文件和网络等资源。操作系统还有个 shell,为用户提供与内核交互的途径。shell 可以分为 GUI(图形界面)和 CLI(命令行)两种,GUI 以 windows 为代表,CLI 以 linux 为代表。linux 也有 Gnome 桌面,但效率和性能不如 CLI,学 linux 必学 CLI。

+

一、引言

操作系统有个内核,负责管理系统的进程、内存、设备、文件和网络等资源。操作系统还有个 shell,为用户提供与内核交互的途径。shell 可以分为 GUI(图形界面)和 CLI(命令行)两种,GUI 以 windows 为代表,CLI 以 linux 为代表。linux 也有 Gnome 桌面,但效率和性能不如 CLI,学 linux 必学 CLI。

CLI 是用户与内核打交道的工具。用户日常使用操作系统,最频繁的场景就是文件操作和网络访问。故而基础篇笔记主要记录 CLI 基本操作、文件系统、网络配置、权限管理等相关知识点。

-

二、CLI 基本操作

(一)查看目录

@@ -245,8 +245,7 @@

-

LibFuzzer 从 0 到 1,原理 + 安装 + 使用 + 优化,一篇讲完

- +

按照 官方定义,LibFuzzer 是一个in-process(进程内的),coverage-guided(以覆盖率为引导的),evolutionary(进化的) 的 fuzz 引擎,是 LLVM 项目的一部分,主要用于对 C/C++ 程序进行 Fuzz 测试。LibFuzzer 三个特性的具体含义为:

    @@ -264,13 +263,13 @@

LibFuzzer 与待测的 library 进行链接,通过向指定的 fuzzing 入口(即target 函数)发送测试数据,并跟踪被触达的代码区域,然后对输入的数据进行变异,以达到代码覆盖率最大的目的,其中代码覆盖率的信息由 LLVM 的 SanitizerCoverage 工具提供。

-

一、使用方法

1. 安装环境

Clang 是一个类似 GCC 的 C/C++ 语言编译工具,此处 简介。LibFuzzer 现在已被集成到 Clang 6.0 之后的版本中,在 Linux 环境下,只需直接安装 Clang 即可:

+

一、使用方法

1. 安装环境

Clang 是一个类似 GCC 的 C/C++ 语言编译工具,此处 简介。LibFuzzer 现在已被集成到 Clang 6.0 之后的版本中,在 Linux 环境下,只需直接安装 Clang 即可:

1
apt install clang

安装完毕之后,可以通过如下命令查看 clang 版本:

1
clang --version
-

2. 构建 target

使用 LibFuzzer 时,第一步就是要实现 target 函数——LLVMFuzzerTestOneInput,该函数以 bytes 数组作为输入,函数体内使用待测 API 对这个 bytes 数组进行处理:

+

2. 构建 target

使用 LibFuzzer 时,第一步就是要实现 target 函数——LLVMFuzzerTestOneInput,该函数以 bytes 数组作为输入,函数体内使用待测 API 对这个 bytes 数组进行处理:

1
2
3
4
5
// fuzz_target.cc
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
DoSomethingInterestingWithMyAPI(Data, Size);
return 0; // Values other than 0 and -1 are reserved for future use.
}

target 函数的名称、参数类型、返回值类型都不能改变。此外,官方文档中还有如下说明:

@@ -336,11 +335,11 @@

libFuzzer 编译链接

关于 LibFuzzer 通过插桩统计代码覆盖率的具体实现,有兴趣可以参考Coverage Control in libFuzzer

-

3. 执行 fuzz

在 target 函数构建成功后,会生成一个可执行的 fuzz 二进制文件,该文件通过命令行方式执行,可以接受指定的参数选项。执行格式为:

+

3. 执行 fuzz

在 target 函数构建成功后,会生成一个可执行的 fuzz 二进制文件,该文件通过命令行方式执行,可以接受指定的参数选项。执行格式为:

1
./fuzz-target -flag1=val1 -flag2=val2 ... path1 path2 ...

在不限制运行时长的情况下,Fuzz 将会一直执行下去,直到出现 crash(通常是因为触发 sanitizer 异常)才会终止。导致 crash 的输入将会作为能触发 bug 的 testcase 保存到磁盘上,并根据 crash 的类型,用不同的文件名前缀进行区分,比如 crash-XXX,leak-XXX,timeout-XXX 等。

-

(1) 参数说明

常见的 flag 选项及作用列举如下,使用 flag 时必须以 -flag=val 的格式:

+

(1) 参数说明

常见的 flag 选项及作用列举如下,使用 flag 时必须以 -flag=val 的格式:

@@ -480,7 +479,7 @@

(2) 语料使用

模糊测试通过随机的方式变异或生成测试用例。提供种子语料库作为随机变异的基础用例,避免从头开始生完全随机地生成用例,在待测函数接受复杂或结构化输入的情况下,能显著提升 Fuzz 测试的效率和覆盖率。

+

(2) 语料使用

模糊测试通过随机的方式变异或生成测试用例。提供种子语料库作为随机变异的基础用例,避免从头开始生完全随机地生成用例,在待测函数接受复杂或结构化输入的情况下,能显著提升 Fuzz 测试的效率和覆盖率。

LibFuzzer 是以覆盖率为引导的,当提供种子语料库时,LibFuzzer 从语料库中读取语料,通过随机变异产生新的测试数据,输入到 Fuzz Target。由于在构建 Fuzz Target 时,通过编译参数进行了代码插桩,因此可以跟踪 Fuzz Target 在接收到输入后,具体执行了哪些代码。如果某个测试输入能执行到之前从未执行的代码,那么这个测试输入就是一个有效变异,将被纳入到语料库,作为后续变异的基础。整体流程大致如下,仅做原理示意,不等于具体实现。

graph LR
     A[从语料库中读取样本] --> B[随机变异生成新样本]
@@ -506,7 +505,7 @@ 

1
./fuzz_target testcase_path  # testcase_path 就是 fuzz crash 时保存的 testcase file 的文件路径
-

4. 输出解读

在 Fuzz 执行过程中,标准错误流 stderr 中,通常会输出以下格式的内容:

+

4. 输出解读

在 Fuzz 执行过程中,标准错误流 stderr 中,通常会输出以下格式的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
INFO: Seed: 1523017872
INFO: Loaded 1 modules (16 guards): [0x744e60, 0x744ea0),
INFO: -max_len is not provided, using 64
INFO: A corpus is not provided, starting from an empty corpus
#0 READ units: 1
#1 INITED cov: 3 ft: 2 corp: 1/1b exec/s: 0 rss: 24Mb
#3811 NEW cov: 4 ft: 3 corp: 2/2b exec/s: 0 rss: 25Mb L: 1 MS: 5 ChangeBit-ChangeByte-ChangeBit-ShuffleBytes-ChangeByte-
#3827 NEW cov: 5 ft: 4 corp: 3/4b exec/s: 0 rss: 25Mb L: 2 MS: 1 CopyPart-
#3963 NEW cov: 6 ft: 5 corp: 4/6b exec/s: 0 rss: 25Mb L: 2 MS: 2 ShuffleBytes-ChangeBit-
#4167 NEW cov: 7 ft: 6 corp: 5/9b exec/s: 0 rss: 25Mb L: 3 MS: 1 InsertByte-
==31511== ERROR: libFuzzer: deadly signal
...
artifact_prefix='./'; Test unit written to ./crash-b13e8756b13a00cf168300179061fb4b91fefbed

第一行显示 Fuzz 测试使用的随机种子,可以通过 -seed 参数来设置。

@@ -591,14 +590,14 @@

二、使用示例

CVE-2016-5180为例,演示 LibFuzzer 的使用过程。CVE-2016-5180是在 c-ares 中出现的堆溢出问题,是使得 ChromeOS 被攻击(重启后以访客模式执行代码)的漏洞之一。

-

1. 下载源码

最新的 c-ares 代码已修复漏洞,为了复现漏洞,clone 代码后,需要恢复到漏洞修复之前。

+

二、使用示例

CVE-2016-5180为例,演示 LibFuzzer 的使用过程。CVE-2016-5180是在 c-ares 中出现的堆溢出问题,是使得 ChromeOS 被攻击(重启后以访客模式执行代码)的漏洞之一。

+

1. 下载源码

最新的 c-ares 代码已修复漏洞,为了复现漏洞,clone 代码后,需要恢复到漏洞修复之前。

1
2
3
git clone https://github.com/c-ares/c-ares.git
cd c-ares/
git reset --hard 51fbb479f7948fca2ace3ff34a15ff27e796afdd
-

2. 编写 Fuzz 主函数

实现 LLVMFuzzerTestOneInput 函数,将 LibFuzzer 输入的字节流进行转换,调用 ares_create_query 函数,并将代码保存为 target.cc 文件。

+

2. 编写 Fuzz 主函数

实现 LLVMFuzzerTestOneInput 函数,将 LibFuzzer 输入的字节流进行转换,调用 ares_create_query 函数,并将代码保存为 target.cc 文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Copyright 2016 Google Inc. All Rights Reserved.
// Licensed under the Apache License, Version 2.0 (the "License");
#include <stdint.h>
#include <stdlib.h>
#include <string>
#include <arpa/nameser.h>

#include <ares.h>

extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
unsigned char *buf;
int buflen;
std::string s(reinterpret_cast<const char *>(Data), Size);
ares_create_query(s.c_str(), ns_c_in, ns_t_a, 0x1234, 0, &buf, &buflen, 0);
free(buf);
return 0;
}
-

3. 编译 Fuzz target

使用 fuzzer-test-suite 提供的编译脚本,执行 build.sh 脚本,会自动调用 custom-build.sh 和 common.sh 进行编译。三个 shell 脚本和 Fuzz 函数的存放路径如下,脚本中会自动使用 git 拉取代码,这里将第 1 步手动拉取的代码删掉了:

+

3. 编译 Fuzz target

使用 fuzzer-test-suite 提供的编译脚本,执行 build.sh 脚本,会自动调用 custom-build.sh 和 common.sh 进行编译。三个 shell 脚本和 Fuzz 函数的存放路径如下,脚本中会自动使用 git 拉取代码,这里将第 1 步手动拉取的代码删掉了:

1
2
3
4
5
6
7
8
9
sqa@twtpesqa03:~/dss/fuzz$ tree
.
├── cares
│ ├── build.sh
│ └── target.cc
├── common.sh
└── custom-build.sh

1 directory, 4 files

注意需要将原始脚本中的 -fsanitize-coverage=trace-pc-guard 替换为 -fsanitize=fuzzer,否则执行 Fuzz 时会出现错误:-fsanitize-coverage=trace-pc-guard is no longer supported by libFuzzer。

@@ -610,22 +609,22 @@

1
2
3
4
5
6
7
8
9
10
sqa@twtpesqa03:~/dss/fuzz$ tree -L 1
.
├── BUILD
├── cares
├── cares-fsanitize_fuzzer
├── common.sh
├── custom-build.sh
└── SRC

3 directories, 3 files
-

4. 执行 Fuzz 测试

直接运行编译得到的二进制文件,如需附带参数,可参考第一章节执行 fuzz 的参数说明。

+

4. 执行 Fuzz 测试

直接运行编译得到的二进制文件,如需附带参数,可参考第一章节执行 fuzz 的参数说明。

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
sqa@twtpesqa03:~/dss/fuzz$ ./cares-fsanitize_fuzzer
INFO: Seed: 817252946
INFO: Loaded 1 modules (10 inline 8-bit counters): 10 [0x5a90e0, 0x5a90ea),
INFO: Loaded 1 PC tables (10 PCs): 10 [0x56c278,0x56c318),
INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes
INFO: A corpus is not provided, starting from an empty corpus
#2 INITED cov: 3 ft: 3 corp: 1/1b exec/s: 0 rss: 27Mb
#3 NEW cov: 4 ft: 4 corp: 2/5b lim: 4 exec/s: 0 rss: 27Mb L: 4/4 MS: 1 CrossOver-
#10 REDUCE cov: 4 ft: 4 corp: 2/4b lim: 4 exec/s: 0 rss: 27Mb L: 3/3 MS: 2 ChangeByte-CrossOver-
#11 REDUCE cov: 4 ft: 4 corp: 2/3b lim: 4 exec/s: 0 rss: 27Mb L: 2/2 MS: 1 EraseBytes-
#1368 REDUCE cov: 6 ft: 6 corp: 3/20b lim: 17 exec/s: 0 rss: 27Mb L: 17/17 MS: 2 InsertByte-InsertRepeatedBytes-
#1524 REDUCE cov: 6 ft: 6 corp: 3/19b lim: 17 exec/s: 0 rss: 27Mb L: 16/16 MS: 1 EraseBytes-
=================================================================
==3049145==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x6030012599c2 at pc 0x000000550e1c bp 0x7fffbad826d0 sp 0x7fffbad826c8
WRITE of size 1 at 0x6030012599c2 thread T0
#0 0x550e1b in ares_create_query (/home/sqa/dss/fuzz/cares-fsanitize_fuzzer+0x550e1b)
#1 0x55053c in LLVMFuzzerTestOneInput /home/sqa/dss/fuzz/cares/target.cc:14:3
#2 0x4586a1 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) (/home/sqa/dss/fuzz/cares-fsanitize_fuzzer+0x4586a1)
#3 0x457de5 in fuzzer::Fuzzer::RunOne(unsigned char const*, unsigned long, bool, fuzzer::InputInfo*, bool*) (/home/sqa/dss/fuzz/cares-fsanitize_fuzzer+0x457de5)
#4 0x45a087 in fuzzer::Fuzzer::MutateAndTestOne() (/home/sqa/dss/fuzz/cares-fsanitize_fuzzer+0x45a087)
#5 0x45ad85 in fuzzer::Fuzzer::Loop(std::__Fuzzer::vector<fuzzer::SizedFile, fuzzer::fuzzer_allocator<fuzzer::SizedFile> >&) (/home/sqa/dss/fuzz/cares-fsanitize_fuzzer+0x45ad85)
#6 0x44973e in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) (/home/sqa/dss/fuzz/cares-fsanitize_fuzzer+0x44973e)
#7 0x472582 in main (/home/sqa/dss/fuzz/cares-fsanitize_fuzzer+0x472582)
#8 0x7fd31481c082 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x24082)
#9 0x41e4dd in _start (/home/sqa/dss/fuzz/cares-fsanitize_fuzzer+0x41e4dd)

0x6030012599c2 is located 0 bytes to the right of 18-byte region [0x6030012599b0,0x6030012599c2)
allocated by thread T0 here:
#0 0x51e20d in malloc (/home/sqa/dss/fuzz/cares-fsanitize_fuzzer+0x51e20d)
#1 0x5508f6 in ares_create_query (/home/sqa/dss/fuzz/cares-fsanitize_fuzzer+0x5508f6)
#2 0x55053c in LLVMFuzzerTestOneInput /home/sqa/dss/fuzz/cares/target.cc:14:3
#3 0x4586a1 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) (/home/sqa/dss/fuzz/cares-fsanitize_fuzzer+0x4586a1)
#4 0x457de5 in fuzzer::Fuzzer::RunOne(unsigned char const*, unsigned long, bool, fuzzer::InputInfo*, bool*) (/home/sqa/dss/fuzz/cares-fsanitize_fuzzer+0x457de5)
#5 0x45a087 in fuzzer::Fuzzer::MutateAndTestOne() (/home/sqa/dss/fuzz/cares-fsanitize_fuzzer+0x45a087)
#6 0x45ad85 in fuzzer::Fuzzer::Loop(std::__Fuzzer::vector<fuzzer::SizedFile, fuzzer::fuzzer_allocator<fuzzer::SizedFile> >&) (/home/sqa/dss/fuzz/cares-fsanitize_fuzzer+0x45ad85)
#7 0x44973e in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) (/home/sqa/dss/fuzz/cares-fsanitize_fuzzer+0x44973e)
#8 0x472582 in main (/home/sqa/dss/fuzz/cares-fsanitize_fuzzer+0x472582)
#9 0x7fd31481c082 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x24082)

SUMMARY: AddressSanitizer: heap-buffer-overflow (/home/sqa/dss/fuzz/cares-fsanitize_fuzzer+0x550e1b) in ares_create_query
Shadow bytes around the buggy address:
0x0c06802432e0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c06802432f0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c0680243300: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c0680243310: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c0680243320: fa fa fa fa fa fa fa fa fa fa fd fd fd fa fa fa
=>0x0c0680243330: fd fd fd fa fa fa 00 00[02]fa fa fa fd fd fd fa
0x0c0680243340: fa fa fd fd fd fa fa fa fd fd fd fa fa fa fd fd
0x0c0680243350: fd fa fa fa fd fd fd fa fa fa fd fd fd fa fa fa
0x0c0680243360: fd fd fd fa fa fa fd fd fd fa fa fa fd fd fd fa
0x0c0680243370: fa fa fd fd fd fa fa fa fd fd fd fa fa fa fd fd
0x0c0680243380: fd fa fa fa fd fd fd fa fa fa fd fd fd fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
Shadow gap: cc
==3049145==ABORTING
MS: 4 ChangeByte-InsertByte-CopyPart-ChangeByte-; base unit: 00b28ff06b788b9b67c6b259800f404f9f3761fd
0x5c,0x2e,0x0,0x6b,0x0,
\\.\x00k\x00
artifact_prefix='./'; Test unit written to ./crash-edef708d314ed627eba0ef2b042e47aa96a9b899
Base64: XC4AawA=

可以看到,LibFuzzer 发现了 c-ares 中的 heap-buffer-overflow 漏洞,触发 crash 的用例保存为 crash-edef708d314ed627eba0ef2b042e47aa96a9b899 文件,该用例包含 5 个字节,字节码分别为:0x5c,0x2e,0x0,0x6b,0x0,其中 0x0 为空字符 NULL,属于不可见字符。

-

5. 提取语料

执行 Fuzz 时,传入一个空文件夹作为 copus 路径,执行完后,可以在文件夹中看到 LibFuzzer 执行过程中,生成的有效语料。

+

5. 提取语料

执行 Fuzz 时,传入一个空文件夹作为 copus 路径,执行完后,可以在文件夹中看到 LibFuzzer 执行过程中,生成的有效语料。

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
sqa@twtpesqa03:~/dss/fuzz/cares$ mkdir corpus
sqa@twtpesqa03:~/dss/fuzz/cares$ ls
BUILD build.sh cares_fuzzer corpus target.cc
sqa@twtpesqa03:~/dss/fuzz/cares$ ./cares_fuzzer corpus/
INFO: Seed: 4140748905
INFO: Loaded 1 modules (57 inline 8-bit counters): 57 [0x5a9100, 0x5a9139),
INFO: Loaded 1 PC tables (57 PCs): 57 [0x5a9140,0x5a94d0),
INFO: 0 files found in corpus/
INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes
INFO: A corpus is not provided, starting from an empty corpus
#2 INITED cov: 13 ft: 14 corp: 1/1b exec/s: 0 rss: 27Mb
#4 NEW cov: 16 ft: 20 corp: 2/3b lim: 4 exec/s: 0 rss: 27Mb L: 2/2 MS: 2 ChangeByte-InsertByte-
#9 NEW cov: 16 ft: 26 corp: 3/6b lim: 4 exec/s: 0 rss: 27Mb L: 3/3 MS: 5 ChangeBit-ChangeByte-ChangeByte-CopyPart-InsertByte-
#10 NEW cov: 16 ft: 32 corp: 4/10b lim: 4 exec/s: 0 rss: 27Mb L: 4/4 MS: 1 CrossOver-
#25 NEW cov: 18 ft: 34 corp: 5/11b lim: 4 exec/s: 0 rss: 27Mb L: 1/4 MS: 5 ChangeBit-ChangeASCIIInt-CMP-CrossOver-EraseBytes- DE: "\x01\x00"-
#94 NEW cov: 19 ft: 35 corp: 6/15b lim: 4 exec/s: 0 rss: 27Mb L: 4/4 MS: 4 CrossOver-ShuffleBytes-ChangeByte-ChangeBit-
#108 NEW cov: 21 ft: 37 corp: 7/16b lim: 4 exec/s: 0 rss: 27Mb L: 1/4 MS: 4 ChangeByte-ShuffleBytes-ChangeBit-ChangeByte-
#125 NEW cov: 22 ft: 38 corp: 8/18b lim: 4 exec/s: 0 rss: 27Mb L: 2/4 MS: 2 ShuffleBytes-InsertByte-
...
==3237059==ABORTING
MS: 2 InsertRepeatedBytes-ChangeByte-; base unit: 1bef8aac927d18852642a96c20e50efba80fdfae
0x5c,0x5,0x98,0x98,0x98,0x98,0x98,0x98,0x98,0x98,0x98,0x98,0x98,0x5c,0x2e,
\\\x05\x98\x98\x98\x98\x98\x98\x98\x98\x98\x98\x98\\.
artifact_prefix='./'; Test unit written to ./crash-157768d6f06d94325fe0e6bcf66cbd2d27dd8db7
Base64: XAWYmJiYmJiYmJiYmFwu
sqa@twtpesqa03:~/dss/fuzz/cares$ ls -al
total 2244
drwxrwxr-x 4 sqa sqa 4096 Sep 26 17:47 .
drwxrwxr-x 5 sqa sqa 4096 Sep 26 15:25 ..
drwxrwxr-x 9 sqa sqa 20480 Sep 26 17:18 BUILD
-rw-rw-r-- 1 sqa sqa 722 Sep 26 17:13 build.sh
-rwxrwxr-x 1 sqa sqa 2250720 Sep 26 17:18 cares_fuzzer
drwxrwxr-x 2 sqa sqa 4096 Sep 26 17:47 corpus
-rw-rw-r-- 1 sqa sqa 15 Sep 26 17:47 crash-157768d6f06d94325fe0e6bcf66cbd2d27dd8db7
-rw-rw-r-- 1 sqa sqa 499 Sep 26 17:14 target.cc
sqa@twtpesqa03:~/dss/fuzz/cares$ ls corpus/
08534f33c201a45017b502e90a800f1b708ebcb3 5175b74bd75b8d90a01f77709deba3982fbbdcb2 78bdce51613f555049a9937095bf469bcb77e94f be566cb17d3bce0b2a8e5e710b3779df720db1f5
0f1c5448bf80343eeac759f8adcbdc2720533d15 5318c4ac20dac95a702bee2e27834d39ea6bc2b6 8e54ed049741d7cf6fb8ef7f4288ef0be3b54f17 be5c29e07560abcf094c3419712a01590bbe8594
0fe509b10d833be6eb3d5ed4947cbe0fbb64ed84 5ba93c9db0cff93f52b521d7420e43f6eda2784f 8ea51a3719d7cbfc3e2dcd3edf6109918d5aad55 caea31b9ef76b9be352ad1054956efbb86d4451a
1bef8aac927d18852642a96c20e50efba80fdfae 60321f72401b49b895535045eb8d3b9ca7db7c7c 91a3f7c503955600f5dac12c1c1a3c5b674a4d98 dea712be0e801f4502a21e04dfbec5bd0cbc677c
24792aa3923c4cd185519d3d445ecfd0801db1c1 6380a9a2d2701df0cb53d880842747cbefef8a5d 9f64357cb30f24cf567513e140e9fb0cbf1a2be5 e716589d09e16cf4a48d2c7f1d357bb481aaf3bc
3a52ce780950d4d969792a2559cd519d7ee8c727 6414bd7955e39106721edf7cc29efdb82f7007ac b534844fd943d8b338025ad82d68283f1bcdb5c0
-

6. 合并语料

当 Fuzz 执行很久之后,会产生大量语料。为了减少语料数量,可以使用 merge 参数进行合并。创建一个 min_corpus 目录,执行 Fuzz 时,启动 merge 参数,传入 min_corpus 目录和 corpus 目录,合并完后,可以在 min_corpus 目录下看到缩减之后的语料。以下示例显示预料数据从 31 个减少到 29 个:

+

6. 合并语料

当 Fuzz 执行很久之后,会产生大量语料。为了减少语料数量,可以使用 merge 参数进行合并。创建一个 min_corpus 目录,执行 Fuzz 时,启动 merge 参数,传入 min_corpus 目录和 corpus 目录,合并完后,可以在 min_corpus 目录下看到缩减之后的语料。以下示例显示预料数据从 31 个减少到 29 个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
sqa@twtpesqa03:~/dss/fuzz/cares$ mkdir min_corpus
sqa@twtpesqa03:~/dss/fuzz/cares$ ./cares_fuzzer -merge=1 min_corpus/ corpus/
INFO: Seed: 1671146211
INFO: Loaded 1 modules (57 inline 8-bit counters): 57 [0x5a9100, 0x5a9139),
INFO: Loaded 1 PC tables (57 PCs): 57 [0x5a9140,0x5a94d0),
MERGE-OUTER: 31 files, 0 in the initial corpus, 0 processed earlier
MERGE-OUTER: attempt 1
INFO: Seed: 1671165588
INFO: Loaded 1 modules (57 inline 8-bit counters): 57 [0x5a9100, 0x5a9139),
INFO: Loaded 1 PC tables (57 PCs): 57 [0x5a9140,0x5a94d0),
INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 1048576 bytes
MERGE-INNER: using the control file '/tmp/libFuzzerTemp.3327149.txt'
MERGE-INNER: 31 total files; 0 processed earlier; will process 31 files now
#1 pulse cov: 9 ft: 10 exec/s: 0 rss: 27Mb
#2 pulse cov: 13 ft: 14 exec/s: 0 rss: 27Mb
#4 pulse cov: 20 ft: 22 exec/s: 0 rss: 27Mb
#8 pulse cov: 28 ft: 38 exec/s: 0 rss: 27Mb
#16 pulse cov: 29 ft: 56 exec/s: 0 rss: 27Mb
#31 DONE cov: 29 ft: 82 exec/s: 0 rss: 28Mb
MERGE-OUTER: succesfull in 1 attempt(s)
MERGE-OUTER: the control file has 2691 bytes
MERGE-OUTER: consumed 0Mb (27Mb rss) to parse the control file
MERGE-OUTER: 29 new files with 82 new features added; 29 new coverage edges
-

三、优化技巧

整体优化思路分两种:

+

三、优化技巧

整体优化思路分两种:

  1. 找到 crash 更快 –> 并行化,多个实例一起运行
  2. 覆盖 edge 更多 –> 加强语料,让输入能走到更深的分支
-

(一)并行化

1、LibFuzzer

(1) jobs 和 workers

+

(一)并行化

1、LibFuzzer

(1) jobs 和 workers

-

2、LibFuzzer 结合 AFL

AFL 可以支持 LibFuzzer 的 target,编译方法参考 此处说明

+

2、LibFuzzer 结合 AFL

AFL 可以支持 LibFuzzer 的 target,编译方法参考 此处说明

1
2
afl-fuzz -i testcase_dir -o findings_dir ./fuzz-target @@ 
./fuzz-target testcase_dir findings_dir # Will write new tests to testcase_dir

需要定期重启 AFL 和 LibFuzzer,以完成语料同步。

-

(二)加强语料

1、字典

运行时使用 -dict 参数指定字典(Dictionary)文件的路径,字典用于指定和控制模糊测试中输入数据的一部分,从而提高模糊测试的效果。

+

(二)加强语料

1、字典

运行时使用 -dict 参数指定字典(Dictionary)文件的路径,字典用于指定和控制模糊测试中输入数据的一部分,从而提高模糊测试的效果。

字典文件:

1
2
3
4
5
6
7
8
9
10
# Lines starting with '#' and empty lines are ignored.

# Adds "blah" (w/o quotes) to the dictionary.
kw1="blah"
# Use \\ for backslash and \" for quotes.
kw2="\"ac\\dc\""
# Use \xAB for hex values
kw3="\xF7\xF8"
# the name of the keyword followed by '=' may be omitted:
"foo\x0Abar"

使用方法(运行时):

1
./fuzz-target -dict=dict_file
-

2、CMP

编译时使用 -fsanitize-coverage=trace-cmp 参数,让 fuzz 拦截 CMP 指令(例如 if 语句中的 compare 条件)并根据拦截到的 CMP 指令的参数来引导变异。这可能会减慢模糊测试的速度,但很可能会改善测试结果。

+

2、CMP

编译时使用 -fsanitize-coverage=trace-cmp 参数,让 fuzz 拦截 CMP 指令(例如 if 语句中的 compare 条件)并根据拦截到的 CMP 指令的参数来引导变异。这可能会减慢模糊测试的速度,但很可能会改善测试结果。

使用方法(编译时):

1
clang++ buggy.cc -fsanitize=fuzzer,address -fsanitize-coverage=trace-cmp -g -o buggy-fuzzer
-

3、Value Profile

需要与 -fsanitize-coverage=trace-cmp 一起使用,让 fuzz 收集 CMP 指令的参数值进行分析,用于发现更多的有效输入。但是有两个缺点:首先可能会导致速度降低 2 倍。 其次语料库可能增长数倍。

+

3、Value Profile

需要与 -fsanitize-coverage=trace-cmp 一起使用,让 fuzz 收集 CMP 指令的参数值进行分析,用于发现更多的有效输入。但是有两个缺点:首先可能会导致速度降低 2 倍。 其次语料库可能增长数倍。

使用方法(运行时):

1
./fuzz-target -use_value_profile=1
-

4、Fuzzer-friendly build mode

程序中可能存在某些对 fuzz 不友好的特性,例如:

+

4、Fuzzer-friendly build mode

程序中可能存在某些对 fuzz 不友好的特性,例如:

  • 随机化:同一个输入,可能走不同的路径
  • 校验和:拦截无效输入
  • @@ -670,7 +669,7 @@

    1
    2
    3
    4
    5
    6
    7
    8
    void MyInitPRNG() {
    #ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
    // In fuzzing mode the behavior of the code should be deterministic.
    srand(0);
    #else
    srand(time(0));
    #endif
    }
    -

    四、 参考资料

      +

      四、 参考资料

      1. libFuzzer – a library for coverage-guided fuzz testing
      2. libFuzzer 使用总结教程
      3. Efficient Fuzzing Guide
      4. @@ -681,7 +680,6 @@

        Sanitizers
      5. The Magic Behind Feedback-Based Fuzzing
      - @@ -703,11 +701,14 @@

    1. - + 上一篇 @@ -776,11 +777,14 @@

    2. - + 上一篇 diff --git "a/2023/10/15/AFL\345\216\237\347\220\206\344\270\216\345\256\236\350\267\265/AFL\350\277\220\350\241\214\347\225\214\351\235\242.png" "b/2023/10/15/AFL\345\216\237\347\220\206\344\270\216\345\256\236\350\267\265/AFL\350\277\220\350\241\214\347\225\214\351\235\242.png" new file mode 100644 index 0000000..d8da759 Binary files /dev/null and "b/2023/10/15/AFL\345\216\237\347\220\206\344\270\216\345\256\236\350\267\265/AFL\350\277\220\350\241\214\347\225\214\351\235\242.png" differ diff --git "a/2023/10/15/AFL\345\216\237\347\220\206\344\270\216\345\256\236\350\267\265/index.html" "b/2023/10/15/AFL\345\216\237\347\220\206\344\270\216\345\256\236\350\267\265/index.html" new file mode 100644 index 0000000..18d5391 --- /dev/null +++ "b/2023/10/15/AFL\345\216\237\347\220\206\344\270\216\345\256\236\350\267\265/index.html" @@ -0,0 +1,575 @@ + + + + + + + + + AFL 原理与实践 - 千里之行,始于足下 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + + + + + + + + + + + + + +
      + +
      + + +
      + +

      + AFL 原理与实践 +

      + + + + + +
      + + +
      +
      + + + +

      在软件开发的世界里,质量和安全性是评估产品成败的重要指标。模糊测试作为一种高效的自动化测试方法,专门用来发现程序中的错误和安全漏洞。本文旨在详细介绍 AFL(American Fuzzy Lop)的基本原理和使用方法。

      +

      一、AFL 的原理介绍

      模糊测试是一种通过向软件输入异常或随机数据的自动化技术,目的是发现程序在处理意外或边缘情况输入时的错误。AFL 是这一测试策略中的杰出代表,它通过不断学习程序反应来改进测试用例,提高测试的覆盖率和发现漏洞的概率。

      +

      AFL 利用遗传算法,不断生成测试用例,并通过动态插桩技术监控程序的行为,特别是程序的代码覆盖情况。当新的输入能触发新的代码路径时,这个输入会被保存以供进一步的测试。该过程可以形成一个反馈循环,不断优化测试用例以探索更多程序状态。

      +

      下面是 AFL 工作流程图,展示了从准备测试用例到监控程序行为的步骤:

      +
      graph TD
      +    A[Compile with afl-gcc] --> B[Initialize seed corpus] --> C[Fuzzing loop] --> D[Select seed from corpus queue]
      +    D[Select seed from corpus queue] --> E[Mutate selected seed] --> F[Execute with mutated testcase]
      +    F[Execute with mutated testcase] --> G[Monitor for crashes and coverage update] --> H{Check for new coverage}
      +    H{Check for new coverage} -- YES --> I[Save mutated testcase to queue] --> D
      +    H{Check for new coverage} -- NO --> D
      + +

      流程图详细展示了 AFL 在模糊测试中的核心步骤:

      +
        +
      1. 编译(Compile with afl-gcc) - 使用 AFL 提供的编译器 afl-gcc,对目标程序进行编译,实现程序的动态插桩。

        +
      2. +
      3. 初始化种子库(Initialize seed corpus) - 创建初始测试用例集(种子库),这些测试用例将作为模糊测试的起点。

        +
      4. +
      5. 模糊测试循环(Fuzzing loop) - 一个不断循环的过程,根据测试结果更新种子库,并反复执行下列子步骤:
        a. 选择种子(Select seed from corpus queue) - 从种子库中选择一个种子文件作为当前测试的输入。
        b. 突变种子(Mutate selected seed) - 对选中的种子文件应用突变算法,生成新的测试用例。
        c. 执行测试(Execute with mutated test case) - 将变异后的测试用例作为输入执行已插桩的目标程序。
        d. 监控结果(Monitor for crashes and coverage update) - 监控程序的执行情况,记录崩溃和代码覆盖率的变化。

        +
      6. +
      7. 覆盖率检查(Check for new coverage) - 判断是否出现新的代码覆盖,如果有,则将其保存为新测试用例。

        +
      8. +
      9. 保存新测试用例(Save new testcase to queue) - 将触发新代码覆盖的测试用例保存到队列中,用于后续的测试。

        +
      10. +
      +

      AFL 采用了 fork 的运行模式,这使得当待测程序出现崩溃时,fuzz 进程不会终止,这一点相较于 LibFuzzer 更具优势。然而,频繁的 fork 操作也导致了 AFL 的效率不如 LibFuzzer。

      +

      二、AFL 安装和运行

      1. 支持的系统和架构

      AFL 设计之初主要针对 UNIX-like 系统,其在 Linux 系统上有最好的支持。然而,在社区的努力下,也有 Windows 版本的 AFL,如 winafl,其可以在 Windows 进行模糊测试。

      +

      AFL 还支持多种 CPU 架构,其中对 x86 和 x64 架构的支持最好。如果要支持 ARM 架构,则需要使用 AFL 的 QEMU 模式。

      +

      2. 安装步骤

      AFL 可以通过源码进行安装:

      +
      1
      2
      3
      apt install git make gcc
      git clone https://github.com/google/AFL.git
      cd AFL && make
      + +

      3. 运行参数

      在安装并编译完 AFL 之后,可以使用 afl-fuzz 命令来启动模糊测试。一个基本的 AFL fuzz 命令如下:

      +
      1
      afl-fuzz -i input_dir -o output_dir -- /path/to/program [options] @@
      + +

      这里:

      +
        +
      • afl-fuzz 是启动 AFL 模糊测试的程序。
      • +
      • -i input_dir 指定包含初始化测试用例的目录。
      • +
      • -o output_dir 指定存放的 fuzzing 结果的目录。
      • +
      +

      -- 之后的部分是运行被测试程序的命令行,其中 /path/to/program 替换为需要进行模糊测试的程序的路径,[options] 是运行该程序的任何选项或参数。

      +

      如果测试程序需要从文件中读取输入,可以在实际输入文件路径的位置使用 @@ 占位符。AFL 将替换 @@ 来插入它正在测试的输入文件。如果省略这个占位符,AFL 将会把模糊测试用例通过 stdin 传递给测试程序。

      +

      三、AFL 的使用示例

      本节使用一个简单的示例,演示 AFL 的操作步骤。

      +

      这是待测源码,其功能是接受一行命令行输入,进行四则运算。其中使用了不安全的 gets 函数,可能导致缓冲区溢出:

      +
      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
      #include <stdio.h>
      #include <string.h>

      int calculate(int a, int b, char op) {
      switch (op) {
      case '+': return a + b;
      case '-': return a - b;
      case 'x': return a * b;
      case '/': return b ? a / b : 0;
      default: return 0;
      }
      }

      int main() {
      char input[100];

      // unsafe method that may cause buffer overflow
      gets(input);

      int a, b, result;
      char op;

      if (sscanf(input, "%d %c %d", &a, &op, &b) != 3)
      return 1;

      result = calculate(a, b, op);
      printf("Result: %d\n", result);

      return 0;
      }


      + +

      1. 编译源码

      使用 AFL 的编译器 afl-gccafl-clang对源文件 hello.c 进行编译,添加所需要的代码覆盖插桩。afl-gccafl-clang 实际上是对常规的 gccclang编译器进行了封装,通过在编译时进行插桩,来统计 fuzz 过程中的代码覆盖率:

      +
      1
      afl-gcc -o hello hello.c
      + +

      2. 准备种子语料库

      不论程序是从 file 还是从 stdin 获取数据,AFL 都需要一个初始语料库来启动模糊测试过程。输入(无论是来自 file 或 stdin)是 AFL 用来开始探索程序行为的基础。初始语料库(seed corpus)是一组文件,其中包含了各种有效的输入示例,这些输入会被 AFL 用作模糊测试的起始点。

      +

      从 stdin 读取输入时,AFL 会将语料库中的每个文件内容作为输入在每次测试运行时送到程序的标准输入流中。使用 AFL 时,必须要有初始语料库。注意这一点 AFL 与 Libfuzzer 不同,Libfuzzer 只接受 stdin,不接受 file,初始语料库为可选项。

      +
      1
      2
      mkdir in
      echo 'abc' > in/seed
      + +

      3. 执行测试

      在运行 AFL 之前,需要执行下面的系统设置命令,将字符串”core”写入到文件 /proc/sys/kernel/core_pattern 中。

      +

      在 Linux 系统中,/proc/sys/kernel/core_pattern 文件用于指定当程序崩溃时,内核转储文件(core dump)的文件名模式。通过修改这个文件,可以控制内核生成 core 文件的行为。

      +

      通过执行这个命令,生成的核心转储文件将以”core”命名,这使得 AFL 更容易检测和处理目标程序的崩溃情况,从而更好地进行模糊测试。

      +
      1
      echo core | tee /proc/sys/kernel/core_pattern
      + +

      在 hello 中使用 gets 函数从 stdin 中读取数据,因此在运行 AFL 时,不需要添加 @@。是否使用@@ 取决于待测程序接受的输入来自 file 还是 stdin。

      +
      1
      afl-fuzz -i in -o out -- ./hello
      + +

      4. 结果解读

      AFL 开始运行后,将会看到如下的界面显示:

      + + +

      从这个界面上可以看到 AFL 找到了多少 crash,但还不能直观地显示覆盖率。pythia是一款 AFL 的扩展工具,可以查看代码覆盖率的情况。

      +

      AFL 在运行过程中,会不断地产生输出。输出目录结构如下:

      +
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      out
      |-- crashes
      | |-- README.txt
      | |-- id:000000,sig:06,src:000000,op:havoc,rep:64
      | |-- id:000001,sig:06,src:000002+000003,op:splice,rep:128
      | |-- id:000002,sig:06,src:000003,op:havoc,rep:32
      | |-- id:000003,sig:06,src:000002+000003,op:splice,rep:16
      | |-- id:000004,sig:06,src:000004,op:havoc,rep:32
      | |-- id:000005,sig:11,src:000003+000002,op:splice,rep:64
      | `-- id:000006,sig:06,src:000004+000005,op:splice,rep:2
      |-- fuzz_bitmap
      |-- fuzzer_stats
      |-- hangs
      |-- plot_data
      `-- queue
      |-- id:000000,orig:seed
      |-- id:000001,src:000000,op:havoc,rep:16,+cov
      |-- id:000002,src:000000+000001,op:splice,rep:4,+cov
      |-- id:000003,src:000001,op:arith8,pos:1,val:-5,+cov
      |-- id:000004,src:000001,op:arith8,pos:1,val:-9,+cov
      `-- id:000005,src:000002+000003,op:splice,rep:4,+cov

      3 directories, 17 files

      + +
        +
      • queue/ - 存放 AFL 生成的触发新代码路径的测试样本

        +
      • +
      • crashes/ - 存放能触发待测程序崩溃的测试样本

        +
      • +
      • hangs/ - 存发导致待测程序超时的测试样本

        +
      • +
      • fuzzer_stats - 文本文件,包含了 fuzzer 的实时统计信息,如执行速度、路径覆盖等度量指标。这个文件不断更新以反映当前的 fuzzing 状态。

        +
      • +
      • plot_data - 文本文件,包含了 AFL 执行过程中的统计数据。使用 AFL 的 afl-plot 工具处理 plot_data 文件,可以生成 fuzz 过程的可视化图像。

        +
      • +
      • fuzz_bitmap - 这是用来记录路径覆盖率的位图(coverage bitmap),非人类可读。AFL 使用这个位图来跟踪程序在处理不同输入时执行的不同分支,用来帮助 AFL 识别新的、唯一的代码路径,以便后续生成更具有探索性的测试样本。

        +
      • +
      +

      四、AFL 的 QEMU 模式

      前面的例子中,AFL 在对待测程序的源码进行编译时插桩。实际中,我们不一定能拿到待测源码,我们要测的可能是一个已经编译好的二进制文件。这种情况下 Libfuzzer 就无能为力了,但 AFL 还能用,这依赖于 AFL 的 QEMU 模式。

      +

      AFL 的 QEMU 模式,也称为 AFL-QEMU,允许你在二进制模糊测试中使用 AFL,即使源代码不可用。这在对闭源应用进行模糊测试时非常有用。这种模式使用 QEMU 的用户模式仿真,来运行并分析非原生的二进制文件。以下是安装 AFL 的 QEMU 模式和使用步骤的指南:

      +

      1. 编译 QEMU 支持

      在 AFL 主目录下有一个专门的 QEMU 模式目录。进入该目录并编译 QEMU 模式,过程中可能缺少依赖项,根据提示进行安装即可:

      +
      1
      2
      cd qemu_mode
      ./build_qemu_support.sh
      + +

      2. AFL-QEMU 使用步骤

      用法很简单,在 AFL 运行命令中添加 -Q 参数即可:

      +
      1
      afl-fuzz -i input -o output -Q -- /path/to/binary @@
      + + +

      五、AFL 的升级版——AFL++

      AFL++ 可以看作是原始 AFL 的”增强版”,对 AFL 的调度策略和变异算法进行了很多改进,同时还引入了很多新特性,如 CMPLOG 和持久化这样的特性。

      +

      1. CMPLOG

      CMPLOG 是 AFL++ 引入的一个新功能,类似于 Libfuzzer 中的 trace-cmp,它可以极大地提高代码覆盖率。CMPLOG 的主要作用是对程序中的所有比较操作进行记录,包括等于、不等于、小于等逻辑比较。当 fuzzer 执行时,CMPLOG 能够捕获比较操作的参数,并将对应的值添加到 fuzzer 的输入测试用例中。这个过程实际上帮助 fuzzer 更好地理解代码中期待的输入,特别是那些用于逻辑分支的字面值和魔法数字。这种理解能够导向更深入的路径覆盖,进而揭露隐藏更深的潜在缺陷。

      +

      2. 持久化模式

      AFL 的持久化(persistent)模式允许目标程序在单个进程周期内重复执行多次模糊测试用例。这对性能产生了显著的提升,因为它减少了程序启动和终止的开销,特别是当测试的目标程序需要很大的初始化成本时。在 AFL++ 中,持久化模式的执行更为高效,它允许模糊器在目标程序中一次性执行多个测试用例,而非每次执行一个用例都重启目标程序。有了持久化模式,AFL++ 能够在相同的时间内执行更多的测试迭代,从而提高漏洞检测的速度。

      +

      六、小结

      AFL 是一款强大的模糊测试工具,实践中 AFL++ 的应用非常广泛。相比 Libfuzzer,AFL++ 不局限于源码,并且支持多种 cpu 架构,还有丰富的插件生态可以使用。

      +

      但模糊测试本身存在覆盖率瓶颈的问题,难以探索复杂的程序路径,这时候可以结合其他的技术,如符号执行来突破。

      + + + + +
      +
      + +
      + + + +
      + + Copyrights © 2024 一瓢清浅. All Rights Reserved. + +
      + +
      + +
      +
      + + +
      + + +
      + + + +
      + + + + + + + +
      +
      +
      + +
      + + 作者的图片 + +

      一瓢清浅

      + +

      个人技术博客
      涉猎开发、测试、数据、算法、安全等领域

      +
      + + +
      + +
      +

      IT工程师

      + +
      + + +
      + +
      + 中国 +
      + +
      +
      + + + +
      + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/all-archives/index.html b/all-archives/index.html index 9b6dc50..a3c3643 100644 --- a/all-archives/index.html +++ b/all-archives/index.html @@ -5,20 +5,20 @@ - - all-archives - 千里之行始于足下 + + all-archives - 千里之行,始于足下 - + - + @@ -74,7 +74,7 @@ href="/" aria-label="" > - 千里之行始于足下 + 千里之行,始于足下

@@ -236,6 +236,24 @@

+ +
    九月 diff --git a/all-categories/index.html b/all-categories/index.html index 9f10194..658306e 100644 --- a/all-categories/index.html +++ b/all-categories/index.html @@ -5,20 +5,20 @@ - - categories - 千里之行始于足下 + + categories - 千里之行,始于足下 - + - + @@ -74,7 +74,7 @@ href="/" aria-label="" > - 千里之行始于足下 + 千里之行,始于足下

@@ -258,7 +258,7 @@

@@ -224,6 +224,10 @@

@@ -248,7 +248,7 @@

-

LibFuzzer从0到1,原理 + 安装 + 使用 + 优化,一篇讲完

+

模糊测试工具 LibFuzzer 从 0 到 1,原理 + 安装 + 使用 + 优化,一篇讲完

+ + + + + + + 归档: 2023/10 - 千里之行,始于足下 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ +
+ + + + +
+
    + + +
  • 第 1 页 共 1 页
  • +
+
+ +
+ + + +
+ + Copyrights © 2024 一瓢清浅. All Rights Reserved. + +
+ +
+ +
+ + + + + + + +
+
+
+ +
+ + 作者的图片 + +

一瓢清浅

+ +

个人技术博客
涉猎开发、测试、数据、算法、安全等领域

+
+ + +
+ +
+

IT工程师

+ +
+ + +
+ +
+ 中国 +
+ +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2023/index.html b/archives/2023/index.html index 70064ab..e532846 100644 --- a/archives/2023/index.html +++ b/archives/2023/index.html @@ -5,8 +5,8 @@ - - 归档: 2023 - 千里之行始于足下 + + 归档: 2023 - 千里之行,始于足下 @@ -16,9 +16,9 @@ - + - + @@ -72,7 +72,7 @@ href="/" aria-label="" > - 千里之行始于足下 + 千里之行,始于足下
@@ -215,6 +215,57 @@

@@ -248,7 +248,7 @@

-

常用的各类Linux操作命令

+

常用的各类 Linux 操作命令

- - 分类: 开发环境 - 千里之行始于足下 + + 分类: 开发环境 - 千里之行,始于足下 @@ -16,9 +16,9 @@ - + - + @@ -72,7 +72,7 @@ href="/" aria-label="" > - 千里之行始于足下 + 千里之行,始于足下
@@ -248,7 +248,7 @@

-

Docker基本原理以及常用命令

+

Docker 基本原理以及常用命令

-

常用的各类Linux操作命令

+

常用的各类 Linux 操作命令

- - 分类: C++ - 千里之行始于足下 + + 分类: C++ - 千里之行,始于足下 @@ -16,9 +16,9 @@ - + - + @@ -72,7 +72,7 @@ href="/" aria-label="" > - 千里之行始于足下 + 千里之行,始于足下
@@ -248,7 +248,7 @@

-

C++和Python都是从C语言演变出来的面向对象(OOP)的编程语言,本文基于OOP三特性,比较C++和Python的异同点

+

C++ 和 Python 都是从 C 语言演变出来的面向对象(OOP)的编程语言,本文基于 OOP 三特性,比较 C++ 和 Python 的异同点

-

介绍C++对象在内存中的存储模型

+

介绍 C++ 对象在内存中的存储模型

-

浅析C++的默认构造函数与拷贝构造函数

+

浅析 C++ 的默认构造函数与拷贝构造函数

-

在循环引用的场景下探讨C++中shard_ptr和weak_ptr原理

+

在循环引用的场景下探讨 C++ 中 shard_ptr 和 weak_ptr 原理

- - 分类: 编程语言 - 千里之行始于足下 + + 分类: 编程语言 - 千里之行,始于足下 @@ -16,9 +16,9 @@ - + - + @@ -72,7 +72,7 @@ href="/" aria-label="" > - 千里之行始于足下 + 千里之行,始于足下
@@ -248,7 +248,7 @@

-

C++和Python都是从C语言演变出来的面向对象(OOP)的编程语言,本文基于OOP三特性,比较C++和Python的异同点

+

C++ 和 Python 都是从 C 语言演变出来的面向对象(OOP)的编程语言,本文基于 OOP 三特性,比较 C++ 和 Python 的异同点

-

介绍C++对象在内存中的存储模型

+

介绍 C++ 对象在内存中的存储模型

-

浅析C++的默认构造函数与拷贝构造函数

+

浅析 C++ 的默认构造函数与拷贝构造函数

-

在循环引用的场景下探讨C++中shard_ptr和weak_ptr原理

+

在循环引用的场景下探讨 C++ 中 shard_ptr 和 weak_ptr 原理

- - 千里之行始于足下 + + 千里之行,始于足下 - + - + - + @@ -72,7 +72,7 @@ href="/" aria-label="" > - 千里之行始于足下 + 千里之行,始于足下
@@ -214,6 +214,57 @@