在64位指针中使用额外的16位

use*_*720 6 64-bit pointers x86-64 memory-access

我读到64位机器实际上只使用48位地址(具体来说,我使用的是英特尔酷睿i7).

我希望额外的16位(位48-63)与地址无关,并将被忽略.但是当我尝试访问这样的地址时,我收到了一个信号EXC_BAD_ACCESS.

我的代码是:

int *p1 = &val;
int *p2 = (int *)((long)p1 | 1ll<<48);//set bit 48, which should be irrelevant
int v = *p2; //Here I receive a signal EXC_BAD_ACCESS.
Run Code Online (Sandbox Code Playgroud)

为什么会这样?有没有办法使用这16位?

这可用于构建更多缓存友好的链表.不是将8个字节用于下一个ptr,而是将8个字节用于密钥(由于对齐限制),可以将密钥嵌入到指针中.

phu*_*clv 9

如果地址总线将来会增加,则保留高位,因此您不能像这样使用它

AMD64体系结构定义了64位虚拟地址格式,其中低阶48位用于当前实现(...)体系结构定义允许在将来的实现中将此限制提升到完整的64位,从而扩展虚拟地址空间为16 EB(2 64字节).这与x86 仅为4 GB(2 32字节)进行比较.

http://en.wikipedia.org/wiki/X86-64#Architectural_features

更重要的是,根据同一篇文章[强调我的]:

...在架构的第一个实现中,实际上只有虚拟地址的最低有效48位用于地址转换(页表查找).此外,任何虚拟地址的位48到63必须是位47的副本(以类似于符号扩展的方式),否则处理器将引发异常.符合此规则的地址称为"规范形式".

由于CPU将检查高位,即使它们未被使用,它们也不是真正"无关紧要".在使用指针之前,您需要确保地址是规范的.其他一些64位架构(如ARM64)可以选择忽略高位,因此您可以更轻松地将数据存储在指针中.


也就是说,在x86_64中,如果需要,你仍然可以自由地使用高16位,但是你必须在解除引用之前通过符号扩展来检查和修复指针值.

请注意,将指针值转换long不正确的方法,因为long不能保证足够宽以存储指针.你需要使用uintptr_tintptr_t.

int *p1 = &val; // original pointer
uint8_t data = ...;
const uintptr_t MASK = ~(1ULL << 48);

// store data into the pointer
//     note: to be on the safe side and future-proof (because future implementations could
//     increase the number of significant bits in the pointer), we should store values
//     from the most significant bits down to the lower ones
int *p2 = (int *)(((uintptr_t)p1 & MASK) | (data << 56));

// get the data stored in the pointer
data = (uintptr_t)p2 >> 56;

// deference the pointer
//     technically implementation defined. You may want a more
//     standard-compliant way to sign-extend the value
intptr_t p3 = ((intptr_t)p2 << 16) >> 16; // sign extend the pointer to make it canonical
val = *(int*)p3;
Run Code Online (Sandbox Code Playgroud)

WebKit的JavaScriptCore和Mozilla的SpiderMonkey引擎在nan-boxing技术中使用它.如果值为NaN,则低48位将存储指向对象的指针,高16位用作标记位,否则为双值.


您也可以使用低位来存储数据.它被称为标记指针.如果int是4字节对齐,则2个低位始终为0,您可以像在32位架构中一样使用它们.对于64位值,您可以使用3个低位,因为它们已经是8字节对齐的.同样,您还需要在解除引用之前清除这些位.

int *p1 = &val; // the pointer we want to store the value into
int tag = 1;
const uintptr_t MASK = ~0x03ULL;

// store the tag
int *p2 = (int *)(((uintptr_t)p1 & MASK) | tag);

// get the tag
tag = (uintptr_t)p2 & 0x03;

// get the referenced data
intptr_t p3 = (uintptr_t)p2 & MASK; // clear the 2 tag bits before using the pointer
val = *(int*)p3;
Run Code Online (Sandbox Code Playgroud)

其中一个着名的用户是具有SMI(小整数)优化的32位版V8 (虽然我不确定64位V8).最低位将用作类型的标记:如果它是0,则它是一个小的31位整数,执行有符号右移1以恢复该值; 如果它是1,则该值是指向实际数据(对象,浮点数或更大整数)的指针,只需清除标记并取消引用它

