用反汇编分析c++RVO开启和关闭时的底层原理以及C++prvalue,xvalue和lvalue的相关知识
前言
本篇文章为笔者的读书笔记,未经允许请勿转载。如果对你有帮助记得点个赞(●’◡’●)
本篇文章主要讲述C++prvalue,xvalue和lvalue的相关知识,会用到部分intel式和ATT式汇编的知识。我会在文章末尾给出测试代码的反汇编代码以及右值引用(Rvalue references)官方文档 😃。
三五法则
三五法则:有析构就应该有拷贝构造函数和拷贝赋值运算法(3)。
c++11下,一个类还可以有移动构造函数和移动赋值运算符(3+2)。
三五法则时候一般情况,如果不想某个函数被普通或者友元使用可以将其定义为=delete或者在private里面声明但不定义,具体参考C++ primer p449。
移动函数的出现提高了类内存转让的效率,而支承这一技术的基础就是prvalue,xvalue,lvalue.
(xvalue和prvalue统称为rvalue,lvalue和xvalue统称为glvalue。只需要记住上面说的三种就可以,这两个统称可以不用记)。
这三种值的出现场合和特点在后文详细说明。
未开启RVO优化与xvalue和prvalue的关系
关闭RVO的方法:
eg:clang++ -g -fno-elide-constructors /home/dengye/test/test.cpp -o /home/dengye/test/test
先设计一个三五法则的类,再分析关闭和开启RVO时程序的堆栈分布图。
废话不多说开整!
测试代码
class B
{
public:
B(int val=0):bVal(val){
}
// B(const B& tmp):bVal(tmp.bVal){}
~B(){
}
int bVal;
};
class Foo
{
public:
Foo(int tmp = 0):ptrb(new B(tmp))
{
std::cout<<"构造函数"<<std::endl;
}
Foo(const Foo& src):ptrb(new B(*(src.ptrb)))//会继续调用B的拷贝构造函数
{
std::cout<<"拷贝构造函数"<<std::endl;
}
Foo& operator=(const Foo& src)
{
std::cout<<"拷贝赋值函数"<<std::endl;
if (this == &src)
return *this;//防止自己给自己赋值。
delete ptrb;//先把自己的这块内存干掉
ptrb = new B(*(src.ptrb));
return *this;
}
Foo(Foo&& src) noexcept :ptrb(src.ptrb)//noexcept:通知标准库我们这个移动构造函数不抛出任何异常(如果编译器确认函数不会抛出异常,它就能执行某些特殊的优化操作,而这些操作不适合用于可能出错的代码),例如我们希望在vector重新分配内存这类情况下对我们自定义类型的对象进行移动而不是拷贝,就必须显式地告诉标准库我们的移动构造函数可以安全使用。《c++primer》p474,690。
{
std::cout<<"移动构造函数"<<std::endl;
src.ptrb = nullptr;//记得将原来指向堆内存的指针置空.
}
Foo& operator=(Foo&& src) noexcept
{
std::cout<<"移动赋值函数"<<std::endl;
if (this == &src)
return *this;
delete ptrb;
ptrb = src.ptrb;//这里不需要在new,直接从src那里拿来
src.ptrb = nullptr;//记得置空
return *this;
}
~Foo()
{
std::cout<<"析构函数"<<std::endl;
delete ptrb;//删一个空指针没任何反应
}
B *ptrb;
};
Foo RVO_test(int val)
{
Foo foo(val);
return foo;
}
int main()
{
Foo fooRvo = RVO_test(100);
return 0;
}
在分析之前我先把该程序的内存分布图画出来帮大家理解,见下图:

像这种random offset的出现,就是为了避免溢出攻击。画这张图要用到
nm命令(寻找代码段,bss段和data段的起始地址),gdb的vmmap命令(寻找堆栈的起始和结束地址)。灰色部分表示这些范围在进程虚拟地址空间中不可用,也就是说,没有为这些区域创建页表(page table)。
反汇编配合堆栈图分析流程
友情提醒:请配合附件中的反汇编代码进行分析。


到达这一步以前发生的事情有:main函数开辟出了0x20字节的空间,将rbp-0x18的地址(临时对象prvalue)给了rdi,然后将0x64给了esi,接着调用了RVO_test(int)函数,还没push rbp。


到达这一步以前发生的事情有:RVO_test(int)函数开辟了0x30字节的空间,rdi寄存器存着临时对象prvalue的地址,然后将其赋给了rbp-0x8,rbp-0x20,rbp-0x28,又将0x64赋给了rbp-0xc(形参val),最后在临时对象foo的地址上进行了构造。


到达这一步以前发生的事情有:编译器将rbp-0x18(局部对象foo)的内容移动到了临时对象的里面。此时局部对象的指针数据成员指向空,在RVO_test(int)函数返回时,会析构这个局部对象,而delete一个空指针没任何反应。最后RVO_test(int)函数将rbp-0x28地址上的内容赋给rax作为返回值。


本文探讨C++中RVO优化技术,并深入分析prvalue、xvalue及lvalue的概念与特性,通过反汇编代码揭示RVO开关时底层原理。
最低0.47元/天 解锁文章
774





