【问题标题】:What are near, far and huge pointers?什么是近、远和巨大的指针?
【发布时间】:2011-04-04 06:52:07
【问题描述】:

谁能用合适的例子向我解释这些指针......以及何时使用这些指针?

【问题讨论】:

  • 闻起来像作业
  • 我记得很久以前 Turbo-C 中的那些...
  • 它们只与 16 位英特尔平台相关;这是过时的。如果你需要在这个平台上维护代码或编写新代码,我很同情你。
  • 如果这是一个家庭作业问题,那么该学校真的需要为其 CS 教室购买新计算机。
  • '它们只与 16 位英特尔平台相关' erm,嵌入式平台、DSP 等呢?不敢相信这里的每个人都说这是旧的和过时的,请先做一些研究

标签: c++ c pointers x86 x86-16


【解决方案1】:

主要的例子是 Intel X86 架构。

Intel 8086 在内部是一个 16 位处理器:它的所有寄存器都是 16 位宽。但是,地址总线是 20 位宽 (1 MiB)。这意味着您无法将整个地址保存在寄存器中,从而将您限制为前 64 kiB。

英特尔的解决方案是创建 16 位“段寄存器”,其内容将左移四位并添加到地址中。例如:

DS ("Data Segment") register:  1234 h
DX ("D eXtended") register:   + 5678h
                              ------
Actual address read:           179B8h

这创造了 64 kiB 段的概念。因此,“近”指针将只是 DX 寄存器(5678h)的内容,除非 DS 寄存器已正确设置,否则将是无效的,而“远”指针是 32 位(12345678h,DS 后跟 DX)和将始终有效(但速度较慢,因为您必须加载两个寄存器,然后在完成后恢复 DS 寄存器)。

(正如下面的 supercat 所指出的,溢出的 DX 偏移量将“翻转”被添加到 DS 以获得最终地址。这允许 16 位偏移量访问64 kiB 段,而不仅仅是 DX 指向的位置 ± 32 kiB 的部分,就像在某些指令中使用 16 位相对偏移寻址的其他架构中所做的那样。)

但是,请注意,您可以有两个不同值但指向相同地址的“远”指针。例如,远指针 100079B8h 指向与 12345678h 相同的位置。因此,远指针上的指针比较是无效的操作:指针可能不同,但仍指向同一个地方。

这就是我认为 Mac(当时配备摩托罗拉 68000 处理器)毕竟不是那么糟糕的地方,所以我错过了巨大的指针。 IIRC,它们只是保证段寄存器中所有重叠位均为 0 的远指针,如第二个示例所示。

摩托罗拉的 6800 系列处理器没有这个问题,因为它们被限制为 64 kiB,当他们创建 68000 架构时,他们直接使用 32 位寄存器,因此从不需要近、远、或巨大的指针。 (相反,他们的问题是地址的低 24 位实际上很重要,所以一些程序员(臭名昭著的 Apple)会使用高 8 位作为“指针标志”,当地址总线扩展到 32 位(4 GiB)时会导致问题.)

Linus Torvalds 一直坚持到 80386,它提供了一种“保护模式”,地址是 32 位,段寄存器是地址的高半部分,不需要加法,从一开始就写 Linux只使用保护模式,没有奇怪的段的东西,这就是为什么你在 Linux 中没有近和远指针支持(以及为什么设计新架构的公司如果想要 Linux 支持就不会回到他们身边)。他们吃了罗宾的吟游诗人,欢欣鼓舞。 (耶...)

