应该在64位x86中对指针比较进行签名还是不签名?

Bee*_*ope 2 x86 assembly pointers x86-64

在编写x86-64用户空间程序集并比较两个指针值时,我们应该使用带符号的条件(例如jl和)jge还是使用无符号的条件(例如jb和)jae

直觉上,我认为指针是无符号的,在64位进程的情况下,指针从0到2 ^ 64-1,并且我认为该模型对于32位代码是准确的。我想这就是大多数人对他们的看法。

但是,在64位代码中,我认为您无法有效地跨越0x7FFFFFFFFFFFFFFF(2 ^ 63-1)处的有符号不连续性,并且许多有趣的内存区域倾向于聚集在有符号0附近(对于代码和静态数据,有时甚至是有时)堆的大小取决于实现),并且0x00007fffffffffff在某些实现1的堆栈地址和堆附近接近规范地址空间下半部分的最大地址(类似于当今的大多数系统)。

因此,我不确定应该采用哪种方式对待它们:带符号的优点是它在0附近是安全的,因为那里没有间断;而无符号的优点是在2 ^ 63附近,因为那里没有间断。但是实际上,您不会在2 ^ 63附近看到任何地址,因为当前商用硬件的虚拟地址空间限制为小于50位。这是否指向签名?


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

Pet*_*des 6

TL:DR:intptr_t在某些情况下可能是最好的,因为有符号溢出边界位于“非规范孔”的中间。如果可以将值从零包装到0xFF...FF反之,则将值视为负值而不是庞大可能会更好,但是任何有效大小的指针+大小都不能将值包装INT64_MAXINT64_MIN

否则,您可能希望对“高半部分”(高位设置)进行无符号比较,使其与低半部分以上的值进行比较。


这完全取决于您要了解的两个指针!

您对问题的先前编辑给出ptrA < ptrB - C了您感兴趣的用例,例如使用进行重叠检查ptrA < ptrB - sizeA,或者使用进行展开的SIMD循环条件current < endp - loop_stride。评论中的讨论也与此有关。

因此,您真正要做的是将其形成ptrB - C为一个指针,该指针可能位于您感兴趣的对象之外,并且可能已被包裹(无符号)。(很好的观察,类似这样的东西可能就是为什么C和C ++使其成为UB在对象之外形成指针的原因,但是即使内核甚至在内核中,它们也允许在最后一页的末尾进行无符号包装的单行结束可以让您对其进行映射。)无论如何,您都想使用带符号的比较,以便它“仍然有效”,而不必检查回卷或检查C或任何东西的符号。这仍然比大多数问题更为具体。

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


如您所指出的,ptrA < ptrB - C如果自动ptrB - C换行(无符号换行),则未签名可能会“失败” 。实际上,对于比0的大小更接近0的静态地址,可能会发生这种情况C

通常,低64kiB是不可映射的(例如,在Linux上,大多数发行版都附带sysctl vm.mmap_min_addr = 65536或至少4096。但是某些系统将其=0用于WINE)。不过,我认为除非您特别要求该地址,否则内核不给您零页是正常的,因为这样可以防止NULL deref出错(出于安全性和可调试性的原因,通常非常希望这样做)。

这意味着loop_stride的情况通常不是问题。该sizeA版本通常可以使用完成ptrA + sizeA < ptrB,作为奖励,您可以使用LEA进行添加而不是复制+减去。 ptrA+sizeA除非您有对象将其指针从2 ^ 64-1缠绕为零(否则即使在进行绕行时分页加载也可以使用),否则将保证不进行包装,但是您将永远不会在“正常”系统中看到它,因为地址通常被视为未签名。)


那么签名签名何时会失败? 什么时候ptrB - C在溢出时签署了绕回。或者,如果您曾经有指向上半部分对象的指针(例如,进入Linux的vDSO页面),则上半部分和下半部分地址之间的比较可能会给您带来意想不到的结果:您将看到“上半部分”地址少于“低半”地址。即使ptrB - C计算不结束,也会发生这种情况。

(我们只是在直接谈论asm,而不是C,所以没有UB,我只是在C符号上使用subor lea/ cmp/ jl。)

带符号的环绕只能在0x7FFF...和之间的边界附近发生0x8000...但是那个界限离任何规范的地址都极其遥远。我将从另一个答案中复制x86-64地址空间的图表(对于当前实现,其中虚拟地址是48位)。另请参见为什么在64位中,虚拟地址比物理地址(52位长)短4位(长48位)?

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

+----------+
| 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
+----------+
Run Code Online (Sandbox Code Playgroud)

英特尔已经为57位虚拟地址(即表的另一9位级别)提出了5级页面表扩展,但是仍然使大多数地址空间不规范。即,任何规范地址仍将与带符号的环绕距离相差2 ^ 63-2 ^ 57。

根据操作系统的不同,您的所有地址都可能位于下半部或上半部。例如在x86-64 Linux上,高(“负”)地址是内核地址,而低(有符号正)地址是用户空间。但是请注意,Linux将内核vDSO / vsyscall页面映射到非常靠近虚拟地址空间顶部的用户空间。(但是这样会使页面未映射到顶部,例如ffffffffff600000-ffffffffff601000 [vsyscall]在我的台式机上是64位进程,但是vDSO页仍在下半部规范范围的顶部附近0x00007fff...。即使在32位进程中,理论上整个4GiB在用户空间中可用,vDSO是最高页面下方的页面,并且mmap(MAP_FIXED)不能在该最高页面上使用。也​​许是因为C允许使用一端的指针?)

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

因此,如果您没有将正号与负号分开的内核/用户拆分,并且在遥远的将来,当x86-64已扩展到完整的64位虚拟地址时您的代码正在运行,则签名地址比较可能会很危险。,因此对象可以跨越边界。后者似乎不太可能,并且如果您可以假设不会发生这种情况而加快速度,那可能是个好主意。

这意味着使用32位指针进行符号比较已经很危险,因为64位内核使整个4GiB可供用户空间使用。(并且32位内核可以使用3:1内核/用户拆分配置)。没有不可用的规范范围。 在32位模式下,对象可以跨越有符号环绕边界。(或在ILP32 x32 ABI中:长模式下的32位指针。)


性能优势

与32位模式不同,没有CPU jge的速度比jae64位模式或其他组合更快。(和setcc / cmovcc的不同条件无关紧要)。因此,任何性能差异都仅来自周围的代码,除非您可以使用cmov或setcc adcsbb代替cmov或setcc 做一些聪明的事情。

Sandybridge系列可以使用带符号或不带符号的比较(并非所有JCC,但不是一个因素)来宏融合测试/ cmp(以及sub,add和其他各种非只读指令)。推土机系列可以与任何 JCC 融合cmp / test 。

Core2只能对cmp带有无符号比较的宏进行熔丝处理,而不能对未签名的对象进行宏熔丝处理,而Core2在64位模式下完全不能进行宏熔丝处理。(它可以test与32位模式BTW中的带符号比较宏融合。)

Nehalem可以宏熔丝testcmp带有符号或不带符号的比较(包括在64位模式下)。

资料来源:Agner Fog的microarch pdf。