指针比较是否应在64位x86中签名或未签名?

时间:2017-12-07 04:49:45

标签: pointers assembly x86 x86-64

在编写x86用户空间程序集并比较两个指针值时,我们是否应该使用签名条件,例如jljge unsigned 条件如jbjae

直观地我认为指针是无符号的,在64位进程的情况下从0到2 ^ 64-1运行,我认为这个模型对于32位代码是准确的。我想这就是大多数人对它们的看法。

在64位代码中,我不认为你可以在0x7FFFFFFFFFFFFFFF处有效地跨越有符号的不连续性,并且许多有趣的内存区域倾向于聚集在有符号0附近(对于代码和静态数据经常,并且有时堆取决于实现),并且在规范地址空间的下半部分的最大地址附近(类似于今天大多数系统上的0x00007fffffffffff)堆栈位置和一些实现上的堆 1 < / SUP>。

所以我不确定应该采用哪种方式处理它们: signed 的优点是它在0附近是安全的,因为那里没有不连续性,并且 unsigned 具有因为在那里没有不连续性,所以在2 ^ 63附近具有相同的优势。但实际上,由于当前商用硬件的虚拟地址空间限制为小于50位,因此您看不到任何接近2 ^ 63的地址。那指向签名吗?

1 ...有时堆和其他映射区域不靠近地址空间的底部或顶部。

1 个答案:

答案 0 :(得分:4)

这完全取决于你想知道的两个指针!

您之前的问题修改将ptrA < ptrB - C作为您感兴趣的用例,例如:使用ptrA < ptrB - sizeA进行重叠检查,或者使用current < endp - loop_stride展开SIMD循环条件。评论中的讨论也是关于这种事情的。

