danran


读Gforth: see R@

学习语言,看代码及写代码都是比较有效的方式。Gforth是一个ANS Forth实现,有较好的跨平台特性,在Gforth中,除去一些primitive words外,都是用Forth编写,有一定的学习价值。可以通过阅读它的代码学习Forth本身的语言特性及解析器实现。

准备把学习的过程整理成一个系列的文档,一方面可以分享心得,更是为敦促自己。

由于牵扯到汇编代码,有必要先交待一下执行环境:Cygwin 1.7.2 + GCC 4.3.4 + Gforth 0.7.0。另外假设读者对Forth及x86汇编有所了解,不然看下面的文字会有些吃力。好,下面进行正题。

不用做过多解释,R@是将return stack的TOS拷贝到data stack。把它作为第一个代码阅读的对象主要是由于它涉及到Forth中主要的两个堆栈,可以了解Gforth是如何实现这两个堆栈。

R@在Gforth中是primitive word,在看R@的代码前,可以先了解一下其它一些primitive words,比如简单的1+,DROP等。因为作为一个Forth的word,它们必定有一些共同点。不难发现,它们首和尾的操作都是相同的。至于具体是哪些操作,请继续往下看。

先把代码贴上:

see r@
Code i
( $401850 )  mov     dword ptr 4297E8 , ebx  \ $89 $1D $E8 $97 $42 $0
( $401856 )  mov     eax , 4297EC  \ $A1 $EC $97 $42 $0
( $40185B )  add     ebx , # 4  \ $83 $C3 $4
( $40185E )  mov     edx , dword ptr [eax]  \ $8B $10
( $401860 )  mov     eax , esi  \ $89 $F0
( $401862 )  lea     esi , dword ptr FC [esi]  \ $8D $76 $FC
( $401865 )  mov     dword ptr FC [eax] , edx  \ $89 $50 $FC
( $401868 )  mov     edi , dword ptr FC [ebx]  \ $8B $7B $FC
( $40186B )  mov     eax , edi  \ $89 $F8
( $40186D )  jmp     eax  \ $FF $E0
end-code

面对4297E8,4297EC这些数值地址必定会有些手足无措。还好可以借助强大的gdb工具,可以方便地得到该地址对应的全局变量名,4297E8对应于saved_ip变量,在engine/main.c中定义,而4297EC则是rp,在engine/engine.c中定义。使用gdb查看变量名的方法如下:

1. gdb /path/to/gforth
2. info symbol 0x4297E8

先来确定一下ebx的作用,浏览整段代码,与ebx相关的操作主要是先把值保存到saved_ip,再自增4,最后跳转到ebx自增前所指向的地址。可以发现,ebx其实就是起到了ip的作用。当然,这只是一种猜测,还需要通过查看一些代码才能确认。

那么就来看一下R@的原始代码,而不是通过see得到的反汇编。

在Gforth中,primitive words的生成过程有些复杂,简单来说,是通过m4由prim转化得到prim.b,再利用prim2x.fs将prim.b转化为engine/prim.i文件,prim.i已经是一个合法的C文件,在engine/engine.c中被包含到gforth_engine()函数中。具体的留待以后再说。现在我们只在engine/prim.i中查找R@相关的定义。这里比较特殊的是,由于R@与I的功能相同,在Gforth中R@为I的别名,在kernel/basics.fs中定义。所以在engine/prim.i中只能找到I的定义。通过see查看,两个word的定义完全相同。

