现在的位置: 首页 > 综合 > 正文

从逆向分析角度看C++ 中的引用

2012年05月29日 ⁄ 综合 ⁄ 共 4355字 ⁄ 字号 评论关闭

几个问题:

1. 引用变量占有内存空间吗?

2. 引用是怎样工作的?

3. 指针是怎样工作的?

4. 引用和指针有什么区别?

1. 何为引用

《C++ Primer》里面是这样说的“引用(Reference)就是对象的另一个名字,引用只是它绑定的对象的另一个名字,作用在引用上的所有操作事实上都是作用在该引用绑定的对象上”,这句话概括得很彻底


2. 引用占有内存空间吗?

一段C++代码:

// Reference_Pointer_Local
#include <cstdio>

int main() {
  int  a = 100;
  int& ref = a;
  int* ptr = &a;
  printf("%d %d %d\n", a, ref, *ptr);
  return 0;
}

使用 /FAs 编译选项,其汇编代码:

PUBLIC	_main
EXTRN	_printf:NEAR
_DATA	SEGMENT
$SG530	DB	'%d %d %d', 0aH, 00H
_DATA	ENDS
_TEXT	SEGMENT
_a$ = -8
_ref$ = -12
_ptr$ = -4
_main	PROC NEAR

; 4    : int main() {

	push	ebp
	mov	ebp, esp
	sub	esp, 12					; 建立堆栈,预留12个字节

; 5    :   int  a = 100;

	mov	DWORD PTR _a$[ebp], 100			; [ebp-8]是a的内存地址

; 6    :   int& ref = a;

	lea	eax, DWORD PTR _a$[ebp]			; 获得a的内存地址
	mov	DWORD PTR _ref$[ebp], eax		; [ebp-12]是 ref 的内存地址,
							; 保存的内存是 a 的内存地址

; 7    :   int* ptr = &a;

	lea	ecx, DWORD PTR _a$[ebp]			; 获得a的内存地址
	mov	DWORD PTR _ptr$[ebp], ecx		; [ebp-4]是 ref 的内存地址,
							; 保存的内存是 a 的内存地址
; 8    :   printf("%d %d %d\n", a, ref, *ptr);

	mov	edx, DWORD PTR _ptr$[ebp]
	mov	eax, DWORD PTR [edx]			; 指针ptr间接获得a的值
	push	eax
	mov	ecx, DWORD PTR _ref$[ebp]
	mov	edx, DWORD PTR [ecx]			; 引用ref间接获得a的值
	push	edx
	mov	eax, DWORD PTR _a$[ebp]
	push	eax
	push	OFFSET FLAT:$SG530
	call	_printf
	add	esp, 16					; 00000010H

; 9    :   return 0;

	xor	eax, eax

; 10   : }

	mov	esp, ebp
	pop	ebp
	ret	0					; 恢复堆栈
_main	ENDP
_TEXT	ENDS
END

汇编代码很清楚地告诉我们,引用也占有内存空间,只不过在这块内存空间上保存的是绑定对象的内存地址,这点与指针很相似!


3. 引用和指针的工作机制

一段C++代码:

// swap_ref_ptr.cpp
#include <cstdio>

void swap_reference(int& ref_x, int& ref_y) {
  int temp = ref_x;
  ref_x = ref_y;
  ref_y = temp;
  return ;
}

void swap_pointer(int* ptr_x, int* ptr_y) {
  int temp = *ptr_x;
  *ptr_x = *ptr_y;
  *ptr_y = temp;
  return ;
}

int main() {
  int x = 4;
  int y = 9;
  swap_reference(x, y);
  swap_pointer(&x, &y);
  return 0;
}

在这个程序中,引用和指针都做了相同的一件事,那就是交换两个数,这里先给出main函数调用swap_reference时栈的表示图(调用swap_pointer时同理):


使用 /FAs 编译选项,得到的汇编代码如下:

PUBLIC	?swap_reference@@YAXAAH0@Z			; swap_reference
_TEXT	SEGMENT
_ref_x$ = 8
_ref_y$ = 12
_temp$ = -4
?swap_reference@@YAXAAH0@Z PROC NEAR			; swap_reference

; 4    : void swap_reference(int& ref_x, int& ref_y) {

	push	ebp
	mov	ebp, esp
	push	ecx

; 5    :   int temp = ref_x;

	mov	eax, DWORD PTR _ref_x$[ebp]		; [ebp+8]保存的是x的内存地址,
							; 还记得在main函数中调用swap_reference时压入的ECX吗
	mov	ecx, DWORD PTR [eax]			; ECX 获得x的值
	mov	DWORD PTR _temp$[ebp], ecx		; x的值暂存于temp的内存空间中,
							; 编译器为temp分配了一块栈空间

; 6    :   ref_x = ref_y;

	mov	edx, DWORD PTR _ref_x$[ebp]		; 可以理解为edx指向x
	mov	eax, DWORD PTR _ref_y$[ebp]		; 可以理解为eax指向y
	mov	ecx, DWORD PTR [eax]
	mov	DWORD PTR [edx], ecx			; 间接地赋值,其结果是直接影响原x,y的值

; 7    :   ref_y = temp;

	mov	edx, DWORD PTR _ref_y$[ebp]
	mov	eax, DWORD PTR _temp$[ebp]
	mov	DWORD PTR [edx], eax

; 8    :   return ;
; 9    : }

	mov	esp, ebp
	pop	ebp
	ret	0
