反汇编看c++引用

继续反汇编系列,本次使用vc2008在x86体系下分析c++中的引用。

定义一个引用类型和将一个变量转换成引用类型一样吗?

引用比指针安全,真的是这样吗,对引用不理解的话比指针还危险。

为什么要用常量引用传参,只是为了只读?

先来说明一下下面使用到的词汇:

对象:不是OO里的对象,而是泛指在c++语言中某种类型(内嵌,结构体,类)的实例,与变量相同的意思。

存储体: “the standard (draft 3225, section [basic.life]) which clearly states that a reference binds to storage and can outlive the object which existed when the reference was created:”,对象(或变量)的存储内容的空间,一般是内存。

下面用到的变量名命名规则:

引用: 以r开头,紧跟接类型缩写,如 float& rf。

指针: 以p开头,紧跟接类型缩写,如 float* pf, const float* pcf, float* const pfc。

c++代码编译成汇编代码后,引用和指针同样是一个指向内存地址的存储体(一般是内存单元,或优化后使用寄存器,存放指向的内存地址)。

    uintptr_t uintptr = 0;
0041C7F6  mov         dword ptr [uintptr],0
    float flt = 0.f;
0041C7FD  fldz
0041C7FF  fstp        dword ptr [flt]
    float flt2 = 2.f;
0041C802  fld         dword ptr ds:[426C64h]
0041C808  fstp        dword ptr [flt2]
    float& rf = (float&)uintptr;
0041C80B  lea         eax,[uintptr]
0041C80E  mov         dword ptr [rf],eax
    float* const pfc = &(float&)uintptr;
0041C81F  lea         edx,[uintptr]
0041C822  mov         dword ptr [pfc],edx

不同的是它们的访问方式,最根本的区别就是,引用的访问直接访问被引用目标(直接反引用),而指针的访问是访问存放着目标内存地址的存储体,对目标的访问必须显式反引用。

也就是说你不能访问到引用存放目标内存地址的存储体(,除非你强行crack)。当你使用赋值符号(=)对引用进行赋值时,编译器首先将引用反引用为目标,你只能对引用的目标进行赋值操作。指针在没有使用反引用(*)时,访问的是存放着目标内存地址的存储体,也就是你可以对指针单元赋值改变指向,而无法对引用单元赋值改变指向。

    rf = 1.;
0041C837  mov         eax,dword ptr [rf]
0041C83A  fld1
0041C83C  fstp        dword ptr [eax]  

同样当你希望通过&符号访问引用单元的地址时,你也是不可能做到的,因为引用先于地址访问符号,反引用到被引用的目标身上,也就是说&访问到的是被引用的目标的地址。以上面的反汇编来说,当你使用&rf访问地址时,rf比&先反引用成引用的目标,&就只能访问到rf引用的目标"dword ptr [rf]"。除非你用汇编代码强行"lea ?, [rf]",你才能取得引用的用于存放引用目标的地址的地址。

    float* pf = &rf;
0041C82B  mov         ecx,dword ptr [rf]
0041C82E  mov         dword ptr [pf],ecx
    void* pv = (void*)&pfc;
0041C831  lea         edx,[pfc]
0041C834  mov         dword ptr [pv],edx
    void* pv2;
    __asm {
        lea eax, [rf];
0041C837  lea         eax,[rf]
        mov dword ptr[pv2], eax;
0041C83A  mov         dword ptr [pv2],eax
    }

如果说引用是被引用目标的别名,那么将一个变量转换成引用类型,是否使用这个变量成为了其它目标的别名?

答案显然不是。那会是什么呢?先来看反汇编代码:

    rf = 1.;
0041C837  mov         eax,dword ptr [rf]
0041C83A  fld1
0041C83C  fstp        dword ptr [eax]
    (float&)rf = 2.;
0041C83E  mov         ecx,dword ptr [rf]
0041C841  fld         dword ptr ds:[426C64h]
0041C847  fstp        dword ptr [ecx]
    (float&)uintptr = 3.;
0041C849  fld         dword ptr ds:[426BC4h]
    (float&)uintptr = 3.;
0041C84F  fstp        dword ptr [uintptr]
    (int&)rf = 2;
0041C852  mov         edx,dword ptr [rf]
0041C855  mov         dword ptr [edx],2
    (int&)uintptr = 3;
0041C85B  mov         dword ptr [uintptr],3  

