默小白 发表于 2019-5-28 10:58

【转帖】C++逆向学习(三) 移动构造函数

转自:https://xz.aliyun.com/t/5149

最近研究了一下C++类的**移动构造函数**,同时也进行了一些逆向分析,过程中碰到一个很奇怪的问题,以此记录

# 相关背景

## 右值引用

右值引用主要是为了解决`C++98/03`遇到的两个问题

> 1. 临时对象非必要的昂贵的拷贝操作
> 2. 模板函数中如何按照参数的实际类型进行转发

本文主要探讨问题1,一些代码尝试和IDA中逆向的分析

学习链接:[从4行代码看右值引用](https://www.cnblogs.com/qicosmos/p/4283455.html),这里就不多说了

## move语义

比如在`vector.push_back(str)`时,`str(类)`作为实参,会复制一份自身成为形参,进入函数调用

而这个过程中就会产生**临时对象**,那么也就会调用**拷贝构造函数**

而如果`vector.push_back(std::move(str))`,就可以匹配**移动构造函数**,省去这个拷贝过程以提高效率

> 链接中已经解释的很详细了,不再赘述,总之就是**给将亡值续命**,延长它的生命周期(原本很可能是一个临时变量)

# 代码分析

接下来的部分内容可以作为上一篇文章(https://xz.aliyun.com/t/4933)的补充,在分析**移动构造函数**时又学到了一些之前没有注意过的`vector`的细节

## Str类源码

```
#include<iostream>
#include<string.h>
#include<vector>

using namespace std;

class Str {
public:
    char* str;
    Str(char value[]) {
      cout << "Ordinary constructor" << endl;
      int len = strlen(value);
      this->str = (char*)malloc(len + 1);
      memset(str, 0, len + 1);
      strcpy(str, value);
    }
    //拷贝构造函数
    Str(const Str& s) {
      cout << "copy constructor" << endl;
      int len = strlen(s.str);
      str = (char*)malloc(len + 1);
      memset(str, 0, len + 1);
      strcpy(str, s.str);
    }
    //移动构造函数
    Str(Str&& s) {
      cout << "move constructor" << endl;
      str = s.str;
      s.str = NULL;
    }
    ~Str() {
      cout << "destructor" << endl;
      if (str != NULL) {
            free(str);
            str = NULL;
      }
    }
};
//g++ xxx.cpp -std=c++17
```

## 代码1

`main`函数中,不使用`move`语义,会调用拷贝构造函数

```
int main(int argc, char** argv) {
    char value[] = "template";
    Str s(value);
    vector<Str> vs;
    vs.push_back(s);
    return 0;
}
```

IDA打开如下

!(https://xzfile.aliyuncs.com/media/upload/picture/20190513221326-464a486a-7589-1.png)

简单的流程,甚至`Str`的高亮都是对称的

最初调用`Str`的拷贝构造函数,匹配的是`Str(char value[])`,接着初始化`vector`,然后一次`push_back(s)`

跟进`push_back`

!(https://xzfile.aliyuncs.com/media/upload/picture/20190513221330-48fdb07e-7589-1.png)

一开始仍然是熟悉的判断`vector`的`size & capacity`的关系,最终调用的是这里的复制构造函数

!(https://xzfile.aliyuncs.com/media/upload/picture/20190513221335-4b94d7d6-7589-1.png)

> 注意第一个参数是`this`,是C++成员函数调用时的第一个参数,类指针

运行结果:

!(https://xzfile.aliyuncs.com/media/upload/picture/20190513221340-4ec00390-7589-1.png)

## 代码2

代码2,只`move(s)`

```
int main(int argc, char** argv) {
    char value[] = "template";
    Str s(value);
    vector<Str> vs;
    //vs.push_back(s);

    //cout<<"-----------------"<<endl;
    vs.push_back(move(s));
    return 0;
}
```

IDA打开如下:

!(https://xzfile.aliyuncs.com/media/upload/picture/20190513221346-522e3ccc-7589-1.png)

注意到其中的`std::move`,跟进发现其实实现只有一句话

!(https://xzfile.aliyuncs.com/media/upload/picture/20190513221351-5536fbd4-7589-1.png)

也印证了`move`实际上不移动任何东西,唯一的功能是**将一个左值强制转换为一个右值引用**

继续跟进

!(https://xzfile.aliyuncs.com/media/upload/picture/20190513221358-598c6cfa-7589-1.png)

仍然是判断大小和容量的代码,接着调用的是**移动构造函数**

!(https://xzfile.aliyuncs.com/media/upload/picture/20190513221402-5bea3a54-7589-1.png)

运行结果:

!(https://xzfile.aliyuncs.com/media/upload/picture/20190513221406-5e7defd6-7589-1.png)

## 代码3

这段代码实际上只是在`单纯move`之前加上了一句`push_back(s)`,但是运行结果差了很多

作为对`vector逆向学习`的补充

> "我全都要"写法,同时用拷贝构造和移动构造

```
int main(int argc, char** argv) {
    char value[] = "template";
    Str s(value);
    vector<Str> vs;
    vs.push_back(s);

    cout<<"-----------------"<<endl;
    vs.push_back(move(s));
    return 0;
}
```

按理来说,输出结果也只应该比代码2多一个`copy constructor`和`destructor`,但实际上多了很多东西

!(https://xzfile.aliyuncs.com/media/upload/picture/20190513221415-63c62c38-7589-1.png)

IDA打开并没有出乎意料的结果,仍然是清晰的两次`push_back`,跟进后也没有什么特别的发现,查看交叉引用也没能找到相关信息

> 为什么在`move`之后还会有一次`copy`,对应的之后又多了一个`desctructor`?

首先,`vector`虽然是值语义,但是`move`过后,既然已经调用了**移动构造函数**,肯定不会再无聊的拷贝一次

在`vs`里调试,输出各个时间点的`capacity`

!(https://xzfile.aliyuncs.com/media/upload/picture/20190513221423-6837120a-7589-1.png)

注意`第一个destructor`和`容量2`的出现时间

跟进源码好久后才发现,多的`copy`的产生原因,是因为`vector`内部动态扩容时,在新开辟的空间上调用了**复制构造函数**

也就是说把原来的一个`Str s`复制到了新内存空间,这个过程并没有调用**移动构造函数**

> 可能这也是写了移动构造函数后,保险起见也要写一个复制构造函数的原因

### 其他

考虑这个问题

> 为什么`vector`内部扩容时,要在新地址调用拷贝构造函数呢?
>
> 之前文章已经分析过,`vector`实际上只存了类型的数据结构
>
> 直接`memcpy(new_memory,old_memory,size)`,再把旧内存空间清零,会造成什么问题?

查了一些资料后发现,扩容是`allocator`的事情,一个可能的实现是**原位new**

而如果直接`memcpy`,会不会出问题取决于`vector`存的类型是否平凡(POD)
POD是`Plain old data structure`的缩写

> 资料提到`shared_ptr`也可能会被影响,取决于引用计数放在哪里

但无论如何,指针的浅拷贝、深拷贝问题值得注意,否则在`vector`内部扩容时,可能2个指针指向同一块内存,析构时会产生严重的错误

> 一个月后的SUCTF会有一道C++底层相关的pwn,欢迎来体验

NSYGHIHI 发表于 2019-5-28 11:20

写的不错学习了。不过代码地方有点小瑕疵,当*this==str,直接就返回*this就行了。没必要再进行下面操作了

青松 发表于 2019-5-28 14:40

向高手,学习一下,

xjt6294922 发表于 2019-5-28 22:09

看着有点生涩

Taokaka 发表于 2019-5-28 23:05

学习一下 大佬辛苦了

theStyx 发表于 2019-5-29 00:10

学习学习{:301_1009:}

学士天下 发表于 2019-5-29 08:51

受教,谢啦!!!

Dolts 发表于 2019-5-29 08:57

围观围观。

zhongjiezhe 发表于 2019-5-29 11:21

还可以更加简单

heang567 发表于 2019-6-11 21:52

谢谢大佬分享
页: [1] 2
查看完整版本: 【转帖】C++逆向学习(三) 移动构造函数