所以你真正在做的是将ptrB - C形成为一个指针,它可能在你感兴趣的对象之外,并且可能已经包裹(无符号)。 (Good observation这样的东西可能就是为什么C和C ++使UB在对象之外形成指针的原因,但它们确实允许在最高页的末尾有一个无符号包装的结尾{{ {3}}。)无论如何,你想要使用一个签名的比较,因此它仍然可以使用&#34;无需检查环绕,或检查C或其中任何内容的标志。这仍然比大多数问题更具体。

是的,对于&#34;相关&#34;从具有合理大小的相同对象派生的指针,签名比较在当前硬件上是安全的,并且只能在具有硬件支持完整64位虚拟地址的不太可能/远期未来的机器上中断。重叠检查也是安全的如果两个指针都在规范范围的下半部分,则无符号,我认为这是所有主流x86-64操作系统上用户空间地址的情况。

正如您所指出的那样,未签名的ptrA < ptrB - C可以&#34;失败&#34; if ptrB - C包装(unsigned wraparound)。对于比C的大小更接近0的静态地址,实际上可能会发生这种情况。

通常低64kiB不可映射(例如在Linux上,大多数发行版附带sysctl vm.mmap_min_addr = 65536,或至少4096.但if the kernel even lets you map it)。尽管如此,我认为内核不会给你一个零页面是正常的,除非你特意请求该地址,因为它会阻止故障中的NULL deref(出于安全性和可调试性的原因,这通常是非常需要的)。

这意味着loop_stride案例通常不是问题。 sizeA版本通常可以使用ptrA + sizeA < ptrB完成,作为奖励,您可以使用LEA来添加而不是复制+减去。 ptrA+sizeA保证不会换行,除非你有指针将指针从2 ^ 64-1包裹到零(some systems have it =0 for WINE,但你永远不会在&#34;正常&#中看到它34;系统因为地址通常被视为无符号。)

那么什么时候可以通过签名比较失败? ptrB - C已经签署了溢出回合时。或者,如果您有指向高半对象的指针(例如,进入Linux的vDSO页面),高半地址和低半地址之间的比较可能会给您带来意想不到的结果:您将看到&#34;高 - 半&#34;地址低于&#34;低半&#34;地址。即使ptrB - C计算没有包装,也会发生这种情况。

(我们只是直接谈论asm,而不是C,所以没有UB,我只是在sublea / {使用C表示法{1}} / cmp。)

签名环绕只能发生在jl0x7FFF...之间的边界附近。 但是,这个边界距离任何规范地址都非常远。我将从which works even with a page-split load at the wraparound重现x86-64地址空间(对于虚拟地址为48位的当前实现)的图表。另请参阅another answer

请记住,x86-64在非规范地址上出现故障。这意味着它会检查48位虚拟地址是否正确地符号扩展为64位,即位0x8000...匹配位[63:48](从0开始编号)。

47

英特尔对于57位虚拟地址(即另一个9位级别的表)有Why in 64bit the virtual address are 4 bits short (48bit long) compared with the physical address (52 bit long)?,但这仍然使大部分地址空间不具有规范性。即任何规范地址仍然是2 ^ 63 - 2 ^ 57远离签名环绕。

根据操作系统的不同,您的所有地址可能都在低半部分或高半部分。例如在x86-64 Linux上,高(&#34;负&#34;)地址是内核地址,而低(有符号正)地址是用户空间。但请注意proposed a 5-level page-table extension进入用户空间非常靠近虚拟地址空间的顶部。 (但它会在我的桌面上将页面未映射到顶部,例如+----------+ | 2^64-1 | 0xffffffffffffffff | ... | high half of canonical address range | 2^64-2^47| 0xffff800000000000 +----------+ | | | unusable | Not to scale: this is 2^15 times larger than the top/bottom ranges. | | +----------+ | 2^47-1 | 0x00007fffffffffff | ... | low half of canonical range | 0 | 0x0000000000000000 +----------+ 在64位进程中,但vDSO页面接近下半部规范范围的顶部ffffffffff600000-ffffffffff601000 [vsyscall]。即使在一个32位的过程,理论上整个4GiB可以被用户空间使用,vDSO是最高页面下面的页面,0x00007fff...没有在最高页面上工作。也许是因为C允许一个-past-the-end指针?)

如果您在mmap(MAP_FIXED)页面中获取了某个函数或变量的地址,则可以混合使用正负地址。 (我不认为有人这样做过,但这是可能的。)

如果您没有内核/用户拆分将签名正数与签名负数分开,那么签名地址比较可能会很危险,您的代码在遥远的未来运行时/ if x86 -64已扩展为完整的64位虚拟地址,因此对象可以跨越边界。后者似乎不太可能,如果你可以从假设它不会发生加速,它可能是一个好主意。

这意味着 signed-compare已经对32位指针造成危险,因为64位内核使整个4GiB可以被用户空间使用。 (并且32位内核可以配置3:1内核/用户拆分)。没有无法使用的规范范围。 在32位模式下,对象可以跨越签名环绕边界。 (或者在ILP32 x32 ABI:长模式下的32位指针。)

性能优势

与32位模式不同,在64位模式或其他组合中,没有vsyscalljge快的CPU。 (并且setcc / cmovcc的不同条件永远不重要)。所以任何perf diff都只来自周围的代码,除非你可以用jaeadc而不是cmov或setcc来做一些聪明的事。

Sandybridge-family可以使用有符号或无符号比较(不是所有JCC,但这不是一个因素)对test / cmp(以及sub,add和各种其他非只读指令)进行宏观融合。 Bulldozer-family可以将cmp / test与任何 JCC融合。

Core2只能使用无符号比较对sbb进行宏保险,而不进行签名,但 Core2在64位模式下无法进行宏保险。 (它可以在32位模式BTW中使用带符号比较对cmp进行宏熔合。)

Nehalem可以使用有符号或无符号比较(包括64位模式)对testtest进行宏观融合。

资料来源:Linux maps the kernel vDSO / vsyscall pages microarch pdf。