?swap_reference@@YAXAAH0@Z ENDP				; swap_reference
_TEXT	ENDS
PUBLIC	?swap_pointer@@YAXPAH0@Z			; swap_pointer
_TEXT	SEGMENT
_ptr_x$ = 8
_ptr_y$ = 12
_temp$ = -4
?swap_pointer@@YAXPAH0@Z PROC NEAR			; swap_pointer

; 11   : void swap_pointer(int* ptr_x, int* ptr_y) {

	push	ebp
	mov	ebp, esp
	push	ecx

; 12   :   int temp = *ptr_x;

	mov	eax, DWORD PTR _ptr_x$[ebp]		; [ebp+8]保存的是x的内存地址,
							; 还记得在main函数中调用swap_pointer时压入的EAX吗
	mov	ecx, DWORD PTR [eax]
	mov	DWORD PTR _temp$[ebp], ecx

; 13   :   *ptr_x = *ptr_y;

	mov	edx, DWORD PTR _ptr_x$[ebp]		; 可以理解为edx指向x
	mov	eax, DWORD PTR _ptr_y$[ebp]		; 可以理解为eax指向y
	mov	ecx, DWORD PTR [eax]
	mov	DWORD PTR [edx], ecx			; 间接地赋值,其结果是直接影响原x,y的值

; 14   :   *ptr_y = temp;

	mov	edx, DWORD PTR _ptr_y$[ebp]
	mov	eax, DWORD PTR _temp$[ebp]
	mov	DWORD PTR [edx], eax

; 15   :   return ;
; 16   : }

	mov	esp, ebp
	pop	ebp
	ret	0
?swap_pointer@@YAXPAH0@Z ENDP				; swap_pointer
_TEXT	ENDS
PUBLIC	_main
_TEXT	SEGMENT
_x$ = -4
_y$ = -8
_main	PROC NEAR

; 18   : int main() {

	push	ebp
	mov	ebp, esp
	sub	esp, 8

; 19   :   int x = 4;

	mov	DWORD PTR _x$[ebp], 4

; 20   :   int y = 9;

	mov	DWORD PTR _y$[ebp], 9

; 21   :   swap_reference(x, y);

	lea	eax, DWORD PTR _y$[ebp]			; 获得y的内存地址
	push	eax
	lea	ecx, DWORD PTR _x$[ebp]			; 获得x的内存地址
	push	ecx
	call	?swap_reference@@YAXAAH0@Z		; 调用时将返回地址Ret压入堆栈,ESP减4
	add	esp, 8

; 22   :   swap_pointer(&x, &y);

	lea	edx, DWORD PTR _y$[ebp]			; 获得y的内存地址
	push	edx
	lea	eax, DWORD PTR _x$[ebp]			; 获得x的内存地址
	push	eax
	call	?swap_pointer@@YAXPAH0@Z		; 调用时将返回地址Ret压入堆栈,ESP减4
	add	esp, 8

; 23   :   return 0;

	xor	eax, eax

; 24   : }

	mov	esp, ebp
	pop	ebp
	ret	0
_main	ENDP
_TEXT	ENDS
END

在这里,我们看到引用和指针都通过间接的操作来完成交换值的操作,在最简单的情况下,引用对其绑定的对象的赋值操作的次数是2次,第一次从引用本身的内存中取出对象的地址,再通过对指定地址的内存(被绑定的对象)操作来直接影响对象,比如说:

// Reference_Pointer_Local
#include <cstdio>

int main() {
  int  a = 100;
  int& ref = a;
  ref = 5;
  return 0;
}

其汇编代码:

PUBLIC	_main
_TEXT	SEGMENT
_a$ = -4
_ref$ = -8
_main	PROC NEAR

; 4    : int main() {

	push	ebp
	mov	ebp, esp
	sub	esp, 8

; 5    :   int  a = 100;

	mov	DWORD PTR _a$[ebp], 100			; [ebp-4]是a的地址

; 6    :   int& ref = a;

	lea	eax, DWORD PTR _a$[ebp]
	mov	DWORD PTR _ref$[ebp], eax		; [ebp-8]是ref的内存地址,保存的是 a 的地址

; 7    :   ref = 5;

	mov	ecx, DWORD PTR _ref$[ebp]		; 先从ref的内存中取出 a 的地址
	mov	DWORD PTR [ecx], 5			; 再对 a 进行间接的操作,直接影响了 a 的值

至于指针的话,自己去探索一下,其工作的机制是类似的


4. 引用和指针的区别

引用和指针都是通过间接的手段直接影响原对象的。

1. 对于指针来说,指针可以不指向任何一个对象(指向NULL),而引用自初始化后就与某个对象绑定了,引用必须在定义的时候就进行初始化,因为引用本身不能与空对象进行绑定

2. 在某种程度上说,比如测试,引用的代码效率比指针高,因为引用总是与某个对象进行绑定,而指针则不然(这点摘自《More Effective C++》Item M1)

3. 引用自定义后就“从一而终”地与某个对象绑定,任何试图让其绑定其它对象的做法都是被禁止的;而指针是相当灵活的,它可以改变其指向的对象,在指针范畴上,不存在“绝对的绑定”
总的来说,如果你需要在任何时刻都能够改变指向的对象时,应该用指针而不是引用;如果你希望总是指向某个对象并且这个指向关系是“从一而终”的,那么你应该使用引用而不是指针

抱歉!评论已关闭.