为什么栈会逆增长
多数的栈是逆增长的,它会从高地址向低地址增长。
历史原因。当计算机尚未小型化的时候,它还有数个房间那么大。在那个时候,内存就分为两个部分,即"堆/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()函数来释放由这个函数申请的内存。
栈的噪音
#includevoid 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 PROCpush ebpmov ebp,espsub esp,12mov DWORD PTR _a$[ebp],1mov DWORD PTR _b$[ebp],2mov DWORD PTR _c$[ebp],3mov esp,ebppop ebpret 0_f1 ENDP_c$=-12 ;size=4_b$=-8 ;size=4_a$=-4 ;size=4_f2 PROCpush ebpmov mov ebp,espsub esp,12mov eax, DWORD PTR _c$[ebp]push eaxmov ecx, DWORD PTR _b$[ebp]push ecxmov edx, DWORD PTR _a$[ebp]push edxpush OFFSET $SG2752; ‘%d, %d, %d'call DWORD PTR __imp_printfadd esp,16mov esp,ebppop ebpret 0_f2 ENDPmain PROCpush ebpmov ebp,espcall _f1call _f2xor eax,eaxpop ebpret 0_main ENDP
在运行第二个函数时,栈中的所有值(即内存中的单元)受前一个函数的影响,而获得了前一个函数的变量的值。来格地说,这些地址的值不是随机值,而是可预测的伪随机值。