翻译 Ruby Hack Challenge[0x01]: MRI 的源码结构

kowalskidark · May 28, 2021 · 270 hits

Ruby Hack Challenge[0x00]: Ruby 的开发文化

(2) MRI 源码结构

关于这份文档

本文介绍了 MRI 的源码结构,同时介绍了 hack MRI 的必要知识。

包括了以下几个话题:

  • 练习:clone MRI 的源码
  • 练习:构建 MRI 并安装构建好的二进制
  • 练习:用构建好的 Ruby 二进制来运行 Ruby 程序
  • MRI 源码结构
  • 练习:初试 hack,修改版本描述信息

前提

要执行以下的命令,我们默认你使用的是 Unix-like 的环境,例如 Linux 和 macOS 等。如果你使用的是 Windows,你需要参考其他的材料。

备注:我们也提供了一个实验性的 docker 镜像:docker pull koichisasada/rhc。通过 su rubydev 命令来切换到 rubydev 账号开始 hack。

我们默认使用以下的目录结构:

  • workdir/
    • ruby/ <- git clone 下来的 ruby 源码目录
    • build/ <- 构建目录(*.o 文件和其他编译过程产生的文件存储在这里)
    • install/ <- 安装目录(workdir/install/bin/ruby 是安装好的 Ruby 二进制)

我们需要 gitrubyautoconfbisongcc(或 clang 等)和 make 命令。 如果依赖的库存在,Ruby 标准扩展(例如 zlib、openssl 等)也会被构建。

如果你用 apt-get(或 apt)来做包管理,可以使用下面的命令来安装所有依赖:

$ sudo apt-get install git ruby autoconf bison gcc make zlib1g-dev libffi-dev libreadline-dev libgdbm-dev libssl-dev

练习:Clone MRI 源码

使用以下命令:

  1. $ mkdir workdir
  2. $ cd workdir
  3. $ git clone https://github.com/ruby/ruby.git # clone 下来的源码将会在 workdir/ruby

练习:构建 MRI 并安装构建好的二进制

  1. 确认上文提到过的所需要的命令可用
  2. $ cd workdir/ # 进入 workdir
  3. $ cd ruby # 进入 workdir/ruby
  4. $ autoconf
  5. $ cd ..
  6. $ mkdir build
  7. $ cd build
  8. $ ../ruby/configure --prefix=$PWD/../install --enable-shared
    • prefix 选项用来指定安装目录,你需要传入一个完整的绝对路径(这里是 workdir/install)。
    • Homebrew 用户需要添加以下选项 --with-openssl-dir="$(brew --prefix openssl)" --with-readline-dir="$(brew --prefix readline)" --disable-libedit
  9. $ make -j # 执行构建。 -j 指定了并行构建.
  10. $ make install # 建议:为了加快安装,使用 make install-nodoc 安装不带 rdoc 的 ruby.
  11. $ ../install/bin/ruby -v 会显示你安装的 ruby 命令的版本信息

备注:在执行 make 时添加 V=1 选项(例如,make V=1 -j 等)会输出构建过程中执行的全部命令。默认使用的是 V=0 即不输出详情。

练习:用你构建好的 Ruby 来运行 Ruby 程序

你可以通过很多方式,使用构建好的 Ruby 来运行脚本。

最简单的是直接启动安装好的 Ruby,例如调用 workdir/install/bin/ruby。这就和调用一个预先构建的 Ruby 二进制一样。但是这意味着每当你修改了 Ruby 的源码,都需要执行一次 make install,非常费时。

这里介绍一些便捷的方法,在不安装的前提下启动我们修改过的 Ruby。

使用 miniruby

在构建 Ruby 过后,workdir/build 下会有一个可用的 miniruby 命令。miniruby 是一个精简版的 Ruby,用于构建 Ruby 本身。然而 miniruby 被精简的部分其实很少:它不能加载扩展库以及缺少完整的字符编码支持。你可以在 miniruby 中使用大部分的 Ruby 语法。

miniruby 是在 Ruby 构建过程中的第一阶段被构建的,因此使用 miniruby 对 MRI 的改动进行早期验证很有帮助。

下述开发流程是十分高效的:

  1. 修改 MRI。
  2. 运行 make miniruby 来构建 miniruby (这比 makemake all 快很多)。
  3. miniruby 执行一段 Ruby 脚本,测试你改动的正确性。

为了支持这样的开发流程,我们在 Makefile 中提供了一个 make run 规则,用来:

  1. 构建 miniruby
  2. 用构建好的 miniruby 运行 workdir/ruby/test.rb (test.rb 是在 Ruby 源码目录里的)。

借助 make run 你可以通过以下几步来测试你的改动:

  1. ruby/test.rb 为你的改动编写测试。记住在 test.rb 里你不能 require gems 或者扩展库。
  2. 修改 MRI
  3. 在构建目录里调用 $ make run

使用完整功能的 Ruby(而不是 miniruby)

如果你想运行“正常的”Ruby,即可以加载扩展库的版本,你可以运行 make runruby。这样不用执行 make install 就能运行 Ruby,节省时间。

  1. ruby/test.rb 编写你的测试代码。
  2. 修改 MRI。
  3. 在构建目录里调用 $ make runruby

用 gdb 调试

备注:在 macOS 上运行 gdb 非常麻烦。我们默认你在 Linux 环境执行下文的指令。

当你修改 MRI 源码,很容易会引入一些严重问题,导致产生 SEGV。为了调试这种问题,我们提供了一条 Makefile 规则来支持使用 gdb 进行调试,当然你也可以使用断点进行调试。

  1. ruby/test.rb 编写测试。记住在 test.rb 里你不能 require gems 或者扩展库。

  2. 调用 $ make gdb,通过 gdb 启动 miniruby。如果没有问题,gdb 会结束执行而不报错。

