什么是近指针、远指针和巨型指针?

33

有人可以给我解释一下这些指针是什么,能否用一个合适的例子来说明这些指针是如何使用的?


9
闻起来像作业。 - Spence
4
我记得很久以前在 Turbo-C 里面有这些东西…… - 0x6adb015
7
它们只在16位英特尔平台上相关,而这些平台已经过时。如果你需要在这个平台上维护代码或编写新代码,我很同情你。 - Philip Potter
15
如果这是一个作业问题,那么这所学校真的需要为其计算机科学教室购置新的电脑。 - dan04
7
“它们只在16位英特尔平台上才相关。” 嗯,嵌入式平台、DSP等呢?我无法相信这里的每个人都说这已经过时了,请先做些研究。 - stijn
显示剩余3条评论
6个回答

31

主要的例子是英特尔X86架构。

英特尔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段的概念。 因此,“near”指针只是DX寄存器的内容(5678h),除非DS寄存器已经正确设置,否则它将无效,而“far”指针为32位(12345678h,后跟DS的DX),始终有效(但较慢,因为您需要加载两个寄存器,然后在完成后恢复DS寄存器)。
(如下面supercat所指出的,DX的溢出偏移量在加上DS以获取最终地址之前会“滚动”。这允许16位偏移量访问64 kiB段中的任何地址,而不仅仅是与DX指向的地方相距±32 kiB的部分,这是其他体系结构中某些指令使用16位相对偏移寻址时所做的。)
但是,请注意,您可以具有指向相同地址但具有不同值的两个“far”指针。 例如,远程指针100079B8h指向与12345678h相同的位置。 因此,远程指针上的指针比较是无效操作:指针可能不同,但仍然指向相同的位置。
这就是我决定Mac(当时带有Motorola 68000处理器)并不那么糟糕的地方,因此我错过了巨大的指针。 我IRC,它们只是保证了段寄存器中所有重叠的位都为0的远程指针,就像第二个示例一样。
Motorola在其6800系列处理器上没有这个问题,因为它们限于64 kiB。 当他们创建68000体系结构时,他们直接采用32位寄存器,因此从未需要近距离,远程或巨大的指针。 (相反,他们的问题是只有地址的底部24位实际上有意义,因此一些程序员(尤其是苹果)将高8位用作“指针标志”,导致地址总线扩展到32位(4 GiB)时出现问题。))
Linus Torvalds一直坚持到80386,它提供了“保护模式”,其中地址为32位,段寄存器为地址的高半部分,不需要进行任何加法,并且从一开始就编写Linux仅使用受保护模式,没有奇怪的段落内容,这就是为什么您在Linux中没有近距离和远程指针支持的原因(以及为什么如果他们想要Linux支持,则没有设计新体系结构的公司会回到它们)。 然后他们吃了罗宾的吟游诗人,众人欢呼雀跃。(耶...)

