ChezScheme揭秘 - 弦外之音

/ 0评 / 0

之前在写《编译系统 - 自底向上研究方法》的时候,本来想用 gcc 的编译器,汇编器,链接器 来做讲解。但 gcc 的代码量太庞大了。不太适合用来制作入门的编译系统课程。

当时我在想,有没一种编程语言,语法比 C 语言简单。编译器的实现不超过 2万行代码。当时没有找到,所以《编译系统》这本书的写作进度就停了。

直到昨天,我深夜没睡着,起来翻自己的书架。翻到了几年前买的一本书《计算机程序的构造与解析》,也就是 SCIP 。

SCIP 这本书我买来了大概 6 年了,我之前一直没法把它看完。

记得我工作了2年后买的这本书,我看到这本书是用 lisp 讲的,所以我就去看了 lisp 一些语法。但我是一个野生程序员,我当时是没有什么计算机基础的,平时的工作就是写写业务逻辑。所以当时我学 lisp ,除了学会敲括号,一些基本的语法,什么也没学到。当时我不会汇编,也不会C语言。


直到昨天,我再次翻开 SICP ,我突然明白要用什么样的方式来读这本书。我个人认为,学 SICP 的前提是掌握汇编,C语言,跟计算机原理。

为什么需要掌握汇编,因为当今的 Scheme 编译器已经能把 Scheme 翻译成 x86 指令集,你想了解这个编译过程,就需要学汇编。

为什么需要掌握C语言,因为 ChezScheme 的编译器就是 C语言写的,还有 GC 垃圾回收实现,这些都是 C 语言。

计算机原理,我这里指的是 进程,线程,互斥,虚拟内存,超线程 之类的基础知识。



所以我决定用 ChezScheme 的源码来写 《编译系统》那本书,因为 Scheme 的语法简单,虽然 Scheme 也可以扩展组合出复杂的语法,但是他的原始语法是原子性的,扩展后的复杂语法也是由原子组成,编译器只需要编译原子语法即可。所以 Scheme 的编译器实现代码会很少。


我为什么喜欢结合源码来分析,来写书?

我比较喜欢张银奎老师 的 ”格物致知“ 的思想,朱熹曾说:”言理则无可捉摸,物有时而离,言物则理自在,自是离不得。“

这句话的意思是,把理论跟实践分离开,空讲理论可能会让人摸不着头脑:相反,讲具体的事物,自然就包含了道理,二者是分不开的。


目前市面上很多书籍,都是讲概念理论,缺乏实践。我这里不是指这些讲概念的书是烂书,有些书讲概念确实很好,我也经常看,作者的水平也是经过验证的。

但是这些讲概念的书,不适合作为入门读物。因为向一个初学者传授知识,至少要给他提供源码,搭建调试环境,一点点调试。


举个例子,在我21岁的时候,我就买了 《算法导论》相关的算法书籍来看,当时我在看 B 树的算法实现,树的节点在 C 语言里都是用指针连接的嘛。我在网上找了好多个demo,都是 C 语言实现,但是这些demo都没有实现存储到硬盘。但是我当时有个疑惑,C语言有指针,但是硬盘里面没有指针这种结构,数据库的实现就用到 B 树。我不可能把一个指针 64 位的数据存到硬盘。这样没用。指针 怎么存储到硬盘?这个问题困扰了我一年多。

因为我不知道 指针 怎么存储到硬盘?B 树的结构在硬盘怎么实现,所以算法相关的书籍,我就不再看了,因为我不知道怎么把他应用到生产环境,生产环境有硬盘。

后来我开始读《unix环境高级编程》,也就是 APUE,当我看到最后一章,调 Linux 的API函数实现一个简单的数据库,好像是一个 hash 数据库。

APUE 提供的代码是可以正常跑的,我花了几天时间把那个数据库实现代码看完,当我看到他用 fseek 那个函数操作文件的时候,困扰我1年多的问题,终于有答案。

其实不是什么指针存储到硬盘。指针是内存条的 offset,控制从内存的哪个位置读写数据。而 硬盘的 offset 就是可以通过 fseek 函数进行操作。

fseek 函数可以控制从硬盘的哪个位置读写数据。位置就是指针,位置就是 offset,offset 就是 指针。


因此,我个人认为,只要有源码,能运行,能调试,计算机领域很多很复杂的东西,其实都可以变得很简单。跟初学者讲概念,堆砌逻辑,还不如给他源码,让他一点点调试,自己把逻辑弄懂。文字描述是有歧义,每个人的理解可能不一样。但是代码调试没有歧义,是什么就是什么。

现在市面上一些书籍没有结合源码调试讲解,我个人觉得是因为,计算机里面的很多软件已经发展得很庞大,例如浏览器的代码几百万行,mysql 的代码几十万行。