注:fld是从内存单元加载浮点数到浮点寄存器,fstp是将浮点寄存器0的浮点数存储进内存单元。

将变量转换成引用类型,不是把变量转换成引用,而是转换成批向变量的引用,按引用的类型来访问。

uintptr是一个非引用类型,(float&)uintptr转换成引用类型,但uintptr不引用其它目标,而是作为一个没有别名的引用目标使用,或者说对uintptr内存单元改变了对它访问的类型(,与*(float*)&uintptr访问的效果一样)。而(float)uintptr则是按uintptr声明的类型读出它的内存单元的数据,然后将数据转换类型。

至于"(float&)rf = 2.;",rf这个引用首先反引用(在rf我们不可能通过c++语言访问到的指针单元,放着uintptr变量单元的地址,反引用成uintptr),实质就是对引用目标改变了对它访问的类型,又如"(int&)rf = 2;",并非改变对引用"rf"的访问类型,而是改变对其引用的目标"uintptr"的访问类型。

说到这里,引用的引用是禁止的,这就不难理解了。当你有一个名为"rf"的引用,你对这个"rf"的访问(不论读写)都会转化成对引用目标的访问(限定在高级语言层)。即使你想定义一个引用去引用这个引用也是无功的。"float& rfx = rf;"这句,rfx并不是在引用rf,而是rf直接反引用成了uintptr,实质就是"float& rfx = uintptr;"。如果你要引用到名为rf的引用,你必须先要能够通过在c++语言层取得名为rf的引用,它存放指针的内存单元地址,而这样是不可能的,所以引用的引用是禁止的。

不知从何起方间流传引用比指针安全。理由大至如下:

1. 引用定义之时必须初始化。

2. 引用不会指向NULL。

3. 引用的指向不可以被修改,只有定义引用的同时才能定义它的指向。

1. 的问题归根到底是程序员不对变量(类构造)进行初始化,这样的程序员不少你还改变不了他们。使用引用,编译器强行要求他们进行初始化。因为由于没有写初始化造成的问题不只专属于指针。往往有程序员认为花时间写初始化是在浪费时间,这么多变量我还要一个一个初始化,每个类这么多成员变量我还要一个一个初始化,这么多类我还要一个一个为它们写几种构造函数?!!我的工作是按需求写逻辑不是写初始化和构造析构函数。

2. 的问题有点无稽,引用为什么就理所当然不会指向NULL。

看一下下面的反汇编代码:

    uintptr_t ptr = 0;
0041C8A6  mov         dword ptr [ptr],0
    float flt = 0.;
0041C8AD  fldz
0041C8AF  fstp        dword ptr [flt]
    float flt2 = 2.;
0041C8B2  fld         dword ptr ds:[426C64h]
    float flt2 = 2.;
0041C8B8  fstp        dword ptr [flt2]
    float& rf0 = *(float*)0;
0041C8BB  mov         dword ptr [rf0],0            ; 反引用到空地址上,
    float& rf = *new float(flt);
0041C8C2  push        4
0041C8C4  call        operator new (040112Eh)
0041C8C9  add         esp,4
0041C8CC  mov         dword ptr [ebp-40h],eax
0041C8CF  cmp         dword ptr [ebp-40h],0
0041C8D3  je          block1+45h (041C8E5h)          ; new是否分配到空间
0041C8D5  mov         eax,dword ptr [ebp-40h]
0041C8D8  fld         dword ptr [flt]
0041C8DB  fstp        dword ptr [eax]
0041C8DD  mov         ecx,dword ptr [ebp-40h]
0041C8E0  mov         dword ptr [ebp-48h],ecx
0041C8E3  jmp         block1+4Ch (041C8ECh)
0041C8E5  mov         dword ptr [ebp-48h],0          ; new失败了,名为rf的引用指向NULL
0041C8EC  mov         edx,dword ptr [ebp-48h]
0041C8EF  mov         dword ptr [rf],edx  

可以看出引用在初始化之时,同样会有些情况,使它们指向空地址。你并不能阻止别人使用指针反引用来对引用进行初始化。的确在使用指针的时候,我们无时无刻都要为安全考虑多写些指针判断,但是使用了引用,我们就可以堂而皇之认为安全了。

