什么是栈简单来说,栈就是一种后进先出(LIFO)形式的数据结构。栈一般从高地址向低地址增长,栈支持两种操作:push(入栈)和pop(出栈)。如下图所示:push操作先将栈顶(sp指针)下移一位,然后将数据写入新的栈顶;而pop操作则是从栈顶读取数据,写入栈顶(sp指针))向上移动一位。例如将0x100入栈,流程如下图所示:我们来看一下入栈操作,如下图所示:stackframe一个栈帧,即一个SackFrame,本质上是一个一种栈,但是这种栈是专门用来保存函数调用时的各种信息(参数、返回地址、局部变量等)的。栈帧分为栈顶和栈底。栈顶地址最低,栈底地址最高。SP(栈指针)总是指向栈顶。在x8632位CPU中,我们使用%ebp寄存器指向栈底,也就是基指针;使用%esp寄存器指向栈顶,也就是栈指针。下面是一个栈帧的示意图:一般来说,我们把%ebp和%esp之间的区域看成是一个栈帧。整个栈空间不只有一个栈帧。每次调用一个函数,都会产生一个新的栈帧。在函数调用的过程中,我们把调用函数的函数称为:调用者(caller),将要调用的函数:被调用者(callee)。在这个过程中:调用者需要知道从哪里得到被调用者返回的值(通常存放在%eax寄存器中)。被调用者需要知道传入参数在哪里,调用后返回地址在哪里。我们需要保证被调用者返回后,%ebp和%esp寄存器的值应该和调用前一样。函数调用现在,让我们看看调用函数时栈帧是如何变化的。我们以一个函数调用的例子来说明,代码如下://stack.cintadd_func(inta,intb){intc,d;c=一个;d=b;返回c+d;}intmain(intargc,char*argv[]){inttotal;总计=add_func(1,2);return0;}我们使用命令gcc-S-m32stack.c编译以上代码,得到的汇编代码如下(去掉一些无关信息):add_func:pushl%ebp//将ebp寄存器存入栈movl%esp,%ebp//设置ebp进程为esp的值subl$16,%esp//为局部变量申请空间movl8(%ebp),%eax//将参数a保存到eax寄存器movl%eax,-8(%ebp)//将eax寄存器的值保存到局部变量c(c=a)movl12(%ebp),%eax//将参数b保存到eax寄存器movl%eax,-4(%ebp)//将eax寄存器的值保存到局部变量d(d=b)movl-8(%ebp),%edx//将d的值保存到edx寄存器movl-4(%ebp),%eax//将c的值保存到eax寄存器addl%edx,%eax//保存eax寄存器和edx寄存器的值添加并保存到eax(返回值)leaveret//functionreturn...可能汇编代码比较难理解,我们用下图来说明说明调用过程:如上图所示,调用过程如下:在main()函数中调用add_func()函数,将调用add_func()函数的参数压入栈中。当调用add_func()函数时,会将返??回地址压栈,然后进入add_func()函数。当执行add_func()函数时,会将原来ebp寄存器的值压入栈中,然后将ebp寄存器的值设置为esp寄存器的值。然后add_func()函数会为局部变量申请空间,也就是将esp寄存器下移。然后将局部变量c设置为参数a的值,将局部变量d设置为参数b的值。最后将局部变量c和d的值相加,放入eax寄存器(C语言规定返回值通过eax寄存器传递),然后调用ret指令返回main()功能。函数返回上面介绍了函数调用的过程。下面介绍一下函数调用完成后如何从被调用函数返回到原函数。从add_func()函数的汇编代码可以看出,当被调用函数执行完毕返回调用函数时,leave指令就会被执行。这条指令相当于两条汇编指令:movl%ebp,%esppopl%ebp的意思,将esp寄存器和ebp寄存器恢复为函数调用前的值。然后调用ret指令返回原函数。ret指令会从栈顶获取返回地址,然后跳转到(jmp指令)这个地址继续执行。此时的栈帧结构如下图所示:栈溢出攻击上面说的都是栈溢出攻击部分。通过前面的学习,我们知道调用函数的参数,函数执行后的返回地址,被调用函数的局部变量,都是存放在栈中的。如果调用函数时返回地址不小心被覆盖,则函数调用后,不会跳转到原函数继续执行,而是跳转到被覆盖的地址执行。如下图所示:那么,如何覆盖返回地址呢?我们可以用下面的例子来说明:#include
