指针通常用来帮助函数处理返回值(请参阅第7章的范例)。当函数需要返回多个值时,它通常都是通过指针传递返回值的。
#include <stdio.h>
void f1 (int x, int y, int *sum, int *product)
{
*sum=x+y;
*product=x*y;
};
int sum, product;
void main()
{
f1(123, 456, &sum, &product);
printf ("sum=%d, product=%d\n", sum, product);
};
经MSVC 2010(启用优化选项/Ox /Ob)编译上述程序,可得到如下所示的指令。
指令清单10.1 Optimizing MSVC 2010 (/Ob0)
COMM _product:DWORD
COMM _sum:DWORD
$SG2803 DB 'sum=%d, product=%d', 0aH, 00H
_x$ = 8 ; size = 4
_y$ = 12 ; size = 4
_sum$ = 16 ; size = 4
_product$ = 20 ; size = 4
_f1 PROC
mov ecx, DWORD PTR _y$[esp-4]
mov eax, DWORD PTR _x$[esp-4]
lea edx, DWORD PTR [eax+ecx]
imul eax, ecx
mov ecx, DWORD PTR _product$[esp-4]
push esi
mov esi, DWORD PTR _sum$[esp]
mov DWORD PTR [esi], edx
mov DWORD PTR [ecx], eax
pop esi
ret 0
_f1 ENDP
_main PROC
push OFFSET _product
push OFFSET _sum
push 456 ; 000001c8H
push 123 ; 0000007bH
call _f1
mov eax, DWORD PTR _product
mov ecx, DWORD PTR _sum
push eax
push ecx
push OFFSET $SG2803
call DWORD PTR __imp__printf
add esp, 28 ; 0000001cH
xor eax, eax
ret 0
_main ENDP
如图10.1所示,我们使用OllyDbg调试这个程序。
图10.1 使用OllyDbg观察全局变量的地址传递到f1()函数的过程
首先,全局变量的地址会被传递给f1()函数。我们用右键单击栈中的元素,选中“Follow in dump”,将会在数据段中看到两个变量的实际情况。
在运行第一条指令之前,在BSS(Block Started by Symbol)域内未被初始化赋值的数据会置零,所以这些变量会被清空(ISO可规范,6.7.8节)。变量驻留在数据段。如图10.2所示,我们可以按Alt-M组合键调用memory map进行确认。
图10.2 使用OllyDbg查看内存映射
按下F7键及跟踪调试f1()函数[1],如图10.3所示。
图10.3 使用OllyDbug跟踪调试f1()
如上图所示,数据栈里会有0x1C8(456)和0x7B(123)两个值,以及这两个全局变量的地址(指针)。
继续跟踪调试程序,待其执行到f1()函数尾部。如图10.4所示,我们会在左下方的窗口看到全局变量的计算结果。
图10.4 使用ollyDbg观察f1()函数执行完毕
而后,全局变量的值将被加载到寄存器中,通过栈传递给printf()函数。如图10.5所示。
图10.5 使用OllyDbg观察printf()函数获取全局变量的地址
下面,我们略微调整下程序,使其通过局部变量完成同样的功能。
指令清单10.2 将和、积变量存储在局部变量里
void main()
{
int sum, product; // now variables are local in this function
f1(123, 456, &sum, &product);
printf ("sum=%d, product=%d\n", sum, product);
};
编译之后,f1()函数的汇编代码与使用全局变量的代码完全相同。而main()函数的汇编代码会有相应的变化。启用/Ox /Ob0的优化选项编译后,会得到如下所示的指令。
指令清单10.3 Optimizing MSVC 2010 (/Ob0)
_product$ = -8 ; size = 4
_sum$ = -4 ; size = 4
_main PROC
; Line 10
sub esp, 8
; Line 13
lea eax, DWORD PTR _product$[esp+8]
push eax
lea ecx, DWORD PTR _sum$[esp+12]
push ecx
push 456 ; 000001c8H
push 123 ; 0000007bH
call _f1
; Line 14
mov edx, DWORD PTR _product$[esp+24]
mov eax, DWORD PTR _sum$[esp+24]
push edx
push eax
push OFFSET $SG2803
call DWORD PTR __imp__printf
; Line 15
xor eax, eax
add esp, 36 ; 00000024H
ret 0
接下来,我们使用OllyDbg来调试这个程序。栈内的两个变量在栈内0x2EF854 和 0x2EF858两个地址上。如图10.6所示,我们观察下它们入栈的过程。
图10.6 使用OllyDbg观察局部变量的入栈过程
在f1()函数启动时,0x2EF854和0x2EF858地址的数据都是随机的脏数据,如图10.7所示。
图10.7 使用OllyDbg追踪f1()的启动过程
f1()函数结束时的情况如图10.8所示。
现在0x2EF854的值是0xDB18,0x2EF8588的值是0x243。它们都是f1()函数的运算结果。
图10.8 使用OllyDbg观察f1()执行完毕时的栈内数据
借助指针,f1()函数可以返回位于任意地址的任意值。这个例子体现了指针的重要作用。C++的引用(指针)/reference的作用与C pointer(指针)相同。本书的51.3节将详细介绍C++的引用。
[1] trace,可以进入被调用的函数里继续进行单步调试。如果使用F8键,则可一步就完成call调用的函数。