【问题标题】:Making sure I'm writing to memory I own in C确保我正在写入我在 C 中拥有的内存
【发布时间】:2015-03-03 10:30:08
【问题描述】:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

struct Person
{
    unsigned long age;
    char name[20];
};

struct Array
{
    struct Person someone;
    unsigned long used;
    unsigned long size;
};

int main()
{
    //pointer to array of structs
    struct Array** city;
    //creating heap for one struct Array
    struct Array* people=malloc(sizeof(struct Array));
    city=&people;

    //initalizing a person
    struct Person Rob;
    Rob.age=5;
    strcpy(Rob.name,"Robert");

    //putting the Rob into the array
    people[0].someone=Rob;

    //prints Robert
    printf("%s\n",people[0].someone.name);
    //another struct
    struct Person Dave;
    Dave.age=19;
    strcpy(Dave.name,"Dave");
    //creating more space on the heap for people. 
    people=realloc(people,sizeof(struct Array)*2);
    //How do I know that this data is safe in memory from being overwritten? 
    people[1].someone=Dave;
    //prints Dave
    printf("%s\n",people[1].someone.name);
    //accessing memory on the heap I do not owe?
    people[5].someone=Rob;
    //prints "Robert" why is this okay? Am I potentially overwriting memory?
    printf("%s\n",people[5].someone.name);

    return 0;
}

在上面的代码中,我尝试创建一个指向动态结构数组的指针,不确定我是否在这部分成功,但我主要关心的是我使用 malloc 在堆上为数组“people”创建空间。稍后在代码中,我创建了另一个结构 Person 并使用 realloc 在堆上为“人”创建更多空间。然后,我通过执行 'people[5].someone=Rob;.' 在我认为给空间的内容之外写入内存。这仍然有效,因为我可以访问该内存位置的值。我的问题是为什么这行得通?我是否有可能通过写入我没有专门为人们定义的内存来覆盖内存?我实际上是否正确使用了 malloc 和 realloc ?正如我确实听说有一些方法可以测试它们是否在另一篇文章中成功。我是 C 的新手,所以如果我的假设或术语不正确,请纠正我。

【问题讨论】:

  • 请告诉我这是作业。如今,任何人都不应该用 C 编写这样的最终用户应用程序代码。
  • 这是我自己练习/自学,对我如此赤裸裸。啊,是的,忘记了解放人,但这并不能回答我的问题。
  • 您绝对是在写入不属于您的内存。有时,幸运的是,该内存存在并且没有被用于其他任何事情,而事情恰好起作用。不过,不要指望这一点。
  • “为什么会这样”你正在调用未定义的行为。更糟糕的是,您将观察到的行为等同于定义的行为。您的people[5].someone=Rob; 位于people 指向的已分配内存区域之外。因此,您的程序格式不正确,并且它表现出的任何行为都不能等同于任何确定的行为。你(不)幸运的是它“有效”。
  • 谢谢,我只是在确认。对于更复杂的代码,有没有办法让我知道我只是在访问我拥有的内存?还是保护我不犯这样的愚蠢错误?

标签: c memory heap-memory


【解决方案1】:

首先,永远不要忘记释放你的记忆。

// NEVER FORGET TO FREE YOUR MEMORY
free(people);

至于这部分

//accessing memory on the heap I do not owe?
people[5].someone=Rob;
//prints "Robert" why is this okay? Am I potentially overwriting memory?
printf("%s\n",people[5].someone.name);

你只是幸运(或者在我看来是不幸的,因为你没有看到你正在做的逻辑错误)。

这是未定义的行为,因为您有两个单元格,但您访问了第 6 个单元格,您超出了范围。

