27

我读到64 位机器实际上只使用 48 位地址(具体来说,我使用的是 Intel Core 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 个字节用于键(由于对齐限制),键可以嵌入到指针中。

4

5 回答 5

40

高位是保留的,以防将来增加地址总线,所以你不能像那样简单地使用它

AMD64 架构定义了 64 位虚拟地址格式,其中低位 48 位在当前实现中使用(...)架构定义允许在未来的实现中将此限制提高到完整的 64 位,扩展了虚拟地址空间为 16 EB(2个 64字节)。相比之下,x86 只有 4 GB(2 32字节)。

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

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

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

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


也就是说,在 x86_64 中,如果需要,您仍然可以自由使用高 16 位(如果虚拟地址不超过 48 位,请参见下文),但您必须先通过符号扩展来检查和修复指针取消引用。

请注意,将指针值转换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
//     can 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 ===
// Sign extend first to make the pointer canonical
// Note: Technically this is implementation defined. You may want a more
//     standard-compliant way to sign-extend the value
intptr_t p3 = ((intptr_t)p2 << 16) >> 16;
val = *(int*)p3;

WebKit 的 JavaScriptCore 和 Mozilla 的 SpiderMonkey 引擎以及LuaJITnan-boxing 技术中使用了这个。如果值为 NaN,则低 48 位将存储指向对象的指针,高 16 位用作标记位,否则为双精度值。

以前Linux也使用GS基地址的第63表示该值是否被内核写入

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


您还可以使用低位存储数据。它被称为标记指针。如果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 ===
// Clear the 2 tag bits before using the pointer
intptr_t p3 = (uintptr_t)p2 & MASK;
val = *(int*)p3;

一个著名的用户是具有SMI(小整数)优化的 V8 引擎。地址中的最低位将用作类型的标记:

  • 如果为 1,则该值是指向真实数据(对象、浮点数或更大的整数)的指针。下一个高位 (w) 指示指针是弱的还是强的。只需清除标签位并取消引用它
  • 如果它是 0,它是一个小整数。在带有指针压缩的 32 位 V8 或 64 位 V8 中,它是一个 31 位 int,执行有符号右移 1 以恢复该值;在没有指针压缩的 64 位 V8 中,它是上半部分的 32 位 int
   32-bit V8
                           |----- 32 bits -----|
   Pointer:                |_____address_____w1|
   Smi:                    |___int31_value____0|
   
   64-bit V8
               |----- 32 bits -----|----- 32 bits -----|
   Pointer:    |________________address______________w1|
   Smi:        |____int32_value____|0000000000000000000|

https://v8.dev/blog/pointer-compression


如下所述,英特尔发布了PML5,它提供了一个57 位的虚拟地址空间,如果你在这样的系统上,你只能使用 7 个高位

不过,您仍然可以使用一些解决方法来获得更多免费位。首先,您可以尝试在 64 位操作系统中使用 32 位指针。在 Linux 中,如果允许 x32abi,则指针只有 32 位长。在 Windows 中,只需清除/LARGEADDRESSAWARE标志,指针现在只有 32 位有效位,您可以将高 32 位用于您的目的。请参阅 如何在 Windows 上检测 X32?. 另一种方法是使用一些指针压缩技巧:V8 中的压缩指针实现与 JVM 的压缩 Oops 有何不同?

您可以通过请求操作系统仅在低区域分配内存来进一步获得更多位。例如,如果您可以确保您的应用程序永远不会使用超过 64MB 的内存,那么您只需要一个 26 位地址。如果所有分配都是 32 字节对齐的,那么您还有 5 位要使用,这意味着您可以在指针中存储 64 - 21 = 43 位信息!

我想ZGC就是一个例子。它仅使用 42 位进行寻址,允许 2 42字节 = 4 × 2 40字节 = 4 TB

因此,ZGC 仅保留从地址 4TB 开始的 16TB 地址空间(但实际上并未使用所有这些内存)。

ZGC 初探

它使用指针中的位,如下所示:

 6                 4 4 4  4 4                                             0
 3                 7 6 5  2 1                                             0
+-------------------+-+----+-----------------------------------------------+
|00000000 00000000 0|0|1111|11 11111111 11111111 11111111 11111111 11111111|
+-------------------+-+----+-----------------------------------------------+
|                   | |    |
|                   | |    * 41-0 Object Offset (42-bits, 4TB address space)
|                   | |
|                   | * 45-42 Metadata Bits (4-bits)  0001 = Marked0
|                   |                                 0010 = Marked1
|                   |                                 0100 = Remapped
|                   |                                 1000 = Finalizable
|                   |
|                   * 46-46 Unused (1-bit, always zero)
|
* 63-47 Fixed (17-bits, always zero)

有关如何执行此操作的更多信息,请参阅


旁注:与指针相比,在键值很小的情况下使用链表会浪费大量内存,而且由于缓存局部性不好,它也会变慢。事实上,你不应该在大多数现实生活中的问题中使用链表

于 2013-08-25T07:04:34.107 回答
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 进行逻辑或运算(无变化)。

于 2020-05-21T08:18:13.430 回答
4

我猜没有人提到在这种情况下可能使用位字段(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 位指针都由编译器完成(当然,在运行时执行)。

于 2020-08-25T14:10:33.417 回答
2

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

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

于 2013-04-25T10:00:55.773 回答
-2

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

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

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

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

于 2013-04-24T18:27:34.310 回答