【讨论】:

  • 有趣的是,许多基于 68000 的程序最终都受到了 32K 的限制,而 8088 软件本来有 64K 的限制。鉴于内存通常很稀缺,并且 8088 可以在 68000 需要使用 32 位数量或接受 32K 限制的地方以 16 位数量获得,因此 8088 实际上是一个非常实用的设计。
  • 68k 的“32k 限制”是当您对分支和跳转使用 16 位相对偏移量时。如果您不尝试对代码段中的函数进行排序以尝试将事物保持在一起,那么您将放弃并在所有内容上设置 32k 限制,而您本来可以有 64k 限制。与 Intel 系统相比,它仍然不是灾难,因为 Intel 系统中不同值的指针实际上可能是相等的。再说一遍,这一切都已成为历史,即使是嵌入式系统在这些天进行 32 位寻址也没有问题。
  • 在经典的 Macintosh 上,许多数据结构被限制为 32K,因为操作系统选择使用 16 位类型而不是 32 位类型用于许多目的;我希望 68000 平台的许多内存受限软件(尤其是 16 位总线变体)也是如此。 68000 的 addr+disp16 指令都对位移进行符号扩展,以允许正位移和负位移。但是,8088 可以允许 16 位位移在单个对象内达到 +/-65535 字节,而不是 +/-32767 字节,前提是偏移适合一个段。
  • 经典的 Mac 限制可以追溯到他们为将东西打包到 128K 总 RAM 中所做的疯狂事情,例如使用系统拥有的指针中的(当时未使用的)高 8 位作为标志,以及事实上,Pascal 中的 integer 是 2 个字节。 (另一方面,Pascal 字符串为您提供 O(1) 字符串长度计算,字符串中包含空值的能力,并且几乎完全没有缓冲区溢出错误。)x86 系列让偏移量在段中“环绕”,但代价是让你使用段寄存器和近远指针。
  • 内存不是免费的,即使在今天,许多应用程序在为 32 位 x86 编译时会比为 x64 编译时运行得更快,尽管事实上 64 位模式具有更大的寄存器集.对于 64 位模式,我能看到的唯一显着性能下降是 64 位对象引用吞噬的缓存是 32 位对象引用的两倍。 8086 确实需要更多的段寄存器,但即便如此,我还是要说它在寻址 1MiB 的地址空间方面比之前或之后的任何其他 16 位架构(M68K 是 32 位架构)做得更好。
【解决方案2】:

远指针和大指针的区别:

我们知道默认情况下指针是near 例如:int *p 是一个near 指针。在 16 位编译器的情况下,near 指针的大小为 2 个字节。而且我们已经非常清楚,编译器的大小因编译器而异;它们只存储它所引用的指针地址的偏移量。仅由偏移量组成的地址范围为 0 - 64K 字节。

Farhuge 指针:

Farhuge 指针的大小为 4 个字节。它们存储段和指针引用的地址的偏移量。那么它们之间的区别是什么?

远指针的限制:

我们不能通过对其应用任何算术运算来更改或修改给定远地址的段地址。也就是说,通过使用算术运算符,我们不能从一个段跳转到另一段。

如果您将远地址增加超过其偏移地址的最大值而不是增加段地址,它将以循环顺序重复其偏移地址。这也称为环绕,即如果偏移量是0xffff,我们加1,那么它就是0x0000,类似地,如果我们将0x0000 减1,那么它就是0xffff,记住段没有变化。

现在我要比较大指针和远指针:

1.当远指针递增或递减时ONLY指针的偏移量实际上是递增或递减的,但在大指针的情况下,段和偏移值都会改变。

考虑以下示例,取自HERE

 int main()
    {
    char far* f=(char far*)0x0000ffff;
    printf("%Fp",f+0x1);
    return 0;
  }

那么输出是:

0000:0000

段值没有变化。

如果指针很大:

int main()
{
char huge* h=(char huge*)0x0000000f;
printf("%Fp",h+0x1);
return 0;
}

输出是:

0001:0000

这是因为增量操作不仅偏移值而且段值也会改变。这意味着在far指针的情况下段不会改变,但在huge指针的情况下,它可以从一个段移动到另一个段。

2.当在远指针上使用关系运算符时,仅比较偏移量。换句话说,如果被比较的指针的段值相同,关系运算符将仅对远指针起作用。并且在不会发生这种情况的情况下,实际上会进行绝对地址的比较。让我们借助far指针的示例来理解:

int main()
{
char far * p=(char far*)0x12340001;
char far* p1=(char far*)0x12300041;
if(p==p1)
printf("same");
else
printf("different");
return 0;
}

输出:

different

huge 指针中:

