【问题标题】:I can use more memory than how much I've allocated with malloc(), why?我可以使用比使用 malloc() 分配的内存更多的内存,为什么?
【发布时间】:2011-03-31 09:32:29
【问题描述】:
char *cp = (char *) malloc(1);
strcpy(cp, "123456789");
puts(cp);

在 gcc (Linux) 和 Visual C++ Express 上的输出都是“123456789”,这是否意味着当有空闲内存时,我实际上可以使用比 malloc() 分配的更多的内存?

为什么malloc(0) 不会导致运行时错误?

谢谢。

【问题讨论】:

  • 正如您在答案中看到的那样,这是一个坏主意 :) 为确保您没有那种代码,您可以使用 valgrind 或 yamd 之类的工具。我很久没有做 C 开发人员了,所以我不知道当前的工具 :)
  • 是的,当然这是个坏主意,我只是想知道为什么 c runtime 允许我这样做
  • 它允许你这样做,因为检查意味着你必须保持每个 malloc()ed 对象的分配大小,并检查该对象的每个可能的访问是否违反了它的界限,C 的设计者认为这样的运行时检查成本太高,无法在使用 C 程序的环境中始终执行。 (同时开发的 Pascal 确实会执行此类检查。)
  • 因为他们使用的任何算法都不会浪费时间检查以确保人们没有弄乱它
  • @woongiap:那你还没有从线程中学到东西。它不起作用,它正在进入未定义的行为。您应该学会将工作与输出分开。 “输出是正确的。”是对作品的更糟糕的定义。

标签: c


【解决方案1】:

您提出了一个非常好的问题,也许这会激起您对操作系统的兴趣。您已经知道您已经设法使用此代码实现了您通常不会期望做的事情。所以你永远不会在你想要移植的代码中这样做。

更具体地说,这完全取决于您的操作系统和 CPU 架构,操作系统会为您的程序分配“页面”内存 - 通常这可能是 4 KB 的数量级。操作系统是页面的守护者,它将立即终止任何试图访问尚未分配的页面的程序。

另一方面,malloc 不是操作系统函数,而是 C 库调用。它可以通过多种方式实现。您对malloc 的调用很可能导致来自操作系统的页面请求。然后malloc 会决定给你一个指向该页面内单个字节的指针。当您从给定位置写入内存时,您只是在操作系统授予您的程序的“页面”中写入,因此操作系统不会看到任何错误。

当您继续调用malloc 分配更多内存时,真正的问题当然会开始。它最终将返回指向您刚刚写入的位置的指针。当您写入合法的内存位置(从操作系统的角度来看)但可能覆盖程序的另一部分也将使用的内存时,这称为“缓冲区溢出”。

如果您继续学习这个主题,您将开始了解如何使用这种“缓冲区溢出”技术来利用程序 - 甚至到您开始将汇编语言指令直接写入内存区域的程度由程序的另一部分执行。

当你到达这个阶段时,你会获得很多智慧。但请保持道德,不要用它在宇宙中造成严重破坏!

PS 当我在上面说“操作系统”时,我的真正意思是“操作系统与特权 CPU 访问相结合”。如果进程尝试使用尚未分配给该进程的页面,CPU 和 MMU(内存管理单元)会触发特定的中断或回调进入操作系统。然后操作系统干净地关闭您的应用程序并允许系统继续运行。在过去,在内存管理单元和特权 CPU 指令出现之前,您几乎可以随时在内存中的任何位置写入 - 然后您的系统将完全受制于内存写入的后果!

【讨论】:

  • 优秀的答案!几年来我一直在做操作系统开发,并在去年解决了 MMU 问题。到现在为止,我从未想过将某些 MMU 行为视为对操作系统的回调——就像被触发的异常和需要完成的工作一样。它简化了事情;谢谢!
【解决方案2】:

没有。你得到未定义的行为。这意味着任何事情都可能发生,从它崩溃(耶)到它“工作”(嘘),再到它重新格式化你的硬盘驱动器并用文本文件填充它,上面写着“UB,UB,UB ...”(wat)。

