8.3 数组作为返回值

8.2 节讲解了数组作为参数的用途。本节将讲解数组在函数中的另一个用处:作为函数返回值的处理过程。

数组作为函数的返回值与作为函数的参数大同小异,都是将数组的首地址以指针的方式进行传递,但是它们也有不同。当数组作为参数时,其定义所在的作用域必然在函数调用以外,在调用之前已经存在。所以,在函数中对数组进行操作是没有问题的,而数组作为函数返回值则存在着一定的风险。

当数组为局部变量数据时,便产生了稳定性问题。当退出函数时,需要平衡栈,而数组是作为局部变量存在,其内存空间在当前函数的栈内。如果此时函数退出,栈中定义的数据将变得不稳定。由于函数退出后esp会回归到调用前的位置上,而函数内的局部数组在esp之下,随时都有可能由在其他函数的调用过程中产生的栈操作指令将其数据破坏。数据的破坏将导致函数返回结果具备不确定性,影响程序的结果,如代码清单8-6所示。

代码清单8-6 不稳定的数组返回—Debug版


//C++源码说明:局部数组作为返回值

char*RetArray(){

char szBuff[]={"Hello World"};

return szBuff;

}

void main(){

printf("%s\r\n",RetArray());

}

//C++源码与对应汇编代码讲解

char*RetArray(){

;在Debug版下保存环境,开辟栈空间略

char szBuff[]={"Hello World"};

;字符串数组初始化为字符串

00401098 mov eax,[string"Hello World"(0042001c)]

0040109D mov dword ptr[ebp-0Ch],eax

004010A0 mov ecx, dword ptr[string"Hello World"+4(00420020)]

004010A6 mov dword ptr[ebp-8],ecx

004010A9 mov edx, dword ptr[string"Hello World"+8(00420024)]

004010AF mov dword ptr[ebp-4],edx

;使用eax保存数组首地址,作为函数返回值。虽然eax保存的地址存在,但是当函数

;结束调用后,此地址中的数据将不稳定,在进行其他对栈空间读写操作时可能破坏此数据

004010B2 lea eax,[ebp-0Ch]

}

;在Debug版下还原环境略

004010BB ret

void main(){

;在Debug版下保存环境,开辟栈空间略

printf("%s\r\n",RetArray());

;调用函数RetArray

0040B7F8 call@ILT+10(RetArray)(0040100f)

;使用RetArray返回数组作为printf参数使用

0040B7FD push eax

0040B7FE push offset string"%s\r\n"(00420f7c)

0040B803 call printf(004010f0)

0040B808 add esp,8

}


在代码清单8-6中,在函数RetArray中定义了数组szBuff,由于数组szBuff为局部变量,因此其所占内存空间的位置在栈空间内,其生命周期随函数的退出而结束。而在函数结束后,将数组的首地址赋值到eax中作为返回值。虽然这个地址始终存在,但这个地址是栈空间中的某段内存空间,其中的数据会在作用域切换时被新数据替换。因此返回局部变量的地址随时会产生错误。在编译期间,VC编译器也对此作出了警告处理。

为了更好地帮助大家认识到这个错误的严重性,我们通过图8-4来查看进入函数后栈中数据的变化。

图 8-4 栈平衡错误演示

在图8-4中,返回了函数GetNumber中定义的局部数组的首地址nArray,其所在地址处于0x0012FF00~0x0012FF1C之间。当函数调用结束后,栈顶指向了地址0x0012FF1C。此时数组nArray中的数据已经不稳定,任何栈操作都有可能将其破坏。

在执行“printf("%d",pArray[7]);”后,由于需要将参数压栈,地址0x0012FF1C~0x0012FF18之间的数据已经被破坏,无法输出正常结果。

如果既想使用数组作为返回值,又要避免图8-4中的错误,可以使用全局数组、静态数组或是上层调用函数中定义的局部数组。

全局数组与静态数组都属于变量,它们的特征与全局变量、静态变量相同,看上去就是连续定义的多个同类型变量,如图8-5所示。

图 8-5 全局数组

图8-5定义了5个4字节数据,分别为1,2,3,4,5,是不是和全局变量非常相似呢?在对全局数组的分析过程中,考察数据的访问方式以及元素长度。对全局数组的识别如图8-6所示。

图 8-6 全局数组的识别

在图8-6中,将地址标号unk_406030表示的地址存入esi中,结合图8-5可知,该标号开头以dword命名,表示标志处为dword数据类型。在接下来的循环代码中,每次对esi保存的地址值取内容,将其作为printf函数的参数输出,并对esi执行自加4操作,由此可见,这里存储的是一个整型数组。在循环次数比较中,使用的指令为“cmp esi, offset Format”,这里是将esi与一个常量值比较。标号“Format”表示的常量地址如图8-7所示。

图 8-7 标号“Format”表示的地址

如图8-7所示,标号“Format”表示地址0x00406044,这是全局数组的结尾地址。图8-6中的循环每次对esi加4,循环5次后esi中保存的地址为0x00406044,根据判断条件,大于等于则会跳转失败,跳出循环。还原图8-5与图8-6可得如下源码:


int g_nArry[5]={1,2,3,4,5};

void main(){

int*pInt=&g_nArry

do{

printf("%d",*pInt);

pInt++;

}while(pInt<g_nArry+5)

}


静态数组在全局情况下和全局数组相同。作为局部作用域定义时,则同样会检查相应的标志位,并对局部静态数组元素赋值。与局部静态变量有些不同,无论局部静态数组有多少个元素,也只会检查一次初始化标志位,如代码清单8-7所示。

代码清单8-7 局部静态数组—Debug版


//C++源码说明:局部静态数组的分析[0]

void ain(){

int nOne;

int nTwo;

scanf("%d%d",&nOne,&nTwo);

static int g_snArry[5]={nOne, nTwo,0};//局部静态数组初始化第二项为常量

//C++源码与对应汇编代码讲解

void main(){

int nOne;

int nTwo;

scanf("%d%d",&nOne,&nTwo);

static int g_snArry[5]={nOne, nTwo,0};

0040B84D xor edx, edx

0040B84F mov dl, byte ptr['main':'2':$S1(004237c8)]

0040B855 and edx,1

0040B858 test edx, edx

0040B85A jne main+70h(0040b890);检测初始化标志位

0040B85C mov al,['main':'2':$S1(004237c8)]

0040B861 or al,1

;将初始化标志位置1

0040B863 mov['main':'2':$S1(004237c8)],al

0040B868 mov ecx, dword ptr[ebp-4]

0040B86B mov dword ptr['main':'2':$S1+4(004237cc)],ecx

0040B871 mov edx, dword ptr[ebp-8]

0040B874 mov dword ptr['main':'2':$S1+8(004237d0)],edx

0040B87A mov dword ptr['main':'2':$S1+0Ch(004237d4)],0

0040B884 xor eax, eax

0040B886 mov['main':'2':$S1+10h(004237d8)],eax

0040B88B mov['main':'2':$S1+14h(004237dc)],eax

}