【问题标题】:C Pointer Arithmetic for Unusual Architectures用于不寻常架构的 C 指针算法
【发布时间】:2018-06-04 19:10:46
【问题描述】:

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

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

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

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

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

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

【问题讨论】:

  • C 表示sizeof char 始终为一。
  • @Dschumanji sizeofsizeof(char)s 形式返回大小
  • @Dschumanji 是的,完全正确。 sizeof char 始终为 1,即使在字符为 24 位宽的 24 位可寻址音频 DSP 上也是如此。
  • 没有。它将sizeof(char) 定义为等于1。
  • 对于sizeof,标准规定“sizeof 运算符产生其操作数的大小(以字节为单位)......”然后“当 sizeof 应用于具有 char 类型的操作数时, unsigned char ,或 signed char ,(或其合格版本)结果为 1"。

标签: c pointers language-lawyer pointer-arithmetic


【解决方案1】:

指针会等于 3 吗?

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

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

所以当你这么说的时候

pointer = pointer + 1;

会使指针等于1,这是错误的。标准没有说明这一点。

在大多数系统上,char 是 8 位的,指针是(虚拟)内存地址,引用 8 位可寻址内存位置。在这样的系统上,增加一个 char 指针会使指针值(也就是内存地址)增加 1。但是,在不寻常的架构上,没有办法知道。

但是,如果您有一个系统,其中每个内存地址引用 4 位并且一个 char 是 12 位,那么++pointer 将指针增加 3 似乎是一个不错的猜测。

【讨论】:

  • 感谢您的回复!我认为这可能是迄今为止最好的答案。所以即使对于 char *,加 1 会给出下一个地址可能不是普遍正确的吗?
  • 所有系统上,一个字符是一个字节,因为这是 C 标准中术语 byte 的定义。
  • @Dschumanji 该地址可能是 3 个半字节或任何您的虚构计算机使用的地址。但增加了 1 个字节。
  • 这个答案的最后一段是错误的。 C 标准不支持内存为 4 位可寻址且字符为 12 位的系统。
  • @user3386109 好的,谢谢,这些符合我的记忆,但他们没有说字节必须是 最小 可寻址单元,因此我不认为他们排除了可寻址单元小于最小 C 对象的机器。
【解决方案2】:

指针以它们“指向”的数据类型的最小宽度递增,但不能保证准确地递增到该大小。

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

因此,一般情况下,您不能假设该指针等于 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 边界,避免额外的计算。这意味着在许多情况下,未仔细对齐的数据可能包含未使用的字节“洞”。

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

【讨论】:

  • 几乎每个 8 位 CPU 和大多数 16 位都不会填充结构,因此my_pointer++ 在这些系统上确实会将地址增加 3 个字节。
  • 更不用说结构本身,如果它被填充,它的填充会反映在它的sizeof中。
  • @ChristianGibbons 你的说法并不总是正确的,这就是为什么会有像 stackoverflow.com/q/119123/302139 这样的 Stackoverflow 问题。当然,大多数人通常不会通过请求以 WORD 对齐结尾、中间有填充的内容来解决极端情况。但是,如果最后有填充,当您发现它们不匹配时,这会是一个令人不快的惊喜。
  • @Lundin 我很乐意同意 8 位系统,但对于 16 位系统,如果内存总线是 16 位宽并且不支持非对齐内存访问,那么 3 似乎会是一个不可能的答案。
  • 这里的重点是指针算法不会选择项目的大小。如果存在对齐限制,示例结构的大小为 4。填充被添加到结构本身,无论它是否涉及算术。这是为了确保它总是被分配到一个对齐的地址。
【解决方案3】:

char *pointer = 0;
执行后指针等于1

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

除了空指针之外,C 语言希望您通过首先指向一个数组来进行指针运算。或者在char 的情况下,您还可以指向一大块通用数据,例如结构。其他所有内容,例如您的示例,都是未定义的行为。

本机的 C 实现将 CHAR_BIT 定义为等于 12

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

即使从学习的角度来看,虚拟计算机也毫无意义。我建议改为专注于现实世界的计算机。

【讨论】:

  • 好点。如果有人想看看一些奇怪的、现实世界的计算机。我推荐 4004(4 位字)、Cyber​​ Mainframes(60 位字、6 位字节)、DEC 10 和 DEC 20(36 位字、9 位字节)等等en.wikipedia.org/wiki/Word_(computer_architecture) 然后是可变字长体系结构(使用停止值来结束这个词),如早期的 IBM 系统。简而言之,有很多奇怪的东西需要仔细观察,而不用想出新的东西。
  • @Lundin 出于这个确切原因,我对使用 0 作为示例犹豫不决。当标准没有将字节定义为正好 8 位时,我的字节示例如何被定义为相互矛盾的位?您的答案和其他一些人表明指针递增 3 是一个明智的答案,但不是标准必须强制执行的。
  • @Dschumanji 8 位字节现在很流行,因为 IBM 在 IBM 360 中大规模采用了这种大小,IBM 360 是第一批将文本处理作为主要功能的计算机之一。 ASCII 适合 8 位,留下几 (128) 个额外字符用于他们梦寐以求的任何需要。
  • @Dschumanji 在具有 4 位可寻址单元但 12 位字符的系统上,您不能真正以合理的方式使用 C。我想他们可以选择uint4_t 作为妥协。通过将字符指针增加 1,可以将地址增加 1 个字节。给定系统上的 1 字节有多大,C 标准没有说明。它只是说它必须至少是 8 位。
