C程序栈原理及例子浅析

    首先看如下图-1演示的用c语言编写的例子程序。
    ┌————————————————————┐
    │ 1.   void fun()                       │
    │ 2.   {                                │
    │ 3.       printf(“Hello World\n”);   │
    │ 4.   }                                │
    │ 5.   int main()                       │
    │ 6.   {                                │
    │ 7.      int *i;                       │
    │ 8.      i = (int*)&i + 2;             │
    │ 9.      *i = fun;                     │
    │ 10.  }                                │
    │               图-1                     │
    └————————————————————┘
 
    如果我跟你说程序运行的结果(我们以下的讨论将在IA32-linux-gcc3.4环境中进行)是将打印出“Helllo World”,你可能会不相信。那么这一个“诡异版HelloWorld程序”究竟是如何做到的呢?
   这就涉及到了栈的问题,不过首先得补充一些理论。
   我们知道:函数中的局部变量是存放于栈中的。栈中除了局部变量外,还包括函数参数、返回地址等一系列内容。其存储过程如下:当执行到某一函数时,首先将函数的各个参数依次压入栈,随后是函数的返回地址(即程序中产生该函数调用处的下一行代码的地址),最后压入栈内的是函数的局部变量。综上所述,一个函数运行时的栈理论上具有如下结构:
   ┌—————————————————————┐
   │高地址  │            │                  │
   │        │    ……    │                  │
   │        ├——————┤                  │
   │        │    参数    │                  │
   │        ├——————┤                  │
   │        │  返回地址  │                  │
   │        ├——————┤                  │
   │        │  局部变量  │                  │
   │        ├——————┤←——  栈顶      │
   │        │            │                  │
   │低地址  │            │                  │
   │        │            │                  │
   │            图-2                          │
   └—————————————————————┘
 
 
    但是现实中栈的结构还受到编译器的限制,从而出现了一些细节上的变化,以IA32体系结构下的例子补充一下我们的理论,就得到了实际应用中的栈结构:
   ┌—————————————————————┐
   │高地址  │            │                  │
   │        │            │                  │
   │        ├——————┤                  │
   │        │ 局部变量n  │    │            │
   │        ├——————┤    │            │
   │        │     …     │    │栈          │
   │        ├——————┤    │增          │
   │        │   参数1    │    │长          │
   │        ├——————┤    │方          │
   │        │  返回地址  │    │向          │
   │        ├——————┤    │            │
   │        │   OLD Ebp  │    │            │
   │        ├——————┤    │            │
   │        │ 局部变量1  │    │            │
   │        ├——————┤    ▼            │
   │        │    …      │                  │
   │        ├——————┤                  │
   │        │ 局部变量n  │                  │
   │        ├——————┤←——  栈顶      │
   │        │            │                  │
   │低地址  │            │                  │
   │        │            │                  │
   │            图-3                          │
   └—————————————————————┘
    作为补充,我们就图-3给出如下的一些解释:
  1、我们不用理解“Old Ebp”项是什么,这也不会影响我们的分析,只需要知道在IA32架构中它是一个四字节的数值就够了。
  2、函数的各个参数入栈顺序与具体的编译时约定有关,对于标准C而言参数是从右到左依次入栈,至于局部变量的入栈顺序在gcc3.4中是按照声明顺序一次入栈。
  3、进入函数之后,栈顶位置指向的就是最后声明的局部变量的位置。
  4、结合c语言的语法,我们可以知道函数可以没有参数和局部变量,所以图-3中的“参数”和“局部变量”不是函数栈结构的必需部分。
 
   好了,我们现在已经可以开始结合图-1的例子程序进行叙述了。
   程序中定义了一个全局函数fun,没有参数,简简单单一个函数。主函数中定义了一个int*型变量i,然后是做了一系列赋值操作。所以刚刚进入主函数的一刻对应的栈就有如下结构:
   ┌—————————————————————┐
   │高地址  │            │                  │
   │        │    ……    │                  │
   │        ├——————┤                  │
   │        │  返回地址  │                  │
   │        ├——————┤                  │
   │        │  OLD Ebp   │                  │
   │        ├——————┤                  │
   │        │  (int*)i   │                  │
   │        ├——————┤←——  栈顶      │
   │        │            │                  │
   │低地址  │            │                  │
   │        │            │                  │
   │            图-4                          │
   └—————————————————————┘
 
    准确地说这是主函数的栈结构,fun函数只是我们想为看结果而补充的一段代码而已。主函数有着与普通函数一致的理论上的栈结构,不同的是它的栈结构中有些内容是系统给它的,即图-4中用省略号代替的部分,而具体细节就与体系结构相关了,还得有心人自己去研究。
    我们从图-4中可以看到,此时栈顶元素就是指针i,紧接着它的就是“Old Ebp”和“返回地址”,而“返回地址”就是主函数结束后系统规定执行的指令地址。我想说到这有聪明的读者就应该想到程序肯定是将“返回地址”做了修改,从而执行了fun函数。果真如此!请看:
    i = (int*)&i + 2;
