为什么我没有分配空间时可以写入和读取内存?

时间:2010-02-25 10:49:59

标签: c memory-management hashtable

我正在尝试从头开始构建我自己的Hash Table作为练习,我一次只做一小步。但我有一点问题......

我将Hash Table结构声明为指针,因此我可以使用我想要的大小初始化它,并在加载因子很高时增加它的大小。

问题是我正在创建一个只有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;分配空间。我也无法读取printfs中过去2 main()的内存位置。

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

修改
好吧,我已经意识到我上面的代码有一些问题,这是一团糟......但我现在有一个班级,无法更新我的问题。我有空的时候会更新这个。对不起。

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

5 个答案:

答案 0 :(得分:7)

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

它可能在意外工作,因为你编写/读取程序实际上没有使用的一些内存。或者它可能导致堆损坏,因为您为了其目的而覆盖堆管理器使用的元数据。或者你可以覆盖其他一些不相关的变量,然后很难调试那个因此而变得疯狂的程序。或者其他任何有害的东西 - 无论是显而易见的还是微妙的还是严重的 - 都可能发生。

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

答案 1 :(得分:2)

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

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

这种安排的失败会导致您正在观察的问题。再一次,并不总是需要你的程序能够在没有崩溃/ seg-faulting的情况下写入未分配的区域 - 这取决于特定二进制文件的内存排列。尝试写入更高的数组偏移 - 你的程序最终会出错。

至于你应该/不应该做什么 - 上面作出回应的人总结得相当好。我没有更好的答案,除非应该防止这些问题,而且只能在分配内存时小心这样做。

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

答案 2 :(得分:1)

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

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

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

所以你可以想象,写入超出你malloc的范围的地址真的会引起问题:

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

所有这些都是调试的真正痛苦,因为崩溃通常比腐败发生的时间晚发生。

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

Malloc,Free和指针在C中非常脆弱(在C ++中程度稍低),并且很容易在脚上意外射击

有许多用于内存检查的第三方工具,它们使用检查代码包装每个内存分配/空闲/访问。它们确实会降低程序的速度,具体取决于应用了多少检查。

答案 3 :(得分:0)

将记忆想象成一个分为小方块的大黑板。写入存储器位置相当于擦除正方形并在那里写入新值。 malloc的目的通常不是为了带来记忆(黑板方块);更确切地说,它确定了一个没有被用于其他任何东西的记忆区域(一组方格),并采取一些行动以确保它不会被用于其他任何事情,直到进一步注意。从历史上看,微处理器将所有系统内存暴露给应用程序是很常见的。理论上,一段代码Foo可以选择一个任意地址并将其数据存储在那里,但有几个主要注意事项:

  1. 其他一些代码`Bar`可能以前存储了一些东西,期望它会保留下来。如果`Bar`读取该位置,希望找回它写的内容,它将错误地将`Foo`写的值解释为它自己的值。例如,如果`Bar`已经存储了接收的小部件的数量(23),并且`Foo`存储了值57,则较早的代码将相信它已经接收了57个小部件。
  2. 如果`Foo`希望它写入的数据保持任何相当长的时间,其数据可能会被其他一些代码覆盖(基本上是上面的反面)。

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

答案 4 :(得分:-1)

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

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

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