旁注:对于指针与指针相比具有微小键值的情况使用链表是一个巨大的内存浪费,并且由于缓存局部性不好而导致速度也较慢.事实上,在大多数现实生活中,你不应该使用链表

  • @Karl 您可以从最重要的位而不是从第 48 位开始使用。这减少了在不久的将来代码被破坏的机会。在可预测的未来,*个人* CPU 极不可能拥有完整的 64 位总线宽度 (3认同)
  • 无论如何,使用低位将 ** 始终** 是安全的,如果不需要这么多位,则应改为使用低位 (3认同)
  • 一个非常非常重要的警告:存在规范形式的原因特别是使得难以将这16位重新用于其他目的.有一天,他们会打开所有64位,然后你的代码就会中断. (2认同)
  • 警告!代码“intptr_t p3 = ((intptr_t)p2 &lt;&lt; 16) &gt;&gt; 16;” 如果前 16 位中的任何一个不为零,则是未定义的行为,因为 C++ 认为它是有符号溢出。您需要使用未签名。不幸的是,要进行符号扩展,您需要使用签名数字。同样不幸的是,有符号右移是实现定义的。无论如何,您想使用 intptr_t p3 = (intptr_t)((uintptr_t)p2 &lt;&lt; 16) &gt;&gt; 16; 它适用于所有已知的 x86-64 编译器,或者如果您想要真正定义良好的,请使用除法:intptr_t p3 = (intptr_t)((uintptr_t)p2 &lt;&lt; 16) / 65536; https://godbolt.org/g/5P4tJF (2认同)
  • PML5 已经记录在案,甚至可能已经在硬件中可用。如果操作系统选择启用,额外级别的页表将为我们提供 57 位虚拟地址。留下7个高位。(对于 16 字节对齐分配,可以更轻松、更高效地清除 4 个低位。)另请注意,在取消引用之前必须修改指针的最佳情况会将指针追逐延迟从 4 个周期变为 6:1 ALU 运算加[击败 SnB 系列的快速路径](//stackoverflow.com/questions/52351397) 实现简单的“[reg+0..2047]”寻址模式。 (2认同)

mor*_*ver 5

我想没有人提到在这种情况下可能使用位字段(https://en.cppreference.com/w/cpp/language/bit_field),例如

template<typename T>
struct My64Ptr
{
    signed long long ptr : 48; // as per phuclv's comment, we need the type to be signed to be sign extended
    unsigned long long ch : 8; // ...and, what's more, as Peter Cordes pointed out, it's better to mark signedness of bit field explicitly (before C++14)
    unsigned long long b1 : 1; // Additionally, as Peter found out, types can differ by sign and it doesn't mean the beginning of another bit field (MSVC is particularly strict about it: other type == new bit field)
    unsigned long long b2 : 1;
    unsigned long long b3 : 1;
    unsigned long long still5bitsLeft : 5;

    inline My64Ptr(T* ptr) : ptr((long long) ptr)
    {
    }

    inline operator T*()
    {
        return (T*) ptr;
    }
    inline T* operator->()
    {
        return (T*)ptr;
    }
};

My64Ptr<const char> ptr ("abcdefg");
ptr.ch = 'Z';
ptr.b1 = true;
ptr.still5bitsLeft = 23;
std::cout << ptr << ", char=" << char(ptr.ch) << ", byte1=" << ptr.b1 << 
  ", 5bitsLeft=" << ptr.still5bitsLeft << " ...BTW: sizeof(ptr)=" << sizeof(ptr);

// The output is: abcdefg, char=Z, byte1=1, 5bitsLeft=23 ...BTW: sizeof(ptr)=8
// With all signed long long fields, the output would be: abcdefg, char=Z, byte1=-1, 5bitsLeft=-9 ...BTW: sizeof(ptr)=8 
Run Code Online (Sandbox Code Playgroud)

我认为,如果我们确实想节省一些内存,尝试使用这 16 位可能是一种相当方便的方法。所有按位(& 和 |)操作以及转换为完整 64 位指针均由编译器完成(当然,尽管是在运行时执行)。


baz*_*zza -2

物理内存是48位寻址的。这足以处理大量 RAM。然而,在 CPU 核心和 RAM 上运行的程序之间是内存管理单元,它是 CPU 的一部分。你的程序正在寻址虚拟内存,MMU负责虚拟地址和物理地址之间的转换。虚拟地址是64位的。

虚拟地址的值不会告诉您相应的物理地址。事实上,由于虚拟内存系统的工作方式,无法保证相应的物理地址时刻相同。如果您对 mmap() 有创意,您可以使两个或多个虚拟地址指向同一物理地址(无论发生在哪里)。如果您随后写入这些虚拟地址中的任何一个,那么您实际上只是写入了一个物理地址(无论发生在哪里)。这种技巧在信号处理中非常有用。

因此,当您篡改指针的第 48 位(指向虚拟地址)时,MMU 无法在操作系统(或您自己使用 malloc())分配给程序的内存表中找到新地址。 。它会引发一个中断以示抗议,操作系统会捕获该中断并用您提到的信号终止您的程序。

如果你想了解更多,我建议你谷歌“现代计算机体系结构”并阅读一些有关支持你的程序的硬件的内容。

  • 在当前的 x86_64 实现上,虚拟内存实际上是 48 位寻址(Intel 手册,第 1 卷,3.3.7.1),其余 16 位是符号扩展的。物理地址范围的大小是特定于实现的(Intel 手册,第 3 卷,3.3.1)。 (5认同)

归档时间:

查看次数:

4559 次

最近记录:

6 年,3 月 前