C 的内存模型,以及它对指针算法的使用,似乎是对平面地址空间的建模。16 位计算机使用分段内存访问。16 位 C 编译器如何处理这个问题并从 C 程序员的角度模拟平面地址空间?例如,以下代码在 8086 上大致会编译成哪些汇编语言指令?
long arr[65536]; // Assume 32 bit longs.
long i;
for(i = 0; i < 65536; i++) {
arr[i] = i;
}
16 位 C 编译器如何处理这个问题并从 C 程序员的角度模拟平面地址空间?
他们没有。相反,他们使 C 程序员可以看到分段,通过使用多种类型的指针来扩展语言:near
、far
和huge
. near
指针只是一个偏移量,而指针far
是huge
一个组合的段和偏移量。有一个编译器选项来设置内存模型,它决定了默认指针类型是近还是远。
即使在今天,在 Windows 代码中,您也经常会看到类似LPCSTR
(for const char*
) 的 typedef。“LP”是 16 位时代的遗留物;它代表“长(远)指针”。
真正的 16 位环境使用可以到达任何地址的 16 位指针。示例包括 PDP-11、6800 系列(6802、6809、68HC11)和 8085。这是一个干净高效的环境,就像简单的 32 位架构一样。
80x86 系列在所谓的“实模式”中强加给我们一个混合的 16 位/20 位地址空间——原生 8086 寻址空间。处理这个问题的常用机制是将指针类型增强为两种基本类型,near
(16 位指针)和far
(32 位指针)。代码和数据指针的默认值可以通过“内存模型”批量设置: tiny
、small
、compact
、medium
、far
和huge
(一些编译器不支持所有模型)。
tiny
内存模型对于整个空间(代码 + 数据 + 堆栈)小于 64K 的小程序很有用。所有指针(默认情况下)都是 16 位或near
; 指针与整个程序的段值隐式关联。
small
模型假设data+stack小于64K且在同一个segment ;代码段仅包含代码,因此最多也可以有 64K,最大内存占用为 128K。代码指针near
与 CS(代码段)隐式关联。数据指针也near
与 DS(数据段)相关联。
该medium
模型有多达 64K 的数据 + 堆栈(就像小的一样),但可以有任意数量的代码。数据指针为 16 位,隐式绑定到数据段。代码指针是 32 位far
指针,并且根据链接器设置代码组的方式具有段值(令人讨厌的簿记麻烦)。
该compact
模型是对媒介的补充:少于 64K 的代码,但任何数量的数据。数据指针是far
,代码指针是near
。
在large
orhuge
模型中,指针的默认子类型是 32 位或far
. 主要区别在于巨大的指针总是自动标准化,因此增加它们可以避免 64K 环绕的问题。看到这个。
C 内存模型绝不意味着平坦的地址空间。它从来没有。事实上,C 语言规范是专门为允许非平面地址空间而设计的。
在使用分段地址空间的最简单的实现中,最大连续对象的大小将受到段大小的限制(在 16 位平台上为 65536 字节)。这意味着size_t
在这样的实现中将是 16 位,并且您的代码根本无法编译,因为您试图声明一个大小大于允许的最大值的对象。
更复杂的实现将支持所谓的大内存模型。你看,在分段内存模型上处理任何大小的连续内存块确实没有问题,它只需要在指针算术上做一些额外的努力。因此,在巨大的内存模型中,实现会做出额外的努力,这会使代码变慢一些,但同时允许寻址几乎任何大小的对象。因此,您的代码将编译得非常好。
在 DOS 16 位中,我不记得能做到这一点。您可以拥有多个每个 64K(字节)的东西(因为可以调整段并将偏移量归零)但不记得是否可以使用单个数组跨越边界。在我们可以编译 32 位 DOS 程序(在 386 或 486 处理器上)之前,您可以随意分配您想要的任何内容并尽可能深入到数组中的平坦内存空间不会发生。也许除了 microsoft 和 borland 之外的其他操作系统和编译器可以生成大于 64kbytes 的平面数组。Win16我不记得win32打到之前的自由,也许我的记忆越来越生锈了……无论如何,你有一个兆字节的内存是幸运的或富有的,256kbyte或512kbyte的机器并非闻所未闻。你的软盘驱动器最终只有几分之一兆到 1.44 兆,
我记得我在学习 DNS 时遇到的特殊挑战,当时您可以下载地球上所有注册域名的整个 DNS 数据库,实际上您必须建立自己的 dns 服务器,这在当时几乎是拥有网络所必需的地点。那个文件有 35 兆字节,而我的硬盘有 100 兆字节,加上 dos 和 windows 处理了其中的一些。可能有 1 或 2 兆内存,当时可能已经能够执行 32 位 dos 程序。如果是我想解析我在多次传递中完成的 ascii 文件,但每次传递都必须将输出转到另一个文件,并且我必须删除先前的文件以在磁盘上为下一个文件留出空间。标准主板上有两个磁盘控制器,一个用于硬盘,一个用于 cdrom 驱动器,这东西又不便宜,
甚至还有用 C 读取 64kbytes 的问题,你通过 fread 你想在 16 位 int 中读取的字节数,这意味着 0 到 65535 而不是 65536 字节,如果你没有读取均匀大小的扇区,性能会急剧下降,所以你一次只读取 32kbytes 以最大限度地提高性能,直到进入 dos32 天,当您最终确信传递给 fread 的值现在是 32 位数字并且编译器不会砍掉高 16 位时才出现 64k使用低 16 位(如果您使用了足够多的编译器/版本,这种情况经常发生)。我们目前在 32 位到 64 位的转换中遇到了与 16 位到 32 位的转换类似的问题。最有趣的是来自像我这样的人的代码,他们了解到从 16 位到 32 位 int 改变了大小,但是 unsigned char 和 unsigned long 没有,所以你适应并且很少使用 int ,这样你的程序就可以在 16 位和 32 位上编译和工作。(那一代人的代码在其他经历过它并使用相同技巧的人中脱颖而出)。但是对于 32 到 64 的转换,情况正好相反,没有重构为使用 uint32 类型声明的代码会受到影响。
看了刚刚进来的wallyk的回答,缠绕的巨大指针确实敲响了警钟,也不总是能编译成巨大的。small 是我们今天熟悉的平面内存模型,并且与今天一样简单,因为您不必担心分段。因此,在可能的情况下编译为小型是可取的。您仍然没有很多内存或磁盘或软盘空间,因此您通常不会处理那么大的数据。
并同意另一个答案,段偏移量是 8088/8086 intel。整个世界还没有被英特尔统治,所以有其他平台只是拥有平坦的内存空间,或者使用其他可能在硬件(处理器之外)的技巧来解决问题。由于段/偏移量,英特尔能够使用 16 位的东西比它应该拥有的时间更长。Segment/offset 有一些你可以用它做的很酷和有趣的事情,但它和其他任何事情一样痛苦。您要么简化生活并生活在平坦的内存空间中,要么不断担心段边界。
真正确定旧 x86 上的地址大小有点棘手。您可以说它是 16 位的,因为您可以对地址执行的算术运算必须适合 16 位寄存器。您也可以说它是 32 位的,因为实际地址是根据 16 位通用寄存器和 16 位段寄存器计算的(所有 32 位都是有效的)。您也可以只说它是 20 位,因为段寄存器向左移动 4 位并添加到 gp 寄存器中以进行硬件寻址。
实际上,您选择其中哪一个并不重要,因为它们都是 c 抽象机的大致相等的近似值。一些编译器允许您选择每次编译时使用的内存模型,而其他编译器只假设 32 位地址,然后仔细检查可能溢出 16 位的操作是否发出正确处理这种情况的指令。
查看此维基百科条目。关于远指针。基本上,它可以指示一个段和一个偏移量,从而可以跳转到另一个段。