MiniForth靴子旁边是相当赤裸裸的单词。 oneereader甚至声称,自从那里没有分支,它是令人作注的,因此不值得被称为!今天是我们证明他们错了。
在上一篇文章中,我描述了Miniforth的突发镜核心,坚持使用巧妙的优化I' ve必须使用它来适应它。重新定位,可用的单词:
+ - ! @ C! C @ DUP DROP SWAP发出u。 > r r> []:加载S:
大多数人都将熟悉的程序员,但是加载和S:可能会发表一些评论。 Load(U - )是来自Aptional块字集的标准单词 - 它加载块U并将其执行为索引源代码。 1这个词对于实际使用这种Smallforth至关重要,正如一旦你的引导到足够的,你可以将代码保存到磁盘,然后重新启动,只需1个负载即可恢复。
但是,要达到那一点,你需要编写相当多的代码。要在内存中提供的源代码可以保存它,我包括:(buf - buf + len),它基本上是一个字符串戳 - 剩下的incut缓冲区被复制到buf。堆栈上缓冲区Isleft的末尾的地址,使您可以使用S:在下一行上,结果会连接。
这并不是说这是唯一的方法。我确实在Miniforth上有一个纯粹的分支机构,我打算在大约一个星期内谈谈它 - 同时,我鼓励你尝试在你所在的地方弄清楚它。我真的很好奇有多少种不同的方法。
同时,让' s探讨了讨论' t在纯度名称中丢弃了大部分性能的方法。对于参考和简单的实验,从本帖子中可以在GitHub上获得。在解释代码时,我有时会添加评论,但自从我们没有实施任何评论的作用,他们又在代码中ann 39; t实际上。
我决定将我的源代码保留在地址1000,在Parameter和返回堆栈之间的空间中。我们的第一件事' ll需要是我们把它送到那里的一个运行方式。 InputPtr被定义为A02,所以Let' s定义运行,这在该地址戳了我们选择的值:
> in是输入缓冲指针的传统名称,所以我一起去了。 2要确保在后续靴子上也可用,我将该代码中的代码保存在内存中:
这是将当前指针窥视到源缓冲区的好时机,用DUP u ..除非您添加了一些写作,答案将是101A,并且介绍我们想要传递的地址稍后运行,以避免重新定义>逃跑。 3.
写入足够的代码以测试它,我会用u打印缓冲区的当前地址。然后从上一个打印的缓冲地址运行新代码。起初,它的重要情况是缓冲区地址未留在堆栈的顶部,因为Miniforth与堆栈上的内置系统变量的地址一起启动,我们将希望访问这些。
事实上,我们要做的几乎所有的变量都需要这些变量,所以第一次的TAGECARE每次需要挖掘堆栈,每次需要哪个变量是不可行的。堆栈开始如下:
通常,我们只会在这里做常量磁盘#,常量,依此类推。但是,我们没有常量,或定义它的任何方式(尚未)。文字更接近,但我们至少需要这里来实现它的最新来标记它。我们可以在[和]的直接问题周围,这表明以下行动课程:
让&#39逐步完成这一步,就像这的作品有点复杂一样。 DP代表数据指针。它是这里的变量支持,thecompilation指针,这里含义简单地定义为
我们希望做的就是将DP放在其中0的地方。由于我们运行了SwapBefore定义了我们的单词,因此DP的地址位于堆栈的顶部。在DUP @ 2之后 - ,我们将指向包含0的单元格,以及! welloverwrite它。如您所见,0并不具有任何特殊的意义,我们可以&#39使用任何文字。
接下来,我们定义单元格+和细胞。我这么做的原因是我最终喜欢做的事情是切换到32位保护码,所以尽可能多的代码应该是细胞宽不可知。
此外,由于我们现在有DP,让'写分配。可以将变量的函数分为+ ::
:+! (U addr - )dup> r @ + r> !! ;:分配(Len - )DP +! ;
这允许定义C,而且,它们分别将字节或单元格写入编译区域:
接下来,我们将写点亮,其中将文字附加到当前定义。要执行此操作,我们需要点亮的地址,处理altiteral的装配例程。我们将其存放在' lit"常量",与DP的赛道有类似的技巧:
:'点亮0 [这里4 - @这里2 - ! ];:点燃,'点亮,;
:磁盘#[点燃];:基础[点燃,];:st [点燃,];:最新[点燃];
I' m调用它而不是状态,因为状态应该是一个小区的调用Variable,其中真正的意味着编译,并且st是一个字节大小的变量随后意味着解释。
如果你'重新努力进行恶作剧,你可以通过一致提到它们的薄空气中的变量。缺乏错误检查将转换为一个数字,基本上给您一个随机地址。例如,srcpos u。输出DA9C。当然,你'重新冒着这些地址将碰撞,且诸如堆栈或字典空间等其他东西。
我不是在恶作剧的心情中,所以我们' ll正确地做到了。任何Defining Word的核心将基于:,因为它已经解析了一个单词并创建adictionary条目。我们只需要回到解释模式。 [那个,但它是一个直接的单词,我们可以' t' t' t尚未定义推迟,所以Let' s demineour niswine' t立即定义:
我们还需要一个非立即变体;。唯一需要Docompile退出的东西。我们知道退出的地址,但我们可以从最近编译的单词中读出来:
要计算我们应该编译的地址,我们需要添加3个单元格 - 一个foreach lit,实际值和退出。
到目前为止,传递给S的指针:并运行必须手动管理。它' SA简单的过程,所以让&#39自动化它。 SRCPO将包含缓冲区的TheCurrent末尾,检查点将在Hasn&#39的部分中指向; Tbeen Ran尚未。
如果你制作一个拼写错误并想要纠正,你只能阅读你需要戳的近似衣服;
我们需要确保没有定义跨越千字节边界,从而可以将缓冲区直接写入块。
:移动检查点( - )srcpos @ checkpoint! ;:DOIT( - )检查点@运行移动检查点;
此行未写入磁盘,因为确切的地址不会有用重新启动。
如果我们想处理那个,我们需要编写一个花哨的解析器,和#39;现在可以在没有分支的情况下做到这一点。让' S调整我们的用途 - 如果允许& t,那么我们也可以。要具体,让' s制作每个指示来解决单词,通过thestrack传递参数:
我选择将参数命令为SRC DST instr,其中数据流向右转。这与数据如何在正常的代码中流动,是Intel的精确镜像' s语法。在短划线之后,我包括尺寸的类型,以相同的顺序 - 寄存器(r),内存(m)或立即(i)。最后,可以是字节和字样的指令具有ab或wsuffix,如在AT& t语法。
请注意,必须手动指定操作数类型' t的基本allimationAllimationAlly。通常,没有什么能阻止在Moresmart中建造这些词,以根据操作数为基础挑选正确的变体。但是,在这种特殊情况下,我们不在任何分支字(因为他们是我们的目标😄)。
:STOSB,AA C,; :Stosw,AB C,; :Lodsb,AC C,; :罗西威,广告C,;
下一个最简单的是包含附近的指令 - Opcode之后立即出现Numeric ArgumentSthat:
有些说明在其操作码字节中使用位域。例如,诸如MOV CX,0x1234`之类的立即加载在oppcode的下3位中编码寄存器:
:AX 0; :CX 1; :DX 2; :BX 3; :SP 4; :BP 5; :SI 6; :DI 7;
你读到了它,它会变得斧头cx dx bx。据我所知,这是没有人在英特尔的人忘记了他们的ABC'因为成交名字的词源像"累加器,计数器,数据,基础"以及他们&#39的事实是前四个字母只是巨大。或者,无论如何,它的JIST和#39;这种retrocomputing.sepost包括一些关于它如何对恒星设计有益的猜测,但没有难事位。
:Al 0; :CL 1; :DL 2; :BL 3; :啊4; :CH 5; :DH 6; :BH 7;
请注意,用户有责任使用MOVB的8位寄存器,以及带有MOVW的16位寄存器。
:INCW,40 + C,;:DECW,48 + C,;:推,50 + C,;:POP,58 + C,;
我们最复杂的指令; LL必须处理modr / mbyte。这是负责的编码机制,如添加AX,[Bx + Si + 16],但也可以像MOV AX,BX一样简单。
操作码本身指定模式,例如MOV R16,R / M16。在这种情况下,这意味着目的地是寄存器,并且源是记录器的存储器。 Opcode后面的modr / m字节指定了操作数的尾声:
中间的三个位指定了R16部分,而REST endiciesthe R / M16部分,根据此表:
正如您所看到的,如果Mod字段设置为3,则下3位just areDode另一个寄存器,与以前相同。否则,我们选择一个用于地址计算的八种可能性的一个可能性,可选的offsetthat可以是8或16位。如有必要,所述偏移直接出现在MODR / MBYTE之后,并在必要时签名到16位。
有一种不规则性,因为如果我们尝试编码一个[BP]而没有任何OFFSet,我们得到的是一个硬编码地址,例如MOV BX,[0x1234],应该在即时字节之后。 4如果您回忆说,那么[BP]的Thel就是为什么切换返回堆栈来使用DI而不是缺陷。
这种编码的特殊方面是寄存器到寄存器操作可以以两种不同的方式编码。例如,让'例如,xor cx,dx,例如:
无论如何,让'■实现这一点。首先,寄存器到寄存器变体。我choseto命名为编译这样的modr / m字节rm-r的单词,这意味着该字段中的寄存器也可以是内存,其次是另一个寄存器。我们没有任何比例,但我们可以用重复的补充方式解决:
:2 * DUP +;:3SHL 2 * 2 * 2 *;:RM-R,(REG-AS-R / M REG - )3SHL + C0 + C,;
使用RM-R时,我们需要确保所使用的操作码是R / M16,R16模板的OPODE - 我们需要否则否则会交换两个寄存器:
:MOVW-RR,8B C,RM-R,;:ADDW-RR,03 C,RM-R,;:ORW-RR,0B C,RM-R,;:ANDW-RR,23 C,RM-R ,;:Subw-RR,2B C,RM-R,;:XORW-RR,33 C,RM-R,;:CMPW-RR,3B C,RM-R,;
内存到寄存器变体aren' t更难。我们定义寻址模式,就像我们为寄存器做过的那样。
:[Bx + Si] 0; :[Bx + Di] 1; :[bp + si] 2; :[bp + di] 3;:[Si] 4; :[di] 5; :[#] 6; :[BP] 6; :[BX] 7;
[#]是绝对地址模式,例如,应该通过手动组装在指令之后组装Theaddress。
我还包括[BP],它与[#]碰撞,因为寻址模式单词可以与[?? + d8]和[?? + d16]模式共享。
MODR / M字节的使用略有不同。即,如果AIN构造只需要一个操作数(例如不是BX或JMP AX),则实际使用R / M一个操作数。在这种情况下,寄存器字段替代地重复使用OPCODE本身的更多位。
英特尔' S手册使用的符号是Opcode / Regbits。例如,间接跳转为FF / 4,而间接呼叫是FF / 2,共享主题操作码字节。我们可以通过简单地推动RM-R之前推动rightValue来编码这样的指示。
为了实际组装原始词,我们' ll还需要一种创建itheher的方法。最简单的方法是调用普通:,然后将dp倒回三个字节,删除对docol的调用:
请注意,下一步,未定义:代码 - 它是Anassembler宏的等效项。
这实际上足以将我们的工作写入磁盘。就像Load,We' ll需要创建一个磁盘地址报文,然后调用int 0x13.one原始词可以用于读写,因为唯一的difference是您需要的斧头的值。保存Si - i&#39至关重要;车辆对这个经验的令人满意地学习。
创建数据包10分配:代码INT13 SI PUSH,\ PUSH SI PACKET SI MOVW-IR,\ MOV SI,数据包BX AX MOVW-RR,\ MOV AX,BX磁盘#DL MOVB-IR,\ MOV DL,磁盘#13 INT ,\ int 0x13 ax bx movw-rr,\ mov bx,ax si pop,\ pop si next,
请注意,我们'重新将轴的返回值保存在堆栈上。这是非零AH值信号产生错误。
用数据,i&#39填充数据包使用一个变体,将写入附加的位置而不是这里:
变量pos:pos,(val - )pos @! 2 POS +! ;:make-packet(块缓冲区 - )包POS! 10 POS,\分组大小2 POS,\扇区计数POS,0 POS,\ Buffer地址2 * POS,0 POS,0 POS,0 POS,\ LBA;
为了阅读,我们使用AH = 0x42,如前所述。写作使用AH = 0x43,但是and案例al控制我们是否希望BIOS验证BIOS - 我们这样做,所以我将它设置为0x02。
很高兴验证我们的新代码是否正确地削弱了它。理想情况下,我们' D写了一点六角蛋白质,但我们仍然没有'任何循环的谁。虽然有一种方法,但是 - 只需输入Wordyou需要连续多次:
:p(buf - buf + 1)dup c @ u。 1 +; \后来...这里10 - P P P P P P P P P P P P P P P P P P P P P P P P P P
另一个好的理智检查是为了确保我们不再WEREN' T期望被推到堆叠上 - 那些通常由未定义的单词变成数字造成的。这样做的方法就是杜普u。 - e0e响应的空堆栈willresult,源于良性堆栈底部滴水'刚刚公布。一个错误的一个例子曾经被抓住是一个错字,我键入了modb-it,而不是movb-ir。
我尝试的第一个磁盘访问是0 4000 Read-Block u。 41FE @ u ..这显示了靴子末尾的AA55魔术号。然后,我写了我的源代码toblocks 1和2,并将它们读入单独的缓冲区以确保它的工作。 Inhindsight,它可能会' vere是一个好主意读取的块,除了0 beforeatempting写一个写,以确保提供LBA正常工作。这个特定的错误纯粹假设。
我还写了相同的数据来阻止0x101和0x102。这样,我可以恢复是否从通常的块号中断启动。
在我们解决分支机构之前,我们' ll需要一个指令 - thecondaritional跳跃。在X86上,跳转偏移被编码为对当前指令指针的符号valueLelative。有不同的编码概要这个值的比特宽度,但我们' ll只需要较短的8位。
具体而言,该值是相对于跳转指令的结尾,SOTHAT它与前向跳转的情况下匹配跳过的字节数:
要组装跳跃距离,我使用两对单词 - 一个用于转发jumps,一个用于向后的:
规则是箭头显示跳跃的方向,箭头"里面" - 换句话说,如果你彼此相邻的两个单词,这个词应该像手套一样适合。这两个词只是通过纪念沟通。例如,j<只需记住当前位置:
:j>这里0 C,;:> J DUP> R 1 +这里交换 - R> C! ;
最后,跳跃指令本身。一些跳跃有多功能纸员。例如,由于随着减法需求的情况来设置携带标志,因此JC具有与下面的无符号比较跳跃相同的行为。这同样适用于JE和JZ,但'直观足够的印版,所以我没'吨觉得有必要界定这两个名字。
:JB,72 C,; :JC,72 C,; :Jae,73 C,; :JNC,73 C,;:JZ,74 C,; :JNZ,75 C,; :JBE,76 C,; :JA,77 C,;:JL,7C C,; :JGE,7D C,; :JLE,7E C,; :JG,7F C,;
按照惯例,被编译成一个定义,而是的话AREN'吨useddirectly都包裹在括号中的名字,所以我们的分支被称为(分支),这是unconditonal,和(0branch),该弹出一个值off thestack和分支机构如果它' s零。
在条件分支的情况下,要记得alwaysread(或跳过)的分支目标,无论分支条件IsTrue运算是很重要的。
:代码(0branch)Lodsw,\ Lodsw Bx Bx Orw-RR,\或Bx,BX JNZ,J> \ jnz .skip ax si movw-rr,\ mov si,ax> j \ .skip:bx pop,\ pop bx下一步,
要处理分支偏移量计算,我将非常相似的单词设置为跳转使用的THE。但是,由于encodingisn' t相对于当前位置,实现更简单:
:BR>在这里0,;:> br在这里交换! ;:BR<这里;:< br,;
要使逻辑编译IFS实际在编译TINE上运行,我们需要托马克这些单词即时。为此,我们立即使用,该标志设置最近编译的词:
:立即( - )最新@ \获取Word Cell + \ Skip链接字段DUP> R C @ \读取当前值的长度+标志字段80 + \设置标志R> C!回写;
我们也需要编译,它在调用作为编译x时,x tothe Word当前正在编译。我们实际上需要使它是动漫的单词,它自行解析下一个单词,只需读出像点亮或(分支)这样的X的Xdress就足够了:
否则有点复杂。我们需要将无条件的分支机构编译到那时,而且还要解决if'在TheuryMondaitional跳跃之后跳到点。我喜欢使用返回堆栈操纵词来实现这一点,因为箭头匹配由&gt的箭头使用的箭头并使代码更容易阅读:
开始...直到ISN' t更难 - 只需使用条件跳跃即时:
最后,唯一的唯一开始......虽然...重复循环,其中loopcondition在循环中间:
:虽然(backjmp - fwdjmp backjmp)编译(0branch)br>交换 ;立即:重复(fwdjmp backjmp - )编译(分支)< br> br;即时
没有任何方式比较东西,分支并不是有用的。 一个常见的commengong所有比较词都将处理器标志转换为Forth Boolean.Recall,其中,Booleans拥有Set或Unset的所有位: 我选择通过首先将AX设置为0,然后基于比较结果来生成这些布尔值。 此代码可以如下: :: CMP:Code Ax AX XORW-RR,;:CMP; j> AX DECW,> J AX BX MOVW-RR,下一步,; 然后,要定义比较,只需编译 ......