矢量化的strlen逃脱阅读未分配的记忆

时间:2014-08-29 10:20:48

标签: c macos memory-management assembly libc

在研究OSX 10.9.4的strlen实现时,我注意到它总是比较一个16字节的块并向前跳过后面的16个字节,直到它遇到'\0'。相关部分:

3de0:   48 83 c7 10             add    $0x10,%rdi
3de4:   66 0f ef c0             pxor   %xmm0,%xmm0
3de8:   66 0f 74 07             pcmpeqb (%rdi),%xmm0
3dec:   66 0f d7 f0             pmovmskb %xmm0,%esi
3df0:   85 f6                   test   %esi,%esi
3df2:   74 ec                   je     3de0 <__platform_strlen+0x40>

0x10是十六进制的16个字节。

当我看到它时,我想知道:这个记忆也可以不被分配。如果我已经分配了一个20字节的C字符串并将其传递给strlen,它将读取36字节的内存。为什么允许这样做?我开始寻找How dangerous is it to access an array out of bounds?

这证实了它绝对不是一件好事,例如,未分配的内存可能未被映射。然而,必须有一些东西使这项工作。我的一些假设:

  • OSX不仅保证其分配是16字节对齐的,而且还保证分配的“量子”是16字节的块。换句话说,分配5个字节实际上将分配16个字节。分配20个字节实际上将分配32个字节。
  • 当你编写asm时读取数组的结尾本身并没有坏处,因为它不是未定义的行为,只要它在界限范围内(在页面内?)。

实际原因是什么?

编辑:刚刚找到Why I'm getting read and write permission on unallocated memory?,这似乎表明我的第一个猜测是正确的。

编辑2 :愚蠢的是,我已经忘记了尽管Apple似乎已经删除了大部分asm实现(Where did OSX's x86-64 assembly libc routines go?)的来源,但它还是留下了strlen:{{3 }}

在评论中我们发现:

//  returns the length of the string s (i.e. the distance in bytes from
//  s to the first NUL byte following s).  We look for NUL bytes using
//  pcmpeqb on 16-byte aligned blocks.  Although this may read past the
//  end of the string, because all access is aligned, it will never
//  read past the end of the string across a page boundary, or even
//  accross a cacheline.

编辑:老实说,我认为所有回答者都应得到一个可接受的答案,而且基本上都包含了解问题所需的信息。所以我找到了声誉最差的人的答案。

5 个答案:

答案 0 :(得分:7)

我是相关例程的作者。

正如其他人所说,关键是读取都是一致的。虽然在数组的边界外读取是C中未定义的行为,但我们不是在写C;我们知道x86架构的许多细节,超出了C抽象机器定义的内容。

特别是,超出缓冲区末尾的读取是安全的(意味着它们不能产生陷阱或其他可观察的副作用),只要它们不跨越页面边界(因为以页面粒度跟踪内存属性和映射) 。由于支持的最小页面大小为4096字节,因此对齐的16字节加载不能跨越页边界。

答案 1 :(得分:4)

如果正在读取的地址对应于未映射的页面,则在大多数体系结构上读取内存只会产生副作用。现代计算机的大多数strlen实现只尝试对齐读取多个字节。他们永远不会跨越两页进行16字节的阅读,因此他们永远不会引起任何副作用。很酷。

答案 2 :(得分:2)

  

Allocating Small Memory Blocks Using Malloc

     

...

     

在分配任何小块内存时,请记住malloc库分配的块的粒度为16个字节。因此,您可以分配的最小内存块是16个字节,任何大于此的块都是16的倍数。例如,如果调用malloc并请求4个字节,则返回一个大小为16个字节的块;如果请求24个字节,则返回一个大小为32字节的块。由于这种粒度,您应该仔细设计数据结构,并尽可能使它们成为16字节的倍数。

对于记录,如果跨越页面边界,引用(读取)过去的分配可能会触发页面错误。这就是guardmalloc的工作原理:

  

每个malloc分配都放在自己的虚拟内存页面(或多个页面)上。默认情况下,分配的返回地址的位置使得分配的缓冲区的末尾位于最后一页的末尾,之后的下一页保持未分配。因此,超出缓冲区末尾的访问会立即导致错误的访问错误。

同时阅读同一页面中的矢量化指令显式引用:

  

从Mac OS X 10.5开始,默认情况下,libgmalloc在16字节边界上对齐已分配缓冲区的开始,以允许正确使用向量指令(例如,SSE)。 (使用向量指令很常见,包括在某些Mac OS X系统库中。

PS。 afaik NSObject和朋友与malloc

共享堆实现

答案 3 :(得分:2)

malloc如何对齐东西是无关紧要的,因为程序员可能会在块内分配一个字符串。一个简单的例子是struct,它有一个嵌入的char数组:

struct Foo
{
    int bar;
    char baz[10];
};

如果你分配了这个struct的实例,它将占用16个字节,但baz将从偏移量4开始。因此,如果你从那里读取16个字节,你将进入下一个您不拥有的16字节块。如果您运气不好,可能会出现在下一页并触发故障。

此外,字符串根本不必在堆中,例如只读数据部分中的常量。 strlen必须适用于所有情况。

我假设strlen函数首先处理字符串的初始部分,直到它的16字节对齐(此代码已从问题中省略),然后以16字节的块继续进行。因此,其工作的实际原因是原因#2:您不会跨越页边界,这是处理器访问检查的粒度。

答案 4 :(得分:2)

请注意,它是一个对齐的读取(暗示它是非vex编码指令的一部分,并非明确地是未对齐的读取)。这意味着虽然它可以(并且经常)读取超出字符串结尾的内容,但它将始终保留在字符串所在的页面上。