由于在prim.i文件中使用了大量的宏,使阅读不大方便,先利用gcc来查看宏替换后的结果,在编译Gforth时有定义GFORTH_DEBUGGING宏,这里通过gcc -E -DGFORTH_DEBUGGING engine.c查看,稍做整理,去除一些像asm(”")等与理解无关内容后,可以看到ip在一开始就被赋值给saved_ip,之后cfa被赋值为*ip,real_ca被赋值为*cfa,程序在执行完word后,跳转到*real_ca。

H_i: I_i:
saved_ip = ip;

{
Cell n;

n=(Cell)(rp[0]);
sp += -1;
sp[0] = (Cell)n;
K_i:
cfa = *ip++;
real_ca = (*cfa);
J_i:
goto *real_ca;
}

这几个变量的类型如下,Xt相当于一个代码段的指针,每个word由Xt的序列组成。而ip为一全局变量,指向下一步要执行的Xt。在Gforth中,Xt是指向Label symbols[]数组元素的指针,所以最后real_ca是symbols[]数组元素中的一个值。需要注意的是,symbols[]中存放的是指向GCC标签的指针,而不是标签的地址,这个的类型名Label不一致。具体可以看这里。所以最后goto跳转的时候,还需要对real_ca使用取值运算符。

typedef void *Label;
typedef Label *Xt;

Xt *ip;
Xt cfa;
Label real_ca;

那么saved_ip的作用是?saved_ip作用的是保存上一个word的地址。可以通过定义并运行下面的word来确认。

: show-saved_ip $4297E8 @ 100 - 200 dump ;

最后来看一下R@的主体部分,了解一下对两个堆栈的处理。不难发现,data stack以寄存器esi做为头指针,而return stack的首地址则是用全局变量rp。

有一点疑惑是,在Label的处理上,C代码地址转化比较复杂,但在汇编代码中反而简略了,这是不是编译器优化的结果?

AtomMorph example

有了之前的基础,现在总算是可以理解AtomMorph class>>example了。代码很简单,先把它全贴出来。

AtomMorph class>>example
	|a|
	a := AtomMorph new openInWorld.
	a color: Color random.
 	[
		1000 timesRepeat: [
			a bounceIn: World bounds.
			(Delay forMilliseconds: 50) wait.
		].
	 	a delete.
	] fork.

其它也没什么好说的,主要是来看看它是如何来实现移动的。这是通过timesRepeat:和bounceIn:这两个相结合实现的。

bounceIn:利用position和position:方法将点移动velocity距离,主要的代码都用于判断边界情况。也就是一个反弹操作。反弹时除了要对坐标进行特殊计算外,还需要对速度重新设置它的正负。

AtomMorph new openInWorld

一个简单的使用AtomMorph的例子,

5000 timesRepeat: [
	atom := AtomMorph new openInWorld.
	atom color: Color random.
	].

这样会在Pharo中出现5000个彩点。位置是随机的,但是被框定在一个有限的范围内。一开始以为Morph的初始位置是openInWorld做的处理,之后才发现是在AtomMorph new时,调用了randomPositionIn:maxVelocity:方法。

要删除这些点,可以运行下面的代码。

AtomMorph allInstances do: [:each | each delete].

Pharo中的窗口切换

一直没发现Pharo里有一个很方便的切换窗口的方式,就是用快捷键,Alt+方向键。再附赠另一个快捷键:Alt+W,关闭当前窗口。

Squeak查找message

经常要用到查找message名这个功能,但每次都需要去World菜单里找,比较麻烦。有没有更为方便的方式?比如可以在Workspace里直接对字符串发送一个消息就可以打开这个窗口。或者Workspace菜单里或是什么快捷键可以完成这一功能。

先需要知道这个查找窗口的class name,可以通过窗口菜单中about功能得到。这个Morph是MessageNames,再就是在System Browser里看看这个class有些什么功能了,特别是要注意有些什么class messages。

这样就会找到一个相关的message methodBrowserSearchingFor:,再看看有哪些地方用到了它,像StandardToolSet class>>browseMessageNames:, ToolSet class>>browseMessageNames:, SystemNavigation>>browseMethodsWhoseNamesContain:, ParagraphEditor>>methodNamesContainingIt。

再去看看methodNamesContainingIt,会有一个惊喜的发现,其实Workspace里已经包含了这一功能,只是隐藏地比较深而已。那就是Shift+右键菜单,而快捷键是Alt+Shift+W。

Pharo和Squeak中的Closure

Smalltalk支持Closure,Squeak是不完全支持,而Pharo中则加入了完全的Closure支持。不过最新的Squeak trunk中也加入了完全Closure的支持。看了挺多资料,但都说得不是很清楚,最是这篇文章讲的浅显易懂。

但他的例子稍显复杂,而且在Pharo中也跑不起来。我这里来一个简单的:

adder := [:base | [:n | base + n]].

add_one := adder value: 1.
Transcript show: (add_one value: 2); cr; endEntry.

add_ten := adder value: 10.
Transcript show: (add_ten value: 2); cr; endEntry.

Transcript show: (add_one value: 2); cr; endEntry.

如果是支持full closure的话,结果应该是3, 12, 3,而如果只是部分支持,则是3, 12, 12。从这个例子中应该可以看出,完全和不完全就要是对block参数的处理。不完全closure共用了参数base。

另一个区别是,支持full closure的支持递归地调用block,下面是Squeak trunk中的一个例子。

fac := [:n| n > 1 ifTrue:[n * (fac value: n-1)] ifFalse:[1]].
fac value: 5.

而在Squeak中,则是直接报错了。