C从怪异机器上获得的可移植性教训

2022-02-22 02:59:28

在本文中,我们将继续从4位微控制器到房间大小的大型机的旅程,并学习如何将C移植到每一个大型机上,帮助人们将语言的本质与其诞生的环境区分开来。我找到了这篇文章的技术手册和视频,帮助每台电脑焕然一新。

令人惊讶的是,通过仔细编写可移植的ANSI C代码并坚持使用标准库函数,您可以创建一个程序,在几乎所有这些奇怪的系统上编译和工作,而无需修改。

我希望接触到这些例子将有助于您更方便地编写代码,并消除这样的信念,即具有多核、缓存层次结构和流水线的当前计算机在某种程度上对C来说太陌生了。一种足以处理旧计算机多样性的语言,足以处理当今相对同质的CPU。

为了准备这篇文章,我从亨利·拉比诺维茨(Henry Rabinowitz)的《便携式C》(Portable C)一书中回顾,寻找能够说明他指出的每一个缺陷的体系结构。你应该阅读这本书,了解作者所说的“C-World”,一个C程序执行的语义模型。

虽然这段视频没有显示运行中的真实计算机,但您仍然可以看到其中一个控制面板的形状。视频中的这位绅士对它有着不切实际的魅力。

该建筑的第一个不寻常之处是它的字数。您可能熟悉具有两位大小幂的数据类型,但这些Unisys系列使用的是9的倍数!字长为18位,平台的C编译器使用:

为了让事情变得更有趣,奇怪大小的整数使用1的补码二进制算法。没错,在这个系统中,正零和负零有不同的值。(CDC的计算机也使用了一个的补充。)

36位整数可以保存很多数据,但是猜猜在这个体系结构中它们不能保存什么?指针值。Unisys 2200的C manaul第8.6.1节规定:

UC中的指针不能视为整数。UC指针是两个字的结构,第一个字是银行的基虚拟地址(VA),第二个字是位字指针。由于2200硬件没有字节指针,所以位字指针是必需的;2200硬件中的基本指针是一个字(36位)VA指针,只能指向字。UC指针的位字部分在字的前6位有位偏移,在字的下24位有字偏移。如果将UC指针转换(强制转换)为36位整数(int、long或unsigned),则位偏移量将丢失。将其转换回C指针会导致它指向单词边界。如果在将整数转换回指针之前将其加1,指针将指向下一个单词,而不是下一个字节。36位整数不能保存UC指针中的所有信息。

如果您认为常规指针要求很高,那么第8.6.2节说函数指针需要整整八个单词!

函数指针的长度为8个字,格式完全不同。UC生成的代码实际上(当前)只使用了8字函数指针中的两个字。(第二和第三个词。)(其他UCS语言如FORTRAN和COBOL使用了更多的单词。)可以将UC数据指针转换为函数指针,将函数指针转换为数据指针,并且不会丢失任何信息。包含信息的两个单词只是来回移动。

最后,如果你认为Unisys仅限于历史,你基本上是对的,但并非完全正确。他们仍在制造和销售使用2200体系结构的“ClearPath Dorado”。

与之前的Unisys机器一样,ClearPath具有不同寻常的字长。以下是ClearPath C编译器的整数数据类型大小:

这台机器既不使用2的补码,也不使用1的补码有符号算术,而是使用符号大小形式。

一台可靠的旧机器,拥有一个忠诚的社区。与当今大多数计算机不同的是,这是一种相当普通的体系结构。默认情况下,char数据类型是无符号的。最后,这种体系结构的标准编译器保证从左到右计算函数参数。

C的可移植性如此之强,以至于有人为一台本机运行Lisp的计算机编写了一个编译器——Symbolics C。瞄准Symbolics Lisp机器需要一些创造力。例如,指针被表示为一对,由对列表的引用和列表中的数字偏移组成。具体来说,空指针是<;零,0>;,基本上是一个没有偏移量的零列表。当然不是按位零整数值。

字长为16位。虽然指令必须位于16位边界上,但对数据没有对齐要求。以下是计算机上编译器定义的整数类型的大小:

这种处理器被应用到许多游戏机、嵌入式系统和打印机中。这是一个非常正常的体系结构,尽管big-endian的编译器默认为无符号字符。指针(32位)的大小也不同于整数(16位)。

一个重要的怪癖是机器对数据对齐非常敏感。处理器有两个字节的粒度,缺乏处理未对齐地址的电路。当出现这样一个地址时,处理器会抛出一个异常。最初的Mac(也基于68000)通常会要求用户在校准错误后重新启动机器。(类似地,一些Sparc机器会因对齐问题引发SIGBUS异常。)

这台机器对字符和整数指针使用不同的编号方案。内存中的同一位置必须由不同的地址引用,具体取决于指针类型。char*和int*之间的转换实际上会改变指针内的地址。克里斯·托雷克讲述了细节。

这台机器提供了另一个警告故事,关于试图像处理整数一样处理指针值。在这种架构中,char*或void*是秘密的字指针,其偏移量存储在三个未使用的高阶位中。因此,将char*作为整数值递增将移动到下一个单词,但保持相同的偏移量。

值得注意的是使用了非位零的空指针地址。特别是,它使用段07777,空指针的偏移量为0。(一些霍尼韦尔公牛大型机使用06000作为NULL指针值,这是非零NULL的另一个例子。)