此外使用引用,还必须要注意它和目标之间的生命周期关系。因为一旦使用了引用,就让“安全”遮眼,不去理会引用目标的生命周期了。因为《effective c++》提到的不要企图返回函数的局部变量的引用或函数new出来的对象的引用,这两处实质都是引用和引用目标之间的生命周期关系之间的问题。你引用了一个目标,而这个目标在你使用引用的范围之内就已经被多销毁。你引用了一个目标,而这个目标的生命周期大于你使用引用的范围,你应不应该去管理这个目标的生命周期。《effective c++》提到如果你返回了函数new出来的对象的引用,你不得不自己去亲手delete引用的目标,这样使用起来就很奇怪。从使用引用的角度来看,是不想也不要去理会目标的生命周期的,但是它的本质还是一个指针,指针有的问题它身上也一样会发生。

比如你写了一个模块,模块内的类之间存在引用关系,这些引用在你模块内的生命周期关系中一切如好。但某天有人或你要重用其中的某些类,这些类聚合的引用由模块外的类实现代替了,又或者某些类被cache起来,生命周期变大了,引用之间的生命周期关系发生了变化,引用就如指针一样,同样可以指向一处无效的地方。因为你使用的是引用而不是指针,你就可能认为引用不会出现问题,因为引用是安全的。

3. 至于引用不能在定义之外通过语言规则改变它的指向。当引用同样有可能被定义指向NULL的时候,它与指针常量就差异不大了。一直以来都认为指针常量不可能像引用那样定义的时候,保证指向非NULL的对象,但是引用会指向NULL只是个慌言。那么指针常量有的问题,引用当然也都有。只是引用的使用过程中,你的赋值访问都转化为对目标反引用的赋值操作,而对指针常量进行赋值时,编译器会报错指出。但是这样都无法避免有人不正确去访问你的对象,例如引用是结构体或类的成员变量。只要某处不正确访问了结构体对象或类对象,同样不论是以指针常量还是引用定义的成员变量,一样会被更改。

引用比指针安全,这只是一个希望,希望通过引用隐藏指针的使用,看起来更加安全。但无论如何引用在汇编层的实现就是指针,让引用代替指针使编程更加安全,只能够是一个慌言。当人人都认为这个慌言是真理的时候就是比使用指针更加坏的事情,相信引用比指针更加安全,麻痹对待同一样的事物了。

C++书籍在讲引用相关的话题,更多的是它作为函数的传递参数,与对象传参作比较。你只需要将对象形参改变成引用形参,你就能访问对象的方式访问对象,还可以避免传参过程临时对象的构造,带来诸多好处。或者在重载类的操作符返回自身的引用,使得类的设计在使用上可以连续书写。

说到传参就不得不说一下常量引用,就是引用一个常量了。

首先你不可以用一个非常量的引用,定义它指向一个常量,你必须用一个常量类型的引用,定义它指向一个常量。那么指向常量会发生什么事呢,请看反汇编代码:

    const float& rcf = 1.f;
00401181  fld1
00401183  fstp        dword ptr [ebp-8]
00401186  lea         eax,[ebp-8]
00401189  mov         dword ptr [rcf],eax
    const float& rcf2 = 1.f;
0040118C  fld1
0040118E  fstp        dword ptr [ebp-2Ch]
00401191  lea         ecx,[ebp-2Ch]
00401194  mov         dword ptr [rcf2],ecx  

可以看到编译器悄悄为常量分配了局部空间,形式就像

const float implict = 1.f;    // dword ptr [ebp-8]
const float& rcf = implict;
const flost implict2 = 1.f;    // dword ptr [ebp-2Ch]
const float& rcf2 = implict2;

为什么对不打算修改其内容的引用形参,使用常量引用呢?让其只读是其一,其二是你不能将一个常量去定义一个引用形参。

void foo(float&);
void foo2(const float&);

main()
{
     foo (1.f); // error
     foo2(1.f);  // ok
}

那么这个foo2函数是如何接受一个常量,去定义一个常量引用形参呢。请看反汇编代码:

    foo2(1.f);
004011A3  fld1
004011A5  fstp        dword ptr [ebp-5Ch]
004011A8  lea         ecx,[ebp-5Ch]
004011AB  push        ecx
004011AC  call        foo2 (0401005h)
004011B1  add         esp,4  

同样,编译器偷偷为这个常量开辟了一个临时变量空间,将其赋值为1.f常量,并定义foo2的形参指向这个没有名字的临时变量。

另外对于临时类对象,赋值给其它变量,则会拷贝这个临时类对象,并随后析构这个临时类对象。但是如果将一个临时类对象去定义一个引用呢?

    B();
