踩内存问题

踩内存问题

踩内存

我们把访问不属于当前对象、当前进程的内存的行为,称为踩内存。

踩内存本身是未定义行为(除非是柔性数组),但由于操作系统的内存页面管理机制,未必会引发段错误(Segmentation Fault)或内存访问错误(Access Violation):如果你强行访问该块内存,假设足够“幸运”——当前该内存的地址范围仍在当前映射给当前进程的物理页中,则不会发生段错误,只不过你会读到一个脏的内存值。而如果这块内存所在物理页没有被映射给当前进程,则访存就触发缺页异常,MMU地址转换后发现该地址不属于当前进程的地址空间,触发段错误。

场景

分为踩栈内存和踩堆内存。其中踩栈内存的表观特点在于,它总是崩溃在发生才内存后第一次使用坏数据的地方,由于是破坏的栈内存,所以往往崩溃点和踩内存点相聚很近,且在同一个函数栈帧内。

访问不属于当前对象的内存

这种场景发生在栈上紧挨着的两个对象。前一个对象访问到了后一个对象的内存。这种做法只有柔性数组是语言标准支持(且只有C支持,C++不支持,不过LLVM给出了类似的实现)的,其他做法都是未定义行为。

#include <stdio.h>
int main() {
    const char * x = "he";
    char y[10] = {'1', '2', '3', '4'};
    printf("%c%c%c%c\n", x[0], x[1], x[2], x[3]); // 输出he1。显然'1'是一个脏数据
    return 0;
}

数据类型错误导致写坏栈内存

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void file_read(char** readbuf, size_t* file_size)
{
    //*readelf = 读取文件内容
    // 在64位平台size_t是8字节
    // 如果调用方实际传进来int*,在64位下会写超,导致栈损坏
    *file_size = strlen(*readbuf);
}

int main()
{
    char* file_buf = NULL;
    int file_size = 0;
    int guard = 0x12345678; // 用来观察是否被踩,注意 guard 是 int 类型
    printf("before: file_size=%d, guard=0x%x, file_buf=%p\n", file_size, guard, (void*)file_buf);
    // file_read 需要size_t*,这里故意传int*
    file_read(&file_buf, (size_t*)&file_size);
    printf("after: file_size=%d, guard=0x%x, file_buf=%p\n", file_size, guard, (void*)file_buf);
	// 64位下,此时栈已经发生了损坏。
    unsigned char mac[6] = { 0 };
    sscanf(file_buf, "%02hhx:%02hhx:%02hhx:%02hhx:%02hhx:%02hhx", &mac[0], &mac[1], &mac[2], &mac[3], &mac[4], &mac[5]);
    free(file_buf);
    return 0;
}

上述代码在32位下运行正常,但是在64位下崩溃。原因是 size_t 在 32 位系统是占 32 位,在 64 位系统是 64 位。而 int 始终是 32 位。导致发生了不安全的指针强转,随后写溢出的数据破坏了栈,进而导致后面的 sscanf 使用栈内存时崩溃。

这种数据类型引发的问题还常见于 scanf/sscanf/printf 等的格式控制符。

访问悬垂指针(delete后指针)所指内存

在指针被delete之后,此时指针被称为空悬指针或者悬垂指针,即指向一块曾经保存数据对象,但现在已经无效的内存的指针。

当我们delete一个指针后,指针所指向的堆地址空间便被释放,指针值变成无效,此时该地址不该访问了,否则是未定义行为。同时,该块内存会归还给内存分配器(如glibc的 ptmalloc ,而后内存分配器决定何时将其归还操作系统)。

但是虽然该指针已经无效,但该指针仍然保存着已经被释放了的动态内存地址。

因此,正确的做法是在使用delete了一个指针之后,及时将该指针置为nullptr

#include <iostream>
class X {
   public:
    X() { std::cout << "X构造函数" << std::endl; }
    ~X() { std::cout << "X析构函数" << std::endl; }
    void func() {
        std::cout << "X的func" << std::endl;
    }
};
int main() {
    X* obj = new X();
    std::cout << obj << std::endl;
    obj->func();
    delete obj;
    obj->func(); // func() 是一个非虚成员函数。调用非虚成员函数时,编译器只需要知道对象的地址(obj的值)来作为this指针传递,它不会检查内存是否有效。由于func()的实现没有访问任何对象的成员变量,所以这次调用侥幸成功了。
    std::cout << obj << std::endl;
    return 0;
}
/*
X构造函数
0000029EFCD0B2D0
X的func
X析构函数
X的func
0000029EFCD0B2D0
*/

使用野指针/野引用

就是访问了未初始化的指针,指向尚未分配的地址空间。

参见:记一次因对象构造顺序引发的踩内存问题 - 3的4次方 - 博客园

posted @ 2023-09-30 21:52  3的4次方  阅读(140)  评论(0)    收藏  举报