6.4 函数的参数
6.2 节中分析的示例函数没有参数,如果在函数的调用过程中有使用栈顶传参的情况,那么esp会有那些变化呢?如以下代码所示。
假设当前esp为0x0012FF10,传递参数为:
push 5;指令执行后esp-4等于0x0012FF0C
push 6;指令执行后esp-4等于0x0012FF08
;函数调用,call指令会将下一条指令地址压栈作为函数的返回地址,本节暂不讨论
call xxxx
函数参数通过栈结构进行传递,在C++代码中,其传参顺序为从右向左依次入栈,最先定义的参数最后入栈。参数也是函数中的一个变量,采用正数标号法来表示局部变量偏移标号时,函数的参数标号和局部变量的标号值都是正数,无法区分,不利于分析。如果使用负数标号法表示,则可以将两者区分,正数表示参数,而负数则表示局部变量,0值表示返回地址(对返回地址的讲解见6.5节)。这样,用户在对反汇编代码进行分析时就省去了计算偏移量的工作,只需查看标号名称就可得知在访问某一变量。根据寻址过程中的计算方式得知访问的变量是局部变量还是参数。
Debug版下的分析相对简单,由于其注重调试功能,因此使用的是ebp寻址方式。在进入函数时,已将ebp调整至当前作用域的栈底,可直接使用。
因为函数的传参是通过栈方式传递的,使用push指令将数据压入到栈中,而push指令将操作数复制到栈顶,所以这时压入栈中的数据和原数据在两个不同地址处,是独立存在的,因此对函数参数的修改,实际上是对当前函数栈内的参数中保存的值进行修改,与原数据没有任何关系。正因如此,在C\C++中,形参是实参的副本,对形参的修改不影响其实参。示例如代码清单6-7所示。
代码清单6-7 函数参数传递—Release版
//C++源码说明:通过esp访问局部变量
void AddNumber(int nOne){
nOne+=1;
printf("%d\r\n",nOne);
}
void main(){
int nNumber=0;
scanf("%d",&nNumber);//防止变量被常量扩散优化
AddNumber(nNumber);
}
;Release版的反汇编代码信息
;main函数分析,IDA识别出main函数并标明其参数信息以及调用方式
;int__cdecl main(int argc, const char**argv, const char**envp)
_main proc near
var_4=dword ptr-4;局部变量标号定义,main函数中只有一个局部变量
;注意这里的push ecx,请读者现在定位到函数末尾去看看,是不是发现没有pop ecx?因此这里并不是
保存寄存器环境,而是使用低周期的push ecx代替高周期的sub esp,4,强度削弱。这样就有了局部变量的空间
push ecx
lea eax,[esp+4+var_4];取出局部变量地址到eax中
mov[esp+4+var_4],0;将局部变量赋值为0
push eax;压入eax作为参数,在eax中保存局部变量地址
push offset aD_0;"%d"
call_scanf;调用scanf函数
mov ecx,[esp+0Ch+var_4];取局部变量内容放入ecx中
push ecx;结合函数sub_401000分析此处是否为参数压栈,考察函数内有
;没有对其引用,有没有使用ret指令平衡参数
call sub_401000;调用函数,标号为sub_401000,双击可跟进到函数实现中
add esp,10h;退出前平衡栈顶esp,共使用4次push指令,由此得出函数
sub_401000为__cdecl调用方式,使用了1个参数
retn
_main endp
;函数sub_401000实现
sub_401000 proc near;函数sub_401000起始处
arg_0=dword ptr 4;正数,为参数标号。在IDA下参数以arg_为前缀
mov eax,[esp+arg_0];访问第一个参数,取出数据到eax中,此函数就一个参数
inc eax;对参数内容加1,main函数中压入的参数为5
push eax;将加1后的eax压入栈中,作为printf函数参数
push offset Format;"%d\r\n"
call_printf;调用printf函数,显示字符串
add esp,8;平衡printf函数使用的两个参数
retn;函数内没有平衡参数,可见此函数为__cdecl调用方式
sub_401000 endp;函数sub_401000结尾处
通过对代码清单6-7的分析,我们学习了函数参数的传递过程,从而理解了C\C++中不定长参数的函数是如何实现的。C\C++将不定长参数的函数定义为:
至少要有一个参数;
所有不定长的参数类型传入时都是dword类型;
需在某一个参数中描述参数总个数或将最后一个参数赋值为结尾标记。
有了这三个特性,就可以实现不定参数的函数。根据参数的传递特性,只要确定第一个参数的地址,对其地址值做加法,就可访问到此参数的下一个参数所在的地址。获取参数的类型是为了解释地址中的数据。上面提到的第三点是为了获取参数的个数,其目的是正确访问到最后一个参数的地址,以防止访问参数空间越界(使用栈传参方式的32位程序,某个参数的地址加4即可得到下一参数所在的地址,但double类型除外,详见2.2.1节的介绍)。
printf函数就是利用第一个参数来获取参数总个数的。只需检查printf函数中第一个参数指向的字符串中包含几个“%”就可以确定其后的参数个数(“%%”形成的转义字符除外)。