🚧

Ruby 0.95をUbuntu 24.04(AMD64)で動かしたかった part 1

2025/01/14に公開

この記事は、ポート株式会社 サービス開発部 その2 Advent Calendar 2024の、20日目の記事です。 (大遅刻となりました🙇)

ポート株式会社でサーバーサイドの開発者として働いている、ito845と申します。今回も会社での取り組みとは関係がない、個人的な記事を書かせていただきました。

概要

以下の内容を試み、その過程をまとめて記述したものになります。私自身、C言語に対して深い知識や理解があるわけではないため、学習しながら問題を一つずつ解決して進めています。

  • Ruby 0.95の64bit版(AMD64向け)のビルド
    • ビルドに際して必要だったソースコードの編集 (途中・make testが通るところまで)

まだ、完全な形で目的を達成することができておらず、本記事は途中までの内容を記載したものとなります。今回の変更についてのパッチの配布は行いませんが、なるべく再現が可能なように、実際の作業の手順に沿って詳細に記載しています。

経緯

以下の3つが混ざって、Ruby 0.95のソースコードに直接を編集した上で、ビルドを試みるに至りました。

  • 唐突にC言語と格闘したくなった
  • Rubyに詳しくなりたい
  • Ruby3.xやRuby2.xのような大きそうなコードベースを、いきなり相手にするのはつらい

(尚、日常の業務の中で、C言語に触れる可能性が0%かと言われるとそうではなく、gemの更新に伴う調査等で低確率で発生し得ます。)

背景

Ruby 0.95 および、古いバージョンのRubyについて

1995年12月21日にfj.sourcesで公開されたものが、Ruby 0.95になるそうです。今現在(2024年12月)から約29年前となります。

私家版Ruby史などのページによると、現在入手できそうな最古のバージョンは「Ruby 0.49」となります。また、当時のメーリングリストのアーカイブを見る限り、Ruby 0.95については0.95, 0.95a, 0.95b, 0.95cの少なくとも4つのバージョンがあり、その後、Ruby 0.96が公開されたようです。

関連する取り組みとの差異

昔のバージョンのRubyをビルドして動作させるような取り組みは以前から行われており、Ruby 0.49から最新のRubyまでを一度に動かすことができるall-rubyや、Ruby 0.49をビルドした記事などの存在を確認することができます。

これらの取り組みは、古いバージョンのRubyのソースコードにパッチをあてた上で、32bit版の実行ファイルをビルドするような方向性となっているように見えます。
(パッチ等の全ファイルに目を通したわけではないので、誤りがありましたら申し訳ありません。)

今回は、ソースコードに修正を加えた上で64bit版の実行ファイルをビルドするような方向性で、作業を実施しました。

所々に粗い部分や誤りが含まれている可能性があること、また、元のソースコード等の柔軟性を排した形で編集を行っていることをご理解いただけますと幸いです。また、当時のRuby 0.95におけるビルドや実行ファイルの挙動とは互換性がない場合がある点についても、ご了承ください。

環境構築

以降、実際に行った作業の順番に沿って、編集した内容や関連情報を記載していきます。作業はWSL2上のUbuntu 24.04 LTSで行っています。

ソースコードのダウンロード

以下のページからruby-0.95.tar.gzをダウンロードしました。メーリングリストのアーカイブを見ると、FTPサーバーで他バージョン(0.95c等)のソースコードが配布されていたことを確認できるのですが、探し当てることができませんでした。

https://ftp.ruby-lang.org/pub/ruby/1.0/

各種パッケージのインストール

一般的な各種ビルドツール(gcc, make等)に加え、yaccは最低限必要でした。また、メーリングリストのアーカイブを見ているとwishコマンドが実行できる環境でないと、ext/tkutilがビルドされないといった情報も見受けられたため、以下のコマンドでそれら全てのインストールを実行します。

sudo apt install build-essential gdb bison tk

コードの編集について

以降、ソースコード等に加えた編集に関して記載していきます。

適宜make 2> make.logmake cleanなどを実行して、適切な対象のログを見ながら作業をしています。

可変長引数の書き方の変更

ダウンロードしてきたファイルを解凍し、./configureの後、makeを実行すると、array.cのコンパイルに失敗してmakeが中断されます。

ログを見てみると以下のようなエラーを確認することができました。

In file included from array.c:44:
/usr/lib/gcc/x86_64-linux-gnu/13/include/varargs.h: At top level:
/usr/lib/gcc/x86_64-linux-gnu/13/include/varargs.h:4:2: error: #error "GCC no longer implements <varargs.h>."
    4 | #error "GCC no longer implements <varargs.h>."
      |  ^~~~~