不像早期,操作系统的代码很少,可能只有几千行。所以很容易结合源码讲解。



所以本文主要讲解如何 搭建 ChezScheme 的调试环境,如何用 clion 来断点调试 ChezScheme 的 C语言写的 Scheme 编译器。

ChezScheme 在 2016 年开源了代码,本文使用的版本是 9.5.6


先来讲解一些 ChezScheme 在 Windows10 系统的编译以及安装。

ChezScheme 下载之后目录如下:

要先安装 msys2 ,然后进入 msys2 命令行,如下:

提示:ChezScheme 的代码文件格式是 dos 的,所以最好用 msys2 编译,如果用 原生的 Linux 系统,会有问题。

cd \msys64
.\msys2_shell.cmd -mingw64

然后进入 ChezScheme-9.5.6 的 wininstall 目录,如下:


然后执行以下命令编译

make workareas

好像不需要执行 make install

上面的命令跑完之后,就会在项目根目录生成 4 个文件夹,a6nt , i3nt,ta6nt,ti3nt

a6 代表 AMD x64,早期 64位技术,intel 跟 AMD 都有各自的名字,现在已经统一,把他理解成 x64 就行。

nt 代表 Windows 的 NT 内核,3代表Intel 386,即32位。

前面的 t 代表是否启动多线程 thread。


现在 直接进入 a6nt 目录,ChezScheme 的编译脚本会把根目录很多文件复制到 a6nt 来进行编译,会发现 a6nt 目录有个 c 文件夹,就是从 根目录复制过去的。

可以看到,有个执行文件 scheme.exe。

新建一个 t.ss 文件,内容如下:

;;; doubly recursive power (expt) function
​
;;; try using trace-lambda to see the nesting.
​
(define power
   (lambda (x n)
      (cond
         [(= n 0) 1]
         [(= n 1) x]
         [else
          (let ([q (quotient n 2)])
             (* (power x q) (power x (- n q))))])))
             
(define y (power 5 2))
​
(pretty-print y)
​

用法如下:

cd C:\msys64\home\loken\ChezScheme-9.5.6\a6nt
.\bin\a6nt\scheme.exe --script t.ss

scheme 语法 看这本书 https://www.scheme.com/tspl4/


上面的流程,好像 scheme.exe 是一个解析器,没有把 ss 编译成 exe 来执行。但实际上里面是编译出字节码执行的了, scheme.exe 好像没办法编译出 exe,因为要根据不同平台做很多链接器的工作,所以他好像是直接把编译后的字节码插入到当前线程空间来执行,这样省事。


因此,现在我们只需要 知道 scheme.exe 这个文件是怎么编译出来的,移植到 clion ,即可断点调试他的编译器实现。

也可以用 WinDbg 来断点,我试一下,也能跳到源码,如下:

然后直接在 用以下命令设置个断点:

bu scheme!wmain

现在就可以断点调试他的编译器实现了。其实 WinDbg 也挺好用,

也可以参考之前的文章《用VsDebug断点调试FFmpeg》,用 VsDebug 来调试。


我发现 WinDbg 或者 VsDebug 已经够用了,移植到 clion 有点没事找事,所以还是不管 clion 了。

vs2019 的调试界面如,VsDebug 真好用:


ChezScheme 编译脚本分析记录。

.PHONY: workareas
workareas:
    cd ..; ./configure -m=a6nt; $(MAKE) -C a6nt
    cd ..; ./configure -m=i3nt; $(MAKE) -C i3nt
    cd ..; ./configure -m=ta6nt; $(MAKE) -C ta6nt
    cd ..; ./configure -m=ti3nt; $(MAKE) -C ti3nt
​

workarea 这个脚本 代码 就是不断地复制文件。

workdir $W/s
(cd $W/s; workln ../../s/Mf-$M Mf-$M)
(cd $W/s; forceworkln Mf-$M Makefile)
(cd $W/s; workln ../../s/Mf-base Mf-base)
(cd $W/s; workln ../../s/Mf-cross Mf-cross)
(cd $W/s; workln ../../s/$M.def $M.def)
(cd $W/s; forceworkln2 $M.def machine.def)
​
workdir $W/mats
(cd $W/mats; workln ../../mats/Mf-$M Mf-$M)
(cd $W/mats; forceworkln Mf-$M Makefile)
(cd $W/mats; workln ../../mats/Mf-base Mf-base)
(cd $W/mats; workln ../../mats/Mf-exobj Mf-exobj)
case $M in
  *nt)
    (cd $W/mats; workln ../../mats/vs.bat vs.bat)
    ;;
esac
​
for dir in `echo examples unicode` ; do
  workdir $W/$dir
  for file in `(cd $dir ; echo *)` ; do
    (cd $W/$dir ; workln ../../$dir/$file $file)
  done
done

发表回复

您的电子邮箱地址不会被公开。