转自:https://xz.aliyun.com/t/5149
最近研究了一下C++类的移动构造函数,同时也进行了一些逆向分析,过程中碰到一个很奇怪的问题,以此记录
相关背景
右值引用
右值引用主要是为了解决C++98/03
遇到的两个问题
- 临时对象非必要的昂贵的拷贝操作
- 模板函数中如何按照参数的实际类型进行转发
本文主要探讨问题1,一些代码尝试和IDA中逆向的分析
学习链接:从4行代码看右值引用,这里就不多说了
move语义
比如在vector.push_back(str)
时,str(类)
作为实参,会复制一份自身成为形参,进入函数调用
而这个过程中就会产生临时对象,那么也就会调用拷贝构造函数
而如果vector.push_back(std::move(str))
,就可以匹配移动构造函数,省去这个拷贝过程以提高效率
链接中已经解释的很详细了,不再赘述,总之就是给将亡值续命,延长它的生命周期(原本很可能是一个临时变量)
代码分析
接下来的部分内容可以作为上一篇文章C++逆向学习(二) vector的补充,在分析移动构造函数时又学到了一些之前没有注意过的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打开如下
简单的流程,甚至Str
的高亮都是对称的
最初调用Str
的拷贝构造函数,匹配的是Str(char value[])
,接着初始化vector
,然后一次push_back(s)
跟进push_back
一开始仍然是熟悉的判断vector
的size & capacity
的关系,最终调用的是这里的复制构造函数
注意第一个参数是this
,是C++成员函数调用时的第一个参数,类指针
运行结果:
代码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打开如下:
注意到其中的std::move
,跟进发现其实实现只有一句话
也印证了move
实际上不移动任何东西,唯一的功能是将一个左值强制转换为一个右值引用
继续跟进
仍然是判断大小和容量的代码,接着调用的是移动构造函数
运行结果:
代码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
,但实际上多了很多东西
IDA打开并没有出乎意料的结果,仍然是清晰的两次push_back
,跟进后也没有什么特别的发现,查看交叉引用也没能找到相关信息
为什么在move
之后还会有一次copy
,对应的之后又多了一个desctructor
?
首先,vector
虽然是值语义,但是move
过后,既然已经调用了移动构造函数,肯定不会再无聊的拷贝一次
在vs
里调试,输出各个时间点的capacity
注意第一个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,欢迎来体验