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

时间:2013-04-24 17:40:47

标签: pointers 64-bit x86-64 memory-access

我读到a 64-bit machine actually uses only 48 bits of address(具体来说,我使用的是英特尔酷睿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.

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

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

5 个答案:

答案 0 :(得分:8)

如果地址总线将来会增加,那么保留高位比特,所以你不能像那样使用它

  

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_t or intptr_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;

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


您还可以使用低位来存储数据。它被称为tagged pointer。如果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;

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

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

答案 1 :(得分:4)

规范化AMD / Intel x64指针的方法(基于当前有关规范指针和48位寻址的文档)是

int *p2 = (int *)(((uintptr_t)p1 & ((1ull << 48) - 1)) |
    ~(((uintptr_t)p1 & (1ull << 47)) - 1));

这首先清除指针的高16位。然后,如果位47为1,则将位47设置为63,但如果位47为0,则对值0进行逻辑或(不变)。

答案 2 :(得分:1)

根据英特尔手册(第1卷,第3.3.7.1节),线性地址必须采用规范形式。这意味着实际上只使用了48位,额外的16位是符号扩展的。此外,需要实现以检查地址是否采用该形式以及是否未生成异常。这就是为什么没有办法使用这些额外的16位。

以这种方式完成它的原因很简单。目前48位的虚拟地址空间绰绰有余(并且由于CPU的生产成本,没有必要使其变大)但毫无疑问,将来需要额外的位。如果应用程序/内核为了自己的目的而使用它们,则会出现兼容性问题,这就是CPU供应商想要避免的问题。

答案 3 :(得分:1)

我猜没有人提到在这种情况下可能使用位字段(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 

如果我们真的想节省一些内存,我认为这可能是尝试使用这16位的一种非常便捷的方法。所有按位(&和|)运算并强制转换为完整的64位指针都由编译器完成(尽管当然要在运行时执行)。

答案 4 :(得分:-1)

物理内存是48位寻址。这足以解决大量内存问题。但是,在CPU内核上运行的程序与RAM之间是内存管理单元,是CPU的一部分。您的程序正在寻址虚拟内存,MMU负责在虚拟地址和物理地址之间进行转换。虚拟地址是64位。

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

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

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