在编写x86用户空间程序集并比较两个指针值时,我们是否应该使用签名条件,例如jl
和jge
或 unsigned 条件如jb
和jae
?
直观地我认为指针是无符号的,在64位进程的情况下从0到2 ^ 64-1运行,我认为这个模型对于32位代码是准确的。我想这就是大多数人对它们的看法。
在64位代码中,我不认为你可以在0x7FFFFFFFFFFFFFFF
处有效地跨越有符号的不连续性,并且许多有趣的内存区域倾向于聚集在有符号0附近(对于代码和静态数据经常,并且有时堆取决于实现),并且在规范地址空间的下半部分的最大地址附近(类似于今天大多数系统上的0x00007fffffffffff
)堆栈位置和一些实现上的堆 1 < / SUP>。
所以我不确定应该采用哪种方式处理它们: signed 的优点是它在0附近是安全的,因为那里没有不连续性,并且 unsigned 具有因为在那里没有不连续性,所以在2 ^ 63附近具有相同的优势。但实际上,由于当前商用硬件的虚拟地址空间限制为小于50位,因此您看不到任何接近2 ^ 63的地址。那指向签名吗?
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,我只是在sub
或lea
/ {使用C表示法{1}} / cmp
。)
签名环绕只能发生在jl
和0x7FFF...
之间的边界附近。 但是,这个边界距离任何规范地址都非常远。我将从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位模式或其他组合中,没有vsyscall
比jge
快的CPU。 (并且setcc / cmovcc的不同条件永远不重要)。所以任何perf diff都只来自周围的代码,除非你可以用jae
或adc
而不是cmov或setcc来做一些聪明的事。
Sandybridge-family可以使用有符号或无符号比较(不是所有JCC,但这不是一个因素)对test / cmp(以及sub,add和各种其他非只读指令)进行宏观融合。 Bulldozer-family可以将cmp / test与任何 JCC融合。
Core2只能使用无符号比较对sbb
进行宏保险,而不进行签名,但 Core2在64位模式下无法进行宏保险。 (它可以在32位模式BTW中使用带符号比较对cmp
进行宏熔合。)
Nehalem可以使用有符号或无符号比较(包括64位模式)对test
或test
进行宏观融合。
资料来源:Linux maps the kernel vDSO / vsyscall pages microarch pdf。