00401163  lea         ecx,[ebp-59h]
00401166  call        B::B (0401019h)
0040116B  lea         ecx,[ebp-59h]
0040116E  call        B::~B (0401028h)
    A& ra = A();
00401173  lea         ecx,[ebp-21h]
00401176  call        A::A (040101Eh)
0040117B  lea         edx,[ebp-21h]
0040117E  mov         dword ptr [ra],edx
    C* const pcc = &C();
00401181  lea         ecx,[ebp-5Ah]
00401184  call        C::C (0401037h)
00401189  mov         dword ptr [pcc],eax
0040118C  lea         ecx,[ebp-5Ah]
0040118F  call        C::~C (040103Ch)
    ...
    return 0;
004011C7  mov         dword ptr [ebp-64h],0
004011CE  lea         ecx,[ebp-21h]
004011D1  call        A::~A (0401023h)
004011D6  mov         eax,dword ptr [ebp-64h]
}
004011D9  mov         esp,ebp
004011DB  pop         ebp
004011DC  ret  

这个局部的临时对象"A()"生命周期延长到了局部范围的结束,因为编译器要保证这个引用不会产生一个迷途引用。而使用指针常量则没有办法延长一个临时对象的生命周期,"C()"这个临时对象在定义了指针常量pcc的指向后,就马上析构了,留下了一个迷途指针。

相同的讨论可以参看stackoverflow

《Why is a c++ reference considered safer than a pointer?》

结合上面的内容,你能说出下面的指向和发生了什么操作吗?

uintptr_t uintptr = 0;
float flt = 0.f;
float flt2 = 2.f;
float& rf = (float&)uintptr;
float& rf2 = (float&)uintptr = flt;
float& rf3 = (float&)rf2 = flt2;
float*& rpf2 = (float*&)uintptr = &flt;

你说对了吗,请参看反汇编代码:

    float& rf2 = (float&)uintptr = flt;
00401138  fld         dword ptr [flt]
0040113B  fstp        dword ptr [uintptr]
0040113E  lea         eax,[uintptr]
00401141  mov         dword ptr [rf2],eax
    float& rf3 = (float&)rf2 = flt2;
00401144  mov         ecx,dword ptr [rf2]
00401147  fld         dword ptr [flt2]
0040114A  fstp        dword ptr [ecx]
0040114C  mov         edx,dword ptr [rf2]
0040114F  mov         dword ptr [rf3],edx
    float*& rpf2 = (float*&)uintptr = &flt;
00401152  lea         eax,[flt]
00401155  mov         dword ptr [uintptr],eax
00401158  lea         ecx,[uintptr]
0040115B  mov         dword ptr [rpf2],ecx 

之前的反汇编分析系列有

objc反汇编分析__strong和__weak

objc里的伪指针TaggedPointer

反汇编分析NSString,你印象中的NSString是这样吗

反汇编分析objc函数枢纽objc_msgSend

objc反汇编分析,block函数块为何物?

反汇编objc分析__block(类型)》

自制反汇编逆向分析工具 与hopper逆向输出对比

自制反汇编逆向分析工具 迭代第六版本 (一)

成员函数指针,动态绑定(vc平台)

函数指针和成员函数指针有什么不同,反汇编带看清成员函数指针的本尊([email protected]平台)

时间: 2024-10-20 13:22:28

反汇编看c++引用的相关文章

从python中copy与deepcopy的区别看python引用

讨论copy与deepcopy的区别这个问题要先搞清楚python中的引用.python的内存管理. python中的一切事物皆为对象,并且规定参数的传递都是对象的引用.可能这样说听起来比较难懂,对比一下PHP中的赋值和引用就有大致的概念了.参考下面一段引用: 1. python不允许程序员选择采用传值还是传引用.Python参数传递采用的肯定是“传对象引用”的方式.实际上,这种方式相当于传值和传引用的一种综合.如果函数收到的是一个可变对象(比如字典或者列表)的引用,就能修改对象的原始值——相当

【cocos2d-x-3.1.1系列5】cocos2d-x 引用计数细节