有趣的是,许多基于68000的程序在某些地方出现了32K的限制,而8088软件本来会有64K的限制。考虑到内存通常很稀缺,而8088可以在需要使用32位量或接受32K限制的地方使用16位量,因此8088实际上是一个非常实用的设计。 - supercat
在68k中,“32k限制”是当您使用16位相对偏移量进行分支和跳转时的情况。如果您没有尝试将代码段中的函数排序以尝试将它们放在一起,那么当您可以使用64k限制时,您只会放弃并对所有内容施加32k限制。这仍然不如英特尔系统那样严重,因为不同值的指针实际上可能是相等的。再次强调,这已经成为历史,即使是嵌入式系统现在也没有问题正确地执行32位寻址。 - Mike DeSimone
在经典的 Macintosh 上,许多数据结构被限制在32K以内,因为操作系统选择了16位类型而不是32位类型来进行许多用途;我预计在68000平台上许多受内存限制的软件也会是这样(特别是16位总线变体)。68000的addr+disp16指令都会对位移进行符号扩展,以允许正负位移。然而,8088可以允许16位位移在单个对象内达到+/-65535字节,而不是+/-32767字节,如果偏移量适合于一个段内。 - supercat
经典的 Mac 限制可以追溯到他们为了将所有东西装入128K总RAM而做出的疯狂举动,例如使用系统拥有的指针中的(当时未使用的)上8位作为标志,以及 Pascal 中的 integer 是2个字节。(另一方面,Pascal 字符串提供了 O(1) 字符串长度计算、在字符串中具有 null 的能力以及几乎完全消除缓冲区溢出错误。)x86 系列允许在段中“环绕”偏移量,但代价是你必须使用段寄存器和近距离和远距离指针。 - Mike DeSimone
2
内存并不是免费的,即使在今天,很多应用程序在编译为x64时运行速度比编译为32位x86时更快,尽管64位模式具有更大的寄存器集。我唯一能看到的64位模式的显着性能劣势是64位对象引用占用的缓存空间是32位对象引用的两倍。8086确实需要更多的段寄存器,但即使这样,我认为它在寻址1MiB地址空间方面比任何其他16位体系结构(M68K是32位体系结构)做得更好。 - supercat
这是不正确的。32位地址中,段寄存器是地址的高半部分。实际上,使用32位寄存器来保存32位偏移量。这些通常被称为“近指针”,因为选择器仍然被使用,只是指向一个平坦的4 GiB描述符。选择器寄存器不保存“地址的高半部分”。 - ecm

24

far指针和huge指针的区别:

默认情况下,指针是near类型的,例如:int *p是一个near指针。在16位编译器中,near指针的大小为2个字节。我们已经知道,不同编译器的大小不同,它们只存储指针所引用地址的偏移量。仅由偏移量构成的地址范围为0-64K字节。

farhuge指针:

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

far指针的限制:

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

如果你将far地址增加到超过其偏移地址的最大值,那么它将以循环顺序重复其偏移地址,而不是增加段地址。这也被称为“wrapping”。例如,如果偏移量是0xffff,我们加1,则为0x0000;类似地,如果我们将0x0000减1,则为0xffff,但请注意,段地址不会改变。

现在我要比较huge指针和far指针:

1.当增加或减少far指针时,仅实际增加或减少指针的偏移量,但是对于huge指针,段和偏移值都会改变。

考虑以下示例,摘自这里

 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:1234,而h11357:0005,已经过规范化。

4.由于规范化,巨型指针的偏移量小于16,而远指针则不是这样。

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

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

输出:

    0000:0010

对于巨型指针的情况:

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

        Output:
        0001:0000

解释:当我们将far指针增加1时,它将变为0000:0010。而当我们将huge指针增加1时,它将变为0001:0000,因为它的偏移量不能大于15,换句话说,它会被规范化。


3
把英语翻译成中文。只返回翻译的文本:+1 等于内容。请确保下次适当格式化您的答案。 - joey rohan
1
我终于明白它们之间的区别了。谢谢。 - hkBattousai
这里缺少提及HMA以及它如何与巨型指针规范化交互的内容。(注意:我不知道编译器如何处理这个问题,但我知道当规范化一个指向HMA的指针时,你必须允许大于15的偏移量。) - ecm

15

在旧时代,根据Turbo C手册的描述,当您的整个代码和数据适合一个段时,一个near指针仅仅是16位。一个far指针由一个段和一个偏移组成,但不执行规范化。而huge指针则会自动进行规范化。两个far指针可以可能指向内存中相同的位置,但却是不同的,然而指向同一内存位置的规范化huge指针总是相等的。


