目录
前言
首先看本篇文章,最好了解一下程程序运行的堆栈图是什么样的?怎么去画?我的这篇文章有讲过《堆栈图》。
变量与参数
什么是变量?
变量就是用来存放值的一块内存,变量名就是存放数据地址的别名,这句话是一定要记住的,我们接下来讲的东西会涉及到这一点。
变量分为全局变量和局部变量。
全局变量与局部变量
相同点:他们都是用来存储数据的一块内存地址。
不同点:
全局变量定义在函数外面,默认会初始化为0,并且如果只编译一次,那么无论任何时候启动这个exe文件的时候,全局变量的地址都不会改变,并且是独一无二的,除非重新编译。
局部变量则是定义在函数内部,不初始化局部变量很有可能会有危险(以后会讲),尽管你只编译一次,你每次运行exe文件的时候,局部变量的地址还是会改变。
什么是参数?
参数就是用来传递值的变量,本质上也就是个变量,主要是用来给函数传值的。
参数分为形式参数和实际参数
形参与实参
形式参数和实际参数几乎没有什么相同点,非要说一个,那就是他们都属于变量。形参就是定义函数的时候括号里的参数,他就是用来占位置的,主要目的就是告诉别人,你想用这个函数你就要给它几个参数,几个形参就对应应该传递几个参数(抛开缺省问题)。实际参数就是你调用这个函数的时候,你给他的值,这个值才会被函数内部使用。
代码准备
了解了变量与参数的基本概念后,我们就需要画堆栈图了,然后才能去分析变量与参数的内存布局。对于全局变量的内存布局,下面就不需要讲了,因为我们只需要知道全局变量的内存是独一无二的,只要不重新编译,内存地址就不会改变就行了。
下面准备画堆栈图用到的代码,并调到反汇编(设置断点,运行,ALT+8)
int plus(int x, int y) // 定义一个函数,返回类型int,函数名plus,形参x、y
{
int z=x+y; // 定义局部变量接收x+y的最终值
return z; // 返回局部变量的值
}
void main() // 程序入口
{
__asm mov eax,eax; // 一行没用的汇编指令,用来设置断点
plus(1,2); // 调用函数
return ; // 返回
}
另外说一下,我们上面返回了局部变量的值,这是没有错的。虽然局部变量在函数结束的时候就回收了内存(至于为什么以后细说),但是这只能说明,我们不能返回局部变量的地址,因为函数结束后地址已经不是我们的了。但是我们可以返回局部变量的值。
变量以及参数的内存布局
我们首先来绘制堆栈图,在__asm处设置断点,F7编译,F5调试,ALT+8调出反汇编窗口如下:
别忘了把内存和寄存器的窗口调出来,之前有讲过
分析代码:执行call指令一定要用F11
它们对应的堆栈图:
初始堆栈:一个框代表四个字节,但是ebp与esp中间差的有点多,这里省略了
执行push 2之后:
push 1:
call plus (1611CCh):
函数内的代码
这里的代码就不一行一行解释了,直接看执行代码后堆栈图的变化
push ebp:
mov ebp,esp:
sub esp,0CCh:
push ebx:
push esi:
push edi:
lea edi,[ebp-0CCh]:(将edi中存放刚提升堆栈时esp的值)
mov ecx,33h:(将ecx中存放十六进制的33,ecx中的值就是rep指令循环的次数)
mov eax,0CCCCCCCCh:(将eax中存放0xCC,0xCC就是断点的值)
rep stos dword ptr es:[edi]:(重复ecx中的值的次数,将eax的值赋值给edi指向的地址。通过上面这几行代码,堆栈的变化如下)
蓝色的区域就是我们俗称的缓冲区
下面几行代码,由于vs2010优化的原因,我们没有办法看到x,y,z对应的地址信息了,所以理解起来会有点困难,我整理了VC6中与下面几行对应的汇编代码,按照这下面的进行分析
代码都是一样的,不用担心。
mov eax,dword ptr [ebp+8]:
可以看到这里是取出第一个参数放在eax里
add eax,dword ptr [ebp+0Ch]:
取得第二个参数,并于eax中的1相加再存入eax中
此时的eax里存放的值是3,他是x和y相加的返回值,我们知道eax就是用来存放返回值的,所以下面这行代码的意思就是,将x+y的返回值赋值给z,ebp-4就是z对应的地址。
mov dword ptr [ebp-4],eax:
下面的代码就先不分析了。
堆栈图总结
通过上面的代码,我们大致可以把堆栈图进行划分一下了,如下:
由堆栈图可知:
ebp中存放原来ebp的值
ebp+4存放的是返回地址
ebp+8开始往下存放的是参数,有几个参数就占几个格子
ebp-4往上蓝色的区域就是缓冲区,也就是局部变量存放的地址
以上便是参数与变量的内存布局
这基本上就是固定的了,当然不同的编译器处理方式不一样,可能他会把局部变量往上放一点,但是换汤不换药,局部变量最终还是在缓冲区的。
所以上面对应的东西一定要记住,就比如ebp+4返回地址,这就是一个很重要的东西,在这里提一下缓冲区溢出,现阶段不讨论这些知识,只是简单的提一下。
黑客常用攻击方式-缓冲区溢出
如果你的缓冲区处理不当,那么黑客就可以往你的缓冲区里使劲塞东西,一旦缓冲区用完了,就会接着往下用,直到使用到了ebp+4这块内存。我们说过,这块内存是存放函数返回地址的,如果这个时候黑客把ebp+4的值填充成了他想要返回的地址了,这个时候我们调用完函数就会返回到他想让你去的地方,这就是缓冲区溢出。
所以我们一定要记住ebp-4、ebp+4、ebp+8等地址存放的是什么东西,这是成为高手的基础。
下面把函数的返回值也一起讲了吧。
函数的返回值
先看图
关于int z = x + y;对应的汇编,我们已经讲过了,就差return z的了。
在C语言中,我们如果要返回一个值,一般都是return + 需要返回的值,但是在汇编中,我们一般都把需要返回的值存放在eax中。
mov eax,dword ptr [ebp-4]:
ebp-4大家还有印象吧,他就是局部变量的地址,那么dword ptr [ebp-4]就是局部变量的值,所以这里是将局部变量的值存入eax里,从这里就能确定了,eax基本上就是用来存放返回值的。
我们执行一下,看会发生什么
没有变化,因为我们上面把eax的值赋值给了变量,这里返回的时候又把变量的值赋值给了eax所以肯定没有变化啊。
我们一直执行直到函数返回
我们观察发现,貌似从把局部变量的值放在eax之后,直达main函数结束,这之间的十几行汇编指令貌似也没有用到eax啊。那肯定啊,因为你主函数都没有使用到这个值,怎么可能会看到eax的值呢?
代码修改
我们简单的修改一下代码,接收eax的值(函数的返回值)就行了
int plus(int x, int y) // 定义一个函数,返回类型int,函数名plus,形参x、y
{
int z=x+y; // 定义局部变量接收x+y的最终值
return z; // 返回局部变量的值
}
void main() // 程序入口
{
__asm mov eax,eax; // 一行没用的汇编指令,用来设置断点
int ret; // 定义接收eax值的局部变量
ret = plus(1,2); // 调用函数并接收返回值
return ; // 返回
}
我们继续F7编译,F5调试,ALT+8反汇编,执行到刚刚的地方(这里在call指令的地方可以直接F10,代表跳过函数内部,也就是单步步过,之前文章说过《Fake F8》 ,这里的F11就相当于F7,F10就相当于F8)
直到下面这一步:
我用的vs2010还是不显示ret局部变量的地址。我们看看VC6里的这行代码是怎么写的
在vc6中就不会以ret的形式去写,而是直接写了ret对应的地址
回想,ebp-4是什么?
局部变量的地址
这就可以确定了,我们的eax在执行这一步的时候,存放的是函数plus的返回值。那么我们将eax中的值又给了ebp-4这个地址,ebp-4就是局部变量的地址。那么这一步汇编指令的意思就是:将plus函数的返回值赋值给ret这个局部变量。
注意
注意!!!main函数也是个普通的函数,所以这里的ebp-4指的是main函数的缓冲区,不是plus函数的缓冲区。如果从main函数开始的地方就查看反汇编,就可以知道,main函数和普通函数一样都会提升堆栈设置缓冲区。所以通过上面可以知道,main函数也是把局部变量存放在缓冲区里的。
总结
通过上面的文章我们知道了,什么是参数什么是变量,以及局部变量是存放在缓冲区中的,这都是必须要记住的,还有ebp-4,ebp+4,ebp+8分别对应的是什么,这些东西都要刻在脑子里的。最后留一个问题:请说一下为什么局部变量必须要赋初始值?不要去往概念方面想多画堆栈图思考为什么。好了,本篇文章到此结束。
结语
本文章如果有讲错的地方,请大佬一定指出,讲的听不懂的地方也可以跟我说,之后我们更改,尽量更加通俗易懂,谢谢大家。