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

Pointers 在64位指针中使用额外的16位,pointers,64-bit,x86-64,memory-access,Pointers,64 Bit,X86 64,Memory Access,我读过(具体来说,我使用的是Intel core i7) 我希望额外的16位(48-63位)与地址无关,将被忽略。但是当我试图访问这样一个地址时,我得到了一个信号EXC\u BAD\u access 我的代码是: int*p1=&val; int*p2=(int*)((长)p1 | 1ll物理内存是48位寻址的。这足以寻址大量RAM。但是,在CPU核心上运行的程序和RAM之间是内存管理单元,CPU的一部分。您的程序寻址虚拟内存,MMU负责在虚拟地址和物理地址之间进行转换。虚拟内存地址是64位的

我读过(具体来说,我使用的是Intel core i7)

我希望额外的16位(48-63位)与地址无关,将被忽略。但是当我试图访问这样一个地址时,我得到了一个信号
EXC\u BAD\u access

我的代码是:

int*p1=&val;

int*p2=(int*)((长)p1 | 1ll物理内存是48位寻址的。这足以寻址大量RAM。但是,在CPU核心上运行的程序和RAM之间是内存管理单元,CPU的一部分。您的程序寻址虚拟内存,MMU负责在虚拟地址和物理地址之间进行转换。虚拟内存地址是64位的

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

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


如果您想了解更多信息,我建议您在谷歌上搜索“现代计算机体系结构”,并阅读一些有关支持您的程序的硬件。

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


这样做的原因很简单。目前48位虚拟地址空间已经足够了(而且由于CPU生产成本的原因,没有必要将其变大)但毫无疑问,未来将需要额外的位。如果应用程序/内核将它们用于自己的目的,则会出现兼容性问题,而这正是CPU供应商希望避免的问题。

高阶位是保留的,以防将来地址总线会增加,因此您不能像那样简单地使用它

AMD64体系结构定义了64位虚拟地址格式,其中低阶48位用于当前实现(…)体系结构定义允许在未来实现中将此限制提高到完整的64位,将虚拟地址空间扩展到16 EB(264字节)。相比之下,x86仅为4 GB(232字节)

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

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

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


这就是说,在x86_64中,如果需要,您仍然可以自由地使用高16位(如果虚拟地址不超过48位,请参见下文),但在取消引用之前,您必须通过它检查并修复指针值

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

int*p1=&val;//原始指针
uint8_t数据=。。。;
const uintpttr_t MASK=~(1全部56;
//==遵从指针===
//首先扩展符号,使指针规范化
//注意:技术上这是实现定义的。您可能需要更多
//符合标准的签名方式扩展值
intptr_t p3=((intptr_t)p2>16;
val=*(int*)p3;
以及在中使用。如果值为NaN,则低48位将存储指向对象的指针,高16位用作标记位,否则为双倍值

以前,用于指示值是否由内核写入

实际上,您通常也可以使用第48位。因为大多数现代64位操作系统将内核和用户空间一分为二,所以第47位始终为零,并且您有17位可供使用


您也可以使用低位来存储数据。它被称为a。如果
int
是4字节对齐的,那么2个低位总是0,您可以像在32位体系结构中一样使用它们。对于64位值,您可以使用3个低位,因为它们已经是8字节对齐的。同样,您还需要在取消引用之前清除这些位g

int*p1=&val;//要将值存储到其中的指针
int tag=1;
const uintpttr_t MASK=~0x03ULL;
//==存储标签===
int*p2=(int*)((uintpttr_t)p1和MASK)|标记);
//获取标签===
标记=(uintptr_t)p2和0x03;
//==获取引用的数据===
//在使用指针之前,清除2个标记位
intptr_t p3=(uintptr_t)p2和掩码;
val=*(int*)p3;
一位著名的用户
int *p2 = (int *)(((uintptr_t)p1 & ((1ull << 48) - 1)) |
    ~(((uintptr_t)p1 & (1ull << 47)) - 1));
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