没有必要想知道之后会发生什么,因为这取决于您的编译器、平台、环境、一天中的时间、最喜欢的苏打水等,所有这些都可以按照他们想要的 (in) 一致的方式做任何他们想做的事情.

更具体地说,使用您尚未分配的任何内存都是未定义的行为。你从malloc(1) 得到一个字节,就是这样。

【讨论】:

  • 语言规范未定义,但这并不意味着无法预测。正如您所说,这取决于操作系统、编译器和虚拟内存的状态。由于这最后一部分非常难以预测和说一些有用的东西,人们称之为未定义。但是看看PP给出的答案。最后,它肯定与您最喜欢的苏打水没有任何关联;)鉴于您有足够的信息,这种行为是非常确定的。顺便说一句,无论如何+1,因为主要思想是正确的并且是有趣的故事:)
  • @Henri:我认为 PP 的回答很好,因为它涉及一些通用平台,但它确实没有谈论语言。重要的是人们要学会“未定义的行为”意味着“不要这样做”,而不是“哦,但是它是如何工作的,它是如何定义的?”人们总是可以出于好奇而了解一个平台,而不是试图理解一些未定义的行为。
  • @Henri:对于 C 语言,“未定义”有一个非常具体的含义。这并不意味着没有人可以猜测会发生什么,但这确实意味着该标准没有以任何方式定义应该做什么,并且不需要编译器编写者这样做。 (后者是与“未指定行为”的主要区别。)在实践中,这通常意味着崩溃或有时更糟的情况可能发生在看似随机的情况下。包括在您的测试中一切正常并且系统在您的客户触摸它时向南运行的不那么罕见的情况。而且只有程序员和 QA 应该受到责备……
  • @Henri:这比你建议的要复杂。编译器可以利用某些代码可能产生未定义行为的知识以不可可预测的方式进行优化,无论您对底层操作系统/硬件的理解程度如何,而无需对编译器的特定情况进行分析内部;此外,相同编译器(或不同编译器)的更高版本可能会有不同的行为。例如,编译器可以假设必须调用 UB 的代码永远不会执行,并且是一致的(以及作为副产品生成高效代码)。
【解决方案3】:

当您向malloc 请求 1 个字节时,它可能会从操作系统获得 1 页(通常为 4KB)。这个页面会被分配给调用进程,只要不超出页面边界,就不会有任何问题。

但是请注意,这绝对是未定义的行为!

考虑以下(假设的)示例,说明使用 malloc可能会发生什么:

  1. malloc(1)
  2. 如果malloc 内部内存不足,它会向操作系统询问更多信息。它通常会收到一个页面。假设它的大小为 4KB,地址从 0x1000 开始
  3. 您的呼叫返回给您地址 0x1000 以供使用。由于您要求 1 个字节,如果您只使用地址 0x1000,这是定义的行为
  4. 由于操作系统刚刚从地址 0x1000 开始为您的进程分配 4KB 内存,因此如果您从地址 0x1000-0x1fff 读取/写入内容,它不会抱怨。所以你可以很高兴地这样做,但这是未定义的行为
  5. 假设你再做一次malloc(1)
  6. 现在malloc 仍然有一些内存,所以它不需要向操作系统询问更多。它可能会返回地址 0x1001。
  7. 如果您使用第一个malloc 中给出的地址写入了超过1 个字节,那么当您使用第二个malloc 中的地址时会遇到麻烦,因为您会覆盖数据。

所以重点是你肯定会从 malloc 获得 1 个字节可能malloc 内部有更多的内存分配给你的进程。

