当前位置: 首页 > 知识库问答 >
问题:

在x86和x64上读过同一页内缓冲区的结尾是否安全?

艾弘义
2023-03-14

在高性能算法中发现的许多方法可以被简化,如果允许它们读取少量超过输入缓冲区末尾的话。这里,“Small Mounty”通常表示超过结尾的W-1字节,其中W是算法的字大小(例如,对于以64位块处理输入的算法,最多为7字节)。

很明显,在输入缓冲区末尾写入数据通常是不安全的,因为您可能会将数据打乱到缓冲区1之外。同样清楚的是,从缓冲区的末尾读到另一个页面可能会触发分段错误/访问冲突,因为下一个页面可能不可读。

然而,在读取对齐值的特殊情况下,页面错误似乎是不可能的,至少在x86上是这样。在该平台上,页(以及因此的内存保护标志)具有4K粒度(更大的页,例如2MIB或1GIB,是可能的,但它们是4K的倍数),因此对齐读取将只访问与缓冲区有效部分相同页中的字节。

下面是一个规范的循环示例,该循环对齐其输入并读取超过缓冲区结尾7个字节的数据:

int processBytes(uint8_t *input, size_t size) {

    uint64_t *input64 = (uint64_t *)input, end64 = (uint64_t *)(input + size);
    int res;

    if (size < 8) {
        // special case for short inputs that we aren't concerned with here
        return shortMethod();
    }

    // check the first 8 bytes
    if ((res = match(*input)) >= 0) {
        return input + res;
    }

    // align pointer to the next 8-byte boundary
    input64 = (ptrdiff_t)(input64 + 1) & ~0x7;

    for (; input64 < end64; input64++) {
        if ((res = match(*input64)) > 0) {
            return input + res < input + size ? input + res : -1;
        }
    }

    return -1;
}

这些类型的覆盖在高性能代码中很常见。避免此类覆盖的特殊尾代码也很常见。有时,您会看到后一种类型取代了前一种类型,从而使ValGrind等工具保持沉默。有时您会看到一个执行这样一个替换的提议,但它被拒绝了,理由是习惯用法是安全的,工具是错误的(或者只是太保守了)3

语言律师须知:

标准中绝对不允许从超过其分配大小的指针读取。我喜欢语言律师的回答,甚至偶尔自己写,当有人挖掘上面的代码是未定义的行为,因此在最严格的意义上不安全时,我甚至会很高兴(我将在这里复制细节)。不过,归根结底,这不是我想要的。作为一个实际问题,许多涉及指针转换、通过指针访问结构等的常见习语在技术上没有定义,但在高质量和高性能代码中广泛存在。通常没有替代方案,或者替代方案以半速或更低的速度运行。

在这方面,这个问题既是C问题,也是x86汇编问题。我所看到的大多数使用这种技巧的代码都是用C编写的,而C仍然是高性能库的主要语言,很容易就使asm等低级语言和 等高级语言黯然失色。至少在FORTRAN仍然发挥作用的核心数字领域之外。所以我对这个问题的C-编译器和以下视图感兴趣,这就是为什么我没有将它表述为一个纯x86汇编问题。

尽管如此,虽然我只对显示这是UD的标准的链接感兴趣,但我对可以使用该特定UD生成意外代码的实际实现的任何细节都非常感兴趣。现在我不认为如果没有一些深入的跨程序分析,这是不可能发生的,但gcc溢出的东西也让很多人感到惊讶...

1即使在表面上无害的情况下,例如,写回相同的值,它也可能破坏并发代码。

2注意:要使这种重叠起作用,需要该函数和match()函数以特定的幂等方式运行--特别是返回值支持重叠检查。因此,由于所有match()调用仍然是按顺序进行的,因此“查找第一个字节匹配模式”可以工作。但是,“计数字节匹配模式”方法不起作用,因为有些字节可能会被重复计数。顺便说一句:一些函数,如“return the minimum byte”调用,即使没有顺序限制也可以工作,但需要检查所有字节。

3这里值得注意的是,对于Valgrind的Memcheck,有一个标志--partical-loads-ok,它控制这样的读操作实际上是否报告为错误。默认值为yes,表示通常不将此类加载视为即时错误,而是努力跟踪加载字节的后续使用情况,其中一些字节有效,一些无效,如果使用超出范围的字节,则标记错误。在上面的例子中,在match()中访问整个单词,这样的分析将得出字节被访问的结论,即使结果最终被丢弃。Valgrind通常无法确定是否实际使用了部分加载中的无效字节(而且通常检测可能非常困难)。

共有1个答案

家西岭
2023-03-14

