关于损坏的研究






4.96/5 (16投票s)
您是否认为内存损坏会立即产生可重复的崩溃?有些程序员确实是这么认为的……
引言
您可能遇到过这样的程序员,他们认为内存损坏总是会立即产生某种可见的结果(最可能是程序崩溃):我遇到过。我希望他们是对的。
背景
内存损坏会在不需要的位置更改内存的内容,从而更改存储在这些位置的变量的值。在现实生活中,变量包含有意义的数据,对这些数据的更改将产生一些不良后果。其中,计算返回错误结果、程序崩溃、程序员失业以及黑客获取敏感信息。 the sample project 展示了一个内存损坏的案例,实际上它只是改变了几个变量的值。没有好莱坞式的爆炸,也没有响亮的尖叫声。
维基百科说:“内存损坏发生在由于编程错误而导致内存位置的内容被无意修改时”
不要只听我的一面之词,尽管去检查一下。链接就在这里:http://en.wikipedia.org/wiki/Memory_corruption。您回来了吗?我们继续。
同样来自维基百科:“在计算机安全和编程中,缓冲区溢出,或缓冲区溢出,是一种异常,其中程序在写入数据到缓冲区时,会超出缓冲区的边界并覆盖相邻的内存。”http://en.wikipedia.org/wiki/Buffer_overflow
到目前为止,我已经证明了我可以复制粘贴。但这是什么意思呢?嗯,这篇文章的重点是,内存损坏与其他类型的损坏一样,可以而且应该被预防。我们最终会讲到这一点。请耐心点。我希望您在此过程中能获得一些乐趣。
让我们来破坏一些内存!
让我们来看一下示例程序的一些部分(提供了 .sln 和 .dsw 文件)。
void OverflowMyBuffer(char *szTest)
{
// Copy a string of size 13 into a buffer
// of unknown size. Its size may be just 3 bytes...
strcpy(szTest, "Hello, world!");
}
这是溢出缓冲区的最简单方法:使用 strcpy
(或 memcpy
)将比其分配大小更大的内容复制到其地址(其第一个字节的地址)中。例如,“Hello, world!”字符串的长度为 14 个字节(包括结尾的零);如果缓冲区 szTest
比这短,您将遇到缓冲区溢出。
void test ()
{
const char format[] = "* * * * * nine = %d, eight = %d, seven = %d, szSmallBuffer = '%s'\n";
// Const everywhere: this cannot change, right?
// The compiler would tell us, wouldn't it? Well, the contents
// of this array will be corrupted by a buffer overflow. Stay tuned...
const int const arr[3] = { 9, 8, 7 };
char szSmallBuffer[3];
// The buffer has no room for the ending zero, but VC++ doesn't seem to mind...
strcpy(szSmallBuffer, "abc");
// The next line prints * * * * * nine = 9, eight = 8,
// seven = 7, szSmallBuffer = 'abc'; not bad.
printf(format, arr[0], arr[1], arr[2], szSmallBuffer);
OverflowMyBuffer(szSmallBuffer);
// The next line prints * * * * * nine = 1998597231,
// eight = 1684828783, seven = 33, szSmallBuffer = 'Hello, world!'
// 33 is the ANSI code for '!'; suspicion arises...
printf(format, arr[0], arr[1], arr[2], szSmallBuffer);
// The next line prints: arr as a string: 'o, world!'
printf("arr as a string: '%s'\n", arr);
}
函数 test()
是有趣的部分。首先声明并初始化几个变量,然后调用 printf()
显示它们的值符合预期。到目前为止,一切正常。
然后调用 OverflowMyBuffer()
:此函数 **不** 接收指向 arr[]
的指针,并且对其一无所知。
在该调用之后,数组 arr[]
的值(该数组 **未** 传递给函数)已更改:最后的两个 printf()
调用显示了这一点,以及双常量数组的新内容。
int _tmain(int argc, _TCHAR* argv[])
{
printf("\n-----\nIn main, before test()\n-----\n");
test();
printf("\n-----\nIn main, after test()\n-----\n");
return 0;
}
这里没什么可看的。上面的函数 'test()
' 才是有趣的。
程序输出是:
-----
In main, before test()
-----
* * * * * nine = 9, eight = 8, seven = 7, szSmallBuffer = 'abc'
* * * * * nine = 1998597231, eight = 1684828783,
seven = 33, szSmallBuffer = 'Hello, world!'
arr as a string: 'o, world!'
-----
In main, after test()
-----
如果 arr[]
的内容不仅仅是几个空闲的整数,而是重要数据呢?例如:损坏的指针可能会导致程序崩溃,存储您飞船与冰山之间距离的损坏变量可能会让您在冰冷的水中畅游。
初学者附注
- 等等! - 有人可能会说: - 如果函数 OverflowMyBuffer()
无法访问变量 arr[]
,它怎么会弄乱它的内容呢?
好吧,这正是内存损坏的问题所在。使用指针,您可以随意操作内存中的任何位置。请考虑以下示例:*((int *) rand()) = 0xDeadBeef;
很酷,对吧?此代码尝试在您的进程的随机内存位置写入任意值:如果那里有一个变量(据我所知,可能存在),它的值将被修改;如果随机指针指向未分配的内存,或被代码占用的内存,将会发生奇怪的事情。
最终要点
考虑到有关于缓冲区溢出和内存损坏的维基百科文章,这篇文章的重点是什么?
有两个
内存损坏不一定会产生 **即时、可重复** 的崩溃
前段时间,一位同事在寻找一个 bug 时,搜索了数千行代码中一个整数变量神秘变化的出现情况,在该变量出现的所有地方设置断点,然后发现该变量的值在没有任何明显原因的情况下发生变化。他问我看看:这怎么可能发生?既然您正在阅读这篇文章,您可能已经推断出这是由于缓冲区溢出引起的内存损坏。当时并不明显:代码大致是这样的
// This is pseudo-code.
int x = 14; // 32 bits = 4 bytes. Contents: { 14, 0, 0, 0 };
// The x86 processor family is little endian.
char str[20]; // Declared after x, which means (str + 20 == &x);
// don't expect (str - 4 == &x).
// ... many, many lines of code: x is not touched ...
// The database field is of size 20, the function
// LoadDatabaseFieldIntoVariable() calls strcpy;
// the ending zero of str[] overflows into the least significant byte of x
LoadDatabaseFieldIntoVariable(SomeDatabaseField, str);
// ... many, many lines of code: x is not touched ...
// x is no longer 14, but zero. Huh?
// Nobody ever touched it! What happened?
char * ptr = malloc(x);
// .. a few more lines of code ...
strcpy(ptr, "Hello, world!"); // Crash!
我们在 str
被触及的任何地方设置断点,并为 x
添加观察点,bug 就解决了。
如果您看到生产代码在客户站点随机失败,在几分钟后成功,使用相同的输入;如果您看到简单的计算有时返回奇怪的结果,一分钟后又返回正确的结果;如果您无法在调试器下重现不当行为;如果您的团队成员都拥有健康的程序员自尊(“如果有 bug,那不是在 **我的** 代码里”);这些是内存问题的典型症状:要么是损坏,要么是内存分配失败且未被检查(这是一个完全不同的文章的主题),或者在多线程代码中,是竞态条件(同样,超出本文范围)。
缓冲区溢出是可以避免的
通过一些简单的预防措施,您可以使缓冲区溢出成为过去
- 不要使用 C 风格字符串:使用
std::string
,或者 WTL/ATL/MFC 的CString
,或者CComBSTR/_bstr_t
。它们都管理自己的内存。 - 不要使用 C 风格数组:使用 STL 容器。同样,它们可以保护您免受缓冲区溢出的侵害。
- 如果您绝对 **必须** 编写一个函数,该函数接收一个 C 风格数组作为参数并修改其内容,请同时将第二个参数的类型设置为
size_t
,包含元素计数,并使用它来避免缓冲区溢出。就像strncpy()
一样。
内存损坏的另一个常见原因是悬空指针;我最近几周没有遇到过。如果您对此类指针的预防感兴趣,并且在 Google 或 Bing 上搜索“如何预防悬空指针”没有帮助,请在下方给我留言。
我希望您喜欢这篇文章:无论如何,感谢您花时间阅读它。祝您编程愉快!
历史
- 2012 年 1 月 - 发布。