/usr/lib/gcc/x86_64-linux-gnu/13/include/varargs.h:5:2: error: #error "Revise your code to use <stdarg.h>."
    5 | #error "Revise your code to use <stdarg.h>."
      |  ^~~~~
array.c: In function ‘ary_new3’:
array.c:49:5: error: expected declaration specifiers before ‘va_dcl’
   49 |     va_dcl
      |     ^~~~~~
array.c:72:5: error: expected ‘=,,,;, ‘asm’ or ‘__attribute__’ before ‘int’
   72 |     int n;
      |     ^~~

varargs.hを用いた可変長引数に関する実装を、stdarg.hを用いた実装に置き換える必要がありそうです。この修正に関しては、all-rubyを作られた方の資料でも言及されています。今回は、単純にソースコードを修正していきます。

以下のような置き換えのイメージです。

#include <varargs.h>

void func(a, b, va_list)
    int a;
    int b;
    va_dcl
{
    int* var;
    va_list vargs;
    va_start(vargs);

    var = va_arg(vargs, int*);
    // 省略
}
#include <stdarg.h>

void func(int a, int b, ...) // 引数の書き方を変更する
{
    int* var;
    va_list vargs;
    va_start(vargs, b); // 一番最後の必須の引数を指定する

    var = va_arg(vargs, int*);
    // 省略
}

以上のような修正を、以下の箇所に実施していきます。

  • array.c
    • ary_new3
  • class.c
    • rb_scan_args
  • error.c
    • Error, Warning, Fatal, Bug, Fail
  • eval.c
    • rb_funcall
  • struct.c
    • struct_define, struct_new

関数プロトタイプの追加・編集 その1

修正が終わったのち、コンパイルを実施していくと以下のようなエラーが発生します。型に関する記載がコンフリクトしているようです。

class.c:353:1: error: conflicting types for ‘rb_scan_args’; have ‘int(int,  VALUE *, char *, ...){aka ‘int(int,  unsigned int *, char *, ...)}
  353 | rb_scan_args(int argc, VALUE *argv, char *fmt, ...)

rb_scan_args の関数プロトタイプを探して、先ほど修正した実装に合わせて記載を修正します。プロトタイプの宣言はruby.hに存在していました。以下のように修正を加えます。

int rb_scan_args(); // 修正前
int rb_scan_args(int argc, VALUE *argv, char *fmt, ...); // 修正後

また、同種の問題を防止するため、同じくruby.hに関数プロトタイプがある、rb_funcallError, Fatal, Bug, Warningについても修正していきます。

rb_funcallwarningについては、rb_scan_argsと同様に修正します。

Error, Fatal, Bugについては、プリプロセッサで分岐しているため、else以下はrb_scan_argsと同様に、volatileが書いてある方は、以下のようにtypedefを追加して修正しました。(命名は適当です)

typedef void voidfn (); // 未修整
typedef void voidargsfn (char *fmt, ...); // 追記
volatile voidargsfn Fail; // 修正
volatile voidargsfn Fatal; // 修正
volatile voidargsfn Bug; // 修正
volatile voidfn WrongType; // 未修整

定義の重複の解消

ここまでの修正でmakeを実行すると、以下のような複数のエラーが発生します。ld(リンカ)実行時のエラーのようです。

/usr/bin/ld: range.o:/home/ito/workspace/ruby-0.95/range.c:15: multiple definition of `mComparable'; compar.o:/home/ito/workspace/ruby-0.95/compar.c:15: first defined here
/usr/bin/ld: errno: TLS definition in /lib/x86_64-linux-gnu/libc.so.6 section .tbss mismatches non-TLS reference in error.o
/usr/bin/ld: /lib/x86_64-linux-gnu/libc.so.6: error adding symbols: bad value
collect2: error: ld returned 1 exit status

1つずつ、上から解消していくことにします。

まずは、以下の定義が重複しているエラーについてです。range.cmComparableと、compar.cmComparableの2つの定義が存在していることが確認できます。

/usr/bin/ld: range.o:/home/ito/workspace/ruby-0.95/range.c:15: multiple definition of `mComparable'; compar.o:/home/ito/workspace/ruby-0.95/compar.c:15: first defined here

幸いどちらも、VALUE mComparable;で、型が同じになっているため、range.ccompar.cの、どちらか一方に対して、externを追加します。(今回はrange.cの方に追加しました)

これにより、VALUE mComparablerange.cの外のファイルで定義されており、そちらを参照してほしい旨を、コンパイラ/リンカに伝えることができると思います。

VALUE mComparable; // 修正前
extern VALUE mComparable; // 修正後

#include <errno.h>の追加

次は以下のエラーを解消します。ネットで調べると「errno.hをインクルードすると解決する」といった情報をいくつか見つけることができました。