【解决方案4】:

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

在“传统”机器上——实际上在绝大多数运行 C 的机器上——CHAR_BIT 只是 机器上存储单元的宽度,所以答案是问题是空洞的“是”(因为CHAR_BIT / CHAR_BIT 是1。)。

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

C 的定义是这样说的:

  • sizeof(char) 正好是 1。

  • CHAR_BITchar中的位数至少为8。也就是说,就C而言,一个字节不能小于8位。 (它可能更大,这让很多人感到惊讶,但这里不关心我们。)

  • 强烈建议(如果不是明确要求)char(或“字节”)是机器的“最小可寻址单元”或类似单位。

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

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

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

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

生成将@​​987654335@ 后面的地址实际增加2 的指令。

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

观看随后的烟花也会很有趣,因为在这种机器上过于聪明的程序员使用类型双关语技术来获取指针的内部值,以便他们可以将指针递增 实际上 1——不是“正确”添加 1 总是会生成的 2——这样他们可以通过访问一个字节的奇数 nybble 来让他们的朋友惊讶,或者通过询问有关它的问题来混淆 SO 上的常客。 “我刚刚将 char 指针增加了 1。为什么 %p 显示的值大了 2?”

【讨论】:

  • 谢谢你的回答,史蒂夫。我在规范中没有看到任何暗示 char 应该是最小可寻址单元的大小的内容。第 3.6 节第 1 段只是说一个字节必须是一个可寻址的存储单元。 sizeof(char) 为 1 字节的规定意味着 char 只需是某个可寻址存储单元的大小。此外,该存储单元在 CHAR_BIT 中定义。
  • 我打错了。我的意思是说“此外,该存储单元的大小由 CHAR_BIT 定义”。该标准似乎旨在将 char 的大小定义为衡量非位字段对象大小的基本单位,而不是机器的最小可寻址单元。似乎它可以容纳奇怪架构的可能性,例如我的问题中假设的架构。
  • 正如其他人所说,没有规定将 1 加到 char 指针必须将地址恰好增加 1。它可能需要增加 3,就像在假设的架构中一样。
  • @Dschumanji 没有,没有规定,但是既然大家都知道sizeof(char) 是1(确实,因为新手经常被骂,因为明确乘以sizeof(char)“因为你不必并且这是不必要和令人困惑的”),如果将 1 加到 char 指针没有加 1,很多人的脑袋会爆炸,相信我。 :-)
【解决方案5】:

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

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

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


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

§3.6 标准中使用的“字节”的定义

[A byte is an] 可寻址的数据存储单元,大到足以容纳 执行环境的基本字符集的任何成员。

注意 1 可以表示每个单独字节的地址 对象的唯一性。

注意 2 字节由连续的位序列组成,数字 其中是实现定义的。最低有效位是 称为低位;最高有效位称为 高位。

§5.2.4.2.1 将 CHAR_BIT 描述为

不是位域(字节)的最小对象的位数

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

[...] 除了位域,对象由连续的序列组成 一个或多个字节,其数量、顺序和编码为 明确指定或实现定义。

[...] 存储在任何其他对象类型的非位域对象中的值 由 n × CHAR_BIT 位组成,其中 n 是该对象的大小 类型,以字节为单位。

§6.2.8 限制对象的对齐方式

完整的对象类型有对齐要求 对该类型对象的地址的限制 分配。对齐是实现定义的整数值 表示连续地址之间的字节数 可以分配给定的对象。

有效的对齐方式仅包括 _Alignof 返回的值 基本类型的表达式,加上一个额外的 实现定义的一组值,可能为空。 每个 有效对齐值应为 2 的非负整数幂

§6.5.3.2 指定 sizeofchar,因此是一个“字节”

当 sizeof 应用于 char 类型的操作数时,无符号 char,或signed char,(或其合格版本)结果是 1.

【讨论】:

  • 在古怪的 DSP 上,他们说一个字节是 16 位或 24 位。任何为所说的 DSP 编写 C 而不是汇编程序的人都可能犯了一个大错误……
  • 好的,现在您尝试将其移植到 8 位 PIC :)
  • 我是在开玩笑,但用 C 语言编写的主要目的是您希望能够(导入)(导入)(部分)代码。无论如何,我都不敢为这种奇特的设备做这件事。传统上,DSP:s 的所有代码都是用汇编程序编写的,因为它们总是非常特定于任务。是的,有用于 DSP 的 C 编译器:s...但它的主要优点是您可以让 C 程序员完成这项工作,而无需学习古怪的 DSP 汇编程序。
  • 这并没有说 C 字节必须是 最小 可寻址单元。
  • @SteveSummit 就在那儿,用普通的律师语言(问题 is 标记为语言律师)。 §3.6 注 1:“可以唯一地表示对象的每个单独字节的地址。” 所以字节必须是可寻址的。 §5.2.4.2.1: "smallest object [...] (byte)" 一个字节是最小的对象。 §6.2.6.1 “任何其他对象类型由 [...] n [...] 个字节组成” 确认了这一点,因此字节是最小的可寻址对象,因为所有其他对象都必须是多个字节数。
【解决方案6】:

以下代码片段演示了 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
}

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-04-01
    • 2018-12-03
    • 2016-06-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多