异常架构的 C 指针算法

C Pointer Arithmetic for Unusual Architectures

我正在尝试更好地理解 C 标准。我特别感兴趣的是指针算法如何在一个不寻常的机器架构的实现中工作。

假设我有一个带有 64 位宽寄存器的处理器,它连接到 RAM,其中每个地址对应一个 8 位宽的单元。本机的 C 实现定义 CHAR_BIT 等于 8。假设我编译并执行以下代码行:

char *pointer = 0;
pointer = pointer + 1;

执行后指针等于1,给人的印象是一般char类型的数据对应机器上最小的可寻址内存单元。

现在假设我有一个带有 12 位宽寄存器的处理器,它连接到 RAM,其中每个地址对应一个 4 位宽的单元。这台机器的 C 实现定义 CHAR_BIT 等于 12。假设为这台机器编译和执行相同的代码行。指针是否等于 3?

更一般地说,当您增加一个指向 char 的指针时,地址是否等于 CHAR_BIT 除以机器上存储单元的宽度?

指针按其数据类型宽度的最小值递增 "point to",但不能保证精确递增到该大小。

出于内存对齐的目的,很多时候指针可能递增到超过最小宽度的下一个内存字对齐。

因此,一般来说,您不能假设此指针等于 3。它很可能是 3、4 或某个更大的数。

这是一个例子。

struct char_three {
   char a;
   char b;
   char c;
};

struct char_three* my_pointer = 0;
my_pointer++;

/* I'd be shocked if my_pointer was now 3 */

内存对齐是机器特定的。不能一概而论,除了大多数机器将 WORD 定义为可以与总线上的内存获取对齐的第一个地址。有些机器可以指定与总线获取不一致的地址。在这种情况下,选择跨越对齐的两个字节可能会导致加载两个 WORDS。

大多数系统不会在不抱怨的情况下接受非对齐边界上的 WORD 加载。这意味着如果需要最大密度,则应用一些样板程序集将提取转换为后续的 WORD 边界。

大多数编译器更喜欢速度而不是最大数据密度,因此他们对齐结构化数据以利用 WORD 边界,避免额外的计算。这意味着在许多情况下,未仔细对齐的数据可能包含 "holes" 未使用的字节。

如果您对上述摘要的详细信息感兴趣,可以继续阅读 Data Structure Alignment,其中将讨论对齐(以及因此)填充。

Would pointer be equal to 3?

好吧,标准没有说明指针是如何实现的。该标准说明了当您以特定方式使用指针时会发生什么,但没有说明指针的值应该是什么。

我们所知道的是,将 1 加到一个 char 指针,将使指针指向下一个 char 对象——无论它在哪里。但是没有关于指针值的内容。

所以当你这么说时

pointer = pointer + 1;

会让指针等于1,这是错误的。该标准对此没有任何说明。

在大多数系统上,char 是 8 位的,指针是引用 8 位可寻址内存位置的(虚拟)内存地址。在这样的系统上,增加一个 char 指针将使指针值(也称为内存地址)增加 1。但是,在 - 不寻常的体系结构 - 上没有办法告诉。

但是如果你有一个系统,其中每个内存地址引用 4 位而一个 char 是 12 位,那么 ++pointer 将指针增加三似乎是一个很好的猜测。

char *pointer = 0;
After execution, pointer is equal to 1

不一定。 这种特殊情况为您提供了一个空指针,因为 0 是一个空指针常量。严格来说,这样的指针不应该指向有效的对象。如果您查看指针中存储的实际地址,它可以是任何内容。

撇开空指针不谈,C 语言希望您首先指向一个数组来进行指针运算。或者在 char 的情况下,您还可以指向一大块通用数据,例如结构。其他一切,就像你的例子一样,都是未定义的行为。

An implementation of C for this machine defines CHAR_BIT to be equal to 12

C 标准定义 char 等于一个字节,所以你的例子有点奇怪和矛盾。指针算法总是会增加指向数组中下一个对象的指针。该标准根本没有真正谈到地址的表示,但是您虚构的示例会明智地将地址增加 12 位,因为那是 char.

的大小

即使从学习的角度来看,讨论虚构的计算机也毫无意义。我建议改为关注真实世界的计算机。

似乎这个问题的混乱来自于 C 标准中的单词 "byte" 没有典型的定义(8 位)。具体来说,C 标准中的 "byte" 一词表示位的集合,其中位数由实现定义的常量 CHAR_BITS 指定。此外,C 标准定义的 "byte" 是 C 程序可以访问的 最小可寻址对象

这留下了一个问题,即 "addressable" 的 C 定义与 "addressable" 的硬件定义之间是否存在一对一的对应关系。换句话说,硬件是否可以寻址小于 "byte" 的对象?如果(如在 OP 中)a "byte" 占用 3 个地址,则意味着 "byte" 访问具有对齐限制。也就是说 3 和 6 是有效的 "byte" 地址,但 4 和 5 不是。讨论对象对齐的第 6.2.8 节禁止这样做。

