读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代码地址转化比较复杂,但在汇编代码中反而简略了,这是不是编译器优化的结果?