int main()
{
char huge * p=(char huge*)0x12340001;
char huge* p1=(char huge*)0x12300041;
if(p==p1)
printf("same");
else
printf("different");
return 0;
}

输出:

same

解释:我们看到pp1 的绝对地址是123411234*10+11230*10+41)但在第一种情况下它们被认为不相等,因为在far 指针的情况下仅比较偏移量,即它将检查是否0001==0041。这是错误的。

并且在大指针的情况下,比较操作是在相等的绝对地址上执行的。

  1. 远指针从不规范化,但 huge 指针已规范化。规范化指针是在段中具有尽可能多的地址的指针,这意味着偏移量永远不会大于 15。

    假设如果我们有0x1234:1234,那么它的规范化形式是0x1357:0004(绝对地址是13574)。 一个巨大的指针只有在对其执行一些算术运算时才会被规范化,而在赋值期间不会被规范化。

     int main()
     {
      char huge* h=(char huge*)0x12341234;
      char huge* h1=(char huge*)0x12341234;
      printf("h=%Fp\nh1=%Fp",h,h1+0x1);
      return 0;
     }
    

    输出:

    h=1234:1234
    
    h1=1357:0005
    

    解释:huge指针在赋值的情况下没有被规范化。但是如果对其进行算术运算,它就会被规范化。所以,h1234:1234h11357:0005which已标准化。

    4.由于规范化,巨大指针的偏移量小于16,而远指针则不然。

    让我们举个例子来理解我想说的话:

     int main()
      {
      char far* f=(char far*)0x0000000f;
      printf("%Fp",f+0x1);
      return 0;
      }
    

输出:

    0000:0010

如果是huge 指针:

      int main()
      {
      char huge* h=(char huge*)0x0000000f;
        printf("%Fp",h+0x1);
        return 0;
        }

        Output:
        0001:0000

解释:当我们将远指针增加 1 时,它将是 0000:0010。当我们将大指针增加 1 时,它将是 0001:0000,因为它的偏移量不能大于 15,换句话说它将被标准化。

【讨论】:

  • 内容+1。确保下次正确格式化您的答案。
  • 我终于明白了其中的区别。谢谢。
  • 这里没有提到 HMA 以及它如何与巨大的指针规范化交互。 (注意,我不知道编译器如何处理这个问题,我只知道在规范化要指向 HMA 的指针时,必须允许大于 15 的偏移量。)
【解决方案3】:

在过去,根据 Turbo C 手册,当您的整个代码和数据适合一个段时,近指针只有 16 位。远指针由一个段和一个偏移量组成,但没有执行规范化。一个巨大的指针被自动归一化。可以想象,两个远指针可以指向内存中的同一位置但不同,而指向同一内存位置的规范化大指针总是相等的。

【讨论】:

  • 谢谢.. bt 你能告诉我例子吗.. 所以它们现在也可以使用。
  • @Vishwanath:不,它们实际上不适用于新代码。它们仅适用于 16 位 Intel 平台,这些平台很久以前就已经过时了(我相信 Intel 386 是第一个有效支持 32 位平面内存模型的芯片)。如果您编写的代码必须关心这一点,那么您就是在编写遗留代码。
  • @BillyONEal 如果我想了解它们(在 16 位机器的时代,它们是如何使用或在哪里使用的)怎么办?
  • @Lucas 最好的方法是获得一本 1980 年代的 PC 编程书 --- 所有老式 DOS 编程都使用这些。尝试查找 Turbo C 或 Pacific C。
【解决方案4】:

此答案中的所有内容仅与旧的 8086 和 80286 分段内存模型相关。

near:一个 16 位指针,可以寻址 64k 段中的任何字节

far:一个 32 位指针,包含一个段和一个偏移量。请注意,由于段可以重叠,因此两个不同的远指针可以指向同一个地址。

huge:一个 32 位指针,其中段被“规范化”,因此没有两个远指针指向相同的地址,除非它们具有相同的值。

tee:加果酱和面包的饮料。

这会让我们回到doh oh oh oh

什么时候使用这些指针?

在 1980 年代和 90 年代直到 32 位 Windows 无处不在,

【讨论】:

  • ^ ^ 总算明白了!