make gdb 用的是 ./miniruby。如果你想调试 ./ruby, 使用 make gdb-ruby

如果你想使用断点,修改 make gdb 命令产生的 run.gdb 文件。 例如 b func_name 这条 gdb 命令会在 func_name 函数开始的地方插入一个断点。

$ make lldb 是为 lldb 准备的类似规则,你可以用 lldb 来替代 gdb(但是 Koichi 不清楚细节,因为他并不用 lldb)。如果你用 macOS 这也许有用。

运行 Ruby 的测试

  1. $ make btest # 运行 ruby/bootstraptest/ 中的 bootstrap 测试
  2. $ make test-all # 运行 ruby/test/ 中的 test-unit 测试
  3. $ make test-spec # 运行 ruby/spec 中的测试

这三种测试有不同的目的和特点。

MRI 源码结构

解释器

你大致可以看到下述的目录结构:

  • ruby/*.c MRI 核心文件
    • Ruby 虚拟机核心
      • Ruby 虚拟机
        • vm*.[ch]: Ruby 虚拟机实现
        • vm_core.h: Ruby 虚拟机数据结构定义
        • insns.def: Ruby 虚拟机指令定义
      • compile.c, iseq.[ch]: 指令序列 (字节码)
      • gc.c: GC 和内存管理
      • thread*.[ch]: 线程管理
      • variable.c: 变量管理
      • dln*.c: 扩展库使用的 dll 管理
      • main.c, ruby.c: MRI 入口
      • st.c: Hash 算法实现 (参看 https://blog.heroku.com/ruby-2-4-features-hashes-integers-rounding)
    • 内置类
      • string.c: String 类
      • array.c: Array 类
      • ... (文件名指明了类名,例如 time.c 对应 Time 类)
  • ruby/*.h: 内部定义,C 扩展库不能使用它们。
  • ruby/include/ruby/*: 外部定义,C 扩展库可以使用他们。
  • ruby/enc/: 字符编码信息。
  • ruby/defs/: 各种定义。
  • ruby/tool/: 构建 MRI 使用的工具。
  • ruby/missing/: 实现了在某些操作系统上缺失的特性。
  • ruby/cygwin/, ruby/nacl/, ruby/win32, ...: 特定操作系统和运行环境相关的代码。

有两种库:

  • ruby/lib/: Ruby 编写的标准库。
  • ruby/ext/: C 编写的扩展库,也会被一起打包。

测试

  • ruby/basictest/: 旧的测试
  • ruby/bootstraptest/: bootstrap 测试
  • ruby/test/: 用 test-unit 表示的测试
  • ruby/spec/: 用 RSpec 表示的测试

杂项

  • ruby/doc/, ruby/man/: 文档

Ruby 的构建流程

Ruby 的构建流程分为几个阶段,包括了代码生成等。其中有几个工具是用 Ruby 编写的,因此 Ruby 的构建需要依赖 Ruby 解释器。发布的 tarball 包括了生成好的代码,因此用发布的 tarball 安装 Ruby 不需要 Ruby 解释器(以及其他开发工具,比如 bison)。

如果你想用通过 Subversion 或 Git 仓库获取的源码来构建 MRI,你需要有一个 Ruby 解释器。

下面描述了构建和安装的流程:

  1. 构建 miniruby
    1. parse.y -> parse.c: 用 bison 将语法规则编译成 C 代码。
    2. insns.def -> vm.inc: 用 ruby(BASERUBY)将 Ruby 虚拟机指令编译成 C 代码。
    3. *.c -> *.o (在 Windows 上是 *.obj): 将 C 代码编译成 object 文件。
    4. 链接 object 文件生成 miniruby
  2. 构建字符编码支持
    1. 通过 miniruby 将 enc/... 翻译成恰当的 C 代码。
    2. 编译 C 代码
  3. 构建 C 扩展库
    1. minirubymkmf.rbextconf.rb 来生成 Makefile
    2. 用生成的 Makefile 来执行 make
  4. 构建 ruby 命令
  5. 生成文档(rdoc, ri
  6. 安装 MRI (安装到 configure --prefix 选项指定的路径)

实际的构建过程中步骤会更多,然而要完整地列出所有步骤是非常难的(甚至我也没有记住全部),因此以上的只是精简过的流程。如果你好奇,可以查看包含了所有的规则的 common.mk 和相关文件。

练习:初试 hack,修改版本描述信息

让我们开始修改 MRI, 默认我们将所有源码放在 workdir/ruby

我们要修改版本描述信息,即 ruby -v(或 ./miniruby -v)会显示的内容,让它显示你定义的 Ruby 版本信息(例如包含了你名字的版本)。

  1. 在编辑器中打开 version.c
  2. 大致阅读整个 version.c
  3. 我们要找的看起来是 ruby_show_version() 函数
  4. C 函数 fflush() 用来输出缓冲区内容,因此我们推测在 fflush() 前添加用于打印的代码应该有用。
  5. 添加一行 printf("...\n");(将 ... 替换成你想要的字符串)
  6. 运行 $ make miniruby 开始构建(别忘了要先切换到构建目录)
  7. 运行 $ ./miniruby -v 并检查结果
  8. 运行 $ make install 来安装 Ruby
  9. 运行 $ ../install/bin/ruby -v,检查对于安装好的 Ruby 是否生效

最后,除了插入 printf(...) 语句,可以试试替换掉整个 ruby... 描述(比如换成 perl...之类的),这会很有趣 ;p

No Reply at the moment.
You need to Sign in before reply, if you don't have an account, please Sign up first.