【讨论】:

  • 这根本不是真的,至少在 Windows 上是这样。你得到的正是你所要求的。堆将分配 1 个字节,不再分配。如果它被写入过去,它是未定义的,但绝对不会给他一个页面。
  • @linuxuser27:我不是 Windows 专家,但我严重怀疑它只有 1 个字节。这将是一种浪费,只是获得一块内存并使用它会更干净,直到您需要另一块内存为止。
  • 它确实得到 1 个字节。当应用程序在 Windows 中启动时,操作系统会为堆预先分配一块内存。对 malloc() 等的调用最终会转到 HeapAlloc() ,然后将预分配堆划分给进程。当然堆可以根据需要增长,但是当你请求 1 个字节时,这就是你得到的。如果您想查看它,可以调用 HeapWalk() 并自己查看。我不想为此坚持下去,但我在 Visual Studio Profiler 上工作,并做了很多内存分析 :)
  • @linuxuser27: "a chunk of memory is pre-allocated" 我想这就是我和 Job 正在谈论的(总内存使用量),而您正在谈论返回的结果。跨度>
  • @linuxuser27:对 malloc 的调用最终会转到 HeapAlloc,对 HeapAlloc 的调用最终会导致对 VirtualAlloc 的调用,后者会分配整页。所以分配 1 个字节分配一整页。整个页面对于该过程是可读/可写的。如果内存已提交,则进程将使用全部 4 KB。
【解决方案4】:

没有。这意味着您的程序表现不佳。它写入不属于它的内存位置。

【讨论】:

  • 好的,那么,我使用的额外内存是否有可能分配给其他人?
  • @woon:任何事情都是可能的。你有 UB,试图猜测接下来会发生什么是死胡同。是的,它可以分发,也许不在一个通用平台上,也许在其他平台上,谁知道呢。
  • 它可能已经分配给其他一些 malloc 调用,在这种情况下,您程序的其他变量将更改值而不实际更改;或仍未使用,您的程序将偶然运行;或被 C 库堆内存管理器用于存储 malloc 内部数据,您的程序可能会在后续调用 malloc 或 free 时崩溃。
【解决方案5】:

你得到未定义的行为 - 任何事情都可能发生。不要这样做,也不要推测它是否有效。也许它会破坏内存,而您不会立即看到它。只访问分配的块大小内的内存。