【解决方案5】:

在某些架构中,可以指向系统中每个对象的指针会比可以指向有用的事物子集的指针更大且处理起来更慢。很多人给出了与 16 位 x86 架构相关的答案。各种类型的指针在 16 位系统上很常见,尽管在 64 位系统中可能会再次出现接近/恐惧的区别,这取决于它们的实现方式(如果许多开发系统都使用 64 位指针,我不会感到惊讶)一切,尽管在许多情况下这将是非常浪费的)。

在许多程序中,很容易将内存使用量划分为两类:小东西加起来只有相当少量的东西(64K 或 4GB),但会经常被访问;大东西总起来可能不超过数量大得多,但不需要经常访问。当应用程序需要处理“大事物”区域中的对象的一部分时,它会将该部分复制到“小事物”区域,进行处理,并在必要时将其写回。

一些程序员抱怨必须区分“近”和“远”内存,但在许多情况下,进行此类区分可以让编译器生成更好的代码。

(注意:即使在许多 32 位系统上,也可以直接访问某些内存区域而无需额外的指令,而其他区域则不能。例如,在 68000 或 ARM 上,一个寄存器指向全局变量存储,可以直接加载该寄存器的前 32K (68000) 或 2K (ARM) 中的任何变量。获取存储在其他位置的变量将需要额外的指令来计算地址。将更常用的变量放在首选区域并让编译器知道将允许更有效的代码生成。

【讨论】:

  • 巨大指针自动归一化而远指针没有自动归一化是什么意思? “规范化”这个词在这里是什么意思?
  • @Destructor:8086 上的每个指针都有两个 16 位部分——一个难以操作的段和一个可以更方便地操作的偏移量。通过将段乘以 16 并加上偏移量来获取硬件地址。最多 65536 字节的对象在 16 字节边界上对齐,可以通过设置段来轻松操作,以便识别对象的开始,然后使用偏移量访问其中的位置,但事实上每个位置都可以识别 4096不同的方式有时会产生问题。
  • @Destructor:规范化指针意味着将其替换为标识相同物理位置但偏移量在 0-15 范围内的指针。对于某些使用模式,完全忽略段的关系运算符和指针算术将符合 C 标准并且可以工作,但意味着一个可能有两个不同的指针,它们的差为零,并且两者都不大于另一个,但是它们仍然是不平等的,并且可以访问不同的东西。让关系运算符将指针视为 32 位值(以段为高位字)...
  • ...适用于许多使用模式,但在某些尝试测试指针重叠的场景中可能仍会出错。我希望标准能够定义内在函数来测试两个指针是否“可能”重叠,或者它们是否肯定重叠,因为前者可以比后者更便宜地确定,但在许多情况下同样有用。
【解决方案6】:

此术语用于 16 位架构。

在 16 位系统中,数据被划分为 64Kb 段。每个可加载模块(程序文件、动态加载的库等)都有一个关联的数据段 - 最多只能存储 64Kb 的数据。

NEAR 指针是一个 16 位存储的指针,并且(仅)引用当前模块数据段中的数据。

具有超过 64Kb 数据要求的 16 位程序可以访问特殊分配器,该分配器将返回一个 FAR 指针 - 它是高 16 位中的数据段 id,以及低 16 位中指向该数据段的指针位。

然而,更大的程序需要处理超过 64Kb 的连续数据。一个 HUGE 指针看起来与远指针完全一样——它具有 32 位存储空间——但分配器已经小心地安排了一系列数据段,具有连续的 ID,因此通过简单地递增数据段选择器,下一个 64Kb 数据块可以到达。

底层的 C 和 C++ 语言标准从未在其内存模型中正式承认这些概念 - C 或 C++ 程序中的所有指针都应该具有相同的大小。所以 NEAR、FAR 和 HUGE 属性是各个编译器供应商提供的扩展。

【讨论】:

    猜你喜欢
    • 2010-12-17
    • 1970-01-01
    • 1970-01-01
    • 2013-03-07
    • 2011-05-19
    • 2010-11-26
    • 1970-01-01
    相关资源
    最近更新 更多