【讨论】:

    【解决方案2】:

    我不是 C 方面的专家,甚至不是中级,大部分时间我都是用 C# 编程的,所以可能会有一些错误。

    现代操作系统有一种称为内存管理器的特殊机制。使用这种机制,我们可以要求操作系统给我们一些内存。在 Windows 中有一个特殊的功能——VirtualAlloc。这是一个非常强大的功能,您可以在 MSDN 上阅读更多信息。

    它工作得非常好,并为我们提供了所需的所有内存,但有一个小问题 - 它为我们提供了整个物理页面 (4KB)。好吧,其实这不是什么大问题,你可以像使用 malloc 分配一样使用这块内存。不会出错。

    但这是一个问题,因为例如,如果我们使用 VirtualAlloc 分配一个 10 字节的块,它实际上会给我们 4096 字节的块,因为内存大小向上舍入到页面大小边界。所以 VirtualAlloc 分配了一个 4KB 的内存块,但我们实际上只使用了其中的 10 个字节。其余的 4086 已“消失”。如果我们创建第二个 10 字节数组,VirtualAlloc 会给我们另一个 4096 字节块,所以两个 10 字节数组实际上会占用 8KB 的 RAM。

    为了解决这个问题,每个 C 程序都使用 malloc 函数,它是 C 运行时库的一部分。它使用 VirtualAlloc 分配一些空间并返回指向它的部分的指针。例如,让我们回到我们之前的数组。如果我们使用 malloc 分配 10 字节数组,运行时库将调用 VirtualAlloc 分配一些空间,malloc 将返回指向它开头的指针。但是如果我们第二次分配 10 字节数组,malloc 就不会使用 VirtualAlloc。相反,它将使用已经分配的页面,我的意思是它的可用空间。分配第一个数组后,我们的内存块中有 4086 字节的未使用空间。所以 malloc 会明智地使用这个空间。在这种情况下(对于第二个数组),它将返回指向 "address of chunk" + 10 的指针(这是一个内存地址)。

    现在我们可以分配大约 400 个“十字节数组”,如果我们使用 malloc,它们将只占用 4096 个字节。使用 VirtualAlloc 的幼稚方式会占用 400 * 4096 bytes = 1600KB,与使用 malloc 的 4096 字节相比,这是一个相当大的数字。

    还有另一个原因 - 性能,因为 VirtualAlloc 是一项非常昂贵的操作。但是,如果分配的块中有空闲空间,malloc 会做一些指针数学运算,但如果没有任何空闲分配的空间,它会调用 VirtualAlloc。实际上它比我说的要复杂得多,但我认为这足以解释原因。

    好的,让我们回到问题上来。您为Array 数组分配内存。让我们计算一下它的大小:sizeof(Person) = sizeof(long) + sizeof(char[20]) = 4 + 20 = 24 bytes; sizeof(Array) = sizeof(Person) + 2 * sizeof(long) = 24 + 8 = 32 bytes。 2 个元素的数组将占用 32 * 2 = 64 字节。所以,正如我之前所说,malloc 会调用 VirtualAlloc 来分配一些内存,它会返回一个 4096 字节的页面。因此,例如让我们假设块的开始地址为 0。应用程序可以在分配页面时修改从 0 到 4096 的任何字节,并且不会出现任何页面错误。什么是数组索引array[n]?它只是数组的基数和计算为array + n * sizeof(*array) 的偏移量的总和。如果是person[5],它将是0 + sizeof(Array) * 5 = 0 + 5 * 64 = 320 bytes。明白了!我们仍然在块的边界,我的意思是我们访问现有的物理页面。如果我们试图访问一个不存在的虚拟页面,就会发生页面错误,但在我们的例子中,它存在于地址 320(我们假设从 0 到 4096)。访问未分配的空间很危险,因为它会导致许多未知的后果,但我们确实可以做到!

    这就是为什么你没有收到任何Access Violation at ****。但实际上要糟糕得多。因为例如,如果您尝试访问零指针,您将收到页面错误并且您的应用程序将崩溃,因此您将在调试器或其他工具的帮助下知道问题的原因。但是如果你超出了缓冲区并且你没有得到任何错误,你会在寻找问题的原因时发疯。因为很难找到这种错误。你甚至可以不知道它。所以永远不要超出堆中分配的缓冲区。实际上微软的 C 运行时有一个特殊的“调试”版本的 malloc 可以在运行时发现这些错误,但是您需要使用“调试”配置编译应用程序。另外,还有一些特殊的东西,比如 Valgrind,不过我对这些东西有点经验。

    嗯,我写了很多,对不起我的英语,我还在学习它。希望对你有帮助。

    【讨论】:

    • 这正是我试图在记忆方面理解的。非常感谢。
    • @Rob 很高兴知道它确实对您有所帮助 :)
    • 请注意,即使您的程序可以访问内存(没有 SIGSEGV),写超出您的 malloc()d/realloc()d 的内容是不安全的 - 如果程序执行任何其他操作malloc()s,那些超出范围的地址可能正在用于其他用途。最坏的情况是,它们用于 C 库中的元数据,覆盖可能会产生意想不到的结果。
    猜你喜欢
    • 1970-01-01
    • 2016-07-07
    • 2012-07-26
    • 1970-01-01
    • 2019-12-10
    • 2021-04-10
    • 1970-01-01
    • 2021-05-30
    • 2015-06-18
    相关资源
    最近更新 更多