前言:
眼前同学们对“汇编jb指令”都比较注意,咱们都需要学习一些“汇编jb指令”的相关内容。那么小编在网络上网罗了一些关于“汇编jb指令””的相关内容,希望兄弟们能喜欢,小伙伴们快快来学习一下吧!汉化理查德琼斯写的forth教程,
由于年代久远, 这份教程里的汇编代码已经不能在当前软件环境中运行,
所以它只能当做文字教程, 仅做参考,
本人能力有限, 利用业余时间汉化, 借助deepl翻译也搞了几个月, 太慢了,
想要实现一个forth解释器, 实在没有人力物力财力可供,
所以伸手党们就好自为之吧,
本着共建开源 共享繁荣的精神, 写解释器的工作就交给别的朋友了,
我认为forth这类栈语言非常适合搞中文汉字编程, 它既没有C语族的沉重包袱, 也没有lisp的括号症,
大家了解了后就知道它是非常简单同时又非常强大的编程方法学, 我时常将其比作筷子一样的发明,
大道至简!
lua Python够简单了吧? 栈语言比lua起码简单2 ~ 4倍, 若用全中文汉字实现其解释器,还能简单更多, 我认为六岁一年级小朋友学会也不是什么难事,
虽然它简单, 但不代表它弱,
forth这样的栈语言是将常用算法思维中的分治思维发挥到极致的产物, 它能创造的复杂度是没有极限的, 它能从最简单的汇编形式通过分治不断积累出复杂的 ... 更复杂的 ... 功能, 反正就是挑战你的心智, 因为它根本就没有什么语法, 不会形成先入为主的语法信息茧房(比如现在那些被困在C++或C语族(C C++ Java Python go rust...等等)这类专门强调语法复杂度的语言里的人是无法理解的) , 有些人认为这跟lisp一样, 但lisp有括号这个语法 还有GC, 而forth根本没有这些, 也不需要, 若想要rust的生存周期功能, 你随时可以给forth这样的栈语言添加这部分组件即可, 而无需专门更新一版编译器, 栈语言就是这样神奇,
所以这样没有语法, 复杂度上不封顶, 又能用最简单的汇编甚至机器语言开始聚合编程语言的功能组件的存在,
是非常适合建设中文汉字编程的, 只是其生态少得可怜, 大家根本没有一个了解的途径, 也就不知其强大之处,
不多说, 慢慢建设吧, 懂就自然懂,
先发用汇编写的原语 jonesforth.S ;
/* 一个有时是最小的 FORTH 编译器和教程,用于 Linux / i386 系统。-*- asm -*- 作者:Richard W.M. Jones <rich@annexia.org> 这是公共领域的(见下面的公共领域发布声明)。 $Id: jonesforth.S,v 1.47 2009-09-11 08:33:13 rich Exp $ gcc -m32 -nostdlib -static -Wl,-Ttext,0 -Wl,--build-id=none -o jonesforth jonesforth.S*/ .set JONES_VERSION,47/* 介绍 ---------------------------------------------------------------------- FORTH是一种陌生的语言,大多数程序员对它的看法与Haskell、LISP等一样。 它是如此的陌生,以至于他们宁愿任何关于它的想法都消失,这样他们就可以继续写这些付费代码了。 但这是错误的,如果你关心编程,那么你至少应该了解所有这些语言,即使你永远不会使用它们。LISP是最终的高级语言,而且每十年都有来自LISP的功能被添加到更常见的语言中。 但FORTH在某些方面是低级编程的终极语言。 开箱即用的它缺乏动态内存管理甚至是字符串等功能。 事实上,在其原始水平上,它甚至缺乏IF语句和循环等基本概念。那你为什么要学习FORTH呢? 有几个非常好的理由。 首先,FORTH是最小的。 你真的可以用比如说2000行代码写出一个完整的FORTH。 我指的不仅仅是一个 FORTH 程序,而是一个完整的 FORTH 操作系统、环境和语言。 你可以在一台裸机上启动这样的FORTH,它将出现一个提示,你可以开始做有用的工作。 你这里的FORTH并不是最小的,它使用了一个Linux进程作为它的 "基础PC"(都是为了使它成为一个好的教程)。完全了解这个系统是可能的。 谁能说自己完全了解Linux的工作原理,或gcc呢? 其次,FORTH有一个奇特的引导/自举特性。 我的意思是说,在写了一点与硬件对话的汇编和实现一些原语后,所有语言其余的部分和编译器都是用FORTH自己写的。 还记得我以前说过FORTH缺乏IF语句和循环吗? 当然不是,因为这样的编程语言是没有用的,但我的意思是,IF语句和循环是由FORTH本身编写的。当然,这在其他语言中也很常见,在那些语言中我们称之为 "库"。 例如,在C语言中,'printf'是一个用C语言编写的库函数。但在FORTH中,这已经远远超出了单纯的库。 你能想象用C语言写'if'吗?这就引出了我的第三个理由:如果你可以在FORTH中写'if',那么为什么要把自己限制在通常的if/while/for/switch结构中? 你想用一个结构来迭代数字列表中的每一个元素? 你可以把它添加到语言中。 如果有一个操作符可以直接从配置文件中提取变量,并将其作为FORTH变量使用,那会怎么样? 或者在语言中加入类似Makefile的依赖性呢? 在FORTH中没有问题。 如何修改FORTH编译器以允许复杂的内联策略 -- 简单。 这个概念在编程语言中并不常见,但它有一个名字(事实上是两个名字)。"宏"(我指的是LISP风格的宏,而不是蹩脚的C预处理器)和 "特定领域语言"(DSL)。本教程并不是要学习FORTH这种语言。 如果你不熟悉FORTH的使用,我将向你指出一些你应该阅读的参考资料。 本教程是关于如何编写FORTH的。 事实上,在你了解FORTH的编写方法之前,你对如何使用它只能有一个非常肤浅的理解。因此,如果你对FORTH不熟悉,或者想复习一下,这里有一些在线参考资料可以阅读: 这里是另一篇 "为什么是FORTH?"的文章: 在此对该FORTH进行讨论和批评: 鸣谢----------------------------------------------------------------------这段代码在很大程度上借鉴了Albert van der Horst设计的LINA FORTH()。 代码中的任何相似之处可能都不是偶然的。这个FORTH的某些部分也是基于1992年的这个IOCCC条目:。当IOCCC条目的原作者Sean Barrett在LtU thread 上评论这个FORTH时,我感到非常自豪。最后,我想感谢ARTIC FORTH的作者(可能被遗忘了?),因为他们的原始程序(我还保存在原始磁带上)这些年来一直困扰着我。公共领域 ----------------------------------------------------------------------我,本作品的版权持有人,特此将其释放到公共领域。这适用于全世界。如果这在法律上是不可能的,我授予任何实体为任何目的无条件地使用这一作品的权利,除非法律要求有这种条件。设定 ----------------------------------------------------------------------让我们先把一些内务整理出来。 首先,因为我需要绘制大量的ASCII-art图表来解释概念,最好的方法是使用一个使用固定宽度字体的窗口,并且至少有这么宽: <------------------------------------------------------------------------------------------------------------------------> 其次,确保TABS被设置为8个字符。 下面应该是一条垂直线。 如果不是,请整理好你的标签。 | | | 第三,我假设你的屏幕至少有50个字符高。汇编 ----------------------------------------------------------------------如果你想实际运行这个FORTH,而不仅仅是阅读它,你将需要在i386上使用Linux。 Linux是因为我没有直接在裸机上对硬件进行编程(我本来是可以这样做的),而是通过假设 "硬件 "是一个具有一些基本系统调用(读、写和退出,仅此而已)的Linux进程来实现更简单的教程。 之所以需要i386,是因为我必须为一个处理器编写汇编,而i386是迄今为止最常见的。 (当然,当我说 "i386 "时,任何32位或64位x86处理器都可以。 我是在一个64位的AMD Opteron上编译的)。同样,要汇编这个文件,你需要gcc和gas(GNU的汇编器)。 汇编和运行代码的命令(将该文件保存为 "jonesforth.S")是:gcc -m32 -nostdlib -static -Wl,-Ttext,0 -Wl,--build-id=none -o jonesforth jonesforth.Scat jonesforth.f - | ./jonesforth如果你想运行你自己的FORTH程序,你可以这样做:cat jonesforth.f myprog.f | ./jonesforth如果你想加载自己的FORTH代码,然后继续读取用户命令,你可以这样做:cat jonesforth.f myfunctions.f - | ./jonesforth汇编器 ----------------------------------------------------------------------(你可以直接跳到下一节 -- 你不需要能够读懂汇编程序来学习这个教程)。然而,如果你确实想阅读汇编代码,这里有一些关于gas(GNU汇编程序)的说明:(1) 寄存器名称的前缀是'%',所以%eax是i386的32位累加器。 在i386上可用的寄存器是:%eax, %ebx, %ecx, %edx, %esi, %edi, %ebp 和 %esp,其中大部分有特殊用途。(2) Add、mov等的参数形式为SRC、DEST。 所以mov %eax,%ecx = 移动%eax -> %ecx(3) 常量的前缀是'$',你一定不要忘记它!如果你忘了它,就会导致从内存中读取。所以: mov $2,%eax 将数字2移到%eax中。 mov 2,%eax 从地址2读取32位字到%eax(即-很可能是一个错误)。(4) gas有一个古怪的本地标签句法,其中'1f'(等)意味着标签'1:'"向前",'1b'(等)意味着标签'1:'"向后"。 注意,这些标签可能会被误认为是十六进制数字(例如,你可能会把1b和$0x1b混淆)。(5) ja'是 "如果高于就跳",'jb'为 "如果低于就跳",'je'为 "如果相等就跳 "等等。(6) gas有一个相当不错的 .macro 句法,我经常使用它们来使代码更短、更不重复。要获得更多阅读汇编程序的帮助,请在Linux提示符下执行 "info gas"。现在,教程开始认真地进行。字典 ----------------------------------------------------------------------如你所知,在FORTH中,函数被称为 "字",就像在其他语言中一样,它们有一个名称和一个定义。 下面是两个FORTH字: : DOUBLE DUP + ; 名称是 "DOUBLE",定义是 "DUP +" : QUADRUPLE DOUBLE DOUBLE ; 名称是 "QUADRUPLE",定义是 "DOUBLE DOUBLE"字,包括内置的字和程序员后来定义的字,都被存储在字典中,而字典只是一个字典条目的链接列表。<-----------字典条目(头部/首部)----------------------->+----------------------+--------+----------+------------| 链接指针 | 长度/ | 名称 | 定义| | 标志 | |+--- (4 字节) ----------+- 字节 -+- n 字节---+-----------我稍后会讲到这个字的定义。 现在只需看一下头部文件。 前4个字节是链接指针。 它指向字典中的前一个字,或者,对于字典中的第一个字,它只是一个 NULL 指针。 然后是一个长度/标志字节。这个字的长度可以达到31个字符(使用了5个位),最上面的3个位用于各种标志,我将在后面谈到。 接下来是名字本身,在这个实现中,名字被四舍五入为4字节的倍数,用零字节填充。这只是为了确保定义在32位的边界上开始。一个名为LATEST的FORTH变量包含一个指向最近定义的字的指针,换句话说,就是这个链接列表的头部。DOUBLE和QUADRUPLE可能看起来像这样: 指向前一个字的指针 ^ |+--|------+---+---+---+---+---+---+---+---+------- ------ - - - -| 链接 | 6 | D | O | U | B | L | E | 0 | (定义 ...)+---------+---+---+---+---+---+---+---+---+------- ------ - - - - ^ 长 填充 |+---|------+---+---+---+---+---+---+---+---+---+--- +---+---+------------- - - - -| 链接 | 9 | Q | U | A | D | R | U | P | L | E | 0 | 0 | (定义 ...)+----------+---+---+---+---+---+---+---+---+---+--- +---+---+------------- - - - - ^ 长 填充 | | 最新的你应该能够从中看到如何实现在字典中寻找一个字的功能(只要从LATEST开始沿着字典条目走,匹配名字,直到找到一个匹配的字或碰到字典末尾的NULL指针);以及向字典中添加一个字(创建一个新的定义,将其LINK设置为LATEST,并设置LATEST指向新字)。 我们将在后面看到用汇编代码精确地实现这些功能。使用链接列表的一个有趣的结果是,你可以重新定义字,而一个字的新定义会覆盖一个旧定义。 这是FORTH中的一个重要概念,因为它意味着任何字(甚至是 "内置 "或 "标准 "的字)都可以用一个新的定义来覆盖,以增强它,使它更快,甚至使它失效。 然而,由于FORTH字的编译方式(你将在下面了解),使用旧定义的字将继续使用旧定义。 只有在新定义之后定义的字才使用新定义。直接线程代码 ---------------------------------------------------------------现在我们将进入理解FORTH的真正关键部分,所以请去喝杯茶或咖啡,然后静下心来。 可以说,如果你不理解这部分内容,那么你就无法 "获取 "FORTH的工作原理,而这将是我没有解释清楚的失败。所以,如果你看了这一节几遍后还不明白,请给我发电子邮件(rich@annexia.org)。让我们先谈谈 "线程代码 "的含义。 想象一下,有一个奇特的C语言版本,只允许你调用没有参数的函数。 (现在不要担心这样的语言会完全没有用处!)所以在我们这个奇特的C语言中,代码会是这样的:f (){ a (); b (); c ();}等等。 一个函数,比如上面的'f',如何被标准的C语言编译器编译?可能是像这样的汇编代码。 在右边,我写了实际的i386机器代码。f: CALL a E8 08 00 00 00 CALL b E8 1C 00 00 00 CALL c E8 2C 00 00 00 ; 暂时不考虑函数的返回值"E8 "是X86的机器代码,用于 "调用 "一个函数。 在计算的前20年,内存是非常昂贵的,我们可能会担心重复的 "E8 "字节所浪费的空间。 我们可以通过将其压缩到仅有的 "E8 "字节来节省20%的代码大小(因此,也节省了昂贵的内存):08 00 00 00 只有函数地址,没有CALL前缀。1C 00 00 00 2C 00 00 00在像最初运行FORTH的16位机器上,节省的费用甚至更大--33%。[历史说明:如果FORTH使用的执行模型从下面的段落看起来很奇怪,那么它的动机完全是为了节省早期计算机的内存。现在,当我们的机器在L1缓存中的内存比那些早期计算机的总内存还要多时,这种代码压缩就不那么重要了,但这种执行模型仍然有一些有用的特性]。当然,这段代码不会再直接在CPU上运行了。 相反,我们需要写一个解释器,接收每一组字节并调用它。在i386机器上,我们可以很容易地编写这个解释器,只需两条汇编指令就可以变成3字节的机器代码。 让我们在%esi寄存器中存储指向下一个要执行的字的指针: 08 00 00 00 <- 我们现在正在执行这个。 %esi是下一个要执行的项目。 %esi -> 1C 00 00 00 2C 00 00 00重要的i386指令被称为LODSL(在Intel手册中称为LODSW)。 它做了两件事。 首先,它将%esi的内存读入累加器(%eax)。 其次,它将%esi增加4个字节。 所以在LODSL之后,现在的情况是这样的: 08 00 00 00 <- 我们还在执行这个 1C 00 00 00 <- %eax 现在包含此地址 (0x0000001C) %esi -> 2C 00 00 00现在我们只需要跳转到%eax的地址。 这又是一条X86指令,写成JMP *(%eax)。 做完跳转后,情况看起来是这样的: 08 00 00 00 1C 00 00 00 <- 现在我们正在执行这个子程序。 %esi -> 2C 00 00 00为了使其发挥作用,每个子程序后面都有两条指令 "LODSL; JMP *(%eax)",这两条指令实际上是跳到下一个子程序。这就给我们带来了第一段实际的代码! 嗯,这是一个宏。*//* 下一个 宏. */ .macro NEXT lodsl jmp *(%eax) .endm/* 这个宏被称为NEXT。 那是一个FORTH的术语。 它可以扩展到这两条指令。我们写的每个FORTH原语都必须由NEXT来结束。 想想看,这有点像一个返回。以上描述的是所谓的直接线程代码。总结一下。我们把函数调用压缩成一个地址列表,并使用一个有点神奇的宏来充当 "跳到列表中的下一个函数"。 我们还使用一个寄存器(%esi)作为一种指令指针,指向列表中的下一个函数。我只是给你一个提示,说一个FORTH的定义,如:: QUADRUPLE DOUBLE DOUBLE ;实际上编译(几乎是,不是精确的,但我们一会儿会看到为什么)为DOUBLE、DOUBLE和一个叫EXIT的特殊函数完成的函数地址列表。在这一点上,真正有鹰眼的汇编专家都说:"琼斯,你犯了一个错误!"。我对JMP *(%eax)说了谎。间接线程代码 -------------------------------------------------------------------事实证明,直接的线程代码是有趣的,但只有当你想只是执行一个用汇编语言编写的函数列表。 因此,只有当DOUBLE是一个汇编语言函数时,QUADRUPLE才能工作。 在直接线程代码中,QUADRUPLE看起来像是: +------------------+ | DOUBLE的地址 --------------------> (汇编代码用于 double) +------------------+ 下一个%esi -> | DOUBLE的地址 | +------------------+我们可以增加一个额外的指令,让我们既可以运行用汇编语言写的字(为速度而写的原语),也可以运行用FORTH写的字本身作为地址列表。额外的指示是JMP *(%eax)中的括号的原因。让我们来看看QUADRUPLE和DOUBLE在FORTH中的真实表现吧: : QUADRUPLE DOUBLE DOUBLE ; +------------------+ | 代码字 | : DOUBLE DUP + ; +------------------+ | DOUBLE 的地址 ---------------> +------------------+ +------------------+ | 代码字 | | DOUBLE 的地址 | +------------------+ +------------------+ | DUP 的地址 --------------> +------------------+ | EXIT 的地址 | +------------------+ | 代码字 -------+ +------------------+ %esi -> | + 的地址 --------+ +------------------+ | +------------------+ | | 汇编到 <-----+ | EXIT 的地址 | | | 实现 DUP | +------------------+ | | .. | | | .. | | | 下一个 | | +------------------+ | +-----> +------------------+ | 代码字 -------+ +------------------+ | | 汇编到 <------+ | 实现 + | | .. | | .. | | 下一个 | +------------------+这一部分,你可能需要多喝杯茶/咖啡/最喜欢的咖啡因饮料。 变化的是,我在定义的开头增加了一个额外的指针。 在FORTH中,这有时被称为 "代码字"。 代码字是一个指向解释器的指针,用于运行函数。 对于用汇编语言编写的原语,"解释器 "只是指向实际的汇编代码本身。它们不需要解释,只是运行。在用FORTH编写的字中(如QUADRUPLE和DOUBLE),代码字指向一个解释器函数。我很快就会向你展示解释器的功能,但让我们回忆一下我们的间接JMP *(%eax)与 "额外 "的括号。 以我们正在执行DOUBLE的情况为例,如图所示,DUP已经被调用。 请注意,%esi指向+的地址。DUP的汇编代码最终做了一个NEXT。 这: (1) 将+的地址读入%eax %eax指向+的代码字。 (2) 将%esi增加4 (3) 跳转到间接的%eax, 跳转到+的代码字中的地址。 即- 实现+的汇编代码 +------------------+ | 代码字 | +------------------+ | DOUBLE 的地址 ---------------> +------------------+ +------------------+ | 代码字 | | DOUBLE 的地址 | +------------------+ +------------------+ | DUP 的地址 --------------> +------------------+ | EXIT 的地址 | +------------------+ | 代码字 -------+ +------------------+ | + 的地址 --------+ +------------------+ | +------------------+ | | 汇编到 <-----+ %esi -> | EXIT 的地址 | | | 实现 DUP | +------------------+ | | .. | | | .. | | | 下一个 | | +------------------+ | +-----> +------------------+ | 代码字 -------+ +------------------+ | 现在,我们 | 汇编到 <-----+ 执行 | 实现 + | 这个 | .. | 函数 | .. | | 下一个 | +------------------+所以我希望我已经说服了你,NEXT大致上做了你所期望的事情。 这是间接的线程代码。我略过了四件事。 我想知道你是否可以不看书就猜到它们是什么? . . .我列出的四件事是。(1) "EXIT "是做什么的? (2) 与(1)相关的是如何调用一个函数,即%esi如何开始时指向QUADRUPLE的一部分,但后来指向DOUBLE的一部分。 (3) 用FORTH写的字的代码字是什么? (4) 如何编译一个除了调用其他函数以外的函数,即一个包含数字的函数,如: DOUBLE 2 * ; ?解释器和返回栈 ------------------------------------------------------------在这些问题上没有特定的顺序,让我们来谈谈问题(3)和(2),解释器和返回栈。在FORTH中定义的字需要一个代码字,这个代码字指向一小段代码,在生活中给它们以 "帮助"。 它们不需要太多,但它们确实需要一个所谓的 "解释器",尽管它并不像过去解释Java字节码那样真正地 "解释"(即缓慢地)。 这个解释器只是设置了一些机器寄存器,这样字就可以使用上面的间接线程模型全速执行。当QUADRUPLE调用DOUBLE时,需要发生的一件事是,我们保存旧的%esi("指令指针"),并创建一个指向DOUBLE中第一个字的新指针。因为我们需要在DOUBLE结束时恢复旧的%esi(这毕竟像一个函数调用),我们将需要一个栈来存储这些 "返回地址"(%esi的旧值)。正如你在背景文档中所看到的,FORTH有两个栈,一个是用于参数的普通栈,另一个是比较神秘的返回栈。 但我们的返回栈只是我在上一段谈到的栈,当从一个FORTH字调用到另一个FORTH字时,用来保存%esi。在这个FORTH中,我们使用普通的栈指针(%esp)作为参数栈。我们将使用i386的 "另外的"栈指针(%ebp,通常称为 "帧指针")作为我们的返回栈。我有两个宏,它们只是概括了使用%ebp作为返回栈的细节。你可以使用它们,例如 "PUSHRSP %eax"(将%eax推到返回栈上)或 "POPRSP %ebx"(将返回栈的顶部弹到%ebx中)。*//* 处理返回栈的宏. */ .macro PUSHRSP reg lea -4(%ebp),%ebp // 将寄存器推到返回栈 movl \reg,(%ebp) .endm .macro POPRSP reg mov (%ebp),\reg // 弹返回栈顶到寄存器 lea 4(%ebp),%ebp .endm/*有了这一点,我们现在可以谈一谈解释器了。在FORTH中,解释器函数通常被称为DOCOL(我认为它的意思是 "DO COLON",因为所有FORTH定义都以冒号开始,如 : DOUBLE DUP + ;解释器"(它并不是真正的 "解释")只需要将旧的%esi推到栈上,并将%esi设置为定义中的第一个字。 还记得我们用JMP *(%eax)跳转到这个函数吗? 那么这样做的一个结果是,方便的%eax包含了这个代码字的地址,所以只要在它上面加4,我们就可以得到第一个数据字的地址。 最后在设置了%esi之后,它只是做出了NEXT,导致第一个字的运行。*//* DOCOL - 解释器! */ .text .align 4DOCOL: PUSHRSP %esi // 将 %esi 推入返回栈 addl $4,%eax // %eax 指向代码字, movl %eax,%esi // 所以让 %esi 指向第一个数据字 NEXT/*为了绝对清楚地说明这一点,让我们看看DOCOL在从QUADRUPLE跳到DOUBLE时是如何工作的: QUADRUPLE: +------------------+ | 代码字 | +------------------+ DOUBLE: | DOUBLE 的地址 ---------------> +------------------+ +------------------+ %eax -> | DOCOL 的地址 |%esi -> | DOUBLE 的地址 | +------------------+ +------------------+ | DUP 的地址 | | EXIT 的地址 | +------------------+ +------------------+ | 等等. |首先,对DOUBLE的调用 调用DOCOL(DOUBLE的代码字)。 DOCOL是这样做的: 它把旧的%esi推到返回栈上。 %eax指向DOUBLE的代码字,所以我们只需在它上面加4就可以获取新的%esi: QUADRUPLE: +------------------+ | 代码字 | +------------------+ DOUBLE: | DOUBLE 的地址 ---------------> +------------------+ +------------------+ %eax -> | DOCOL 的地址 |返回栈顶部的指针 -> | DOUBLE 的地址 | + 4 = +------------------+ +------------------+ %esi -> | DUP 的地址 | | EXIT 的地址 | +------------------+ +------------------+ | 等等. |然后我们做出NEXT,由于线程代码的魔力,再次增加%esi并调用DUP。嗯,这似乎是有效的。这里有一个小问题。 因为DOCOL是这个文件中实际定义的第一个汇编位(其他的只是宏),而且我通常在编译这段代码时,文本段从地址0开始,所以DOCOL的地址是0。 因此,如果你在拆解代码时看到一个代码字为0的字,你会立即知道这个字是用FORTH写的(它不是一个汇编原语),所以用DOCOL作为解释器。开机启动 ----------------------------------------------------------------------现在,让我们来谈谈螺母和螺栓。 当我们启动程序时,我们需要设置一些东西,如返回栈。 但我们要尽快进入FORTH代码(尽管许多 "早期 "的FORTH代码仍然需要写成汇编语言原语)。这就是设置代码的作用。 做了一点内务工作,建立了独立的返回栈(注:Linux已经给了我们普通的参数栈),然后立即跳转到一个名为QUIT的FORTH字。 尽管它的名字叫QUIT,但它并没有退出任何东西。 它重置了一些内部状态并开始读取和解释命令。(之所以叫QUIT,是因为你可以在自己的FORTH代码中调用QUIT来 "退出 "你的程序,回到解释状态)。*//* 汇编器入口点. */ .text .globl _start_start: cld mov %esp,var_S0 // 在FORTH变量S0中保存初始数据栈指针. mov $return_stack_top,%ebp // 初始化返回栈. call set_up_data_segment mov $cold_start,%esi // 初始化解释器. NEXT // 运行解释器! .section .rodatacold_start: // 没有代码字的高级代码. .int QUIT/* 内建字 ----------------------------------------------------------------------还记得我们的字典条目(头部)吗? 让我们把这些与代码字和数据字放在一起,看看 : DOUBLE DUP + ; 在内存中的实际情况。 指向前一个字的指针 ^ | +--|------+---+---+---+---+---+---+---+---+------------+------------+------------+------------+ | 链接 | 6 | D | O | U | B | L | E | 0 | DOCOL | DUP | + | EXIT | +---------+---+---+---+---+---+---+---+---+------------+--|---------+------------+------------+ ^ 长度 填充 代码字 | | V 链接在下一个字中 指向 DUP 的代码字最初,我们不能直接在这里写": DOUBLE DUP + ;"(即那个字面字符串),因为我们还没有任何东西来读取字符串,在空格处将其分割,解析每个字,等等。因此,我们必须使用GNU汇编器的数据构造器(如.int、.byte、.string、.ascii等--如果你不确定的话,可以在gas信息页面上查找)来定义内建字。漫长的道路将是: .int <链接到上一个字> .byte 6 // 长度 .ascii "DOUBLE" // 字符串 .byte 0 // 填充DOUBLE: .int DOCOL // 代码字 .int DUP // 指向 DUP 代码字的指针 .int PLUS // 指向 + 代码字的指针 .int EXIT // 指向 EXIT 代码字的指针这很快就会变得相当繁琐,所以在这里我定义了一个汇编宏,这样我就可以直接写出: defword "DOUBLE",6,,DOUBLE .int DUP,PLUS,EXIT而我将获取完全相同的效果。不要太担心这个宏的具体实现细节——它很复杂!*//* 标志 - 这些将在稍后讨论. */ .set F_IMMED,0x80 .set F_HIDDEN,0x20 .set F_LENMASK,0x1f // 长度掩码 // 链接的存储链 .set link,0 .macro defword name, namelen, flags=0, label .section .rodata .align 4 .globl name_\labelname_\label : .int link // 链接 .set link,name_\label .byte \flags+\namelen // 标志 + 长度字节 .ascii "\name" // 其名称 .align 4 // 填充到下一个4字节边界 .globl \label\label : .int DOCOL // 代码字 - 解释器 // 列表中的字指向如下 .endm/*同样地,我希望有一种方法来编写用汇编语言写的字。 在开始的时候会有很多这样的方法,因为,在有足够的 "基础设施 "开始写FORTH字之前,一切都必须从汇编开始,但我也想用汇编语言定义一些常见的FORTH字,以提高速度,尽管我可以用FORTH来写它们。这是 DUP 在内存中的样子: 指向前一个字的指针 ^ | +--|------+---+---+---+---+------------+ | LINK | 3 | D | U | P | code_DUP ---------------------> 指向用于编写DUP的汇编代码,该代码以NEXT结束。 +---------+---+---+---+---+------------+ ^ 长度 代码字 | 下一个字中的 LINK同样,为了简洁地编写头部,我将编写一个名为defcode的汇编器宏。和上面的defword一样,不要担心宏的复杂细节。*/ .macro defcode name, namelen, flags=0, label .section .rodata .align 4 .globl name_\labelname_\label : .int link // 链接 .set link,name_\label .byte \flags+\namelen // 旗 + 长度字节 .ascii "\name" // 其名称 .align 4 // 填充到下一个4字节边界 .globl \label\label : .int code_\label // 代码字 .text //.align 4 .globl code_\labelcode_\label : // 汇编代码如下 .endm/* 现在是一些简单的FORTH原语。 为了提高速度,这些都是用汇编编写的。 如果你了解i386汇编语言,那么就值得阅读这些内容。 但如果你不懂汇编,你可以跳过这些细节。*/ defcode "DROP",4,,DROP pop %eax // 丢栈顶 NEXT defcode "SWAP",4,,SWAP pop %eax // 交换栈顶两个元素 pop %ebx push %eax push %ebx NEXT defcode "DUP",3,,DUP mov (%esp),%eax // duplicate top of stack 重复栈顶 push %eax NEXT defcode "OVER",4,,OVER mov 4(%esp),%eax // 获取栈的第二个元素 push %eax // 并将其推到顶部 NEXT defcode "ROT",3,,ROT pop %eax pop %ebx pop %ecx push %ebx push %eax push %ecx NEXT defcode "-ROT",4,,NROT pop %eax pop %ebx pop %ecx push %eax push %ecx push %ebx NEXT defcode "2DROP",5,,TWODROP // 丢弃栈顶两个元素 pop %eax pop %eax NEXT defcode "2DUP",4,,TWODUP // duplicate top two elements of stack 复制栈顶两个元素 mov (%esp),%eax mov 4(%esp),%ebx push %ebx push %eax NEXT defcode "2SWAP",5,,TWOSWAP // 交换栈顶的两对元素 pop %eax pop %ebx pop %ecx pop %edx push %ebx push %eax push %edx push %ecx NEXT defcode "?DUP",4,,QDUP // duplicate top of stack if non-zero 如果非零,则重复栈顶 movl (%esp),%eax test %eax,%eax jz 1f push %eax1: NEXT defcode "1+",2,,INCR incl (%esp) // 递增栈顶 NEXT defcode "1-",2,,DECR decl (%esp) // 递减栈顶 NEXT defcode "4+",2,,INCR4 addl $4,(%esp) // 将 4 添加到栈顶 NEXT defcode "4-",2,,DECR4 subl $4,(%esp) // 从栈顶减去 4 NEXT defcode "+",1,,ADD pop %eax // 获取栈顶 addl %eax,(%esp) // 并将其添加到栈上的下一个字 NEXT defcode "-",1,,SUB pop %eax // 获取栈顶 subl %eax,(%esp) // 并从栈上的下一个字中减去它 NEXT defcode "*",1,,MUL pop %eax pop %ebx imull %ebx,%eax push %eax // 忽略溢出 NEXT/* 在这个FORTH中,只有/MOD是原语。 以后我们将根据原语/MOD来定义 /和MOD 字。 i386汇编指令idiv的设计同时留下了商和余数,因此这是一个明显的选择。*/ defcode "/MOD",4,,DIVMOD xor %edx,%edx pop %ebx pop %eax idivl %ebx push %edx // 推余数 push %eax // 推商 NEXT/* 大量的对比操作,如=、<、>等。 ANS FORTH说,对比字应该返回所有(二进制)1表示TRUE,所有0表示FALSE。 但这是一个奇怪的约定,所以这个FORTH打破了它,返回更正常的(对C程序员来说...)1表示TRUE,0表示FALSE。*/defcode "=",1,,EQU // 顶部的两个字是否相等? pop %eax pop %ebx cmp %ebx,%eax sete %al movzbl %al,%eax pushl %eax NEXTdefcode "<>",2,,NEQU // 顶部的两个字是不相等的? pop %eax pop %ebx cmp %ebx,%eax setne %al movzbl %al,%eax pushl %eax NEXTdefcode "<",1,,LT pop %eax pop %ebx cmp %eax,%ebx setl %al movzbl %al,%eax pushl %eax NEXT defcode ">",1,,GT pop %eax pop %ebx cmp %eax,%ebx setg %al movzbl %al,%eax pushl %eax NEXT defcode "<=",2,,LE pop %eax pop %ebx cmp %eax,%ebx setle %al movzbl %al,%eax pushl %eax NEXT defcode ">=",2,,GE pop %eax pop %ebx cmp %eax,%ebx setge %al movzbl %al,%eax pushl %eax NEXTdefcode "0=",2,,ZEQU // 栈顶等于0? pop %eax test %eax,%eax setz %al movzbl %al,%eax pushl %eax NEXTdefcode "0<>",3,,ZNEQU // 栈顶不是0? pop %eax test %eax,%eax setnz %al movzbl %al,%eax pushl %eax NEXTdefcode "0<",2,,ZLT // 与 0 的对比 pop %eax test %eax,%eax setl %al movzbl %al,%eax pushl %eax NEXTdefcode "0>",2,,ZGT pop %eax test %eax,%eax setg %al movzbl %al,%eax pushl %eax NEXT defcode "0<=",3,,ZLE pop %eax test %eax,%eax setle %al movzbl %al,%eax pushl %eax NEXT defcode "0>=",3,,ZGE pop %eax test %eax,%eax setge %al movzbl %al,%eax pushl %eax NEXTdefcode "AND",3,,AND // 按位 与 pop %eax andl %eax,(%esp) NEXTdefcode "OR",2,,OR // 按位 或 pop %eax orl %eax,(%esp) NEXTdefcode "XOR",3,,XOR // 按位 异或 pop %eax xorl %eax,(%esp) NEXTdefcode "INVERT",6,,INVERT // 这是 FORTH 按位 “NOT” 函数(参见 NEGATE 和 NOT) notl (%esp) NEXT/* 返回来自FORTH 的字 ---------------------------------------------------------------------- 是时候谈谈当我们EXIT一个函数时会发生什么。 在这个图中,QUADRUPLE调用了DOUBLE,而DOUBLE即将退出(看%esi指向的地方): QUADRUPLE +------------------+ | 代码字 | +------------------+ DOUBLE | DOUBLE的地址 ---------------> +------------------+ +------------------+ | 代码字 | | DOUBLE的地址 | +------------------+ +------------------+ | DUP 的地址 | | EXIT的地址 | +------------------+ +------------------+ | + 的地址 | +------------------+ %esi -> | EXIT的地址 | +------------------+ 当+函数进行NEXT时会发生什么? 好吧,下面的代码被执行。*/ defcode "EXIT",4,,EXIT POPRSP %esi // 弹出返回栈进到 %esi NEXT/*EXIT获取我们之前保存在返回栈中的旧%esi,并将其放在%esi中。所以在这之后(但就在NEXT之前)我们得到:QUADRUPLE +------------------+ | 代码字 | +------------------+ DOUBLE | DOUBLE的地址 ---------------> +------------------+ +------------------+ | 代码字 | %esi -> | DOUBLE的地址 | +------------------+ +------------------+ | DUP 的地址 | | EXIT的地址 | +------------------+ +------------------+ | + 的地址 | +------------------+ | EXIT的地址 | +------------------+而NEXT只是完成了工作,嗯,在这种情况下只是再次调用DOUBLE :-)字面 ----------------------------------------------------------------------我之前 "忽略 "的最后一点是如何处理那些除了调用其他函数之外还做任何事情的函数。 例如,假设DOUBLE是这样定义的:: DOUBLE 2 * ;它做的是同样的事情,但由于它包含字面意义上的2,我们该如何编译它呢? 一种方法是建立一个名为 "2 "的函数(你必须用汇编程序来写),但你需要为每一个你想使用的字面符号建立一个函数。FORTH通过编译函数来解决这个问题,使用一个叫做LIT的特殊字: +---------------------------+-------+-------+-------+-------+-------+ | (通常DOUBLE的头部) | DOCOL | LIT | 2 | * | EXIT | +---------------------------+-------+-------+-------+-------+-------+LIT是以正常方式执行的,但它接下来做的事情绝对不正常。 它看着%esi(现在指向数字2),抓住它,把它推到栈上,然后操纵%esi,以便跳过这个数字,就像它从未出现过一样。最巧妙的是,整个抓取/操纵过程可以用一条单字节单i386指令完成,即我们的老朋友LODSL。 与其让我画更多的ASCII艺术图,不如看看你能不能找到LIT的工作原理:*/defcode "LIT",3,,LIT // %esi指向下一个命令,但在本例中它指向下一个32位整数的字面。 //把这个字面放入%eax,然后递增%esi。 //在x86上,这是一个方便的单字节指令!(参见NEXT宏) lodsl push %eax // 将字面数字推到栈上 NEXT/*内存 ----------------------------------------------------------------------FORTH的一个重要特点是,它使你可以直接访问机器的最低层。 在FORTH中经常会直接操纵内存,这些是直接操纵的原语字:*/defcode "!",1,,STORE pop %ebx // 地址到存储 pop %eax // 数据到存储那里 mov %eax,(%ebx) // 存储它 NEXT defcode "@",1,,FETCH pop %ebx // 地址到拾取 mov (%ebx),%eax // 拾取它 push %eax // 推值上到栈 NEXT defcode "+!",2,,ADDSTORE pop %ebx // 地址 pop %eax // 要加的数量 addl %eax,(%ebx) // 加上它 NEXT defcode "-!",2,,SUBSTORE pop %ebx // 地址 pop %eax // 要减去的数量 subl %eax,(%ebx) // 加上它 NEXT/* ! 和 @(STORE和FETCH)存储32位字。 能够读写字节也很有用,所以我们也定义了标准字 C@ 和 C!。 面向字节的操作只在允许它们的架构上工作(i386就是其中之一)。 */ defcode "C!",2,,STOREBYTE pop %ebx // 地址到存储 pop %eax // 数据到存储那里 movb %al,(%ebx) // 存储它 NEXT defcode "C@",2,,FETCHBYTE pop %ebx // 地址到拾取 xor %eax,%eax movb (%ebx),%al // 拾取它 push %eax // 推值上到栈 NEXT/* C@C! 是一个有用的字节复制原语. */ defcode "C@C!",4,,CCOPY movl 4(%esp),%ebx // 源的地址 movb (%ebx),%al // 获取源的字符 pop %edi // 目的地址 stosb // 复制到目的地 push %edi // 递增目的地地址 incl 4(%esp) // 递增源的地址 NEXT/* and CMOVE 是一个块复制操作. */ defcode "CMOVE",5,,CMOVE mov %esi,%edx // 保留 %esi pop %ecx // 长度 pop %edi // 目的地地址 pop %esi // 源地址 rep movsb // 复制源到目的地 mov %edx,%esi // 恢复 %esi NEXT/* 内建变量----------------- 这些是一些内置变量和相关的标准FORTH字。 其中,到目前为止,我们唯一讨论过的是LATEST,它指向FORTH字典中最后一个(最近定义的)字。 LATEST也是一个FORTH字,它将LATEST(变量)的地址推到栈上,因此可以用@和!运算符来读或写它。 例如,要打印LATEST的当前值(这可以适用于任何FORTH变量),你可以这样做: LATEST @ . CR 为了使定义变量更简短,我使用了一个叫做defvar的宏,与上面的defword和defcode类似。 (事实上,defvar宏使用defcode来做字典的头部)。*/ .macro defvar name, namelen, flags=0, label, initial=0 defcode \name,\namelen,\flags,\label push $var_\name NEXT .data .align 4var_\name : .int \initial .endm/* 内建变量是: STATE 解释器是在执行代码(0)还是在编译一个字(非零)? LATEST 指向字典中的最新(最近定义的)字。 HERE 指向内存的下一个空闲字节。 编译时,被编译的字在这里。 S0 存储参数栈顶的地址。 BASE 当前用于打印和读取数字的基础。*/ defvar "STATE",5,,STATE defvar "HERE",4,,HERE defvar "LATEST",6,,LATEST,name_SYSCALL0 // SYSCALL0必须是内建字典中的最后一个。 defvar "S0",2,,SZ defvar "BASE",4,,BASE,10/*内建常量 -------------------------将一些常量暴露给FORTH也是很有用的。 当这个字被执行时,它将一个常量值推到栈上。内建常量是: VERSION 是这个FORTH的当前版本。 R0 返回栈顶部的地址。 DOCOL 指向DOCOL的指针。 F_IMMED IMMEDIATE标志的实际值。 F_HIDDEN HIDDEN标志的实际值。 F_LENMASK 在flags/len字节中的长度掩码。 SYS_* 以及各种Linux系统调用的数字代码(来自<asm/unistd.h>)。*///#include <asm-i386/unistd.h> // 你可能需要用这个来代替#include <asm/unistd.h> .macro defconst name, namelen, flags=0, label, value defcode \name,\namelen,\flags,\label push $\value NEXT .endm defconst "VERSION",7,,VERSION,JONES_VERSION defconst "R0",2,,RZ,return_stack_top defconst "DOCOL",5,,__DOCOL,DOCOL defconst "F_IMMED",7,,__F_IMMED,F_IMMED defconst "F_HIDDEN",8,,__F_HIDDEN,F_HIDDEN defconst "F_LENMASK",9,,__F_LENMASK,F_LENMASK defconst "SYS_EXIT",8,,SYS_EXIT,__NR_exit defconst "SYS_OPEN",8,,SYS_OPEN,__NR_open defconst "SYS_CLOSE",9,,SYS_CLOSE,__NR_close defconst "SYS_READ",8,,SYS_READ,__NR_read defconst "SYS_WRITE",9,,SYS_WRITE,__NR_write defconst "SYS_CREAT",9,,SYS_CREAT,__NR_creat defconst "SYS_BRK",7,,SYS_BRK,__NR_brk defconst "O_RDONLY",8,,__O_RDONLY,0 defconst "O_WRONLY",8,,__O_WRONLY,1 defconst "O_RDWR",6,,__O_RDWR,2 defconst "O_CREAT",7,,__O_CREAT,0100 defconst "O_EXCL",6,,__O_EXCL,0200 defconst "O_TRUNC",7,,__O_TRUNC,01000 defconst "O_APPEND",8,,__O_APPEND,02000 defconst "O_NONBLOCK",10,,__O_NONBLOCK,04000/* 返回栈 ---------------------------------------------------------------------- 这些字允许你访问返回栈。 回顾一下,寄存器%ebp总是指向返回栈的顶部。*/defcode ">R",2,,TOR pop %eax // 弹出形参栈进到 %eax PUSHRSP %eax // 推它到返回栈上 NEXT defcode "R>",2,,FROMR POPRSP %eax // 弹出返回栈到 %eax 上 push %eax // 并且推到形参栈上 NEXT defcode "RSP@",4,,RSPFETCH push %ebp NEXT defcode "RSP!",4,,RSPSTORE pop %ebp NEXT defcode "RDROP",5,,RDROP addl $4,%ebp // 弹出返回栈并扔掉 NEXT/* 参数(数据)栈 / [形参栈]---------------------------------------------------------------------- 这些函数允许你操纵参数栈。 回顾一下,Linux为我们设置了参数栈。 并通过%esp访问它。*/defcode "DSP@",4,,DSPFETCH mov %esp,%eax push %eax NEXT defcode "DSP!",4,,DSPSTORE pop %esp NEXT/* 输入和输出 ---------------------------- 这些是我们第一个真正有意义的/复杂的 FORTH 原语。 我选择了用汇编程序来写,但令人惊讶的是,在 "真正的 "FORTH实现中,这些往往是用更基本的FORTH原语来写的。 我选择了避免这种做法,因为我认为这只会使实现变得模糊不清。 毕竟,你可能不了解汇编程序,但你可以把它看作是一个不透明的代码块,做它所说的事情。 让我们先讨论一下输入。 FORTH字KEY从stdin中读取下一个字节(并将其推到参数栈中)。因此,如果KEY被调用,有人按了空格键,那么数字32(空格的ASCII码)就被推到栈上。 在FORTH中,阅读代码和阅读输入是没有区别的。 我们可能是在阅读和编译代码,可能是在阅读要执行的字,可能是要求用户输入他们的名字--最终都是通过KEY来实现的。 KEY的实现使用一个一定大小的输入缓冲区(定义在本文件的末尾)。 它调用Linux的read(2)系统调用来填充这个缓冲区,并使用几个变量来跟踪它在缓冲区中的位置,如果它的输入缓冲区用完了,它就会自动重新填充。 KEY做的另一件事是,如果它检测到stdin已经关闭,它就退出程序,这就是为什么当你点击^D时,FORTH系统会干净地退出。 缓冲区 缓冲顶部 | | V V +-------------------------------+--------------------------------------+ | 从stdin中读取输入 ....... | 缓冲区的未使用部分 | +-------------------------------+--------------------------------------+ ^ | currkey(下一个要读的字符) <---------------------- 缓冲区_大小 (4096字节) -------------------------->*/defcode "KEY",3,,KEY call _KEY push %eax // 推返回值到栈上 NEXT_KEY: mov (currkey),%ebx cmp (bufftop),%ebx jge 1f // 用尽了输入缓冲区? xor %eax,%eax mov (%ebx),%al // 从输入缓冲区获取下一个键 inc %ebx mov %ebx,(currkey) // 递增currkey ret1: // 输入用完了。使用read(2)从stdin中获取更多的输入。. xor %ebx,%ebx // 第1个参数: stdin mov $buffer,%ecx // 第2个参数:缓冲区 mov %ecx,currkey mov $BUFFER_SIZE,%edx // 第3个参数:最大长度 mov $__NR_read,%eax // syscall: 读 int $0x80 test %eax,%eax // 如果 %eax <= 0,则退出. jbe 2f addl %eax,%ecx // 缓冲区+%eax = bufftop mov %ecx,bufftop jmp _KEY2: // 错误或输入结束:退出程序. xor %ebx,%ebx mov $__NR_exit,%eax // syscall:退出 int $0x80 .data .align 4currkey: .int buffer // 输入缓冲区中的当前位置(下一个要读的字符)。.bufftop: .int buffer // 输入缓冲区的最后有效数据+1./* 相比之下,输出就简单多了。 FORTH字EMIT写出一个字节到stdout。这个实现只是使用了写系统调用。 没有尝试对输出进行缓冲,但如果能增加输出,将是一个很好的练习。*/defcode "EMIT",4,,EMIT pop %eax call _EMIT NEXT_EMIT: mov $1,%ebx // 第一参数:stdout // 写需要写的字节的地址 mov %al,emit_scratch mov $emit_scratch,%ecx // 第二参数:地址 mov $1,%edx // 第三参数: nbytes = 1 mov $__NR_write,%eax // 写系统调用 int $0x80 ret .data // 注:更容易装入.data部分emit_scratch: .space 1 // EMIT使用的划痕/* 回到输入,WORD是一个FORTH字,它读取输入的下一个完整字。 它的具体做法是,首先跳过任何空白(空格、制表符、换行符等)。然后,它调用KEY将字符读入一个内部缓冲区,直到遇到一个空白。 然后,它计算读到的字的长度,并将地址和长度作为两个字返回到栈中(长度在栈的顶部)。 请注意,WORD有一个单一的内部缓冲区,它每次都会覆盖这个缓冲区(类似于静态C语言的字符串)。 另外,注意到WORD的内部缓冲区只有32字节长,而且没有溢出检查。 31字节恰好是我们支持的FORTH字的最大长度,这就是WORD的用途:在编译和执行代码时读取FORTH字。 返回的字符串不是以NUL为结尾的。 起始地址+长度是FORTH中表示字符串的正常方式(不是像C语言那样以ASCII NUL字符结束),因此FORTH字符串可以包含任何字符,包括NUL,也可以是任何长度。 由于上述所有的特殊性和局限性,WORD并不适合仅仅读取字符串(如用户输入)。注意,在执行时,你会看到。 WORD FOO将 "FOO "和长度3放在栈中,但在编译时。 : BAR WORD FOO ;是一个错误(或者至少它没有做你可能期望的事情)。 稍后我们将讨论编译和立即模式,你会明白为什么。*/defcode "WORD",4,,WORD call _WORD push %edi // 推基本地址 push %ecx // 推长度 NEXT_WORD: /* 搜索第一个非空白字符。 同时跳过\注释。*/1: call _KEY // 获取下一个键,在%eax中返回 cmpb $'\\',%al // 评论的开始? je 3f // 如果是,跳过评论 cmpb $' ',%al jbe 1b // 如果是,保持寻找 /* 搜索字的结尾,边搜索边存储字符. */ mov $word_buffer,%edi // 指向返回缓冲区2: stosb // 将字符添加到返回缓冲区 call _KEY // 获取下一个键,在%al中返回 cmpb $' ',%al // 是空白? ja 2b // 如果没有,保持循环 /* 返回字(好吧,静态缓冲区)和长度. */ sub $word_buffer,%edi mov %edi,%ecx // 返回字的长度 mov $word_buffer,%edi // 返回字的地址 ret /* 代码到跳过\注释至当前行末. */3: call _KEY cmpb $'\n',%al // 行结束了吗? jne 3b jmp 1b .data /* NB:更容易装入.data部分 一个静态缓冲区,WORD在这里返回。 随后的调用会覆盖这个缓冲区。 最大字长为32个字符.*/word_buffer: .space 32/* 除了读入字之外,我们还需要读入数字,为此我们使用了一个名为NUMBER的函数。 这个函数解析一个数字字符串,比如由WORD返回的数字,并将数字推到参数栈上。 该函数使用变量BASE作为转换的基数(radix),例如,如果BASE是2,那么我们期待一个二进制数字。 通常,BASE是10。 如果该字以'-'字符开头,那么返回值为负数。 如果字符串不能被解析为数字(或包含当前BASE以外的字符),那么我们需要返回一个错误提示。所以NUMBER实际上在栈中返回两个项目。在栈顶部,我们返回未转换的字符数(即-如果是0,那么所有字符都被转换了,所以没有错误)。 栈顶的第二项是解析后的数字,如果有错误,则是一个部分值。*/defcode "NUMBER",6,,NUMBER pop %ecx // 字符串的长度 pop %edi // 字符串的起始地址 call _NUMBER push %eax // 解析的数字 push %ecx // 未解析的字符数(0 = 无错误)。 NEXT_NUMBER: xor %eax,%eax xor %ebx,%ebx test %ecx,%ecx // 试图解析一个零长度的字符串是一个错误。但会返回0。 jz 5f movl var_BASE,%edx // 获取 BASE (in %dl) // 检查第一个字符是否为'-'。. movb (%edi),%bl // %bl = 字符串中的第一个字符 inc %edi push %eax // 推0到栈上 cmpb $'-',%bl // 负数? jnz 2f pop %eax push %ebx // 推 <>0 到栈上,表示负数 dec %ecx jnz 1f pop %ebx // 错误:字符串只有'-'。 movl $1,%ecx ret // 循环读取数字。1: imull %edx,%eax // %eax *= BASE movb (%edi),%bl // %bl = 字符串中的下一个字符 inc %edi // 将0-9,A-Z转换为0-35的数字。2: subb $'0',%bl // < '0'? jb 4f cmp $10,%bl // <= '9'? jb 3f subb $17,%bl // < 'A'? (17 is 'A'-'0') jb 4f addb $10,%bl3: cmp %dl,%bl // >= BASE? jge 4f // 好吧,那就把它加到%eax里,然后循环。 add %ebx,%eax dec %ecx jnz 1b // 如果第一个字符是'-',则结果是负数(保存在栈上)。4: pop %ebx test %ebx,%ebx jz 5f neg %eax5: ret/* 字典查询 ---------------------------------------------------------------------- 我们正在建立关于FORTH代码如何编译的序幕,但首先我们需要更多的基础设施。 FORTH 字 FIND 接收一个字符串(由 WORD 解析的字--见上文)并在字典中查找它。 如果找到的话,它实际返回的是字典头的地址,如果没有找到,则返回 0。 因此,如果DOUBLE在字典中被定义,那么WORD DOUBLE FIND返回以下指针: 指针指向这个 | | V +---------+---+---+---+---+---+---+---+---+------------+------------+------------+------------+ | LINK | 6 | D | O | U | B | L | E | 0 | DOCOL | DUP | + | EXIT | +---------+---+---+---+---+---+---+---+---+------------+------------+------------+------------+ 另见 >CFA 和 >DFA。 FIND不能找到被标记为HIDDEN的字典条目。 原因见下文。*/defcode "FIND",4,,FIND pop %ecx // %ecx = 长度 pop %edi // %edi = 地址 call _FIND push %eax // %eax = 字典条目的地址(或NULL)。 NEXT_FIND: push %esi // 保存%esi,这样我们可以在字符串对比中使用它。 // 现在我们开始在字典中向后搜索这个字。 mov var_LATEST,%edx // LATEST 指向字典中最新字的名称头。1: test %edx,%edx // NULL指针?(链表的末端)。 je 4f // 对比预期的长度和该字的长度。 // 请注意,如果在这个字上设置了F_HIDDEN标志。 // 那么通过一点小技巧,这将不会选中这个字(长度会出现错误)。 xor %eax,%eax movb 4(%edx),%al // %al = 标志+长度字段 andb $(F_HIDDEN|F_LENMASK),%al // %al = 名字长度 cmpb %cl,%al // 长度是一样的吗? jne 2f // 详细对比一下这些字符串。 push %ecx // 保存长度 push %edi // 保存地址(重复cmpsb将移动这个指针)。 lea 5(%edx),%esi // 我们要检查的字典字符串。 repe cmpsb // 对比一下字符串。 pop %edi pop %ecx jne 2f // 不一样的。 // 字符串是一样的--返回%eax中的头指针 pop %esi mov %edx,%eax ret2: mov (%edx),%edx // 通过链接字段向后移动到前一个字 jmp 1b // ...和循环。4: // 未找到。 pop %esi xor %eax,%eax // 返回0表示未找到。 ret/* FIND返回字典指针,但在编译时我们需要代码字指针(记得FORTH定义被编译成代码字指针的列表)。 标准 FORTH 字符 >CFA 将字典指针变成代码字指针。 下面的例子显示了如下结果: WORD DOUBLE FIND >CFA FIND 返回一个指向这个 | >CFA 将其转换为一个指向这个 | | V V +---------+---+---+---+---+---+---+---+---+------------+------------+------------+------------+ | LINK | 6 | D | O | U | B | L | E | 0 | DOCOL | DUP | + | EXIT | +---------+---+---+---+---+---+---+---+---+------------+------------+------------+------------+ 代码字 注意: 因为名字的长度不同,这不是一个简单的递增。在这个FORTH中,你不能轻易地将一个代码字指针转回一个字典条目指针,但在大多数FORTH实现中不是这样,他们在定义中存储了一个回溯指针(有明显的内存/复杂性代价)。 他们这样做的原因是,为了快速反编译FORTH定义,能够向后走(代码字->字典条目)是非常有用的。CFA代表什么? 我最好的猜测是 "代码字段地址" / "Code Field Address"。*/ defcode ">CFA",4,,TCFA pop %edi call _TCFA push %edi NEXT_TCFA: xor %eax,%eax add $4,%edi // 跳过链接指针。 movb (%edi),%al // 将flags+len加载到%al中。 inc %edi // 跳过flags+len字节。 andb $F_LENMASK,%al // 只是长度,而不是标志。 add %eax,%edi // 跳过这个名字。 addl $3,%edi // 代码字是4字节对齐的。 andl $~3,%edi ret/* 与>CFA相关的是>DFA,它接收由FIND返回的字典条目地址,并返回一个指向第一个数据字段的指针。 FIND返回一个指针,指向这个 | >CFA 将其转换为一个指向这个 | | | | >DFA 将其转换为一个指向这个 | | | V V V +---------+---+---+---+---+---+---+---+---+------------+------------+------------+------------+ | LINK | 6 | D | O | U | B | L | E | 0 | DOCOL | DUP | + | EXIT | +---------+---+---+---+---+---+---+---+---+------------+------------+------------+------------+ 代码字 (请关注FIG-FORTH / ciforth来源的人注意。我的>DFA定义与他们的不同,因为他们有一个额外的间接性)。你可以看到>DFA在FORTH中很容易定义,只需在>CFA的结果上加4即可。*/defword ">DFA",4,,TDFA .int TCFA // >CFA (获取代码字段地址) .int INCR4 // 4+ (加4以获取下一个字) .int EXIT // EXIT (从FORTH字返回)/* 编译 ---------------------------------------------------------------------- 现在我们来谈谈FORTH是如何编译字的。 回顾一下,一个字的定义是这样的: : DOUBLE DUP + ; 而我们必须将其转化为: 指向前一个字的指针 ^ | +--|------+---+---+---+---+---+---+---+---+------------+------------+------------+------------+ | LINK | 6 | D | O | U | B | L | E | 0 | DOCOL | DUP | + | EXIT | +---------+---+---+---+---+---+---+---+---+------------+--|---------+------------+------------+ ^ len pad 代码字 | | V LATEST 指在这里 指向DUP的代码字 有几个问题需要解决。 把新字放在哪里? 我们如何阅读字? 我们如何定义字 :(COLON)和 ;(SEMICOLON)? FORTH相当优雅地解决了这个问题,正如你所期望的那样,它以一种非常低级的方式允许你改变编译器在自己代码上的工作方式。FORTH有一个INTERPRET函数(这次是真正的解释器,而不是DOCOL),它在一个循环中运行,读取字(使用WORD),查找它们(使用FIND),把它们变成代码字指针(使用>CFA)并决定如何处理它们。它做什么取决于解释器的模式(在变量STATE中)。当STATE为零时,解释器只是在查找每个字时运行它们。 这就是所谓的立即模式。当STATE为非零时,有趣的事情发生了--编译模式。 在这种模式下,解释器将代码字指针附加到用户内存中(HERE变量指向用户内存的下一个空闲字节--见下面的DATA SEGMENT部分)。 因此,你也许能看出我们如何定义 :(COLON)。 总的计划是: (1) 使用WORD读取已定义函数名。 (2) 构造字典条目 -- 仅头部 -- 在用户内存里: 指向前一个字的指针(来自LATEST)。 +-- 之后,HERE指向这里, ^ | 解释器将在这里开始附加代码字, | V +--|------+---+---+---+---+---+---+---+---+------------+ | LINK | 6 | D | O | U | B | L | E | 0 | DOCOL | +---------+---+---+---+---+---+---+---+---+------------+ len pad 代码字 (3) 设置LATEST指向新定义的字,......。 (4) ......最重要的是在新的代码字后留下HERE的指向。 这是解释器将附加代码字的地方。 (5) 将STATE设置为1。 这就进入了编译模式,因此解释器开始附加代码字到我们的部分形式的头部. 运行后,我们的输入是这样的: : DOUBLE DUP + ; ^ | 由KEY返回的下一个字节将是DUP的'D'字符。 所以解释器(现在它处于编译模式,所以我猜它实际上是编译器)读取 "DUP"。 在字典中查找它,得到它的代码字指针,然后把它加到字典中。: +-- 这里更新到指向这里. | V +---------+---+---+---+---+---+---+---+---+------------+------------+ | LINK | 6 | D | O | U | B | L | E | 0 | DOCOL | DUP | +---------+---+---+---+---+---+---+---+---+------------+------------+ len pad 代码字 接下来,我们读取+,获取代码字指针,并附加它: +-- 这里更新到指向这里. | V +---------+---+---+---+---+---+---+---+---+------------+------------+------------+ | LINK | 6 | D | O | U | B | L | E | 0 | DOCOL | DUP | + | +---------+---+---+---+---+---+---+---+---+------------+------------+------------+ len pad 代码字 问题是接下来会发生什么。 显然,我们不希望发生的是,我们读到";"并编译它,然后继续编译一切。在这一点上,FORTH使用了一个技巧。 记得字典中定义的长度字节并不只是一个普通的长度字节,还可以包含标志。 有一个标志被称为IMMEDIATE标志(在这段代码中为F_IMMED)。 如果字典中的一个字被标志为IMMEDIATE,那么解释器就会立即运行它_即使它处于编译模式_。这就是字 ; (SEMICOLON) 的作用--作为一个在字典中被标记为IMMEDIATE的字。它所做的就是将EXIT的代码字附加到当前的定义上,并切换回立即模式(将STATE设为0)。 不久我们将看到;的实际定义,我们将看到它实际上是一个非常简单的定义,声明为IMMEDIATE。在解释器读取了;并 "立即 "执行后,我们获取了这个结果: +---------+---+---+---+---+---+---+---+---+------------+------------+------------+------------+ | LINK | 6 | D | O | U | B | L | E | 0 | DOCOL | DUP | + | EXIT | +---------+---+---+---+---+---+---+---+---+------------+------------+------------+------------+ len pad 代码字 ^ | HERE STATE被设置为0. 就这样,工作完成了,我们的新定义被编译了,我们又回到了立即模式中,只是读和执行字,也许包括调用测试我们的新字DOUBLE。这里面唯一的问题是,当我们的字被编译的时候,它处于半成品的状态。 我们当然不希望DOUBLE在这段时间内以某种方式被调用。 有几种方法可以阻止这种情况的发生,但在FORTH中,我们所做的是在编译过程中用HIDDEN标志(在这段代码中为F_HIDDEN)来标志这个字。 这可以防止FIND找到它,从而在理论上阻止它被调用的机会。上面解释了编译 :(COLON)和;(SEMICOLON)是如何工作的,一会儿我将对它们进行定义。 :(COLON)函数可以通过把它写成两部分而变得更通用一些。 第一部分,称为CREATE,仅制作头部: +-- 之后,HERE指在这里. | V +---------+---+---+---+---+---+---+---+---+ | LINK | 6 | D | O | U | B | L | E | 0 | +---------+---+---+---+---+---+---+---+---+ len pad 和第二部分,实际定义的 :(COLON) , 调用CREATE并附加DOCOL代码字,所以留下了: +-- 之后,HERE指在这里. | V +---------+---+---+---+---+---+---+---+---+------------+ | LINK | 6 | D | O | U | B | L | E | 0 | DOCOL | +---------+---+---+---+---+---+---+---+---+------------+ len pad 代码字 CREATE是一个标准的FORTH字,这种拆分的好处是我们可以重复使用它来创建其他类型的字(不仅仅是包含代码的字,还有包含变量、常量和其他数据的字)。*/ defcode "CREATE",6,,CREATE // 获取名称长度和地址. pop %ecx // %ecx = 长度 pop %ebx // %ebx = 名称的地址 // 链接指针. movl var_HERE,%edi // %edi是头部的地址 movl var_LATEST,%eax // 获取链接指针 stosl // 并将其存储在头部处. // 字节长度和字本身. mov %cl,%al // 获取长度. stosb // 存储长度/标志字节. push %esi mov %ebx,%esi // %esi = 字 rep movsb // 复制该字 pop %esi addl $3,%edi // 对齐到下一个4字节的边界. andl $~3,%edi // 更新 LATEST 与 HERE. movl var_HERE,%eax movl %eax,var_LATEST movl %edi,var_HERE NEXT/* 因为我想在FORTH中定义:(COLON),而不是汇编,所以我们还需要一些FORTH字来使用。第一个是, (COMMA),这是一个标准的FORTH字,它将一个32位的整数附加到HERE所指向的用户内存上,并在HERE上加4。 所以,(COMMA)的作用是: HERE的前一个值 | V +---------+---+---+---+---+---+---+---+---+-- - - - - --+------------+ | LINK | 6 | D | O | U | B | L | E | 0 | | <data> | +---------+---+---+---+---+---+---+---+---+-- - - - - --+------------+ len pad ^ | HERE的新值 而<data>是栈顶的任何32位整数。 ,(COMMA) 是编译时的一个基本操作。 它用于将代码字附加到正在编译的当前字上。*/ defcode ",",1,,COMMA pop %eax // 代码指针到存储. call _COMMA NEXT_COMMA: movl var_HERE,%edi // HERE stosl // 存储它。 movl %edi,var_HERE // 更新 HERE (增量) ret/* 我们对:(COLON)和;(SEMICOLON)的定义将需要在编译模式中进行切换。立即模式和编译模式的对比存储在全局变量STATE中,通过更新这个变量,我们可以在两种模式之间切换。 由于各种原因,FORTH定义了两个标准字 [ 和 ] (LBRAC和RBRAC)来切换模式: 字 汇编器 动作 效果 [ LBRAC STATE := 0 切换到立即模式. ] RBRAC STATE := 1 切换到编译模式. [ (LBRAC) 是一个IMMEDIATE字。 原因如下:如果我们处于编译模式,而解释器看到了[,那么它就会编译它而不是运行它。 我们将永远无法切换回立即模式!所以我们将这个字标志为IMMEDIATE。 这样即使在编译模式下,这个字也会立即运行,把我们切换回立即模式。*/ defcode "[",1,F_IMMED,LBRAC xor %eax,%eax movl %eax,var_STATE // 设置STATE为0. NEXT defcode "]",1,,RBRAC movl $1,var_STATE // 设置STATE为1. NEXT/* 现在我们可以用CREATE定义: (COLON) 。 它只是调用CREATE,附加DOCOL(代码字),设置HIDDEN这个字,然后进入编译模式。*/ defword ":",1,,COLON .int WORD // 获取新字的名称 .int CREATE // CREATE 字典条目/头部 .int LIT, DOCOL, COMMA // 附加 DOCOL (代码字). .int LATEST, FETCH, HIDDEN // 使这个字隐藏起来(定义见下文). .int RBRAC // 进入编译模式. .int EXIT // 从函数中返回./* ;(SEMICOLON)也很优雅简单。 注意F_IMMED标志。*/ defword ";",1,F_IMMED,SEMICOLON .int LIT, EXIT, COMMA // 附加EXIT(所以这个字会返回). .int LATEST, FETCH, HIDDEN // 套索扣隐藏标志 -- 解除对该字的隐藏(定义见下文). .int LBRAC // 回到IMMEDIATE(立即)模式. .int EXIT // 从该函数返回。/* 扩展编译器 ---------------------------------------------------------------------- 标志为IMMEDIATE(F_IMMED)的字不只是FORTH编译器可以使用的。 你也可以定义你自己的IMMEDIATE字,这是扩展基本FORTH时的一个重要方面,因为它实际上允许你扩展编译器本身。 gcc允许你这么做吗?标准的FORTH字,如IF、WHILE、. " 等,都是作为基本编译器的扩展写的,都是IMMEDIATE字。IMMEDIATE字在最近定义的字上切换F_IMMED(IMMEDIATE标志),如果你在定义过程中调用它,则在当前的字上。典型的用法是: : MYIMMEDWORD IMMEDIATE ...definition... ; 但有些FORTH程序员却这样写: : MYIMMEDWORD ...definition... ; IMMEDIATE 这两种用法是等同的, 一级近似.*/ defcode "IMMEDIATE",9,F_IMMED,IMMEDIATE movl var_LATEST,%edi // LATEST 字. addl $4,%edi // 指向名称/标志的字节. xorb $F_IMMED,(%edi) // 套索扣 IMMED 位. NEXT/* 'addr HIDDEN' 拴牢addr处定义的字的隐藏标志(F_HIDDEN)。 要隐藏最近定义的字(在上面的 : 和 ; 定义中使用),你应该这样做: LATEST @ HIDDEN 'HIDE word'在一个被命名为'word'上拴牢标志. 设置这个标志可以阻止该字被FIND找到。 所以可以用来制作 "private "字。 例如,若要把一个大的字拆成小的部分,你可以这样做: : SUB1 ... subword ... ; : SUB2 ... subword ... ; : SUB3 ... subword ... ; : MAIN ... 以下列方式定义 SUB1, SUB2, SUB3 ... ; HIDE SUB1 HIDE SUB2 HIDE SUB3 在这之后,只有MAIN被 "导出 "或被程序的其他部分看到。*/ defcode "HIDDEN",6,,HIDDEN pop %edi // 字典条目. addl $4,%edi // 指向名称/标志的字节. xorb $F_HIDDEN,(%edi) // 套索扣HIDDEN位. NEXT defword "HIDE",4,,HIDE .int WORD // 获取这个字(在HIDE之后). .int FIND // 在字典中查找. .int HIDDEN // 设置F_HIDDEN标志. .int EXIT // 返回./* ' (TICK)是一个标准的FORTH字,返回下一个字的代码字指针. 通常的用法是: ' FOO , 它将FOO的代码字附加到我们正在定义的字上(这只在编译后的代码中起作用). 你倾向于在IMMEDIATE字中使用 ' 。 例如,定义2的另一种方式(而且相当无用)可能是: : LIT2 IMMEDIATE ' LIT , \ 将LIT附加到当前-正在-定义的-字上。 2 , \ 将数字2添加到当前-正在-定义的-字中。 ; 所以你可以做: : DOUBLE LIT2 * ; (如果你不了解LIT2是如何工作的,那么你应该复习一下关于编译字和立即模式的材料). 这个 ' 的定义使用了一个我从buzzard92那里复制的作弊器。 因此,它只能在编译的代码中工作。 我们可以在WORD、FIND、>CFA的基础上编写一个 ' 的版本,它也可以在立即模式下工作.*/ defcode "'",1,,TICK lodsl // 获取下一个字的地址并跳过它。 pushl %eax // 把它推到栈上。 NEXT/* 分支 ---------------------------------------------------------------------- 事实证明,为了定义循环结构、IF语句等,你只需要两个原语。BRANCH是一个无条件的分支。0BRANCH是一个有条件的分支(它只在栈顶为零时才进行分支)。下图显示了BRANCH在一些假想的编译字中是如何工作的。 当BRANCH执行时,%esi开始指向偏移域(与上面的LIT对比): +---------------------+-------+---- - - ---+------------+------------+---- - - - ----+------------+ | (Dictionary header) | DOCOL | | BRANCH | offset | (skipped) | word | +---------------------+-------+---- - - ---+------------+-----|------+---- - - - ----+------------+ ^ | ^ | | | | +-----------------------+ %esi添加到偏移中 偏移量被添加到%esi中,成为新的%esi,结果是当NEXT运行时,在分支目标处继续执行。 负偏移量的作用与预期的一样。0BRANCH与此相同,只是分支的发生是有条件的。现在标准的FORTH字,如IF、THEN、ELSE、WHILE、REPEAT等,可以完全用FORTH实现。 它们是IMMEDIATE字,将BRANCH或0BRANCH的各种组合附加到当前正在编译的字中。举个例子,像这样写的代码: condition-code IF true-part THEN rest-code 编译到: condition-code 0BRANCH OFFSET true-part rest-code | ^ | | +-------------+*/ defcode "BRANCH",6,,BRANCH add (%esi),%esi // 把偏移加到指令指针上 NEXT defcode "0BRANCH",7,,ZBRANCH pop %eax test %eax,%eax // 栈顶是零? jz code_BRANCH // 若是,就跳回上面的分支函数 lodsl // 否则我们需要跳过偏移 NEXT/* 字面字符串 ---------------------------------------------------------------------- LITSTRING是用于实现 . " 和 S " 运算符(用FORTH编写)的一种原语。 请看后面对这些运算符的定义。TELL只是打印一个字符串。 在汇编中定义这个更有效,因为我们可以使它成为一个单一的Linux系统调用。*/ defcode "LITSTRING",9,,LITSTRING lodsl // 获取字符串的长度 push %esi // 推字符串的起始地址 push %eax // 把它推到栈上 addl %eax,%esi // 跳过经过的字符串 addl $3,%esi // 但四舍五入到下一个4字节边界 andl $~3,%esi NEXT defcode "TELL",4,,TELL mov $1,%ebx // 第1个参数:stdout pop %edx // 第3个参数:字符串的长度 pop %ecx // 第2个参数:字符串的地址 mov $__NR_write,%eax // 写入 syscall int $0x80 NEXT/* 退出和解释 ---------------------------------------------------------------------- QUIT是FORTH的第一个函数,几乎是在FORTH系统 "启动 "后立即调用。如前所述,QUIT并不 "退出 "任何东西。 它做了一些初始化工作(特别是清除了返回栈),并在一个循环中调用INTERPRET来解释命令。 它之所以被称为QUIT,是因为你可以从自己的FORTH字中调用它,以便 "退出 "你的程序并在用户提示符下再次开始。INTERPRET是FORTH解释器("toploop"、"toplevel "或 "REPL "可能是一个更准确的描述--见:)。*/ // QUIT必须不返回(即-不得调用EXIT)。 defword "QUIT",4,,QUIT .int RZ,RSPSTORE // R0 RSP!,清除返回栈 .int INTERPRET // 解释下一个字 .int BRANCH,-8 // 和循环(无限期)。/* 这个解释器相当简单, 但请记住,在FORTH中, 你可以随时用一个更强大的解释器来覆盖它。*/ defcode "INTERPRET",9,,INTERPRET call _WORD // 返回 %ecx = 长度, %edi = 字的指针。 // 它在字典里吗? xor %eax,%eax movl %eax,interpret_is_lit // 不是一个字面的数字(反正还不是......)。 call _FIND // 返回 %eax = 指向头部的指针,若未找到则返回0。 test %eax,%eax // 找到了吗? jz 1f // 在字典里。 它是一个IMMEDIATE的代码字吗? mov %eax,%edi // %edi = 字典条目 movb 4(%edi),%al // 获取 名称+标志。 push %ax // 暂时先保存起来。 call _TCFA // 将字典条目(在%edi内)转换为代码字指针。 pop %ax andb $F_IMMED,%al // 是否设置了IMMED标志? mov %edi,%eax jnz 4f // 若是IMMED,直接跳到执行。 jmp 2f1: // 字典中没有(不是一个字),所以假设它是一个字面数字。 incl interpret_is_lit call _NUMBER // 返回在 %eax 中解析出的数字,%ecx > 0 如果错误的话 test %ecx,%ecx jnz 6f mov %eax,%ebx mov $LIT,%eax // 这个字是LIT2: // 我们是在编译还是在执行? movl var_STATE,%edx test %edx,%edx jz 4f // 如果执行则跳转。 // 编译--只需将该字附加到当前的字典定义中。 call _COMMA mov interpret_is_lit,%ecx // 这是一个字面意思吗? test %ecx,%ecx jz 3f mov %ebx,%eax // 是的,所以LIT后面是一个数字。 call _COMMA3: NEXT4: // 执行 - 运行它! mov interpret_is_lit,%ecx // 字面意思? test %ecx,%ecx // 字面意思? jnz 5f //不是字面,现在就执行它。 这永远不会返回,但代码字最终会调用NEXT,这将重新进入QUIT的循环。 jmp *(%eax)5: // 执行一个字面,也就是把它推到栈上。 push %ebx NEXT6: // 解析错误(不是已知字或当前BASE中的数字)。 // 打印一条错误信息,后面是最多40个字符的上下文。 mov $2,%ebx // 第1个参数:stderr mov $errmsg,%ecx // 第2个参数:错误消息 mov $errmsgend-errmsg,%edx // 第3个参数:字符串的长度 mov $__NR_write,%eax // 写 syscall int $0x80 mov (currkey),%ecx // 错误发生在currkey 位置之前 mov %ecx,%edx sub $buffer,%edx // %edx = currkey - 缓冲区 (currkey前的缓冲区长度) cmp $40,%edx // 如果>40,则只打印40个字符 jle 7f mov $40,%edx7: sub %edx,%ecx // %ecx =要打印的开始区域,%edx =长度 mov $__NR_write,%eax // 写 syscall int $0x80 mov $errmsgnl,%ecx // 换行 mov $1,%edx mov $__NR_write,%eax // 写 syscall int $0x80 NEXT .section .rodataerrmsg: .ascii "PARSE ERROR: "errmsgend:errmsgnl: .ascii "\n" .data // 注:更容易装入.data部分 .align 4interpret_is_lit: .int 0 // 标志,用于记录是否读取一个字面/* ODDS AND ENDS 价值和意义 ---------------------------------------------------------------------- CHAR把下列字的第一个字符的ASCII码放在栈中。 例如,CHAR A将65放在栈中。EXECUTE是用来运行执行牌的 execution tokens。 更多细节见FORTH代码中关于执行牌 execution tokens的讨论。SYSCALL0, SYSCALL1, SYSCALL2, SYSCALL3进行标准的Linux系统调用。 (参见<asm/unistd.h>中的系统调用号码列表)。 顾名思义,这些形式需要0到3个syscall参数,加上系统调用号码。在这个FORTH中,SYSCALL0必须是内建(汇编)字典中的最后一个字,因为我们将LATEST变量初始化为指向它。 这意味着如果你想扩展汇编部分,你必须在SYSCALL0之前放上新的字,或者改变LATEST的初始化方式。*/ defcode "CHAR",4,,CHAR call _WORD // 返回 %ecx = 长度, %edi = 字的指针。 xor %eax,%eax movb (%edi),%al // 获取该字的第一个字符。 push %eax // 把它推到栈上。 NEXT defcode "EXECUTE",7,,EXECUTE pop %eax // 将 xt 放入%eax jmp *(%eax) // 并跳转到它。 // xt运行后,其NEXT将继续执行当前字。 defcode "SYSCALL3",8,,SYSCALL3 pop %eax // 系统调用号码(见<asm/unistd.h>)。 pop %ebx // 第一个参数。 pop %ecx // 第二个参数 pop %edx // 第三个参数 int $0x80 push %eax // 结果(-errno为负数) NEXT defcode "SYSCALL2",8,,SYSCALL2 pop %eax // 系统调用号码(见<asm/unistd.h>)。 pop %ebx // 第一个参数。 pop %ecx // 第二个参数 int $0x80 push %eax // 结果(-errno为负数) NEXT defcode "SYSCALL1",8,,SYSCALL1 pop %eax // 系统调用号码(见<asm/unistd.h>)。 pop %ebx // 第一个参数。 int $0x80 push %eax // 结果(-errno为负数) NEXT defcode "SYSCALL0",8,,SYSCALL0 pop %eax // 系统调用号码(见<asm/unistd.h>)。 int $0x80 push %eax // 结果(-errno为负数) NEXT/* 数据段 ---------------------------------------------------------------------- 这里我们设置了Linux的数据段,用于用户定义,也可以称为 "数据段"、"用户内存 "或 "用户定义区"。 它是一个向上增长的内存区域,用来存储新定义的FORTH字和各种全局变量。它完全类似于C语言的堆,只是没有通用的 "malloc "和 "free"(但与FORTH中的一切一样,编写这样的函数只是一个简单的编程事项)。 在正常使用中,数据段只是随着新的FORTH字的定义/添加而向上增长。GNU工具链的各种 "特性 "使得数据段的设置比实际需要的更复杂。 一个是GNU链接器,它插入了一个随机的 "建造ID "段。 另一个是地址空间随机化,这意味着我们无法知道内核将选择在哪里放置数据段(或栈)。因此,编写这个set_up_data_segment汇编程序比它真正需要的要复杂一些。 我们使用brk(2)系统调用询问Linux内核认为数据段开始的位置,然后要求它保留一些初始空间(同样使用brk(2))。你不需要担心这段代码。*/ .text .set INITIAL_DATA_SEGMENT_SIZE,65536set_up_data_segment: xor %ebx,%ebx // 调用 brk(0) movl $__NR_brk,%eax int $0x80 movl %eax,var_HERE // 初始化HERE,使其指向数据段的开头。 addl $INITIAL_DATA_SEGMENT_SIZE,%eax // 为初始数据段保留n个字节的内存。 movl %eax,%ebx // 调用 brk(HERE+INITIAL_DATA_SEGMENT_SIZE) movl $__NR_brk,%eax int $0x80 ret/* 我们为返回静态和输入缓冲区(在读入文件和用户输入的文本时使用)分配静态缓冲区。*/ .set RETURN_STACK_SIZE,8192 .set BUFFER_SIZE,4096 .bss/* FORTH返回栈. */ .align 4096return_stack: .space RETURN_STACK_SIZEreturn_stack_top: // 初始化返回栈的顶部/* 当从文件或终端读取时,它被用作临时输入缓冲区。*/ .align 4096buffer: .space BUFFER_SIZE/* FORTH代码的开始 ---------------------------------------------------------------------- 现在我们已经达到了FORTH系统运行和自我托管的阶段。 所有进一步的字都可以写成FORTH本身,包括像IF、THEN、. " 等在大多数语言中被认为是相当基本的字。我曾经在汇编文件中追加了这些内容,但我厌倦了与gas的多行字符串句法作斗争(缺乏)。 所以,现在那就是在一个单独的文件中,叫做jonesforth.f。如果你还没有这个文件,从 下载它,以便继续学习教程。*//* 结束jonesforth.S */
标签: #汇编jb指令 #汇编 jz跳转指令 #java地区循环怎么取到最后一级 #r语言退出状态不是0 #java字符替换成换行符