谢谢!但是你能给我一个例子吗?这样它们在目前也可以使用。 - 2easylogic
9
@Vishwanath: 不,它们对于新代码并不真正可用。它们只适用于16位英特尔平台,这些平台很久以前就过时了(我相信英特尔386是第一款有效支持32位平面内存模型的芯片)。如果你正在编写必须关心这个问题的代码,那么你正在编写旧系统的代码。 - Billy ONeal
@BillyONeal 如果我想了解它们的使用方式或者在16位机时代的应用,该怎么办? - Shubham
2
@Lucas 最好的方法是找一本上世纪80年代的个人电脑编程书籍——所有老派的DOS编程都使用这些。尝试查找Turbo C或Pacific C。 - David Given

3

这个回答中的所有内容都只与旧的8086和80286分段内存模型相关。

near:16位指针,可以寻址64k段内的任何字节。

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

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

tee:一种带果酱和面包的饮料。

那会让我们回到 doh oh oh oh。

这些指针什么时候使用?

在1980年代和90年代直到32位Windows变得无处不在之前。


最后终于得到了简要的东西! - joey rohan

3
在某些架构中,一个可以指向系统中每个对象的指针比一个可以指向有用子集的指针更大且速度较慢。许多人提到了16位x86架构相关的答案。在16位系统上,各种类型的指针很常见,虽然在64位系统上,根据它们的实现方式,近距离/远距离的区别可能会重新出现(我不会感到惊讶,即使在许多情况下这将非常浪费,许多开发系统也会为所有东西使用64位指针)。
在许多程序中,将内存使用划分为两类是相当容易的:小东西总共加起来不多(64K或4GB),但经常被访问,而较大的东西可能总量更大,但不需要那么经常访问。当应用程序需要处理“大事物”区域中的一部分对象时,它将该部分复制到“小事物”区域,进行处理,如果必要,就将其写回。
一些程序员抱怨必须区分“近”和“远”内存,但在许多情况下,做出这样的区分可以让编译器生成更好的代码。
(注意:即使在许多32位系统上,某些内存区域也可以直接访问,而其他区域则不能。例如,在68000或ARM上,如果保持一个寄存器指向全局变量存储,它将能够直接加载该寄存器前32K(68000)或2K(ARM)内的任何变量。获取存储在其他地方的变量将需要额外的指令来计算地址。将更频繁使用的变量放置在优选区域并让编译器知道,可以允许更有效的代码生成。)

巨大指针被自动归一化,而远指针则不是。这里的**"normalize"**是什么意思? - Destructor
@Destructor:8086上的每个指针都有两个16位部分——一个段,不方便操作,和一个偏移量,可以更方便地操作。通过将段乘以16并加上偏移量来获取硬件地址。对齐在16字节边界上的大小为65536字节的对象可以通过设置段以标识对象的起始位置,然后使用偏移量访问其中的位置来轻松操作,但是每个位置可以有4096种不同的标识方式,有时可能会出现问题。 - supercat
1
@Destructor:规范化指针意味着用一个在0-15范围内的偏移量替换它,以便识别相同的物理位置。对于某些使用模式,忽略段的关系运算符和指针算术将符合C标准并且有效,但这意味着可能有两个不同的指针其差值为零,且没有一个大于另一个,但它们仍然不相等且访问不同的内容。使关系运算符将指针视为32位值(其中段作为上位字)... - supercat
这段代码可以适用于许多使用模式,但在一些试图测试指针重叠的情况下仍可能出现问题。我希望标准能够定义内置函数来测试两个指针是否“可能”重叠,或者它们是否绝对重叠,因为前者比后者更便宜,但对于许多情况仍然非常有用。 - supercat

2

这个术语被用于16位架构。

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

NEAR指针是一个具有16位存储器的指针,仅引用当前模块数据段中的数据。

需要超过64Kb数据的16位程序可以访问特殊的分配器,返回FAR指针-它是上16位的数据段id和下16位的数据段内指针。

然而,更大的程序将处理超过64Kb的连续数据。HUGE指针看起来与远指针完全相同-它具有32位存储器-但分配器已经安排了一系列数据段,具有连续的ID,因此通过简单地增加数据段选择器,可以访问下一个64Kb数据块。

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接