一个栈溢出C++示例

本文主要测试函数内变量越界的问题,即栈溢出。 持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第5天,点击查看活动详情

本文主要演示、分析、测试函数内变量越界的问题,即栈溢出。

问题提出

很久前在测试某个工程时,发现一直能工作的模块出现了段错误。由于代码复杂,又有其它事耽搁,直到最近才集中时间调试。那个模块是循环vector,在其中计算数据再组装成字符串,最终将所有结果写到文件中。经测试发现,是在某次循环时出错。抽象化后的代码示例如下:

len = vPath.length();
for (int i = 0; i < len; i++)
{
    // 处理逻辑
    // 处理逻辑
    // 若干次调用sprintf()组装字符串
    
    // 循环第N次出错
}
复制代码

开始以为是处理逻辑部分出错,后发现在某次调用sprintf之后,i的值变得十分大。超过了vector容量,因此造成段错误。

后来确认,是sprintf组装的缓冲区越界,i的值被覆盖了。因为当时加代码片段时,没有留意缓冲区大小问题,加大容量即可解决问题。

工程代码

先给出变量的设计,如下:

    int ret = 0;
    int type = 10;
    int id = 0;
    char buffer[32] = {0};
复制代码

因为本文就是模拟栈溢出情况,而栈是向下(低地址)增长的,为了让缓冲区buffer溢出覆盖其它变量,因此将其放到最后定义,其大小为32(十六进制为0x20),这样一旦溢出,就会越界波及干扰到rettypeid这几个变量,它们均为int类型,指针大小为4字节。

完整代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
​
// 打印buffer的内存数据
void dump(const char *buffer, int len)
{
    int i, j, n;
    int line = 16;
    char c;
    unsigned char* buf = (unsigned char *)buffer;    // 必须是unsigned char类型
​
    n = len / line;
    if (len % line)
        n++;
​
    for (i=0; i<n; i++)
    {
        //printf("0x%08x: ", (unsigned int)(buf+i*line)); // linux ok
        printf("0x%8p: ", buf+i*line); // windows ok
        
        for (j=0; j<line; j++)
        {
            if ((i*line+j) < len)
                printf("%02x ", buf[i*line+j]);
            else
                printf("   ");
        }
​
        printf(" ");
        for (j=0; j<line && (i*line+j)<len; j++)
        {
            if ((i*line+j) < len)
            {
                c = buf[i*line+j];
                printf("%c", c >= ' ' && c < '~' ? c : '.');
            }
            else
                printf("   ");
        }
​
        printf("\n");
    }
}
​
int main(void)
{
    int ret = 0;
    int type = 10;
    int id = 0;
    char buffer[32] = {0};
​
    //dump(buffer, 48);
    
    for (int i = 0; i < 16; i++)
    {
        printf("---- type: %d id: %d i:%d \n", type, id, i);
        ret += sprintf(buffer+ret, "helloworld type: %d id: %d ", type, id);
        printf("write total len: %d(0x%x)\n", ret, ret);
        printf("++++ type: %d(0x%x) id: %d(0x%x) i: %d(0x%x)\n", type, type, id, id, i, i);
        type ++;
        id ++;
    }
    printf("ptr ret: %p type: %p id: %p\n", &ret, &type, &id);
​
    dump((char*)(buffer), 60);
    return 0;
}
复制代码

代码比较简单,循环组装字符串再保存到buffer中,注意,buffer的内容是累加的——这正是溢出的根本问题。为了方便观察,同时打印其它变量的值及地址。本文在 x86 平台测试,其为小端模式,因为打印的值需要倒着看。

测试

一次测试结果如下:

---- type: 10 id: 0 i:0
write total len: 26(0x1a)
++++ type: 10(0xa) id: 0(0x0) i: 0(0x0)
---- type: 11 id: 1 i:1
write total len: 824195711(0x31203a7f)
++++ type: 1887007776(0x70797420) id: 1684828783(0x646c726f) i: 1684611121(0x64692031)
ptr ret: 000000000022FE48 type: 000000000022FE44 id: 000000000022FE40
0x000000000022FE20: 68 65 6c 6c 6f 77 6f 72 6c 64 20 74 79 70 65 3a  helloworld type:
0x000000000022FE30: 20 31 30 20 69 64 3a 20 30 20 68 65 6c 6c 6f 77   10 id: 0 hellow
0x000000000022FE40: 70 72 6c 64 21 74 79 70 7f 3a 20 31 32 20 69 64  prld!typ.: 12 id
0x000000000022FE50: 3a 20 31 20 00 00 00 00 c7 13 40 00              : 1 ......@.
复制代码

下面分析执行情况:

  • 循环开始,第一次一切正常。
  • 循环到第二次时,缓冲区溢出,retid变量的值十分大。i亦然,故循环退出,由于代码没有用i作索引,因为没有段错误。

溢出数值分析如下:

  • buffer地址为0x000000000022FE20,变量id靠近buffer,其地址buffer后的32字节偏移处,为000000000022FE40,接着是type,偏移4字节,地址为000000000022FE44ret地址是000000000022FE48

  • id溢出后的值是1684828783(0x646c726f),观察对应打印的二进制:0x000000000022FE40: 70 72 6c 64 21 74 79 70 7f 3a 20 31 32 20 69 64 prld!typ.: 12 id:。如下:

    0x000000000022FE40: 70 72 6c 64 ...   prld
    复制代码

    应该是hello world最后4字节orld,但有乱码。

  • type溢出后的值是1887007776(0x70797420),观察对应打印的二进制:0x000000000022FE40: 70 72 6c 64 21 74 79 70 7f 3a 20 31 32 20 69 64 prld !typ.: 12 id:。

  • ret溢出后的值是824195711(0x31203a7f),观察对应打印的二进制:0x000000000022FE40: 70 72 6c 64 21 74 79 70 7f 3a 20 31 32 20 69 64 prld!typ .: 12 id:。

综合测试分析情况,那些变量溢出后的数值,基本上就是写到buffer越界后的数据。

扩展知识

栈、堆是不同的概念——因此前面提及的仅是栈,理论上栈、堆都有溢出的可能。

栈溢出一般有2种可能:无限递归,变量或数组定义很大。以linux系统为例,栈的大小为8MiB。可用ulimit -a查看:

$ ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 253387
max locked memory       (kbytes, -l) 64
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 4096
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited
复制代码

其中stack size为8192,即8MiB。另外也能从中知晓文件句柄数量最大为1024(open files字段)。

文中的“溢出”是指写到buffer数组的内容超过其容量,占用了其它变量的空间。至于其它的溢出情况,就不再深究了。

小结

本文出现的问题,主要原因是缓冲区容量不足引起溢出的。幸好不是生产环境的,否则又得急忙救火了,但也给自己提了个醒。编码一定要注意细节,内存的操作,数组的索引,指针的判断,等,都要谨慎。所谓“小心驶得万年船”,作为一名编码工具人,对代码要常怀敬畏之心。

猜你喜欢

转载自juejin.im/post/7104832377699958814