看了引用计数之后  那时好像懂了   今天突然想起一个问题: Scene也是继承自Ref ,然后也是静态生成一个autorelease后的对象  那计数就变成1了 class CC_DLL Scene : public Node { public: /** creates a new Scene object */ static Scene *create(); { Scene *Scene::create() { Scene *ret = new Scene(); if (ret &&

基类中定义的虚函数在派生类中重新定义时,其函数原型,包括返回类型、函数名、参数个数、参数类型及参数的先后顺序,都必须与基类中的原型完全相同 but------> 可以返回派生类对象的引用或指针

您查询的关键词是:c++primer习题15.25 以下是该网页在北京时间 2016年07月15日 02:57:08 的快照: 如果打开速度慢,可以尝试快速版:如果想更新或删除快照,可以投诉快照. 百度和网页 http://bbs.csdn.net/topics/380238133 的作者无关,不对其内容负责.百度快照谨为网络故障时之索引,不代表被搜索网站的即时页面. 首页 精选版块 移动开发 iOS Android Qt WP 云计算 IaaS Pass/SaaS 分布式计算/Hadoop J

【转】C++引用和指针

[C++再学习系列] 引用和指针 2010-11-09 13:18 by zhenjing, 2203 阅读, 10 评论, 收藏, 编辑 下面是网上关于引用和指针区别的常见答案: 引用和指针有如下三种区别: 1 引用必须在声明时初始化,而指针不用: 2 NULL不能引用,而指针可指向NULL: 3 引用一旦声明,引用的对象不能改变(但对象的值可以改变):而指针可以随时改变指向的对象. 引用能做到的,指针也可以,但指针更危险: (1)引用被创建的同时必须被初始化(指针则可以在任何时候被初始化).

C#值传递与引用传递

知识点: 值类型和引用类型  对于值类型来说,栈中存储的是直接使用的数据        对于引用类型来说,栈中存储的是堆中对象的地址 值传递和引用传递 对于值传递,传递的是栈中保存的数据        对于引用传递,传递的是栈本身的地址 先看一下值传递(传递值类型和引用类型)    class Program { static void Main(string[] args) { //引用类型的赋值,只是赋值了对象在堆中的地址 Person p = new Person(); //声明的一个Pe

C++的那些事:你真的了解引用吗

一.引用的本质是什么 说到引用,一般C++的教材中都是这么定义的: 1,引用就是一个对象的别名. 2,引用不是值不占内存空间. 3,引用必须在定义时赋值,将变量与引用绑定. 那你有没有想过,上面的定义正确吗?编译器是如何解释引用的? 这里先给出引用的本质定义,后面我们再进一步论证. 1,引用实际是通过指针实现的. 2,引用是一个常量指针. 3,引用在内存中占4个字节. 4,在对引用定义时,需要对这个常量指针初始化. 二.探究本质 我们从最简单的变量的定义开始,看编译器会做哪些事情. int va

让你彻底弄懂指针、引用与const

今天重温了一下C++ Primer,对上面三个概念有了更清晰的认识,自我认为已经有了比较全面的理解了,所以赶紧记录下来,也请大家批评指正. 1.引用 引用,简单来说就是为对象起了一个别名,可以用别名来等同于操作对象,通过将声明符写成&d的形式来定义引用类型,其中d是声明的变量名,即引用变量的别名: int i =1; int &r = i; //r指向i(r是i的别名,可以通过操作r来改变i的值). r=2; cout<<r<<" "<&l

前端学习(26)~js学习(四):基本数据类型vs引用数据类型

在上一篇文章中,我们介绍过,变量有以下数据类型: 基本数据类型(值类型):String 字符串.Number 数值.Boolean 布尔值.Null 空值.Undefined 未定义. 引用数据类型(引用类型):Object 对象. 本文,我们针对这两种类型,做进一步介绍.我们先来看个例子. 基本数据类型举例: var a = 23; var b = a; a++; console.log(a); // 打印结果:24 console.log(b); // 打印结果:23 上面的代码中:a 和

20145301赵嘉鑫《网络对抗》逆向及Bof基础

20145301赵嘉鑫<网络对抗>逆向及Bof基础 实践目标 本次实践的对象是一个名为pwn1的linux可执行文件. 该程序正常执行流程是:main调用foo函数,foo函数会简单回显任何用户输入的字符串. 该程序同时包含另一个代码片段,getShell,会返回一个可用Shell.正常情况下这个代码是不会被运行的.我们实践的目标就是想办法运行这个代码片段. 本次实践主要是学习两种方法: 利用foo函数的Bof漏洞,构造一个攻击输入字符串,覆盖返回地址,触发getShell函数. 手工修改可执