踩内存问题
踩内存问题
踩内存
我们把访问不属于当前对象、当前进程的内存的行为,称为踩内存。
踩内存本身是未定义行为(除非是柔性数组),但由于操作系统的内存页面管理机制,未必会引发段错误(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
*/
使用野指针/野引用
就是访问了未初始化的指针,指向尚未分配的地址空间。

浙公网安备 33010602011771号