是的,它在x86 asm中是安全的,现有的libcstrlen(3)实现在手写asm中利用了这一点。甚至是glibc的回退C,但它没有LTO编译,所以它永远不能内联。它基本上是使用C作为一个可移植的汇编程序为一个函数创建机器代码,而不是作为内联的更大的C程序的一部分。但这主要是因为它也有潜在的严格别名UB,见我在链接问答中的回答。您可能还需要一个GNU C__attribute__((may_alias))typedef而不是像您已经使用的更广泛的类型(如__m128i等)那样的普通无符号长

它是安全的,因为对齐的加载永远不会越过更高的对齐边界,而且对齐的页会进行内存保护,因此至少有4K边界1任何触及至少1个有效字节的自然对齐加载都不会出错。同样安全的做法是,只检查离下一页边界是否足够远,可以执行16字节的加载,比如if(p&4095>(4096-16))do_special_case_fallback。有关更多细节,请参见下面的部分。

据我所知,在为x86编译的C中,它通常也是安全的。当然,在C语言中读取对象以外的内容是未定义的行为,但在C-targeting-x86中是可行的。我不认为编译器明确地/故意地定义行为,但实际上它是这样工作的。

在asm中,处理隐式长度的字符串时速度必须超过1个字节。在C语言中,理论上编译器可以知道如何优化这样的循环,但实际上他们不知道,所以你必须这样做。在这种情况发生改变之前,我怀疑人们关心的编译器通常会避免破坏包含这种潜在UB的代码。

对于知道对象有多长的代码来说,当覆盖的代码不可见时,就没有危险了。编译器必须使asm工作于我们实际读取的数组元素的情况。我可以看到未来编译器可能存在的危险是:在内联之后,编译器可能会看到UB,并决定永远不能采用这种执行路径。或者终止条件必须在最后一个非全向量之前找到,并在完全展开时将其省略。

你得到的数据是不可预测的垃圾,但不会有任何其他潜在的副作用。只要你的程序不受垃圾字节的影响,就没问题。(例如,使用bithacks查找UINT64_T的某个字节是否为零,然后使用字节循环查找第一个零字节,而不管它之外有什么垃圾。)

