深入理解任务堆栈

先来看这一个小函数,猜猜他的运行结果(VC6环境)?

#include

void  b()
{
    int data[10];
    printf("helloworld!/r/n");
    data[11]-=5;
}

int main()
{
    b();
    return 0;
}

堆栈溢出,肯定不正常,马上有人叫起来了。

没错, 那么结果是什么呢,为什么会不停打印helloworld呢,我们将用堆栈揭开他的奥秘。

且看main函数汇编代码。  

很简单,  L12  调用b函数,   L13对返回值赋0.

这里有个很关键的东东: call

call包含2部分操作,call的下一条指令地址入栈,跳转,也就是从效果来说,包含push  0040108D 和 jmp  00401005两条操作。 假如,你打开内存窗口,你会看到,堆栈里已经有0040108D 这个值了。

10:   int main()
11:   {
         ...........
12:       b();
00401088   call        @ILT+0(b) (00401005)
13:       return 0;
0040108D   xor         eax,eax
14:   }

再来看函数b

当你把  printf("helloworld!/r/n"); 替换为 printf("%08x!/r/n",data[11]);时,你会发现,程序在不停的打印0040108D!, 显而易见,你修改的data[11]其实就是函数b的返回值地址,而data[11] -= 5;更是巧妙的利用 call    00401005 这条指令正好是5个字节的特点,将返回地址正好修改到了 0040108D ,也就是说函数返回时会再次调用函数b。每次b()都会把返回值改为b返回的地址,导致b()被不停的调用。

 

 

为什么data[11]正好是函数的返回值呢,让我们来看堆栈和任务有和关系
 

    任务(线程)都有一个堆栈,任务创建时创建,任务撤销时撤销。 任务的创建本质上包含2点。

    1  任务资源的分配(任务TCB和任务堆栈),很多嵌入式操作系统把TCB和堆栈是分配在一起的,比如Vxworks操作系统,其任务ID,堆栈基地址,TCB指针其实指向同一块内存。 创建任务时要指定任务大小,分配堆栈空间其实是一个特殊的malloc函数,他从堆栈空间分配,而不是从系统空间分配内存。任务堆栈windows下默认比较大,嵌入式OS则比较小,经常64k左右。 而局部变量就保存在堆栈中,当访问局部变量越界时,就发生了我们常说的"堆栈被踩了",堆栈被踩得话后果严重,轻则导致某次运行结果不对(这种问题很难定位),重则导致程序崩溃,例如把上面程序改为data[11]-=4,则程序直接崩溃。

 

    2  任务的初始化,包含2部分,任务TCB的初始化,并且把TCB和操作系统关联。

        TCB中包含任务的很多东西,   比如任务拥有的信号量的链表,文件描述符的链表,CPU寄存器的值(任务切换时用的),任务优先级,堆栈地址,任务名称等等,这些都需要初始化。初始化完成之后,操作系统会把这个任务TCB假如调度队列,如果加入调度队列时任务状态是就绪,那么当他拿到CPU时就可以直接运行了。

 

    堆栈中包含任务的栈帧,也就是说在函数调用链(A call B,B call C,C call D,D call E),那么堆栈中,ABCDE函数分别对应自己的一段栈帧。以E为例  E的栈帧包含A函数的传入参数,函数返回值,局部变量和临时保存的寄存器值。

 

     函数栈帧在主调函数和被掉函数中分配,在函数返回时释放,这就是为什么局部变量地址在函数返回后其值可能失效。

     例如 下面代码FuncB分配的函数栈帧在FuncB执行完后又被分配给FuncC,FuncC中很可能会踩到FuncB曾经的局部变量。

      FuncA{

         FuncB();

         FuncC();

      }

    任务(线程)的栈以及上面函数b的栈为下图。

深入理解任务堆栈_第1张图片 

深入理解任务堆栈_第2张图片

    *debug版本的函数b其实除了data[10],还在局部变量位置分配了一部分内存用来做调试,不过我们不用关系他。

    *为什么是data[11],而不是data[10]/data[12]或者其他? x86下编译器函数入口一般会有2条指令。

      push  ebp

      move ebp,esp

      其实就是将ebp作为帧指针来用(函数帧即为栈中一个函数所拥有的一段内存)。

      而这样就可以在函数中采用ebp-XXX表示局部变量,用ebp+XXX来表示传入参数。 函数中经常会有一些push操作,

      采用esp对局部变量和参数寻址远不如用ebp来的省事了,因为esp是经常变化的,而ebp是相对横的的。

 

     

你可能感兴趣的:(OS)