【问题标题】:Why does calling malloc() not make a difference?为什么调用 malloc() 没有区别?
【发布时间】:2015-12-17 09:48:03
【问题描述】:

这是一个基本的例子:

#include <all the basic stuff>

int main(void) {
    char *name = (char *) malloc(2 * sizeof(char));
    if(name == NULL) {
        fprintf(stderr, "Error: Unable to allocate enough memory!\n");
        return EXIT_FAILURE;
    }
    strcpy(name, "Bob Smith");
    printf("Name: %s\n", name);
    free(name);
    return EXIT_SUCCESS;
}

因为我只分配了 2 个字节的信息(2 个字符),所以当我执行 strcpy 时应该会出现某种错误,对吧?这不会发生,它只是将字符串复制过来,打印出来,释放内存并成功退出。为什么会发生这种情况,如何正确使用 malloc?

【问题讨论】:

  • 否;你得到未定义的行为,未定义行为的一个有效选项是“它似乎按预期工作”(这意味着“直到它没有”,这总是发生在最不方便的时间,例如当老板和大客户正在观看演示,或在产品交付生产后立即观看)。由于malloc() 可能已经分配了一个16 字节的空间(您只能合法访问其中的2 个字节),所以您可能很干净。或者free 可能已经破坏了你的内存分配系统,但是由于你不再做任何事情,所以没有人注意到。
  • 这是未定义的行为。 C在内存访问等方面不牵你的手。你想覆盖一堆内存,没问题!您可以使用 strncpy() 来限制复制的#bytes。
  • "当我执行 strcpy 时应该有某种错误,对吧?"不。因为,正如 strcpy 手册页的第一段所述:“目标字符串 dest 必须足够大以接收副本。小心缓冲区溢出!(请参阅错误。)”换句话说:这是你的责任。你最好使用 strncpy。
  • 两个人建议使用strncpy();我反对这个建议,因为strncpy() 对大多数人来说有两种不良行为。首先,也是迄今为止最重要的一点,尤其是在这种情况下,当它不复制整个字符串时,它不会以空值终止字符串。第二个是如果你有一个很大的目标空间(比如 2 KiB)并且你将一个小字符串复制到其中(strncpy(space, 2048, "hi")),那么strncpy() 将写入 2046 个空字节,而不仅仅是终止字符串所需的那个.如果您知道这些问题并且可以承受后果,请仅使用strncpy()
  • 再说一遍,不要投射malloc() 返回值!

标签: c memory-management malloc allocation


【解决方案1】:

您的程序调用undefined behavior

未定义行为是超出语言规范的行为。根据定义,这意味着您不能保证获得任何类型的明确定义的行为(例如错误)。该程序明显无效。

当您使用strcpy 时,该函数只是假设您传递给它的缓冲区足够大以容纳您要复制的字符串。如果假设错误,它会尝试写入缓冲区之外的区域。如果发生这种情况,程序就属于 C 规范的这种情况,在 J.2 未定义行为中:

在以下情况下行为未定义:

  • 对数组对象和整数类型的指针进行加法或减法运算会产生不指向或仅超出同一数组对象的结果

因此,要正确使用strcpy,您必须手动确保上述关于字符串长度和缓冲区长度的假设成立。为此,一种简单的方法是将缓冲区的长度保存在某处,计算要复制的字符串的长度,然后比较它们。

例如:

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

int main(void) {
    size_t bufferSize = 2 * sizeof(char);       
    char *name = malloc(bufferSize);
    if(name == NULL) {
        fprintf(stderr, "Error: Unable to allocate enough memory!\n");
        return EXIT_FAILURE;
    }
    size_t length = strlen("Bob Smith");
    if(length + 1 > bufferSize) {
        fprintf(stderr, "Error: The target buffer is too small!\n");
        return EXIT_FAILURE;
    }
    strcpy(name, "Bob Smith");
    printf("Name: %s\n", name);
    free(name);
    return EXIT_SUCCESS;
}

作为一个不相关的附注,您会注意到 I didn't cast the result of malloc,因为 void* 可以隐式转换为 char*


最后一点:

当您尝试确保代码的正确性(因为您正在学习该语言或因为您打算发布该软件)时,这方面的 C 语言可能听起来不切实际。

这就是为什么有一些工具会在您的程序执行无效操作时给您一个错误。 Valgrind 就是这样一种工具(正如 Jonathan Leffler 在 cmets 中提到的那样)。

【讨论】:

  • 我喜欢这个答案,但是如果 *strData 已经拥有我需要的所有信息,为什么我首先需要 *name?
  • @DonkeyCore 一个原因是为了演示mallocstrcpy 的用法。如果我只是做了char *name = "Bob Smith" 我会错过你问题的重点,不是吗?其次,在我的代码中,strData 指向一个只读字符串,所以它与name 不太一样。例如,尝试写入只读字符串也是未定义的行为。无论如何,为了避免混淆,我已将其改回原来的样子。
  • 没关系,我只是想知道当我们可以做char name[] = getInput()时,拥有这些动态记忆功能的意义。
  • @DonkeyCore 我明白了。这仅仅是因为您只能在不使用动态内存(和可写缓冲区)的情况下解决有限范围的问题。
  • 好的,感谢您的时间、帮助、参考和很好的例子 :)
【解决方案2】:

如果您使用AddressSanitizer 编译和运行它,则会收到错误报告:

$ gcc -g a.c -Wall -Wextra -fsanitize=address
$ ./a.out
=================================================================
==3362==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60200000eff2 at pc 0x7f9ff2b02dc4 bp 0x7fffe9190650 sp 0x7fffe918fdf8
WRITE of size 10 at 0x60200000eff2 thread T0
    #0 0x7f9ff2b02dc3 in __asan_memcpy (/lib64/libasan.so.2+0x8cdc3)
    #1 0x4009df in main /home/m/a.c:11
    #2 0x7f9ff26d678f in __libc_start_main (/lib64/libc.so.6+0x2078f)
    #3 0x400898 in _start (/home/m/a.out+0x400898)

0x60200000eff2 is located 0 bytes to the right of 2-byte region [0x60200000eff0,0x60200000eff2)
allocated by thread T0 here:
    #0 0x7f9ff2b0ea0a in malloc (/lib64/libasan.so.2+0x98a0a)
    #1 0x400977 in main /home/m/aa.c:6
    #2 0x7f9ff26d678f in __libc_start_main (/lib64/libc.so.6+0x2078f)

SUMMARY: AddressSanitizer: heap-buffer-overflow ??:0 __asan_memcpy

【讨论】:

    【解决方案3】:
    1. malloc 将在分配内存失败时返回 null,例如您的系统内存不足。这不太可能是 2 个字节!
    2. 如果复制的字节数多于分配的字节数,则会出现未定义的行为。而这种未定义的行为可能是您的程序按预期运行!
    3. 作为关于“正确”使用您所询问的malloc 的更一般说明,我推荐char *name = malloc(2 * sizeof(*name));。更简洁,如果忘记包含stdlib.h也不会隐藏错误,并且将来更改name的类型会更容易。
    4. 关于strcpy 的安全使用,您不应将其替换为strncpy,因为如果缓冲区不够大(非空终止)并且可能效率低下,它本身就是不安全的。检查您的系统是否有strcpy_sstrlcpy

    【讨论】:

      【解决方案4】:

      有足够的答案,我会尽量让他们更基础的水平,并给你以下作为根本原因:

      C 不包括任何边界检查。

      好处是,C 运行时非常小且高效。回溯是,您通常不会收到任何错误消息,就像您在问题中所做的那样......只是(可能在错误本身很久之后)错误行为甚至崩溃。

      【讨论】:

        【解决方案5】:

        为什么你可以向malloc() 的缓冲区写入比缓冲区大小更多的数据?除了您无法预测未定义行为的结果这一事实之外,实际上还有一个解释,为什么有时向malloc()'d 缓冲区写入比您询问的字节数更多的字节似乎是完全安全的为。

        这是因为C standard7.20.3 内存管理功能部分中设置的要求的含义:

        连续调用分配的存储顺序和连续性 callocmallocrealloc 函数未指定。 如果分配成功,则返回的指针适当对齐,因此 可以将它分配给指向任何类型对象的指针,然后 用于访问空间中的此类对象或此类对象的数组 已分配(直到空间被显式释放)。

        注意斜体文本:“分配成功时返回的指针经过适当对齐,以便可以将其分配给指向任何类型对象的指针”。

        这些对齐限制意味着malloc() 和相关函数实际上必须在对齐的块中分配内存,并且任何对malloc() 的成功调用很可能实际上返回的内存正好是对齐限制@987654329 的倍数@ 正在运行。

        在 IIRC 具有 8 字节对齐限制的 x86 机器上,malloc( 11 ) 之类的调用可能会返回指向实际上是 16 字节的缓冲区的指针。

        这就是为什么覆盖 malloc()'d 缓冲区的末尾有时似乎无害的原因之一。

        【讨论】:

          【解决方案6】:

          strcpy(name, "Bob Smith"); 将调用 未定义的行为name 不足以存储 "Bob Smith"。 解决方案是 -

           char a[]="Bob Smith";
           char *name = malloc(strlen(a)+1);   //you should not cast return of malloc
           if(name == NULL) { 
              fprintf(stderr, "Error: Unable to allocate enough memory!\n");
              return EXIT_FAILURE;
           }
          strncpy(name,a,strlen(a));
          

          【讨论】:

          • 在你给我看的例子中,如果a拥有所有需要的信息,为什么还需要*name?
          • @DonkeyCore 您使用的是硬编码方式。那为什么连你都需要strcpy直接使用fgets
          • @DonkeyCore 你是如何得到字符串"Bob Smith" 的?
          • 我以 Bob Smith 为例。它可能来自任何地方。我只是想知道为什么我需要创建一个 *name 变量,因为您示例中的输入 a[] 已经有数据。
          • @DonkeyCore 我不知道你所说的任何地方是什么意思。但如果是来自 input 。然后直接用fgets读入name
          【解决方案7】:

          尝试:

          strncpy(name, "Bob Smith", 2 * sizeof(char));
          

          【讨论】:

          • 这行不通。 sizeof(name) 是指针大小,而不是分配给它的内存。但它仍然得到了支持!
          • @WeatherVane 迷人!!
          猜你喜欢
          • 2015-11-21
          • 2013-06-10
          • 2014-08-13
          • 2016-07-19
          • 2015-12-17
          • 2013-10-25
          • 1970-01-01
          • 2013-09-01
          • 1970-01-01
          相关资源
          最近更新 更多