/usr/bin/ld: errno: TLS definition in /lib/x86_64-linux-gnu/libc.so.6 section .tbss mismatches non-TLS reference in error.o

error.oで問題が出ているため、error.cのファイル先頭付近に、#include <errno.h>を追記します。

関数を参照できないエラーを解消するため、プリプロセッサを編集

再びmakeをすると、複数のリンクエラーが発生しました。

大雑把に分割すると、glob.o(glob.c)allocaを参照することができないエラーと、io.o(io.c)ReadDataPendingを参照することができないエラーの2つです。

/usr/bin/ld: glob.o: in function `glob_vector':
/home/ito/workspace/ruby-0.95/glob.c:228:(.text+0x23a): undefined reference to `alloca'
/usr/bin/ld: /home/ito/workspace/ruby-0.95/glob.c:276:(.text+0x356): undefined reference to `alloca'
/usr/bin/ld: glob.o: in function `glob_filename':
/home/ito/workspace/ruby-0.95/glob.c:404:(.text+0x475): undefined reference to `alloca'
/usr/bin/ld: io.o: in function `f_select':
/home/ito/workspace/ruby-0.95/io.c:1083:(.text+0x1771): undefined reference to `ReadDataPending'
collect2: error: ld returned 1 exit status

まず、allocaに関するエラーから解消していきます。

allocaはCの標準ライブラリのalloca.hに存在する、alloca関数のことをさしていると思われます。glob.cの関連する記載を見に行くと、以下のようなコードを見つけることができます。

#if defined(HAVE_ALLOCA_H) && !defined(__GNUC__)
#include <alloca.h>
#else
char *alloca ();
#endif

defined(HAVE_ALLOCA_H) && !defined(__GNUC__)が0でなければ、alloca.hをインクルードし、そうでなければallocaメソッドの関数プロトタイプを宣言しています。

missing/alloca.cの中に、allocaの実装が提供されているため、そちらをコンパイルして参照するように変更を加えることもできますが、今回の環境では、<alloca.h>をインクルードする方が手っ取り早いと思いますので、条件文を以下のように修正します。
また、ruby.hdln.cにも、同様の書き方がされている箇所があるため、そちらも修正していきます。

(!defined(__GNUC__)で否定されていることについては、おそらく、当時のgccに存在したallocaを使うことに、何らかの不都合があったのだろうと推測しています。)

#if defined(HAVE_ALLOCA_H) && !defined(__GNUC__) // 修正前
#if defined(HAVE_ALLOCA_H) // 修正後

次は、ReadDataPendingに関するエラーを解消します。io.cを探すと、以下のようなコードを見つけることができます。ReadDataPendingが定義されていないというエラーなので、プリプロセッサでの分岐で2回ともelseの方に流れたようです。

#ifdef _STDIO_USES_IOSTREAM  /* GNU libc */
#  ifdef _IO_fpos_t
#    define READ_DATA_PENDING(fp) ((fp)->_IO_read_ptr < (fp)->_IO_read_end)
#  else
#    define READ_DATA_PENDING(fp) ((fp)->_gptr < (fp)->_egptr)
#  endif
#else
#  ifdef FILE_COUNT
#    define READ_DATA_PENDING(fp) ((fp)->FILE_COUNT > 0)
#  else
extern int ReadDataPending();
#    define READ_DATA_PENDING(fp) ReadDataPending(fp)
#  endif
#endif

試しに、((fp)->_gptr < (fp)->_egptr)((fp)->_IO_read_ptr < (fp)->_IO_read_end)を、それぞれ有効にした状態でコンパイルすると、後者の ((fp)->_IO_read_ptr < (fp)->_IO_read_end)の実装では、エラーがでないことを確認することができました。

とりあえず、常にそちらのコードを使用するように変更を加えます。単純に、分岐の部分をまるごと削除して、以下のマクロの定義だけが存在するようにしました。

#define READ_DATA_PENDING(fp) ((fp)->_IO_read_ptr < (fp)->_IO_read_end)

関数プロトタイプの追加・編集 その2

ここまで、編集を加えていくと、標準出力にCompiling ext modulesと出力された後、Segmentation Faultが発生してmakeが中断されるようになりました。Makefileの中身を見ると、ビルドした実行ファイルのminirubyを実行して、拡張モジュールに関するセットアップをしようとしていることを確認できます。

extruby:	miniruby ext/Setup 
		@if test -z "$$UNDER_EXTMAKE_RB"; \
		then echo "Compiling ext modules"; \
		UNDER_EXTMAKE_RB=yes; export UNDER_EXTMAKE_RB; \
		cd ext; ../miniruby ./extmk.rb; fi

デバッガーを用いて、./miniruby ./ext/extmk.rbを実行して、エラーが発生した箇所を確かめることで何らかのヒントを得られそうだと考えました。

今回はgdbをデバッガーとして利用することにしました。gdbを利用するにあたり、gccを実行する際に-gオプションを指定してデバッグ情報を付与すると良さそうですが、Makefileや今までのmakeの実行ログを見ると、すでに-gオプションが指定されているので、オプションの追加に関する編集は不要でした。

CFLAGS = -g -O -I.

以下のようにして、gdbを起動してデバッグを開始し、Segmentation Faultが発生している箇所を確認します。

gdb --quiet --args ./miniruby ./ext/extmk.rb 
(gdb) run

# その他、以下のような操作も可
gdb ./miniruby
(gdb) run ./ext/extmk.rb

すると、以下のようなエラーを確認することができます。ruby_init関数の中、eval.cの524行目でPUSH_SCOPE();でエラーが発生しているようです。

Program received signal SIGSEGV, Segmentation fault.
0x000055555556224b in ruby_init (argc=2, argv=0x7fffffffd688, 
    envp=0x7fffffffd6a0) at eval.c:524
524         PUSH_SCOPE();

PUSH_SCOPEは、以下のような形で、eval.c内に定義されています。NEWOBJOBJSETUPは、ruby.hで定義されています。struct SCOPEのオブジェクトを作成して、the_scopeに格納されたスコープを_oldに格納し、the_scopeに新たに作成されたstruct SCOPEを格納するコードのようです。

(一緒に定義されているPOP_SCOPE()では、_oldthe_scopeに代入するような操作が行われています)

#define PUSH_SCOPE() {			\
    struct SCOPE *_old;			\
    NEWOBJ(_scope, struct SCOPE);	\
    OBJSETUP(_scope, Qnil, T_SCOPE);	\
    _old = the_scope;			\
    the_scope = _scope;			\
#define NEWOBJ(obj,type) type *obj = (type*)newobj()
#define OBJSETUP(obj,c,t) {\
    RBASIC(obj)->class = (c);\
    RBASIC(obj)->flags = (t);\
}

まずは、ステップ実行やプリントデバッグで、エラーの発生箇所を特定していきます。

gdbrunを実行する前に、break newobjを入力して、newobjの呼び出し以降をステップ実行していくと、newobj内ではSegmentation Faultが発生しないことを確認することができました。

また、その他のSegmentation Faultの引き金となりうるRBASIC(obj)->classの処理を含んだ、printf("%p\n", RBASIC(obj)->class)newobj関数の内部に入れても、Segmentation Faultが発生しませんでした。ここまで来ると、newobjの実装の内部ではなく、その外側で問題が発生していると推測することができます。

試しに、NBEOBJOBJSETUPの間に、printf("%p\n", obj); \を挟んで、OBJSETUPに使用しているnewobjの返り値を確認すると以下のような出力となります。

230             return obj;
(gdb) print obj
$1 = (struct RBasic *) 0x7ffff7c802e0
(gdb) n
0xfffffffff7c802e0

Program received signal SIGSEGV, Segmentation fault.

newobj関数内では、0x7ffff7c802e0として扱われていた値が、newobjの呼び出し側では0xfffffffff7c802e0として扱われていることがわかります。下位の数バイトを残して上位の値が別のものに書き変わっています。

これにより、今発生しているSegmentation Faultの原因は、オブジェクトのために確保したメモリ領域のアドレスが正しく扱えなくなっていることだと推測することができます。次に試すことは、newobjの返り値を関数内部の値と揃えられるようにすることです。

GoogleやChatGPT等で調べた結果、外部のファイルで定義された関数を利用する場合、適切なプロトタイプ宣言を行わないと返り値がint型の扱いとなるといった情報を見つけることができました。 (日本語の参考資料)

たしかに、ruby.h中やeval.c中には、newobjの関数プロトタイプが存在しないため、この問題が関連している可能性が高く、makeのログを見ていると、以下のような関連する警告を確認することもできました。上は、int型の値がポインタにキャストされていることを表す警告で、下が宣言されていない関数を使っていることに関する警告です。

ruby.h:128:38: warning: cast to pointer from integer of different size [-Wint-to-pointer-cast]
  128 | #define NEWOBJ(obj,type) type *obj = (type*)newobj()
ruby.h:128:45: warning: implicit declaration of function ‘newobj’ [-Wimplicit-function-declaration]
  128 | #define NEWOBJ(obj,type) type *obj = (type*)newobj()

newobjの内部ではポインタ(8byte)として扱われますが、呼び出し側で一度int(4byte)に変換され、_scopeに格納する際に再度ポインタ(8byte)に変換されるといった過程を得て、正しいアドレスが間違ったアドレスに変換されてしまったと推測することができます。

gc.cで実装されているnewobjの実装を見つつ、ruby.hに以下のような関数のプロトタイプ宣言を追加して、再度makeし、問題が解消されたか確認します。

struct RBasic* newobj();

以下のように、newobjの帰り値が、printfの位置まで正常に維持されていることを確認できました。

230             return obj;
(gdb) print obj
$1 = (struct RBasic *) 0x7ffff7c802e0
(gdb) n
0x7ffff7c802e0
ruby_init (argc=2, argv=0x7fffffffd688, envp=0x7fffffffd6a0) at eval.c:526
526         the_scope->local_vars = 0;
(gdb) 

続きを実行すると、以下のように、別の場所でSegmentation Faultが発生します。これも同種の問題であると推測することができます。エラーを解消するために、ログのimplicit declaration of functionの警告から、同様に関数のプロトタイプ宣言を追加する必要がある箇所を探して追記していきます。

Program received signal SIGSEGV, Segmentation fault.
0x000055555558766d in st_lookup (table=0x5555555ab8a0, 
    key=key@entry=0x555555590c57 "Kernel", 
    value=value@entry=0x7fffffffd364) at st.c:122
122         FIND_ENTRY(table, ptr, hash_val);

.cファイルに対応するヘッダファイルを作る方針でも良いのですが、プリプロセッサ周りで問題がおこると面倒なため、各.cファイルにextern int func(int a);といった形でプロトタイプ宣言を追加する方針としました。また、#includeを追加して、関数の定義を参照させることで解消したパターンも存在します。結果的には、35のファイルに対して約400行程度の追記を行い、yyparseに関する2件を除き、implicit declaration of functionの警告を解消しました。

関数呼び出しの引数等を変更

プロトタイプ宣言を追加した後、コンパイルを行うと、以下の3箇所でコンパイルエラーが発生します。

  • eval.c
    • rb_ivar_definedの呼び出し箇所
  • process.c
    • proc_getpgrp内のgetpgrpの呼び出し箇所
    • proc_getpgrp内のsetpgidの呼び出し箇所

rb_ivar_definedについては、他のrb_ivar_*や、rb_gvar_*の雰囲気に合わせて、rb_ivar_defined(Qself, node->nd_vid)に変更します。(人力BERTです。つまり、適当になります。)

proc_getpgrp内の、getpgrp(pid)については、getpgid(pid)に置き換えます。こちらのgetpgrpmanの翻訳からgetpgidの存在を知り、ドキュメントを見て代替として合致すると推測しました。

proc_getpgrp内の、getpgrp(ipid, ipgrp)については、setpgid(ipid, ipgrp)に置き換えます。proc_getpgrpの名称とgetpgrpが引数なしの関数として定義されていること、getpgidのドキュメントの内容から、代替として合致すると推測しました。

VALUEでポインタが欠損しないように、VALUEの定義をunsinged longに変更

ここまでの変更で、再度makeして動作を確かめると、rb_ivar_setの中で、Segmentation Faultが発生しました。先ほどのエラー発生箇所とは違う場所となります。

バックトレースを見ると、以下のように、rb_name_class関数の中でrb_ivar_setを呼び出していることが確認できます。rb_ivar_setに対して渡している引数のclassVALUE型として定義されています。VALUE型はruby.hにおいて、UINT型で定義されており、UINT型はunsigned int型で定義されています。ここでポインタの値が書き換わってエラーが発生していると推測することができます。

本来は警告と実装を見つつ丁寧に型や処理を修正していくべきですが、今回はVALUE型をunsinged longにして問題を一旦解消しました。

typedef unsigned int UINT;
typedef unsigned long ULONG; // 追記
typedef ULONG VALUE; // UINTからULONGに編集
typedef UINT ID;

stack smashing detectedの調査と解消

再度ビルドすると *** stack smashing detected ***: terminated のエラーが発生します。gdbでバックトレースを表示すると、以下のような出力を確認することができます。rb_internでエラーが発生しているようです。

省略
#7  0x00007ffff7dbaea4 in __stack_chk_fail () at ./debug/stack_chk_fail.c:24
#8  0x0000555555577286 in rb_intern (
    name=name@entry=0x555555591c4c "__classname__")
    at /home/ito/workspace/ruby-0.95/parse.y:3039
#9  0x000055555558eb73 in rb_name_class (class=class@entry=140737350468176, 
    id=id@entry=2716) at variable.c:143
#10 0x0000555555573d8f in boot_defclass (
    name=name@entry=0x5555555902cb "Object", super=<optimized out>)
    at object.c:385
#11 0x0000555555574027 in Init_Object () at object.c:400
省略

*** stack smashing detected ***: terminatedのエラーメッセージの通り、スタック領域の破壊が発生しています。parse.yrb_internの実装を見ると、再帰でrb_internを呼び出すパスがあります。当初は再帰呼び出しによるスタック領域の破壊を疑いましたが、break rb_internでステップ実行していくと、そのようなことは発生していなさそうでした。

関連する情報を調べると、ローカル変数に対して領域外のアクセスを行うと発生する場合もあるということがわかりました。また、コンパイル時に-fsanitize=undefined -fsanitize=addressを指定することで、関連するエラーに関して、より詳細に調査することが可能なようです。

MakefileCFLAGSにオプションを追加して実行すると、以下のようなメッセージを確認することができました。

==8105==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7f9941500160 at pc 0x5625d36730b0 bp 0x7ffc45afb8a0 sp 0x7ffc45afb890
WRITE of size 8 at 0x7f9941500160 thread T0
    #0 0x5625d36730af in st_lookup /home/ito/workspace/ruby-0.95/st.c:130
    #1 0x5625d3628806 in rb_intern /home/ito/workspace/ruby-0.95/parse.y:2989
    #2 0x5625d3696944 in rb_name_class /home/ito/workspace/ruby-0.95/variable.c:143
---省略---
  This frame has 1 object(s):
    [32, 36) 'id' (line 2986) <== Memory access at offset 32 partially overflows this variable
HINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork
      (longjmp and C++ exceptions *are* supported)
SUMMARY: AddressSanitizer: stack-buffer-overflow /home/ito/workspace/ruby-0.95/st.c:130 in st_lookup
Shadow bytes around the buggy address:

rb_internが呼び出しているst.cst_lookup関数の中で、スタック領域を破壊してしまっているようです。また、rb_internの中で宣言されているidと呼ばれる変数が関連していることも推測することができます。

以下にrb_internst_lookupの実装を示します。int型のローカル変数idへのポインタをst_lookupvalueに対応する引数として渡しています。st_lookup内では、ポインタであるvalueが示す先(int型のローカル変数)に、ptr->recordを格納しています。ptr->recordの型はポインタ(char*)です。

// parse.yのrb_intern関数
ID
rb_intern(name)
    char *name;
{
    static ID last_id = LAST_TOKEN;
    int id;
    int last;
    
    if (st_lookup(sym_tbl, name, &id))
    return id;
    // 省略
}
// st.c
int
st_lookup(table, key, value)
    st_table *table;
    register char *key;
    char **value;
{
    // 省略
    if (ptr == nil(st_table_entry)) {
	  return 0;
    } else {
    if (value != nil(char *))  *value = ptr->record;
    return 1;
    }
}

ここまでの情報で、int型の4byteの領域に、ポインタの8byteの値を書き込んでいることで、スタック領域の破壊を引き起こしていると推測することができます。今回はrb_internint id;long id;に変更することで対処し、スタック領域の破壊を解消することができました。

拡張モジュールのリンクエラーの解消

再度makeでビルドをすると、ext/socketなどで大量のリンクエラーが発生します。ext/ディレクトリ以下のファイルは、拡張モジュールとして扱われるものです。

処理を追っていくと、extmk.rbの中で、Makefileの作成と拡張モジュールのビルドを実施しているように見えます。ldはその中で実行されているので、拡張モジュールのビルドでエラーが発生していると推測することができます。ldコマンドが適切に動作するように、様々な箇所を調整することは心が折れるので、gcc -sharedを代わりに使用することにしました。これにより、面倒なオプションの指定を任せることができるようです。

extmk.rbconfigureで作成されます。ldコマンドを指定する箇所を見に行くと、以下のような形になっていました。当時はLinuxの環境では、gcc-elf -sharedを実行するようにセットアップされたようです。gcc -sharedを指定することがある程度対処法として正しそうということを感じつつ LDSHARED='ld'LDSHARED='gcc -shared'を書き換えました。

# 2450行目付近
  case "$host_os" in
	hpux*)		LDSHARED='ld -b' ;;
	solaris*) 	LDSHARED='ld -G' ;;
	sunos*) 	LDSHARED='ld -assert nodefinitions' ;;
	svr4*|esix*) 	LDSHARED="ld -G" ;;
	linux*) 	LDSHARED="gcc-elf -shared" ;;
	*) 		LDSHARED='ld' ;;
  esac

$host_osがlinuxになるようにするには、config.subを修正する必要がありそうですが今回はスキップしました。このあたりを調べていくと様々な知識を得られそうです。

関数の引数の型を定義して、ポインタの扱いを修正

ここまでくると、makeがエラーを出さずに終了するようになりました。READMEによると、次の段階は、make testを実行してtest succeededが表示されるかを確認することになります。

make testを実行してみると、test failedと表示されました。まだ正常に動作するようにはなっていないことが確認できます。Makefileを見ると./ruby sample/test.rbの実行結果を見ているようです。./ruby sample/test.rbを普通に実行すると、以下のような出力を確認することができ、structの2番目のテストでSegmantation Faultがしていることが分かります。

~/workspace/ruby-0.95$ ./ruby sample/test.rb
condition
ok 1
# 省略
struct
ok 1
Segmentation fault

該当のRubyのコードは以下になります。struct_test.new(1, 2)で失敗していることがわかります。

check "struct"
# ok1と出力された箇所 (省略)
test = struct_test.new(1, 2)
if test.foo == 1 && test.bar == 2
  ok
else
  notok
end

最小限のコードを書いて、gdbでデバッグしていくと、以下のようになっていることが確認できます。struct_s_newの呼び出しで、objが負の値になっていることが確認できます。

Program received signal SIGSEGV, Segmentation fault.
0x000055555558e83b in rb_ivar_get (obj=obj@entry=0xf7c70410, id=4992)
    at variable.c:560
560         switch (TYPE(obj)) {
(gdb) bt
#0  0x000055555558e83b in rb_ivar_get (obj=obj@entry=0xf7c70410, id=4992)
    at variable.c:560
#1  0x000055555558c275 in struct_alloc (class=class@entry=4157015056, 
    values=0x7ffff7c701a0) at struct.c:174
#2  0x000055555558c322 in struct_s_new (argc=<optimized out>, 
    argv=<optimized out>, obj=-137952240) at struct.c:216
#3  0x0000555555563469 in rb_call (class=<optimized out>, 
    recv=recv@entry=140737350403088, mid=2904, argc=argc@entry=2, 
    argv=argv@entry=0x7fffffffb7c0, scope=scope@entry=0) at eval.c:2235
#4  0x0000555555564d93 in rb_eval (node=<optimized out>) at eval.c:1110

struct_s_newの実装を見に行くと、以下のようになっていることが確認できます。objに関しては型が指定されていないため、ここでint型として扱われてポインタの値が意図せず欠損していると推測することができます。以下のように、objVALUE型として指定しました。これでstruct_test.new(1, 2)をエラーを出さずに実行することができるようになりました。

// 変更前
static VALUE
struct_s_new(argc, argv, obj)
    int argc;
    VALUE *argv;
{ // 省略

// 変更後
static VALUE
struct_s_new(argc, argv, obj)
    int argc;
    VALUE *argv;
    VALUE obj;
{ // 省略

拡張モジュールの関数プロトタイプの追加

再度、./ruby ./sample/test.rbを実行すると、structのチェックをパスし、gcのチェックでエラーが発生することを確認することができます。

ok 8
gc
Segmentation fault

該当のコードは以下です。

check "gc"
begin
  1.upto(10000) {
    tmp = [0,1,2,3,4,5,6,7,8,9]
  }
  tmp = nil
  ok
rescue
  notok
end

最小限のコードを書いてデバッグしていくと、gc_markに対して渡すobjのアドレスが正しくなく、構造体のメンバにアクセスすることができずにSegmentation Faultが発生していることがわかります。

Program received signal SIGSEGV, Segmentation fault.
gc_mark (obj=0xf7c70ad0) at gc.c:364
364         if (obj->as.basic.flags == 0) return; /* free cell */
(gdb) print obj
$1 = (RVALUE *) 0xf7c70ad0
(gdb) print obj->as
Cannot access memory at address 0xf7c70ad0
(gdb) 

printfでアドレスを出力させながら二分探索しつつ、デバッガーで追っていくと、rb_define_const(class, name, val)の引数val対してint型の範疇に収まる値が指定されていることがわかります。ext/tkutil/tkutil.cInit_tkutilの中で呼び出した際に発生しており、str_new2(WISHPATH)の結果がvalに指定されています。

先ほどのimplicit declaration of functionの警告を修正した時には、ext/tkutil/tkutil.cなどの拡張モジュールに関する警告を確認することができていませんでした。改めて、ext/dbm/dbm.c, ext/etc/etc.c, ext/marshal/marshal.c, ext/socket/socket.c, ext/tkutil/tkutil.cに関数プロトタイプの追加を行っていきます。

bignumに自ら埋め込んだバグの修正

再度、make testを実行すると、bignumの1つ目のテストと、arrayの2つ目のテストが失敗します。まずは、bignumを修正していきます。bignumの失敗するテストは以下になります。

def fact(n)
  return 1 if n == 0
  return n*fact(n-1)
end
if fact(40) == 815915283247897734345611269596115894272000000000
  ok
else
  notok
end

間違いなく、VALUEunsigned longに置き換えた影響が出ています。計算結果を見ていると、途中から計算結果が0になることを確認することができました。

処理を追っていくと、numeric.cfix_mulの内部でマイナスの値が発生することがわかります。ruby.hFIX2INTで値をキャストした際に問題が発生していることが確認できます。

いろいろ試行錯誤をした結果、周辺も併せて、以下のような修正を加えました。

// bignum.c 変更前
VALUE
int2big(n)
    int n;
// bignum.c 変更後
VALUE
int2big(n)
    long n;
// numeric.c 変更前
extern VALUE int2big(int n);
// 省略
fix_mul(x, y)
    // 省略
        int a = FIX2INT(x), b = FIX2INT(y);
        int c = a * b;
// numeric.c 変更後
extern VALUE int2big(long n);
// 省略
fix_mul(x, y)
    // 省略
        long a = FIX2INT(x), b = FIX2INT(y);
        long c = a * b;
// ruby.h 変更前
#define INT2FIX(i) (VALUE)(((int)(i))<<1 | FIXNUM_FLAG)
#define FIX2INT(x) RSHIFT((int)x,1)
// ruby.h 変更後
#define INT2FIX(i) (VALUE)(((long)(i))<<1 | FIXNUM_FLAG)
#define FIX2INT(x) RSHIFT((ULONG)x,1)

その他の修正

arrayの2つ目のテストが失敗については、以下のメーリングリストのログにもあるような修正を加えました。

https://blade.ruby-lang.org/ruby-list/2

また、ここまで他のcast to pointer from integer of different sizeの警告に関しては何も修正していなかったため、以下の4つの警告に関して修正を行いました。(int)(ULONG)に書き換えます。

eval.c:94:23: warning: cast from pointer to integer of different size [-Wpointer-to-int-cast]
   94 | #define EXPR1(c,m) ((((int)(c)>>3)^(m))&CACHE_MASK)

regex.c:1954:12: warning: cast from pointer to integer of different size [-Wpointer-to-int-cast]
 1954 |     temp = (int) *--stackp;     /* How many regs pushed.  

st.c:39:38: warning: cast from pointer to integer of different size [-Wpointer-to-int-cast]
   39 |     ((table->hash == ST_PTRHASH) ? (((int) (key) >> 2) % table->num_bins) :\

st.c:40:40: warning: cast from pointer to integer of different size [-Wpointer-to-int-cast]
   40 |         (table->hash == ST_NUMHASH) ? ((int) (key) % table->num_bins) :\

コードの編集の途中経過

ここまでの作業で、Ruby 0.95をビルドし、make testtest succeededの出力を確認することができます。また、sudo make installを実行して、ruby -vを実行すると、以下の出力を確認することができます。新たなバグを埋め込んだかもしれませんが、Ruby 0.95をある程度動かすことができるようになったと考えていました。

$ ruby -v
ruby - version 0.95 (95/12/21)

しかし、sample/ディレクトリ以下のサンプルコードでどれだけ動くか確かめていくと、正常に動作するものと、動作しないものが存在しました。特に、拡張モジュール周りはundefined symbolのエラーやSegmentation Faultが発生しており、全滅です。まだまだ、修正を加えていく必要があります。

続きは、part2の記事に記載する予定です。

まとめ

Ruby 0.95をUbuntu 24.04(AMD64)向けにビルドするために行った作業を順に説明しました。まだ、正常に動作するとは言えない状態ですが、ビルドができ、make testをパスする程度には動作する状態です。

今回の作業を通じて、32bitの環境で動作するプログラムを64bitの環境で動かす際に気を付けることや、スタック領域の破壊に対する対処について学ぶことができました。また、Ruby 0.95の仕組みに関して、なんとなくの感覚を得ることができました。

今後は、引き続きエラーの解消を行い、実装に関してもきちんと理解をして行こうと考えています。part2の投稿に関しても数週間以内に行われると思いますので、しばらくの間、お待ちいただければと思います。(現在は、lambda/proc関連の修正を一旦諦め、tk.rb周りを少しずつ進めています)

ポート株式会社 サービス開発部が実施している今年のアドベントカレンダーは、その1その2が存在します。私が参加しているアドベントカレンダーその2の次の日の投稿は、22日目のCookieGardenさんのレビュー状況の可視化に関する取り組みの記事となります。また、アドベントカレンダーその1の方はほぼ毎日投稿があり、 20日目はharayuさんのAWS LambdaのCI/CDパイプライン構築の記事、21日目はshibadazoさんの弊社交流会についての記事が投稿されています。そちらの方も、ぜひご覧ください。

Discussion