【问题标题】:Does malloc() allocate a contiguous block of memory?malloc() 是否分配连续的内存块?
【发布时间】:2010-10-12 03:14:45
【问题描述】:

我有一段由一位非常老派的程序员编写的代码:-)。它是这样的

typedef struct ts_request
{ 
  ts_request_buffer_header_def header; 
  char                         package[1]; 
} ts_request_def; 

ts_request_def* request_buffer = 
malloc(sizeof(ts_request_def) + (2 * 1024 * 1024));

程序员基本上是在研究缓冲区溢出的概念。我知道代码看起来很狡猾。所以我的问题是:

  1. malloc 是否总是分配连续的内存块?因为在这段代码中,如果块不连续,代码将大量失败

  2. free(request_buffer),它会释放malloc分配的所有字节,即sizeof(ts_request_def) + (2 * 1024 * 1024), 或者只有结构大小的字节sizeof(ts_request_def)

  3. 您是否发现这种方法有任何明显的问题,我需要与我的老板讨论这个问题,并希望指出这种方法的任何漏洞

【问题讨论】:

标签: c coding-style malloc


【解决方案1】:

回答你的编号。

  1. 是的。
  2. 所有字节。 Malloc/free 不知道也不关心对象的类型,只关心大小。
  3. 严格来说,这是未定义的行为,但是许多实现都支持的常见技巧。有关其他替代方案,请参见下文。

最新的 C 标准 ISO/IEC 9899:1999(非正式 C99)允许flexible array members

这方面的一个例子是:

int main(void)
{       
    struct { size_t x; char a[]; } *p;
    p = malloc(sizeof *p + 100);
    if (p)
    {
        /* You can now access up to p->a[99] safely */
    }
}

这个现在标准化的功能使您可以避免使用您在问题中描述的常见但非标准的实现扩展。严格来说,使用非灵活的数组成员并超出其边界访问是未定义的行为,但许多实现记录并鼓励这样做。

此外,gcc 允许 zero-length arrays 作为扩展名。零长度数组在标准 C 中是非法的,但是 gcc 在 C99 给我们灵活的数组成员之前就引入了这个特性。

在回复评论时,我将解释为什么下面的 sn-p 在技术上是未定义的行为。我引用的章节编号参考 C99 (ISO/IEC 9899:1999)

struct {
    char arr[1];
} *x;
x = malloc(sizeof *x + 1024);
x->arr[23] = 42;

首先,6.5.2.1#2 表明 a[i] 等同于 (*((a)+(i))),因此 x->arr[23] 等同于 (*((x->arr )+(23)))。现在,6.5.6#8(关于添加指针和整数)说:

"如果指针操作数和结果都指向同一个数组对象的元素,或者超过数组对象的最后一个元素,则计算不应产生溢出;否则,行为未定义。”

由于这个原因,因为 x->arr[23] 不在数组中,所以行为是未定义的。您可能仍然认为这没关系,因为 malloc() 意味着数组现在已扩展,但严格来说并非如此。信息性附录 J.2(列出了未定义行为的示例)通过示例提供了进一步的说明:

一个数组下标超出范围,即使一个对象显然可以用 给定下标(如左值表达式 a[1][7] 给定声明 int a[4][5]) (6.5.6)。

【讨论】:

  • +1,用于灵活的零长度数组。您还可以补充一点,这种做法的好处是您可以为一个指针节省内存并将其减少到只有一个(昂贵的)分配。
  • 我不同意未定义的行为。 malloc() 保证返回连续的内存块,因此您可以使用数组索引的指针算法安全地访问结构之外的内存 - 根据标准它们是相同的。所以它是定义的行为。
  • @qrdl:标准明确禁止超出数组的访问。我已经编辑了我的帖子来解释为什么它是未定义的。
  • @Robert S. Barnes:您没有错,但物理布局与 C 标准完全无关。重要的是,当以明确定义的方式访问时,它与程序是连续的。指出内存可能不是连续的,因为它可能跨越多个硅片,这同样正确且无关紧要。
  • 对于char 类型,这不是UB。