【讨论】:

    【解决方案6】:

    您可能被允许使用,直到内存达到某个程序内存或您的应用程序很可能因访问受保护内存而崩溃的其他点

    【讨论】:

    • 在为客户做狗和小马表演时总是会发生这种情况。他们说未定义的行为错误是无意识的,哈!
    【解决方案7】:

    这么多回复,只有一个给出了正确的解释。虽然页面大小、缓冲区溢出和未定义的行为故事是真实的(而且很重要),但它们并不能完全回答最初的问题。事实上,任何理智的malloc 实现将至少分配intvoid * 的对齐要求的大小。为什么,因为如果它只分配 1 个字节,那么下一块内存将不再对齐。在您分配的块周围总是有一些簿记数据,这些数据结构几乎总是与 4 的某个倍数对齐。虽然某些架构可以访问未对齐地址 (x86) 上的字,但它们确实会因此受到一些惩罚,因此分配器实现者避免这种情况.即使在平板分配器中,也没有必要使用 1 字节的池,因为在实践中小尺寸的分配器很少见。所以很有可能你的 malloc 字节中有 4 或 8 个字节的实际空间(这并不意味着你可以使用那个“功能”,这是错误的)。

    编辑:此外,大多数malloc 保留比要求更大的块,以避免在调用realloc 时进行许多复制操作。作为测试,您可以尝试在分配大小不断增长的循环中使用realloc,并比较返回的指针,您会发现它仅在某个阈值后才会发生变化。

    【讨论】:

    • 您的答案很有趣,但与我认为您所指的答案不一致。另一位作者暗示 malloc 最终可能会将其中一些字节从第一次分配中返回给其他 malloc 调用。因此,有问题的代码现在可能看起来可以工作,但是如果多次错误地写入缓冲区,那么最终可能会导致问题。您似乎在暗示其他事情,即分配中可能存在一些“浪费”的字节。从某种意义上说,您也可能是对的,因为每个系统都以不同的方式处理这个问题。
    • 当时我提到了我使用 Solaris 分配器的经验,它的行为不像 glibc 的分配器。 Solaris 总是将分配大小四舍五入到至少高 16 字节的倍数。例如 malloc(1) 将返回大小为 16 的缓冲区。这意味着您可以在分配大小之后写入而不会出现问题。 Solaris 上的分配器非常宽松,在 free 之后使用很少会崩溃,缓冲区溢出什么都不做等。当我们将软件移植到 Linux 时,我们惊讶地触发了很多 Solaris 不关心的段错误。
    【解决方案8】:

    你在那里很幸运。您正在写入不属于您的位置,这会导致未定义的行为。

    【讨论】:

      【解决方案9】:

      在大多数平台上,您不能只分配一个字节。 malloc 通常还会做一些整理工作以记住分配的内存量。这产生了这样一个事实,即您通常“分配”内存,四舍五入到接下来的 4 或 8 个字节。但这不是一个明确的行为。

      如果你多使用几个字节,你很可能会遇到访问冲突。

      【讨论】:

        【解决方案10】:

        为了回答您的第二个问题,该标准明确规定 malloc(0) 是合法的。返回值取决于实现,可以是NULL 或常规内存地址。无论哪种情况,您都可以(并且应该)在完成后合法地在返回值上调用free。即使不是NULL,也不得访问该地址的数据。

        【讨论】:

          【解决方案11】:

          ma​​lloc 分配您在堆中请求的内存量,然后返回一个指向 void (void *) 的指针,该指针可以转换为您想要的任何内容。

          程序员有责任只使用已分配的内存。 在你不应该写的地方(甚至在受保护的环境中阅读)可能会在执行时导致各种随机问题。如果您幸运,您的程序会立即崩溃并出现异常,您可以很容易地找到错误并修复它。如果你不走运,它会随机崩溃或产生意想不到的行为。

          对于 墨菲定律“任何可能出错的事情都会出错” 并且作为推论,“它会在正确的时间,产生最大的伤害”。 可悲的是,这是真的。防止这种情况发生的唯一方法是避免使用您实际上可以执行类似操作的语言。

          现代语言确实允许程序员在他/她不应该在内存中写入(至少进行标准编程)。这就是Java获得很大吸引力的原因。我更喜欢 C++ 而不是 C。您仍然可以使用指针造成损害,但可能性较小。这就是 智能指针 如此受欢迎的原因。

          为了解决这类问题,malloc 库的调试版本 会很方便。您需要定期调用检查函数来检测内存是否已损坏。 当我以前在工作中集中研究 C/C++ 时,我们使用了 Rational Purify,它在实践中替换了标准的 malloc(C++ 中的新功能)和 free(C++ 中的删除),并且它能够返回相当准确报告程序在哪里做了它不应该做的事情。但是,您永远无法 100% 确定您的代码中没有任何错误。如果您有一个极少发生的情况,当您执行程序时,您可能不会遇到这种情况。它最终会在最繁忙的一天对最敏感的数据进行生产(根据墨菲定律 ;-)

          【讨论】:

            【解决方案12】:

            可能是您处于调试模式,其中对 malloc 的调用实际上会调用 _malloc_dbg。调试版本将分配比您处理缓冲区溢出请求更多的空间。我想如果你在发布模式下运行它,你可能(希望)会崩溃。

            【讨论】:

              【解决方案13】:

              您应该在 c++ 中使用 new 和 delete 运算符...并且控制该操作的安全指针未达到分配数组的限制...

              【讨论】:

              • 对不起,我误解了;)我以为你在用 C++ 编程。
              【解决方案14】:

              没有“C 运行时”。 C是美化的汇编程序。它会很高兴地让你在地址空间中走动,并用它做任何你想做的事情,这就是为什么它是编写 OS 内核的首选语言。您的程序是堆损坏错误的示例,这是一个常见的安全漏洞。如果你向那个地址写了一个足够长的字符串,你最终会超出堆的末端并得到一个分段错误,但在你首先覆盖了很多其他重要的事情之前。

              当 malloc() 在其保留池中没有足够的空闲内存来满足分配时,它会从内核中以至少 4 kb 的块获取页面,并且通常更大,因此您可能正在写入 reserved但是当您最初超出分配范围时会取消 malloc() 空间,这就是您的测试用例始终有效的原因。实际上,尊重分配地址和大小是完全自愿的,因此您可以为指针分配一个随机地址,根本不需要调用 malloc(),并开始将其作为字符串处理,只要该随机地址恰好在像堆或堆栈这样的可写内存段,一切似乎都可以工作,至少在你尝试使用你这样做破坏的任何内存之前。

              【讨论】:

              • @woongiap:没必要。它可以根据需要返回内存,但不能使用该内存。
              • 由于编译器不检查你的函数参数是否愚蠢,许多 malloc() 实现将为 malloc(0) 调用分配它们的最小分配单元,通常是 8 个字节。一些 malloc() 实现会将错误打印到标准输出或崩溃,如果您设置环境变量以打开检查此类愚蠢的事情,但默认情况下,它通常会尝试执行您要求的操作,无论它可能是愚蠢的。
              • 编译器不检查也没关系。 malloc(0) 可能很愚蠢,但 malloc(a-b) 不是。我认为如果给定大小不大于 0,malloc() 实现应该简单地返回 NULL,因为实际的分配算法已经足够复杂了。
              • 在现代线程库之前,malloc(0) 有时被滥用于多线程程序中生成唯一令牌,因为同步是自动处理的。 malloc(1) 也可以,但 malloc(0) 是首选,因为它很明显你在滥用 malloc。已经存在足够长的平台可以为他们编写这样的代码,他们不愿意改变这种行为并破坏旧代码。
              【解决方案15】:

              strcpy() 不检查它正在写入的内存是否已分配。它只是获取目标地址并逐个字符写入源字符,直到它到达'\0'。因此,如果分配的目标内存小于源内存,则您只是覆盖了内存。这是一个危险的错误,因为它很难追踪。

              puts() 写入字符串直到它到达 '\0'。

              我的猜测是 malloc(0) 只返回 NULL 而不会导致运行时错误。

              【讨论】:

                【解决方案16】:

                我的回答是回复Why does printf not seg fault or produce garbage?

                来自

                Denis Ritchie & Kernighan 的 C 编程语言

                 typedef long Align;    /* for alignment to long boundary */
                   union header {         /* block header */
                       struct {
                           union header *ptr; /* next block if on free list */
                           unsigned size;     /* size of this block */
                       } s;
                       Align x;           /* force alignment of blocks */
                   };
                   typedef union header Header;
                

                Align 字段从未使用过;它只是强制每个标头在最坏情况的边界上对齐。 在malloc 中,请求的字符大小被四舍五入到适当的header-sized 单位数;将被分配的块包含 多一个单位,为header 本身,这是记录在 size 标头字段。 ma​​lloc 返回的指针指向空闲空间,而不是标题本身

                用户可以对请求的空间进行任何操作,但如果在分配的空间之外写入任何内容,则列表可能会被打乱。

                   -----------------------------------------
                   |        |     SIZE     |               |
                   -----------------------------------------
                     |        |
                  points to   |-----address returned touser
                   next free
                   block
                        -> a block returned by malloc 
                

                在声明中

                char* test = malloc(1);
                

                如果请求的字节可用,malloc() 将尝试从 RAM 的堆部分搜索连续字节,并返回 address,如下所示

                 --------------------------------------------------------------
                | free memory  | memory in size allocated for user |           |
                ----------------------------------------------------------------
                                                                              0x100(assume address returned by malloc)
                                                                              test
                

                所以当malloc(1) 执行时,它不会只分配1 字节,它分配了一些extra 字节来维护上面的结构/堆表。您可以通过打印 test[-1] 来找出仅请求 1 字节时分配的实际内存量,因为就在该块之前包含大小。

                char* test = malloc(1);
                printf("memory allocated in bytes = %d\n",test[-1]);
                

                【讨论】:

                  【解决方案17】:

                  如果传递的大小为零,并且 ptr 不为 NULL,则调用等效于 free。

                  【讨论】:

                    猜你喜欢
                    • 2020-10-30
                    • 2013-11-14
                    • 2021-06-06
                    • 1970-01-01
                    • 2016-05-08
                    • 2017-01-28
                    • 1970-01-01
                    相关资源
                    最近更新 更多