为什么栈会逆增长
多数的栈是逆增长的,它会从高地址向低地址增长。
历史原因。当计算机尚未小型化的时候,它还有数个房间那么大。在那个时候,内存就分为两个部分,即"堆/heap"和栈/stack。当然,在程序执行过程中,堆和栈到底会增长到什么地址并不好说,所以人们干脆把它们分开:
栈的用途
1)保存函数结束时的返回地址
x86
当程序使用call指令调用其他函数时,call指令结束后的返回地址将被保存在栈里;在call所调用的函数结束之后,程序将执行无条件跳转指令,跳转到这个返回地址。
CALL指令等价于PUSH返回地址和JMP函数地址的指令对。
被调用函数里的RET指令,会从栈中读取返回地址,然后跳转到这个地址,就相当于POP返回地址+JMP返回地址指令。
栈是有隐姓埋名的,溢出它很容易。直接使用无限递归,栈就会满
ARM
如果一个函数不调用其他函数,它就像树上的枝杈末端的叶子那样。这种函数就叫作叶函数leaf function。
叶函数的特点是,它不必保存LR寄存器的值。如果叶函数的代码短到用不了几个寄存器,那么它也可能根本不会使用数据栈。所以,调用叶函数的时候确实可能不会涉及栈操作。
2)参数传递
在x86平台的程序中,最常用的参数传递约定是cdecl。
push arg3
push arg2
push arg1
call f
add esp,12
被调用方法函数通过栈指针获取其所需的参数。
ESP |
返回地址 |
ESP+4 |
arg1,它在IDA里记为arg_0 |
ESP+8
|
arg2,它在IDA里记为arg_4 |
ESP+0xC |
arg3,它在IDA里记为arg_8 |
3)存储局部变量
通过向栈底调整栈指针的方法,函数即可在数据栈里分配也一片可用于存储局部变量的内存空间。可见,无论函数声明了多少个局部变量,都不影响它分配栈空间的速度。
4)SEH结构化异常处理
5)缓冲区溢出保护
6)alloca()函数
直接使用栈来分配内存,除些之外,它与malloc()比偶没有显著的区别。
函数尾声的代码会还原ESP的值,把数据还原为函数启动之前的状态,直接抛弃由alloca()函数分配的内存。所以程序不需要特地使用free()函数来释放由这个函数申请的内存。
栈的噪音
#include
void f1()
{
int a=1,b=2,c=3;
};
void f2()
{
int a,b,c;
printf("%d, %d, %d\n",a,b,c);
}
int main()
{
f1();
f2();
}
MSVC
$SG2752 '%d, %d, %d',0aH,00H
_c$=-12 ;size=4
_b$=-8 ;size=4
_a$=-4 ;size=4
_f1 PROC
push ebp
mov ebp,esp
sub esp,12
mov DWORD PTR _a$[ebp],1
mov DWORD PTR _b$[ebp],2
mov DWORD PTR _c$[ebp],3
mov esp,ebp
pop ebp
ret 0
_f1 ENDP
_c$=-12 ;size=4
_b$=-8 ;size=4
_a$=-4 ;size=4
_f2 PROC
push ebp
mov mov ebp,esp
sub esp,12
mov eax, DWORD PTR _c$[ebp]
push eax
mov ecx, DWORD PTR _b$[ebp]
push ecx
mov edx, DWORD PTR _a$[ebp]
push edx
push OFFSET $SG2752; ‘%d, %d, %d'
call DWORD PTR __imp_printf
add esp,16
mov esp,ebp
pop ebp
ret 0
_f2 ENDP
main PROC
push ebp
mov ebp,esp
call _f1
call _f2
xor eax,eax
pop ebp
ret 0
_main ENDP
在运行第二个函数时,栈中的所有值(即内存中的单元)受前一个函数的影响,而获得了前一个函数的变量的值。来格地说,这些地址的值不是随机值,而是可预测的伪随机值。