【问题标题】:Why can I write and read memory when I haven't allocated space?为什么在没有分配空间的情况下可以读写内存?
【发布时间】:2011-01-20 23:02:44
【问题描述】:

我正在尝试从头开始用 C 语言构建自己的哈希表作为练习,我一次只做一小步。但我有一个小问题......

我将哈希表结构声明为指针,这样我就可以用我想要的大小对其进行初始化,并在负载因子高时增加它的大小。

问题是我正在创建一个只有 2 个元素的表(这只是为了测试目的),我只为这 2 个元素分配内存,但我仍然能够写入我不应该的内存位置'吨。而且我还可以读取我没有写入的内存位置。

这是我当前的代码:

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


#define HASHSIZE 2


typedef char *HashKey;
typedef int HashValue;

typedef struct sHashTable {
    HashKey key;
    HashValue value;
} HashEntry;

typedef HashEntry *HashTable;


void hashInsert(HashTable table, HashKey key, HashValue value) {
}

void hashInitialize(HashTable *table, int tabSize) {
    *table = malloc(sizeof(HashEntry) * tabSize);

    if(!*table) {
        perror("malloc");
        exit(1);
    }

    (*table)[0].key = "ABC";
    (*table)[0].value = 45;
    (*table)[1].key = "XYZ";
    (*table)[1].value = 82;
    (*table)[2].key = "JKL";
    (*table)[2].value = 13;
}


int main(void) {
    HashTable t1 = NULL;

    hashInitialize(&t1, HASHSIZE);

    printf("PAIR(%d): %s, %d\n", 0, t1[0].key, t1[0].value);
    printf("PAIR(%d): %s, %d\n", 1, t1[1].key, t1[1].value);
    printf("PAIR(%d): %s, %d\n", 3, t1[2].key, t1[2].value);
    printf("PAIR(%d): %s, %d\n", 3, t1[3].key, t1[3].value);

    return 0;
}

您可以很容易地看到我没有为(*table)[2].key = "JKL";(*table)[2].value = 13; 分配空间。我也不应该能够读取main() 中最后两个printfs 中的内存位置。

有人可以向我解释一下吗?如果我可以/应该对此做些什么?

编辑:
好的,我已经意识到关于我上面的代码的一些事情,这是一团糟......但我现在有一个班级,无法更新我的问题。有时间我会更新这个。对此感到抱歉。

编辑 2:
对不起,我不应该发布这个问题,因为我不想要我上面发布的代码。我想做一些稍微不同的事情,这使得这个问题有点无关紧要。所以,我只是假设这是我需要答案的问题,并接受以下正确答案之一。然后我会发布我的正确问题...

【问题讨论】:

标签: c memory-management hashtable


【解决方案1】:

把记忆想象成一块巨大的黑板,分成小方块。写入内存位置相当于擦除一个正方形并在那里写入一个新值。 malloc 的目的通常不是让记忆(黑板方块)存在;相反,它是识别一个没有被用于其他任何事情的内存区域(一组正方形),并采取一些措施确保它不会被用于其他任何事情,直到另行通知。从历史上看,微处理器将系统的所有内存都暴露给应用程序是很常见的。一段代码Foo 理论上可以选择一个任意地址并将其数据存储在那里,但有几个主要警告:

  1. 其他一些代码 `Bar` 之前可能已经在其中存储了一些东西,并期望它会保留下来。如果 `Bar` 读取该位置并期望取回它写入的内容,它将错误地将 `Foo` 写入的值解释为它自己的值。例如,如果 `Bar` 存储了接收到的小部件的数量(23),而 `Foo` 存储了值 57,那么之前的代码会认为它已经收到了 57 个小部件。
  2. 如果 `Foo` 期望它写入的数据会保留很长时间,它的数据可能会被其他代码覆盖(基本上是上述代码的另一面)。

较新的系统包括更多监控,以跟踪哪些进程拥有哪些内存区域,并终止访问它们不拥有的内存的进程。在许多这样的系统中,每个进程通常会从一个小黑板开始,如果尝试malloc 的方格比可用的多,则可以根据需要为进程分配新的黑板区域块。尽管如此,通常会有一些可供每个进程使用的黑板区域尚未被保留用于任何特定目的。代码理论上可以使用这些区域来存储信息而无需先分配它,如果没有发生任何事情将内存用于任何其他目的,这样的代码会起作用,但不能保证这样的内存区域不会用于在某个意想不到的时间有其他目的。