这意味着 OP 提出的体系结构 受 C 规范支持。特别是,当 CHAR_BIT 等于 12 时,实现可能没有指向 4 位对象的指针。


以下是 C 标准的相关部分:

§3.6 标准

中使用的"byte"的定义

[A byte is an] addressable unit of data storage large enough to hold any member of the basic character set of the execution environment.

NOTE 1 It is possible to express the address of each individual byte of an object uniquely.

NOTE 2 A byte is composed of a contiguous sequence of bits, the number of which is implementation-defined. The least significant bit is called the low-order bit; the most significant bit is called the high-order bit.

§5.2.4.2.1 将 CHAR_BIT 描述为

number of bits for smallest object that is not a bit-field (byte)

§6.2.6.1 将所有大于 char 的对象限制为 CHAR_BIT 位的倍数:

[...] Except for bit-fields, objects are composed of contiguous sequences of one or more bytes, the number, order, and encoding of which are either explicitly specified or implementation-defined.

[...] Values stored in non-bit-field objects of any other object type consist of n × CHAR_BIT bits, where n is the size of an object of that type, in bytes.

§6.2.8 限制对象的对齐方式

Complete object types have alignment requirements which place restrictions on the addresses at which objects of that type may be allocated. An alignment is an implementation-defined integer value representing the number of bytes between successive addresses at which a given object can be allocated.

Valid alignments include only those values returned by an _Alignof expression for fundamental types, plus an additional implementation-defined set of values, which may be empty. Every valid alignment value shall be a nonnegative integral power of two.

§6.5.3.2 指定 sizeof 一个 char,因此一个 "byte"

When sizeof is applied to an operand that has type char, unsigned char, or signed char, (or a qualified version thereof) the result is 1.

When you increment a pointer to a char, is the address equal to CHAR_BIT divided by the width of a memory cell on the machine?

在 "conventional" 机器上 -- 实际上在绝大多数运行 C 的机器上 -- CHAR_BIT 只是 内存单元的宽度在机器上,所以问题的答案是空洞的 "yes" (因为 CHAR_BIT / CHAR_BIT 是 1.)。

内存单元小于 CHAR_BIT 的机器会非常非常奇怪——可以说与 C 的定义不兼容。

C 的定义是这样说的:

  • sizeof(char) 正好是 1。

  • CHAR_BIT,一个char中的位数至少为8,即对于C而言,一个字节不得小于8 位。 (可能更大,这对很多人来说是一个惊喜,但我们在这里不关心。)

  • 强烈建议(如果不是明确要求)char(或"byte")是机器的"minimum addressable unit"或类似的。

所以对于一台可以一次寻址 4 位的机器,我们将不得不为 sizeof(char)CHAR_BIT 选择不自然的值(否则可能希望是 24,分别),我们将不得不忽略类型 char 是机器的最小可寻址单元的建议。

C 不要求指针的内部表示(位模式)。可移植 C 程序最接近于使用指针值的内部表示做任何事情是使用 %p 将其打印出来——并且明确定义为实现定义。

所以我认为在“4 位”机器上实现 C 的唯一方法是使用代码

char a[10];
char *p = a;
p++;

生成实际上将 p 后面的地址增加 2 的指令。

%p 应该打印实际的原始指针值还是除以 2 的值,这将是一个有趣的问题。

观看随后的烟花也会很有趣,因为在这种机器上太聪明的程序员使用类型双关技术来获取指针的内部值,以便他们可以将它们递增 实际上 1 -- 不是 "proper" 加法 1 总是会生成的 2 -- 这样他们就可以通过访问一个字节的奇数 nybble 来让他们的朋友惊叹不已,或者让 SO 上的常客感到困惑通过询问有关它的问题。 "I just incremented a char pointer by 1. Why is %p showing a value that's 2 greater?"

下面的代码片段演示了 C 指针算法的 不变性 -- 不管 CHAR_BIT 是什么,不管硬件最小可寻址单元是什么,并且没有不管指针的实际位表示是什么,

#include <assert.h>
int main(void)
{
    T x[2]; // for any object type T whatsoever
    assert(&x[1] - &x[0] == 1); // must be true
}

并且由于sizeof(char) == 1根据定义,这也意味着

#include <assert.h>
int main(void)
{
    T x[2]; // again for any object type T whatsoever
    char *p = (char *)&x[0];
    char *q = (char *)&x[1];
    assert(q - p == sizeof(T)); // must be true
}

但是,如果在执行减法之前转换为整数,不变量就会消失:

#include <assert.h>
#include <inttypes.h>
int main(void);
{
    T x[2];
    uintptr_t p = (uintptr_t)&x[0];
    uintptr_t q = (uintptr_t)&x[1];
    assert(q - p == sizeof(T)); // implementation-defined whether true
}

因为通过将指针转换为相同大小的整数(反之亦然)执行的转换是实现定义的。我认为它必须是双射的,但我对此可能是错误的,绝对不需要保留上述任何不变量。