它将i的地址(就本程序而言i的地址相当于是栈顶位置)加2之后再赋给i。不过这里加2可不是简简单单的一个“地址+2”,而是一个(int*)类型的加法,实际将按照字节大小换算成“地址+2×sizeof(int*)”,再加上(int*)类型的大小是4所以最终编译的结果将是加8(或者说是两个“int*”大小)。怎么样?很拗口吧,不单单你这样想,我也是。这是高级语言的优势,也是它的劣势:我们节省了大量繁琐的计算,可同时也丧失了对程序底层的深入理解和自由控制(如果你不深究个中原委的话)。那么此时指针i控制的这个地址指向了哪里呢?我们还是从图上看来得明白:
   ┌—————————————————————┐
   │高地址  │            │                  │
   │        │    ……    │                  │
   │        ├——————┤                  │
   │ 4字节{ │  返回地址  │                  │
   │        ├——————┤←——            │
   │ 4字节{ │  OLD Ebp   │     栈顶+8       │
   │        ├——————┤                  │
   │ 4字节{ │  (int*)i   │                  │
   │        ├——————┤←——  栈顶      │
   │        │            │                  │
   │低地址  │            │                  │
   │        │            │                  │
   │            图-5                          │
   └—————————————————————┘
 
    如图-5,指针i的地址(也相当于栈顶)加上八个字节,我们一查,不就是“返回地址”的地址吗?这是指针i刚好就指向了返回地址。好啊,既然得到了,还等什么,直接:
    *i = fun;
   一下子我们就把“‘返回地址’的地址中的内容”(也就是返回地址)修改成了函数fun的地址。这样我们完成了所有的准备工作,剩下的内容自然是:
   Main函数结束后,转而取出自己栈内的“返回地址”(当然是已经被我们修改过的了),跳到那里去执行,这时就进入了函数fun中,一切顺理成章,我们的分析结束。
   不过我在这里刻意隐瞒了一个情况:即运行例子程序时,除了正常输出之外,还包含了错误提示。因为我们修改的是main函数的返回地址,这是系统的东西,自然要有纰漏从而出现错误。至于如何能正常退出,还是看看本文开头介绍的陈硕的文章好了。
   事实上本文的目的有两个:一来为今年选修“unix高级技术”的同学们介绍一点先行知识,这有助于为解决某些上机实验提供思路;二者也是想说一些关于代码安全的问题。
    我们从上可以看到基于栈行为我们甚至可以改变程序的正常执行过程。当然没有那个人会傻到写一个如我们的例子程序这样的东西被别人攻击,可实际上类似的漏洞是隐蔽的,并且大量存在着。“基于缓冲区的溢出攻击”一直以来就是包括微软公司在内的软件公司及个人在研究和防范的要点。鉴于这个课题的广泛,我不想展开说,只是想给出两条用得上,简单有效的经验以资借鉴:
(1)不要为接受的数据预留相对过小的缓冲区,同时对传入的数据进行越界检查。
(2)不要定义过大的局部缓冲区,这里说的“大”是“很大”的意思,一般是以超过系统页大小为限。解决之道是定义一个全局的缓冲区来代替之。
  与本文相关的论题应该说是已经有了很成熟的理论,只不过我们还有很长大的路要走
才能和时代的最前沿齐头并进,希望能与有兴趣的同学有所交流,共同进步。

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值