【讨论】:

    【解决方案2】:

    在 C 中,您可以读取任何映射的地址,也可以写入映射到具有读写区域的页面的任何地址。

    实际上,操作系统以通常 8K 的块(页)形式提供进程内存(但这取决于操作系统)。然后,C 库管理这些页面并维护空闲和分配的列表,并在 malloc 请求时提供这些块的用户地址。

    所以当你从 malloc() 得到一个指针时,你指向的是一个 8k 页面内的一个可读写的区域。这个区域可能包含垃圾,或者它包含其他 malloc 的内存,它可能包含用于堆栈变量的内存,或者它甚至可能包含 C 库用于管理空闲/已分配内存列表的内存!

    因此您可以想象,写入超出您 malloc 范围的地址确实会导致问题:

    • 其他 malloc 数据损坏
    • 堆栈变量或调用堆栈本身损坏,导致函数返回时崩溃
    • C 库的 malloc/free 管理内存损坏,在调用 malloc() 或 free() 时导致崩溃

    所有这些都很难调试,因为崩溃通常比损坏发生的时间晚得多。

    只有当您从与映射页面不对应的地址读取或写入时,您才会崩溃...例如从地址 0x0 (NULL) 读取

    Malloc、Free 和指针在 C 中非常脆弱(而在 C++ 中的程度略低),很容易误伤自己的脚

    有许多用于内存检查的 3rd 方工具,它们使用检查代码包装每个内存分配/释放/访问。它们确实会减慢您的程序速度,具体取决于应用了多少检查..

    【讨论】:

    • 据我所知,并非所有平台上的页面大小都固定为 8K。
    【解决方案3】:

    通常malloc 将分配比您需要的更多的内存用于对齐目的。还因为该进程确实具有对堆内存区域的读/写访问权限。因此,在分配区域之外读取几个字节很少会触发任何错误。

    但你仍然不应该这样做。由于您正在写入的内存可以被视为未占用或实际上已被其他人占用,因此任何事情都可能发生,例如第二个和第三个键/值对稍后将变成垃圾,或者由于您在其malloc-ed 内存中踩到的一些无效数据,无关的重要功能将崩溃。

    (另外,要么使用char[≥4]作为密钥类型,要么使用malloc作为密钥,因为如果密钥不幸存储在堆栈中,它将在以后变得无效。)

    【讨论】:

    • "ABC" 是字符串文字,存储在只读数据段中,而不是在堆栈上。 OP 是在复制指针,而不是实际的字符串,所以 HashEntry 的声明是完全正确的。
    • @qrdl:所以在您看来,堆将只支持常量字符串文字键?
    • @Kenny 我没有收到你的问题,也没有在评论中提到堆。我说过所有字符串文字都位于只读数据段中,而不是在堆栈上,因此在函数内为某个全局指针分配字符串文字的地址是完全安全的。
    • @qrdl:好的,我改写了那句话。
    • 您的修改没有解决问题。 OP 现在这样做的方式非常好——他没有使用strcmp() 来复制字符串,他只是在分配指针。你不需要在分配之前为指针分配内存,而且,如果你这样做,你会泄漏内存。
    【解决方案4】:

    一般来说(不同平台的不同实现),当进行 malloc 或类似的基于堆的分配调用时,底层库会将其转换为系统调用。当库这样做时,它通常会在 regions 集合中分配空间 - 这将等于或大于程序请求的数量。

    这样的安排是为了防止频繁的系统调用内核进行分配,并更快地满足程序对堆的请求(这当然不是唯一的原因!!-可能还有其他原因)。

    这种安排的失败会导致您正在观察的问题。再一次,您的程序不一定每次都能够写入未分配的区域而不会崩溃/段错误 - 这取决于特定二进制文件的内存安排。尝试写入更高的数组偏移量 - 你的程序最终会出错。

    至于你应该/不应该做什么 - 上面回答的人已经总结得很好。除了应该防止此类问题并且只能通过在分配内存时小心来完成之外,我没有更好的答案。

    通过这个粗略的示例可以理解:当您在用户空间中请求 1 个字节时,内核必须分配至少一整个页面(例如,在某些 Linux 系统上是 4Kb - 内核级别最精细的分配)。为了通过减少频繁调用来提高效率,内核将整个页面分配给调用库——当更多请求进入时,库可以分配它。因此,向这样的 region 写入或读取请求可能不会必然产生故障。这只会意味着垃圾。

    【讨论】:

      【解决方案5】:

      不要这样做,这是未定义的行为。

      它可能会意外工作,因为您写入/读取了一些程序实际上并未使用的内存。或者它可能导致堆损坏,因为您覆盖了堆管理器为其目的使用的元数据。或者您可以覆盖一些其他不相关的变量,然后很难调试因此而发疯的程序。或者任何其他有害的事情——无论是明显的或微妙的但严重的——都可能发生。

      只是不要这样做 - 只读取/写入您合法分配的内存。

      【讨论】:

      • +1:欢迎来到 C 编程。没有界限检查这是语言的一部分。所以你可以做各种你不应该做的事情。
      猜你喜欢
      • 2012-07-21
      • 1970-01-01
      • 2012-10-21
      • 1970-01-01
      • 2014-05-31
      • 1970-01-01
      • 1970-01-01
      • 2019-02-24
      • 2021-08-28
      相关资源
      最近更新 更多