dram.me

读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查看变量名的方法如下:

gdb /path/to/gforth
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_casymbols[]数组元素中的一个值。需要注意的是,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做为头指针,而returnstack的首地址则是用全局变量rp。

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