DECstation使用R3000处理器。它可以由程序员自行决定切换到小端或大端模式。一个怪癖是,处理器会引发有符号整数溢出异常,这与许多其他处理器不同,它们会自动换行为负值。因此,允许有符号整数溢出(例如在循环中)是不可移植的。

这台计算机实际上是ARM架构的起源,我们通常在手机和Arduinos中找到这种架构。Acorn特别使用ARM2,具有32位数据总线和26位地址空间。与摩托罗拉68000一样,ARM2为未对齐的内存访问引发了SIGBUS异常。(请注意,Arduino是仍然使用16位整数的编译器的一个实例。)

每一位写英特尔286编程的人都说它的分段内存架构是多么痛苦。每个内存段的地址可达64KB,这是C可以为每个数据对象分配的最大连续内存区域。(因此,在这种体系结构中,size_t小于unsigned int。)

因为内存中任何单词的完整地址都是由一个段和偏移量指定的,所以有4096种方法可以通过两者的某种组合来引用它。(例如,地址0x1234可以引用为0123:0004、0122:0014等)另外,相邻声明的变量可能位于不同的段中,在内存中相距很远。这打破了人们使用的一些非常不可取的伎俩,比如通过记忆设置地址之间的整个内存范围来将多个变量块归零。

尽管有这种尴尬,个人电脑还是很热,到1983年字节杂志(第8卷,第8期)发现IBM PC有九种不同的C编译器!我找到了其中一款产品Lattice C的手册。它与IBM其他产品(如System 370)上使用的编译器相同。

在晶格C中,short和int都是16位,但long是32位。默认情况下,Char是有符号的,当然x86是little endian。

按照记忆并发症的主题,进入8051。它是一个微控制器,采用“哈佛体系结构”这意味着它与连接到同一系统的不同类型的内存进行通信。它对ROM空间使用面向字的寻址,对RAM空间使用面向字节的寻址。它需要不同大小的指针。

许多地址模棱两可,可能有意义地指向RAM或ROM库。Crossware等8051编译器使用稍大的“通用”指针,将内存类标记为高位字节,以解决歧义。

Saturn系列是Hewlett-Packard在20世纪80年代为可编程科学计算器和微型计算机开发的4位微处理器。上面的视频显示了HP-71B计算器,它实际上更像是一台包装奇特的通用计算机。你可以通过某种读卡器插槽压缩一条磁条,将大量数据加载到其中。

土星处理器没有硬件指令来执行有符号算术。必须使用其他汇编指令的组合来模拟。因此,无符号数运算更有效。char默认为未签名也就不足为奇了。

记忆很有趣。它的地址是基于半字节的,可以寻址1M半字节=512Kb。指针为20位,但存储为32位。土星C数据类型非常正常:

这是最早的低成本8位微处理器之一,它被用于各种系统,包括苹果II、Commodore 64和任天堂娱乐系统。这个处理器完全不适合C编译器。查看CC65编译器的疯狂优化建议。

程序集中没有乘法或除法操作,必须与其他指令一起模拟。

访问任何高于“零页”(0x0到0xFF)的地址都会导致性能下降。

然而,它在一个周期内访问内存,因此程序员可以使用零页作为256个8位寄存器的池。

6502有助于揭示便携性的优势,即C的“奢侈品”过于昂贵的地方。

C星球的家园。没什么好说的,因为事情进展顺利。真正的惊喜发生在将PDP代码移植到其他机器上时。所有类型和整数的指针都可以在不强制转换的情况下互换。

这台机器的一个奇怪之处是,虽然16位的字是以小尾端存储的,但32位长的整数使用奇怪的混合尾端格式。当存储在PDP-11中时,字符串“Unix”中的四个字节如果被解释为big-endian,则被安排为“nUxi”。事实上,当将代码从PDP移植到big-endian机器时,会产生加扰字符串本身。

VAX就像一个32位的PDP。这是PDP进化中的下一台机器。人们喜欢为VAX编写代码,因为它有漂亮的扁平内存和各种类型的统一指针。人们非常喜欢它,以至于“VAXocentric”一词指的是那些对体系结构太过熟悉,又懒得去了解其他计算机的不同之处的人的草率编码。

x86-64的汇编在外观上与VAX相似,人们最初认为VAX会比英特尔更持久。事实证明,这是不正确的,因为“微型计算机攻击”摧毁了大型机和小型计算机市场。

与IBM 360、PDP-11、Interdata 8/32等以前的体系结构不同,程序在正确对齐数据的情况下运行得更快,但没有严格的对齐要求。大小和对齐属性在逻辑上是独立的。VAX-11 C编译器将地址边界上的所有基本数据类型对齐,地址边界是每种类型大小的倍数。

其他事实:VAX C编译器不能保证从左到右计算函数参数。字符是默认签名的。PDP接受零除法并返回股息,但VAX会导致无法掩盖的陷阱。

贝尔实验室写了一篇关于将程序从PDP移植到VAX的有趣报告,他们的一些建议被ANSI C采纳。

如果这些数字化的乐趣让你想学习更多关于编写可移植代码的知识,那么最好的学习地点就是关于这一主题的优秀书籍之一。前面提到的亨利·拉比诺维茨(Henry Rabinowitz)的作品很棒,马克·霍顿(Mark Horton)的作品也是如此。好书是C语言的另一个优势。与新的流行语言不同,C语言已经存在了足够长的时间,积累了专业的、广受好评的文献。