找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2007

积分

0

好友

261

主题
发表于 4 小时前 | 查看: 2| 回复: 0

在智能指针普及之前,很多人使用裸指针,使用裸指针时,如果我们访问内存没有做好恰当的边界检查,就可能导致非法内存访问。随之而来的问题是,为什么程序有时候会因此触发段错误(segmentation fault),有时候却又安然无恙?

今天,我们就借助一个具体的例子来聊聊这个现象。

首先看下面这段C++代码:

#include <vector>
#include <iostream>

void access(const std::vector<int>& vec, size_t offset){
    size_t index = vec.size() - 1 + offset;

    std::cout << "Accessing index: " << index << std::endl;

    int value = vec[index];  // Intentional out-of-bounds access
}

int main(){
    std::vector<int> vec(32, 0);

    std::cout << "Vector Size: " << vec.size() << std::endl;

    access(vec, 1);
    access(vec, 10);
    access(vec, 100);
    access(vec, 1000);
    access(vec, 10000);
    access(vec, 100000);
}

代码的作用是演示访问 std::vector 越界时可能发生的行为(Undefined Behavior),并尝试通过不断增大越界偏移量来提高触发 segmentation fault(段错误) 的概率。

编译并运行上述代码,可能的输出如下:

Vector Size: 32
Accessing index: 32
Accessing index: 41
Accessing index: 131
Accessing index: 1031
Accessing index: 10031
Accessing index: 100031
Segmentation fault

我们可以看到一个有趣的现象:当程序尝试访问那个长度为32的向量中,索引为32、41、131、1031和10031的元素时,它并没有崩溃。但是,当尝试访问索引为100031的元素时,程序终于因为段错误而崩溃了。

这是为什么呢?关键点在于,并不是所有的非法访问都会被操作系统立刻捕捉。这就涉及到了程序内存分配的边界。

借助 Valgrind 这样的内存调试工具,我们可以获得程序实际分配的内存情况。使用 Valgrind 运行同一个程序,会得到更详细的报告:

Vector Size: 32
Accessing index: 32
==15073== Invalid read of size 4
==15073==    at 0x400AAE: access(std::vector<int, std::allocator<int> > const&, unsigned long) (in /home/lj7/a.out)
==15073==    by 0x400B3C: main (in /home/lj7/a.out)
==15073==  Address 0x5ba6100 is 0 bytes after a block of size 128 alloc'd
...

Accessing index: 41
==15073== Invalid read of size 4
==15073==    at 0x400AAE: access(std::vector<int, std::allocator<int> > const&, unsigned long) (in /home/lj7/a.out)
==15073==    by 0x400B4D: main (in /home/lj7/a.out)
==15073==  Address 0x5ba6124 is 28 bytes before an unallocated block of size 4,120,224 in arena "client"
...

Accessing index: 131
...
Accessing index: 1031
...
Accessing index: 10031
...
Accessing index: 100031
==15073== Invalid read of size 4
==15073==    at 0x400AAE: access(std::vector<int, std::allocator<int> > const&, unsigned long) (in /home/lj7/a.out)
==15073==    by 0x400B91: main (in /home/lj7/a.out)
==15073==  Address 0x5c07b7c is 399,932 bytes inside an unallocated block of size 4,120,224 in arena "client"

...

==15073== HEAP SUMMARY:
==15073==     in use at exit: 0 bytes in 0 blocks
==15073==   total heap usage: 2 allocs, 2 frees, 73,856 bytes allocated

从 Valgrind 的 HEAP SUMMARY 可以看到,程序总共分配了 73,856 字节的堆内存。假设每个 int 占4字节,这部分内存大约可以容纳 18,464 个整数。这就解释了现象:

  • 访问索引 100031 的操作,已经远远超出了程序被分配的这 73,856 字节内存的范围,因此操作系统会介入并触发段错误。
  • 而访问索引 32、41、131、1031 和 10031 的操作,虽然对于 std::vector 对象本身来说是越界的,但这些访问的目标地址,恰好还落在程序被分配的那块大内存内部。操作系统只关心程序有没有“越界使用分配给它的整片内存”,并不关心程序内部一个vector的边界。因此,操作系统不会触发段错误。

值得注意的是,Valgrind 检测并报告了所有6次越界访问(索引32到100031),而不仅仅是触发段错误的那一次。这是因为 Valgrind 运行了一个虚拟内存管理器,它能追踪每一个具体对象的分配和释放。它知道向量 vec 只分配了128字节(32个int),所以任何超出这个范围的访问,在 Valgrind 看来都是非法的。

我们可能会想用一些“技巧”绕过 Valgrind 的检查,比如这样写:

float const* ptr{&vec[31]}; ptr[1];

这行代码在效果上等同于 vec[32]。但这样做是徒劳的——因为 Valgrind 追踪的是指针的来源(Provenance),而不仅仅是它的目标地址。Valgrind 会知道 ptr 来源于 vec,因此 ptr[1] 仍然被判定为对 vec 的越界访问。

简单来说,操作系统的内存保护机制作用于整个程序的层面,而 Valgrind 这样的工具则工作在单个对象的层面。因此,后者能更精准地检测出此类非法内存访问。

这个例子清晰地表明:操作系统并不总是会对非法内存访问触发段错误。这完全取决于这次非法访问的地址,是否还停留在操作系统分配给该程序的那片内存区域内。如果超出了这片区域,就会触发段错误,这反而能让开发者警觉问题;但如果仍在这片区域内,就不会触发段错误,程序会继续运行但进入未定义行为(Undefined Behavior)的状态,这种隐藏的问题往往更加棘手,也更难调试。

对C++内存安全和调试技术感兴趣的开发者,欢迎到云栈社区C/C++板块深入探讨。了解内存模型的底层原理,对于编写健壮的系统程序至关重要,这也是计算机基础知识的重要组成部分。




上一篇:Python与C++实战:生成与检测欺骗性LNK文件工具解析
下一篇:《时代》封面报道:全球最具颠覆力AI独角兽Anthropic与Claude的激进与克制
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-14 06:52 , Processed in 0.540198 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表