Ruby 0.95をUbuntu 24.04(AMD64)で動かしたかった part 1
この記事は、ポート株式会社 サービス開発部 その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等)のソースコードが配布されていたことを確認できるのですが、探し当てることができませんでした。
各種パッケージのインストール
一般的な各種ビルドツール(gcc
, make
等)に加え、yacc
は最低限必要でした。また、メーリングリストのアーカイブを見ているとwish
コマンドが実行できる環境でないと、ext/tkutil
がビルドされないといった情報も見受けられたため、以下のコマンドでそれら全てのインストールを実行します。
sudo apt install build-essential gdb bison tk
コードの編集について
以降、ソースコード等に加えた編集に関して記載していきます。
適宜make 2> make.log
やmake 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_funcall
とError, Fatal, Bug, Warning
についても修正していきます。
rb_funcall
とwarning
については、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.c
のmComparable
と、compar.c
のmComparable
の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.c
とcompar.c
の、どちらか一方に対して、extern
を追加します。(今回はrange.c
の方に追加しました)
これにより、VALUE mComparable
がrange.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.h
とdln.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
内に定義されています。NEWOBJ
とOBJSETUP
は、ruby.h
で定義されています。struct SCOPE
のオブジェクトを作成して、the_scope
に格納されたスコープを_old
に格納し、the_scope
に新たに作成されたstruct SCOPE
を格納するコードのようです。
(一緒に定義されているPOP_SCOPE()
では、_old
をthe_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);\
}
まずは、ステップ実行やプリントデバッグで、エラーの発生箇所を特定していきます。
gdb
でrun
を実行する前に、break newobj
を入力して、newobjの呼び出し以降をステップ実行していくと、newobj内ではSegmentation Fault
が発生しないことを確認することができました。
また、その他のSegmentation Fault
の引き金となりうるRBASIC(obj)->class
の処理を含んだ、printf("%p\n", RBASIC(obj)->class)
をnewobj
関数の内部に入れても、Segmentation Fault
が発生しませんでした。ここまで来ると、newobj
の実装の内部ではなく、その外側で問題が発生していると推測することができます。
試しに、NBEOBJ
とOBJSETUP
の間に、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)
に置き換えます。こちらのgetpgrp
のman
の翻訳から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
に対して渡している引数のclass
はVALUE
型として定義されています。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.y
のrb_intern
の実装を見ると、再帰でrb_intern
を呼び出すパスがあります。当初は再帰呼び出しによるスタック領域の破壊を疑いましたが、break rb_intern
でステップ実行していくと、そのようなことは発生していなさそうでした。
関連する情報を調べると、ローカル変数に対して領域外のアクセスを行うと発生する場合もあるということがわかりました。また、コンパイル時に-fsanitize=undefined -fsanitize=address
を指定することで、関連するエラーに関して、より詳細に調査することが可能なようです。
Makefile
のCFLAGS
にオプションを追加して実行すると、以下のようなメッセージを確認することができました。
==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.c
のst_lookup
関数の中で、スタック領域を破壊してしまっているようです。また、rb_intern
の中で宣言されているid
と呼ばれる変数が関連していることも推測することができます。
以下にrb_intern
とst_lookup
の実装を示します。int
型のローカル変数id
へのポインタをst_lookup
のvalue
に対応する引数として渡しています。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_intern
のint id;
をlong id;
に変更することで対処し、スタック領域の破壊を解消することができました。
拡張モジュールのリンクエラーの解消
再度make
でビルドをすると、ext/socket
などで大量のリンクエラーが発生します。ext/
ディレクトリ以下のファイルは、拡張モジュールとして扱われるものです。
処理を追っていくと、extmk.rb
の中で、Makefile
の作成と拡張モジュールのビルドを実施しているように見えます。ld
はその中で実行されているので、拡張モジュールのビルドでエラーが発生していると推測することができます。ld
コマンドが適切に動作するように、様々な箇所を調整することは心が折れるので、gcc -shared
を代わりに使用することにしました。これにより、面倒なオプションの指定を任せることができるようです。
extmk.rb
はconfigure
で作成されます。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
型として扱われてポインタの値が意図せず欠損していると推測することができます。以下のように、obj
をVALUE
型として指定しました。これで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.c
のInit_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
間違いなく、VALUE
をunsigned long
に置き換えた影響が出ています。計算結果を見ていると、途中から計算結果が0になることを確認することができました。
処理を追っていくと、numeric.c
のfix_mul
の内部でマイナスの値が発生することがわかります。ruby.h
のFIX2INT
で値をキャストした際に問題が発生していることが確認できます。
いろいろ試行錯誤をした結果、周辺も併せて、以下のような修正を加えました。
// 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つ目のテストが失敗については、以下のメーリングリストのログにもあるような修正を加えました。
また、ここまで他の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 test
でtest 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