返回入门教程首页 主页 | 开发工具 | 应用芯片 | 核心模块  
 
   
 

Win32Forth

Win32Forth 的内部结构

介绍

互联网上关于 Forth 系统结构的最好的(甚至是用我可以理解的术语)描述是 Brad Rodriguez 的 "Moving Forth" (其简体中文译本可以在 http://www.forthchina.com 上得到),它首次发布在   The Computer Journal #59 (January/February 1993) 上。我非常感谢作者允许我修改他的原始工作并发表在这里说明 Win32Forth 的内部结构。请花时间读一下原作,其中包含有构建一个 Forth 系统的深入信息。比我这里所选的要多得多。

我首先声明一个小的创意。我创建的图增加了颜色,并使它们能够以更好的分辨率打印。我不是个语言大师,这里的任何错误可能都是我的。如果您发现了任何拼写错误(除了 British 拼法,我别无选择)、技术错误、不清楚的或者仅仅是一般的不可理解。我真的想知道。

由于 Tom Zimmer 把 Win32Forth 留在了公共域, Win32Forth 从他的最后版本 ( 版本 4.2) 开始有一些内部的变化,本文描述的是版本 6.10 及以后的内部结构。就像生活中的许多事情一样,这里也没有保险。用一句通俗的话说就是,如果使用 Win32Forth 或者这些文章的信息,不论发生了什么,那可全都是您自己的事儿。

Alex McDonald, January 2004

Win32Forth 内核的设计决策

要点

当然,设计任何 Forth 系统的时候都要有一些需求分析。 Tom Zimmer 和 Andrew McKewan 开发 Win32Forth 是为了一个项目的特别需求,其中的细节可以见 Win32Forth 的历史。 但是我们也应该看到 , Win32Forth 除了想成为一个运行在 Intel 平台上 Windows 操作系统上的全 32 位 Forth 系统外,也没有想别的什么。这样就使得一些不得不为之的设计策略在今天的代码中依然可见。我们特别感兴趣的困难是在一个像 Windows 这样的以 C 为中心的环境中设计一个 Forth 系统。有些困难已经克服,有些却依然存在。

单元尺寸

Forth 使用的字的大小并不需要与 CPU 硬件一致。最小的可实际使用的 Forth 模型是 16 位的, Forth 社区称之为单元大小,因为“字”在 Forth 中是指一个定义。 8 位处理器一成不变地支持 16 位的 Forth 系统,它们通常需要对双字节的算术操作进行显式的编码。尽管有些 8 位处理器上带有一些 16 位的操作。 16 位的处理器通常运行 16 位的 Forth ,尽管同样的双精度数据技术可以用于为 16 位的处理器编写一个 32 位的 Forth 系统。至少为 8686/8088 处理器编写过一个 32 位的 Forth 系统。

32 位的处理器通常运行 32 位的 Forth ,它的 CELL 大小是 32 位的。基于 Windows 的 W32F 能够在运行 Windows 的所有 32 位处理器上运行,对于今天的实用系统,那就是 Pentium 以上的系统。尽管 W32F 最初运行在 WIndows3.1 上并使用 WIN32S , 3.1 是一个运行在 32 位处理器上的 16 位的操作系统,但是很不好。我们有很长时间不支持 3.1 了,这里给出了 哪些系统是被支持的

  串线技术

“串线代码”是 Forth 的特点。一个 Forth “串线”就是将要执行的子程序地址的列表,你可以把这些想象成一个子程序调用列表,只是省略了 CALL 指令。这些年来发明了许多串线技术,每一种都能够最好地适用 CPU 和应用程序。为了作出决策,你需要理解它们如何工作,以及它们的优缺点。 .

间接串线代码 (ITC)

这是经典的 Forth 串线技术,用于 fig-Forth 、 F83 和 Win32Forth 中,在几乎所有的 Forth 书中都有描述,所有其它的串线技术都是这种技术的“变形”,所以你需要理解 ITC 。

让我们看 Forth 字 SQUARE 的定义

: SQUARE DUP * ;

在典型的 ITC Forth 中,其存储器如图 1 所示(黄色的首部将在以后的文章中描述,它与串线没有关系。棕色的部分是代码,蓝色和青色的部分是 CFA 指针,在下面解释) .

Fig.1 Indirect Threaded Code

假设在其它一些字的执行过程中遇到了 SQUARE 。 Forth 解释指针( IP )将指向一个存储器单元 -- 这个单元包含在“其它一些字”中 -- 那里保存着字 SQUARE 的地址。更准确地说,那个单元包含着 SQUARE 的代码域,按 Win32Forth 的说法是 ' cfa ' ( 代码域地址, code field address) 或者 ' xt ' ( 执行标记 ) 。解释器读取这个地址,然后通过它来读取 SQUARE 的代码域内容。这些内容又是另外的地址 -- -- 执行字 SQUARE 的机器语言子程序的地址,就像下面这样:

(IP) -> W

读取 IP 指向的存储器内容到 "W" 寄存器, W 现在有代码域地址 (the cfa)

IP+4 -> IP

增量 IP, 就像是程序计数器 ( 假设串线中的地址是 4 个字节长 )

(W) -> X

通过 W 读取存储器内容到 "X" 寄存器, X 现在含有机器代码的地址

JP (X)

跳转到 X 寄存器中指向的地址

这里解释了一个重要的但是很少被提及的原理: 刚刚进入的 Forth 字的地址保存在 W 寄存器中 。 CODE 字不需要这个信息,但是其它的 Forth 字却需要它。

如果 SQUARE 是用机器代码编写的,事情就到此为止了。接着将执行那些机器代码,然后跳回到 Forth 解释器 -- 因为 IP 已经增量,下一个字将被执行。这就是为什么 Forth 解释器被称为 NEXT 的原因。

但是, SQUARE 是一个高级“冒号”定义 -- 它保持着一个“串线”,也就是一些地址的列表。为了执行这个定义, Forth 解释器必须在一个新的位置上重新开始,这个位置就是 SQUARE 的参数域。当然解释器原来的位置必须保存,以便在 SQUARE 执行完后能够恢复“其它一些字”。这很像是一个子程序调用。 SQUARE 的机器语言动作就是简单地保存旧的 IP ,把 IP 设置到一个新的位置上,在 SQUARE 完成后弹出 IP (正如您所看到的, IP 就是高级 Forth 的“程序计数器”) 在 W32F 中,这被称为 DOCOL:

PUSH IP

写入返回地址栈

W+4 -> IP

W 正指向代码域,所以 W+4 是定义体的地址

JUMP

解释 ("NEXT")

同样的代码片段被用于所有的高级(也就是串线) Forth 定义!这就为什么使用一个指针指向这些代码片段,而不是直接放放置代码本身到 Forth 定义中。几百个定义,将省去多少空间!这也是为什么被称为间接串线的原因。

这里的“从子程序返回”是字 EXIT ,当 Forth 遇到定义中的分号 ' ; ' 时被“编译”。 EXIT 只是执行下列动作一个机器语言子程序:

POP IP

从 " 返回地址栈 "

JUMP

到解释器

可以分析几个 Forth 定义,确保您自己已经理解了这些工作。

注意 ITC 的特点 : 每个 Forth 字只有一个单元的代码域。冒号定义为定义中的每个字编译一个单元。 Forth 解释器实际需要执行 两次 间接才能得到下一个要执行的机器代码的地址。 ( 先通过 IP, 再通过 W) 。

ITC 既不是最小也不是最快的串线技术,但它可能是最简单的,尽管 DTC (在下面描述)实际上也不是很复杂。那么为什么有这么多的 Forth 使用间接串线技术呢?主要是由于作为模型的早期的 Forth 系统是间接串线的。今天, DTC 是更加普遍的。

那么什么时候应该使用 ITC 呢?相比各种技术, ITC 产生最清晰和最文雅的定义 -- 它只是地址。如果您是这样考虑的,那 ITC 就适合您。如果您的代码是围绕着定义的内部做很多事情,那么 ITC 的简单性和一致性能够增强可移植性。 ITC 是经典的 Forth 模型,因此可以用作教学。最后,在 x86 体系结构处理器中,执行 CALL 和 RET 比两次间接要昂贵得多。

还有其它的技术, DTC 和 STC 是常见的变形。其中的细节,请参阅 see Brad Rodriguez' "Moving Forth" . (其简体中文译本可以在 http://www.forthchina.com 网站上得到)


寄存器分配

在串线技术之后, CPU 的寄存器使用是最关键的设计决策。它可能是最困难的,可用的 CPU 寄存器可以决定选用何种串线技术以及 Forth 的效率。

经典的 Forth 寄存器

经典的 Forth 模型有 5 个“虚拟寄存器”。它们是用于 Forth 原语操作的抽象实体。 NEXT 、 ENTER ( 在 Win32Forth 中称为 DOCOL ) 和 EXIT 是在这些抽象寄存器之前定义的。

每个寄存器的宽度都是一个单元 -- 也就是说,在一个 32 位的 Forth 中,寄存器是 32 位的寄存器。它们 可以不全是 CPU 寄存器 。我将按重要性来描述它们,也就是说,最后描述的项目可以最先被考虑放入存储器。

W 是工作寄存器。它被用来做许多事情,包括存储器引用,所以它应该是一个地址寄存器,也就是说您应该能够使用 W 寄存器的内容作为地址来读写存储器,您还应该能够在 W 寄存器上进行算术运算。 ( 在 DTC Forth 中,您还应该能够使用 W 寄存器进行间接跳转 )W 寄存器被解释器用于 每个 Forth 字中 。 如果一个 CPU 只有一个寄存器,您也应该把这个寄存器用于 W 寄存器而把其它东西放到存储器里去。 Win32Forth 用 EAX 寄存器 作为 W 。

IP 是解释指针。它也被用在每个 Forth 字中(通过 NEXT 、 ENTER 或者 EXIT )。 IP 必须是一个地址寄存器,您也需要增量 IP 。在 Win32Forth 中,出于使用 LODSD 指令和把 EAX 作为 W 等原因,使用 ESI 寄存器作为 IP LODSD 不再用于 Win32Forth ,它太慢了。

PSP 是参数栈 ( 或者“数据栈” ) 指针,有时也简称为 SP 。我选择 PSP 是因为 SP 常常用来命名 CPU 的物理寄存器,它们是不能混淆的。大多数 CODE 字都使用它。 PSP 必须是一个堆栈指针,或者是一个能够进行增量和减量的地址寄存器,外加 PSP 进行索引寻址。在 Win32Forth 中,这就是 ESP 寄存器。 使用这个寄存器向 Window 传递数据非常方便。 Windows 使用 Pascal 调用约定,参数是通过堆栈传递的,被调用者有责任在返回时清除堆栈。使用 ESP 寄存器减少了数据处理的开销。

RSP 是返回栈指针,有时称为 RP 。在 ITC 中被冒号定义使用。它必须是一个堆栈指针,或者是一个能够被增量和减量的地址寄存器,外加 PSP 进行索引寻址。 Win32Forth 使用 EBP 寄存器。

UP 是用户指针,保存任务的用户区基地址。 UP 通过加上一个偏移量用于高级 Forth 代码,以保证它可以被存放在指定的地方。但是如果 CPU 能够从 UP 寄存器进行索引寻址, CODE 字就能够更快更容易地访问用户变量。如果您有多余的地址寄存器,把它用作 UP 。单任务 Forth 不需要 UP 。 Win32Forth 支持多任务,使用 EDX 寄存器 作为用户区地址。

X 是一个工作寄存器,不作为经典的 Forth 寄存器考虑,尽管经典的 ITC Forth 使用它进行第二次间接。在 ITC 中,您必须能够使用 X 进行间接跳转。 X 寄存器也可以被几个 CODE 字进行算术一类的运算。有时另一个工作寄存器 Y 也被定义。在 Win32Forth 中, EAX 寄存器 ECX 寄存器 可以用于工作寄存器(为什么 EAX 在用作 W 寄存器的同时还可以用于这个任务呢?为什么 EDI 寄存器根本不用呢?其中的原因在后面解释)。

使用硬件堆栈

大多数的 CPU 都有堆栈指针作为它们硬件的一部分用于中断和子程序调用。它们如何映射到 Forth 寄存器呢?应该作为 PSP 还是 RSP ?

一句话,看情况。通常在 ITC 和 DTC 中, PSP 比 RSP 更常用。如果您的 CPU 只有很少的地址寄存器, PUSH 的 POP 比显式的引用要快,则应该把硬件堆栈作为参数栈。另一方面,如果您的 CPU 有丰富的寻址模式 -- 允许进行索引寻址 -- 把 PSP 作为一个通用的地址寄存器。在这种情况下,应该把硬件堆栈作为返回栈。

在 Win32Forth 中决策非常简单。有许多的 Windows 调用把数据放到堆栈上, Windows 通常都要避免在调用中把数据从一个堆栈移到另一个堆栈上, Windows 的参数被压入堆栈(它使用 Pascal 调用约定),它是通过 ESP 寻址的,所以 PSP 就是 ESP 。

把栈顶元素放在寄存器中

Forth 的性能可以通过把参数栈栈顶元素放到寄存器中而得到提高。这样,许多的 Forth 字(比如 0= )就不需要使用堆栈了。其它的字仍然使用相同数量的压栈和出栈操作,只是在代码中的位置不同了。只有几个 Forth 字( DROP 和 2DROP ) 变得更加复杂,因为您不能再简单地调整堆栈指针 -- 您还必须更新 TOS 寄存器。

当把栈顶元素放到寄存器中时,编写 CODE 字有几个规则:

•  一个从堆栈中移出项目的字必须弹出一个新的 TOS 到这个寄存器中;

•  一个向堆栈中加入项目的字必须把旧的 TOS 压入堆栈(除非是它被这个字消耗了掉了);

如果把两个堆栈元素放到寄存器会怎么样呢?您把次栈顶元素放到寄存器中,操作性能维持不变。一个 PUSH 还是一个 PUSH ,不论它在您执行的操作之前还是之后。另一方面,缓冲两个元素到寄存器中增加了大量的指令 -- 一个 PUSH 变成了一个 PUSH 加一个 MOVE 。只有那些专门的 Forth 处理器比如 RTX2000 还有那些空想的优化编译器才能从把两个元素放到寄存器的策略中受益。

Win32Forth 把 TOS 放到 EBX 寄存器 中( EBX 寄存器是 8086Forth 系统的历史遗留物, AX 寄存器不能用于索引寻址模式,那么 BX 就是一个很自然的替代,而在 Win32Forth 中也就成了 EBX )

 
   

(C) ForthChina.com 版权所有 2004-2010
Email:forthchina@163.com