(1)内存的不同用途
不管什么样的操作系统、计算机结构, 一个进程使用的内存按照功能大致分为以下4个部分
代码区:存储着被转入执行的二进制代码,处理器会到这个区域取指并执行1. 数据区:用于存储全局变量等1. 堆区:进程可以再堆区动态地请求一定大小的内存,并在用完之后归还给栈区。动态分配和回收是堆区的特点1. 栈区:用于动态存数函数之间的调用关系,以保证被调用函数在 返回时恢复到母函数中继续执行
在windows平台下,高级语言写出的程序经过编译连接后,最终变成PE文件(可执行文件)。当PE文件被装载运行后,就是进程。
PE文件的机器代码会被载入到内存的代码区,处理器将到内存的 这一区域取指,然后送入到算数逻辑单元执行;如果代码请求开辟动态内存,则会在内存的堆区分配一块大小合适的区域返回给代码区的代码使用;当函数调用发生时,函数的调用关系在等信息会动态地保存在栈区, 以供处理器在执行完函数调用代码时,返回母函数。过程如图
如果把CPU比作一个有条不紊的工厂,我们可以得到如下类比:
CPU是完成工作的工人
数据区、栈区、堆区是则是存放 原料、成品等的场所
存在代码区的指令则告诉CPU做什么,如何做
(2)函数调用时发生了什么
通过以下例子研究一下函数调用和递归等性质是如何通过系统栈巧妙实现的
intfunc_B(int arg_B1, int arg_B2)
{
int var_B1, var_B2;
var_B1=arg_B1+arg_B2;
var_B2=arg_B1-arg_B2;
return var_B1\*var_B2;
}
intfunc_A(int arg_A1, int arg_A2)
{
int var_A;
var_A = func_B(arg_A1,arg_A2) + arg_A1 ;
return var_A;
}
intmain(int argc, char \*\*argv, char \*\*envp)
{
int var_main;
var_main=func_A(4,3);
return var_main;
}
经过编译后,各函数对用的机器指令在代码区的分布如左下,不同的函数可能相邻,可能不相邻
当CPU执行调用fun_A 函数时,会从代码区中main函数对应的机器指令的区域跳转到fun_A 对应的机器指令区域,在那里取指并执行;当fun_A 返回时,又回到main的机器指令区域,条用fun_A后面的指令继续执行main函数的代码,CPU的执行轨迹如右下
在函数调用的过程中,伴随的系统栈的操作如下
在main函数调用fun_A的时候,首先在自己的栈中压入函数返回地址,然后为fun_A创建新栈帧压入系统栈1. 在fun_A调用fun_B的时候,同样自己的栈中压入函数返回地址,然后为fun_B创建新栈帧压入系统栈1. 在fun_B返回时,fun_B的栈帧被弹出系统栈,fun_B栈中的返回地址被“露”在栈顶,此时CPU按照这个返回地址回到fun_a的代码区执行1. 在fun_A返回时,fun_A的栈帧被弹出系统栈,main栈中的返回地址被“露”在栈顶,此时CPU按照这个返回地址回到main的代码区执行
函数调用大致包括以下几个步骤
参数入栈:参数从右到左的入栈1. 返回地址入栈1. 代码区跳转1. 栈顶调整
调用时用到的指令序列如下
;调用前
push 参数3 ; 假设该函数有3个参数,将从右向左依次入栈
push 参数2
push 参数1
call 函数地址 ; call指令将同时完成两项工作:a)向栈中压入当前指令在内存中的位置, ; 即保存返回地址;b)跳转到所调用函数的入口地址
;函数入口处
push ebp ; 保存旧栈帧的底部
mov ebp,esp ; 设置新栈帧的底部(栈帧切换)
sub esp,xxx ; 设置新栈帧的顶部(抬高栈顶,为新栈帧开辟空间)
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)