>

  • 从给定地址加载时触发的硬件数据断点(观察点)。如果在数组后面有一个你正在监视的变量,你可能会得到一个虚假的命中。对于调试正常程序的人来说,这可能是一个小麻烦。如果您的函数将是使用x86调试寄存器D0-D3的程序的一部分,并且由于某些可能影响正确性的东西而产生异常,那么对此要小心。

    或者类似地,像valgrind这样的代码检查器可能会抱怨读取对象之外的内容。

    在假设的16或32位操作系统下,可以使用分段:段限制可以使用4K或1字节粒度,因此可以创建第一个故障偏移量为奇数的段。(除了性能之外,将段的基与缓存行或页对齐是不相关的)。所有主流的x86操作系统都使用平面内存模型,x86-64取消了对64位模式段限制的支持。

    strlen是处理隐式长度缓冲区的循环的典型示例,因此如果不读取缓冲区的末尾,就无法进行向量化。如果需要避免读取超过终止的0字节,则每次只能读取一个字节。

    例如,Glibc的实现使用一个序言来处理直到第一个64b对齐边界的数据。然后在主循环(到asm源的gitweb链接)中,它使用四个SSE2对齐加载加载整个64B缓存行。它将它们合并为一个带有pminub(无符号字节的最小值)的向量,因此只有当四个向量中的任何一个都为零时,最终的向量才会有零元素。在发现字符串的末尾位于缓存行的某个位置后,它会分别重新检查四个向量中的每一个,以查看其位置。(对全零向量使用典型的PCMPEQBPMOVMSKB/BSF查找向量中的位置。)glibc过去有两种不同的strlen策略可供选择,但当前的策略适用于所有x86-64 CPU。

    通常,这样的循环避免接触任何额外的缓存行--它们不需要接触,而不仅仅是页面,因为性能原因,比如glibc的strlen。

    当然,一次加载64B只能避免64B对齐的指针,因为自然对齐的访问不能跨越缓存行或页行的边界。

    如果您确实提前知道缓冲区的长度,则可以通过使用在缓冲区的最后一个字节结束的未对齐加载来处理超过最后一个完全对齐向量的字节,从而避免读过结尾。

    对象的非缺陷覆盖是一种UB,如果编译器在编译时看不到它,它肯定不会受到伤害。产生的asm工作起来就像额外的字节是某个对象的一部分一样。

    但是即使它在编译时可见,它通常对当前的编译器没有影响。

    PS:这个答案的前一个版本声称int*的未对齐deref在为x86编译的C中也是安全的。那不是真的。三年前我写这部分的时候有点太傲慢了。您需要__attribute__((aligned(1)))typedef或memcpy,以确保安全。

    这对于Strlen的第一个向量是有用的;在此之后,您可以p=(P+16)&-16转到下一个对齐的向量。如果p不是16字节对齐,这将部分重叠,但是执行冗余工作有时是设置高效循环的最紧凑的方法。避免它可能意味着一次循环1个字节,直到一个对齐边界,这当然更糟。

    例如,check((p+15)^p)&0xfff...f000==0(LEA/XOR/TEST),它告诉您16字节加载的最后一个字节与第一个字节具有相同的页地址位。或p+15<=p0xfff(LEA/或/CMP,具有更好的ILP)检查加载的最后一个字节地址是<=包含第一个字节的页的最后一个字节。

    或者更简单地说,P&4095>(4096-16)(MOV/和/CMP),即P&(pgsize-1)<(PGSize-VecWidtth)检查页面内的偏移量是否离页面末尾足够远。

    您可以使用32位操作数大小来保存此检查或任何其他检查的代码大小(REX前缀),因为高位并不重要。有些编译器没有注意到这种优化,所以您可以强制转换为无符号int而不是uintptr_t,尽管要使关于不是64位干净代码的警告保持沉默,您可能需要强制转换(unsigned)(uintptr_t)P。使用((unsigned int)p<<20)>((4096-vectorlen)<<20)(MOV/SHL/CMP)可以进一步节省代码大小,因为SHL reg,20是3个字节,而和eax,IMM32是5个字节,对于任何其他寄存器来说是6个字节。(使用EAX还允许CMP EAX,0xfff的no-modrm缩写形式。)

    如果在GNU C中这样做,您可能需要typedef无符号长aliasing_unaligned_ulong__attribute__((aligned(1),may_alias));,以便安全地进行非对齐访问。

  •  类似资料:
    • 以下是我的情况: 我已经编写了一个小型的延迟3D引擎,使用MRT(多渲染目标)填充我的G缓冲区(同时填充我的所有位置、法线、颜色和镜面反射纹理)。 此外,我还在我的G-Buffer FBO上附加了一个渲染缓冲区对象(RBO),该对象初始化为GL_DEPTH_STENCIL_ATTACHMENT(GL_DEPTH24_STENCIL8)。因此,在G缓冲区执行期间,我的4个纹理同时被填充,渲染缓冲区同

    • 我发现了一个有趣的协议缓冲区问题。如果您有两条类似的消息,那么可以使用C API或命令行解析其中一条消息,就像解析另一条消息一样。 ParseFromString的有限文档没有提到它不需要使用所有字符串,如果不使用,它也不会失败。 我原以为ParseFromString无法解析类型为a的消息,如果它显示的是类型为B的消息。毕竟消息包含了额外的数据。然而,事实并非如此。示例脚本演示了该问题: 输出为

    • 正在为以下内容编写javadoc: 但是,将缓冲的输入流传入真的是一个问题吗?因此: 是否将is缓冲到bis中,或者java是否检测到is已缓冲并设置bis=is?如果是,不同的缓冲区大小是否会有所不同?如果没有,为什么不呢<注意:我说的是输入流,但实际上这个问题也适用于输出流

    • 我试图读取名为使用。JS代码: 但是运行代码会产生这个错误: 这个错误对我来说毫无意义。由返回的缓冲区怎么可能不是的实例?将替换为或也不能解决此问题。我目前正在使用节点。JS版本14.17.6(LTS)。

    • 在DirectX中,您可以有单独的渲染目标和深度缓冲区,因此可以绑定渲染目标和一个深度缓冲区、执行一些渲染、移除深度缓冲区然后使用旧的深度缓冲区作为纹理进行更多渲染。 你会如何在opengl中做到这一点?根据我的理解,您有一个帧缓冲区对象,其中包含颜色缓冲区和可选的深度缓冲区。我不认为我可以同时绑定多个帧缓冲器对象,我是否必须在每次更改时(可能一帧几次)重新创建帧缓冲器对象?普通的 opengl

    • 问题内容: 从公告博客文章的评论中: 关于JSON:JSON的结构类似于协议缓冲区,但是协议缓冲区二进制格式仍然更小且编码更快。但是,JSON为协议缓冲区提供了一种出色的文本编码- 编写编码器/解码器是很简单的,该编码器/解码器使用protobuf反射将任意协议消息与JSON相互转换。这是与AJAX应用程序通信的好方法,因为让用户在访问您的页面时下载完整的protobuf解码器可能太多了。 编写