【解决方案2】:

3 - 这是一个非常常见的 C 技巧,用于在结构的末尾分配一个动态数组。另一种方法是将指针放入结构中,然后单独分配数组,并且不要忘记释放它。不过,大小固定为 2mb 似乎有点不寻常。

【讨论】:

  • 非常感谢您的 cmets 。基本上我们从 socket 接收数据。我们不知道我们将要接收的确切大小并将其限制为 2 MB。我们收到的数据被复制到这个结构中。之所以进行此更改,是因为这是影响最小的更改。
  • @unknown (google),如果大小是固定的,您也可以将数组大小从 1 更改为您的固定大小。这个技巧只对可变长度的数组有意义。
【解决方案3】:

这是一个标准的 C 技巧,并不比任何其他缓冲区更危险。

如果您想向老板表明您比“非常老派的程序员”更聪明,那么这段代码不适合您。老学校不一定坏。似乎“老派”家伙对内存管理足够了解;)

【讨论】:

    【解决方案4】:

    1) 是的,如果没有足够大的连续块可用,malloc 将失败。 (malloc 失败会返回一个 NULL 指针)

    2) 会的。内部内存分配将跟踪使用该指针值分配的内存量并释放所有内存。

    3) 这有点像一种语言破解,而且对它的使用有点怀疑。它仍然会受到缓冲区溢出的影响,只是攻击者可能需要稍长的时间才能找到导致它的有效负载。 “保护”的成本也相当高(你真的需要每个请求缓冲区> 2mb吗?)。这也很丑陋,尽管您的老板可能不喜欢这种说法:)

    【讨论】:

      【解决方案5】:

      我认为现有的答案并没有完全抓住这个问题的本质。你说老派程序员正在做这样的事情;

      typedef struct ts_request
      { 
        ts_request_buffer_header_def header; 
        char                         package[1]; 
      } ts_request_def;
      
      ts_request_buffer_def* request_buffer = 
      malloc(sizeof(ts_request_def) + (2 * 1024 * 1024));
      

      我认为他不太可能完全这样做,因为如果这是他想做的,他可以使用不需要任何技巧的简化等效代码来完成;

      typedef struct ts_request
      { 
        ts_request_buffer_header_def header; 
        char                         package[2*1024*1024 + 1]; 
      } ts_request_def;
      
      ts_request_buffer_def* request_buffer = 
      malloc(sizeof(ts_request_def));
      

      我敢打赌,他真正在做的事情是这样的;

      typedef struct ts_request
      { 
        ts_request_buffer_header_def header; 
        char                         package[1]; // effectively package[x]
      } ts_request_def;
      
      ts_request_buffer_def* request_buffer = 
      malloc( sizeof(ts_request_def) + x );
      

      他想要实现的是分配具有可变包大小x的请求。用变量声明数组的大小当然是非法的,所以他用一个技巧来解决这个问题。看起来他好像知道他在对我做什么,这个把戏非常适合 C 欺骗音阶的可敬和实用的结尾。

      【讨论】:

        【解决方案6】:

        至于#3,没有更多代码很难回答。我看不出有什么问题,除非它发生了很多。我的意思是,你不想一直分配 2mb 的内存块。您也不想不必要地这样做,例如如果你只用过 2k。

        您出于某种原因不喜欢它的事实不足以反对它,或者证明完全重写它是正当的。我会仔细查看用法,尝试了解原始程序员的想法,仔细查看使用此内存的代码中的缓冲区溢出(正如 workmad3 指出的那样)。

        您可能会发现很多常见错误。例如,代码是否检查以确保 malloc() 成功?

        【讨论】:

          【解决方案7】:

          漏洞利用(问题 3)实际上取决于您的这种结构的接口。在上下文中,这种分配可能是有意义的,如果没有进一步的信息,就不可能说它是否安全。
          但是,如果您的意思是分配比结构更大的内存的问题,这绝不是一个糟糕的 C 设计(我什至不会说它是那么老派……;))
          这里只是最后一点 - 有一个 char[1] 的要点是终止 NULL 将始终在声明的结构中,这意味着缓冲区中可以有 2 * 1024 * 1024 个字符,您不必考虑用“+1”表示 NULL。可能看起来像一个小壮举,但我只是想指出。

          【讨论】:

          • 此外,标准不允许大小为 0 的数组,尽管一些编译器允许。
          • 不,他不能; char * 将完全寻址其他地方的内存,而不是与结构连续。对于 C99,正确的声明是一个灵活大小的数组“char package[]”。但几乎所有支持的编译器也支持大小为 0 的 GNU 扩展。
          【解决方案8】:

          我经常看到并使用这种模式。

          它的好处是简化内存管理,从而避免内存泄漏的风险。只需释放 malloc 的块。使用辅助缓冲区,您将需要两个免费缓冲区。但是,应该定义并使用析构函数来封装此操作,以便您始终可以更改其行为,例如切换到辅助缓冲区或在删除结构时添加要执行的其他操作。

          访问数组元素的效率也稍高一些,但对于现代计算机而言,这一点越来越不重要了。

          如果内存对齐在不同编译器的结构中发生变化,代码也将正确工作,因为这种情况非常频繁。

          我看到的唯一潜在问题是编译器是否改变了成员变量的存储顺序,因为这个技巧要求包字段在存储中保持在最后。我不知道C标准是否禁止排列。

          还要注意,分配的缓冲区的大小很可能比所需的大,至少增加一个字节,如果有的话,还有额外的填充字节。

          【讨论】:

          • C 标准要求成员按照您将它们放入结构中的顺序排列。但是,由于我在回答中解释的原因,这是未定义的行为。
          【解决方案9】:

          是的。 malloc 只返回一个指针——它怎么可能告诉请求者它已经分配了多个不连续的块来满足请求?

          【讨论】:

          • 对,这就是操作系统和虚拟内存通过MMU的工作。实际的物理 RAM 块可能遍布各处。
          • "void *malloc(size_t size); malloc() 函数分配 size 字节并返回指向其中之一的指针。"好的,我已经弥补了:)
          【解决方案10】:

          想补充一点,这并不常见,但我也可以将其称为标准做法,因为 Windows API 充满了这种用途。

          例如检查非常常见的 BITMAP 标头结构。

          http://msdn.microsoft.com/en-us/library/aa921550.aspx

          最后一个 RBG 四边形是一个大小为 1 的数组,这完全取决于这种技术。

          【讨论】:

            【解决方案11】:
            【解决方案12】:

            回答你的第三个问题。

            free 总是一次性释放所有分配的内存。

            int* i = (int*) malloc(1024*2);
            
            free(i+1024); // gives error because the pointer 'i' is offset
            
            free(i); // releases all the 2KB memory
            

            【讨论】:

              【解决方案13】:

              问题 1 和 2 的答案是肯定的

              关于丑陋(即问题3)程序员试图用分配的内存做什么?

              【讨论】:

                【解决方案14】:

                这里要意识到的是malloc 没有看到这里正在进行的计算

                malloc(sizeof(ts_request_def) + (2 * 1024 * 1024));
                

                和这个一样

                  int sz = sizeof(ts_request_def) + (2 * 1024 * 1024);
                   malloc(sz);
                

                你可能会认为它分配了 2 块内存,在你的脑海中它们是“结构”,“一些缓冲区”。但是 malloc 根本看不到这一点。

                【讨论】:

                  猜你喜欢
                  • 2011-01-15
                  • 1970-01-01
                  • 2019-06-10
                  • 1970-01-01
                  • 2023-04-08
                  • 1970-01-01
                  • 2014-02-15
                  • 1970-01-01
                  相关资源
                  最近更新 更多