39

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

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 ie 分配的所有字节sizeof(ts_request_def) + (2 * 1024 * 1024),还是只释放结构大小的字节sizeof(ts_request_def)

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

4

14 回答 14

53

回答你的编号点。

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

最新的 C 标准 ISO/IEC 9899:1999(非正式地 C99)允许灵活的数组成员

这方面的一个例子是:

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允许零长度数组作为扩展。零长度数组在标准 C 中是非法的,但是 gcc 在 C99 给我们灵活的数组成员之前就引入了这个特性。

在回复评论时,我将解释为什么下面的代码片段在技术上是未定义的行为。我引用的章节编号参考 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(列出了未定义行为的示例)通过示例提供了进一步的说明:

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

于 2009-03-09T08:40:27.480 回答
10

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

于 2009-03-09T07:41:12.380 回答
8

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

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

于 2009-03-09T08:23:16.820 回答
7

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

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

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

于 2009-03-09T07:23:22.050 回答
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 诡计规模的可敬和实用的目的。

于 2009-03-10T23:19:47.547 回答
3

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

于 2009-03-11T00:47:23.607 回答
3

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

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

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

于 2009-03-09T07:28:08.903 回答
3

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

于 2009-03-09T07:37:39.440 回答
3

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

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

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

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

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

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

于 2009-03-09T08:29:48.847 回答
2

回答你的第三个问题。

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
于 2009-03-13T07:31:54.167 回答
2

这个常见的 C 技巧也在这个 StackOverflow 问题中进行了解释(有人可以解释这个在 solaris 中定义的 dirent 结构吗?)

于 2009-03-13T07:37:43.107 回答
2

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

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

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

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

于 2009-03-09T08:38:42.773 回答
1

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

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

于 2009-03-09T07:22:48.307 回答
0

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

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

它与

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

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

于 2017-09-05T21:46:25.043 回答