Ruby 0.95をUbuntu 24.04(AMD64)で動かしたかった part 2
前回、Advent Calenderで作成したpart1の続きの記事です。
概要
Ruby 0.95の64bit版のビルドを試み、その過程をまとめて記述したものになります。私自身、C言語に対して深い知識や理解があるわけではないため、学習しながら問題を一つずつ解決して進めています。
前回と同様に、パッチの公開は行いませんが、なるべく再現が可能なように、実際の作業の手順に沿って詳細に記載しています。
背景等 (part 1の省略版)
Rubyに詳しくなりたく、唐突にC言語と格闘したくなったという経緯で、コードに変更を加えつつ64bit版のRuby 0.95をコンパイルするという取り組みに至りました。(詳細はpart1に記載)
私自身に至らぬ点が多く、所々に粗い部分や誤りが含まれている可能性があることや、元のソースコード等の柔軟性を排した形で編集を行っていることをご理解いただけますと幸いです。また、当時のRuby 0.95におけるビルドや実行ファイルの挙動とは互換性がない場合がある点についても、ご了承ください。
前回のまとめ
-
UINT
やVALUE
の定義をunsigned long
に変更し、ポインタに関する修正について雑に実施しています。 -
make test
でtest succeeded
の出力が確認できました。 -
sample/
以下のRubyのサンプルプログラムについては、拡張モジュールが絡むプログラムについては全滅、それ以外も動作するものとしないものが存在しています。
前回の範囲の修正点
前回の修正で、unsigned long
をULONG
として定義し、それをVALUE
に対応する型としていました。これにより、関数の引数や戻り値の受け渡しでポインタの値が欠損しないように変更を加えました。
「バグが発生したから修正した」という訳ではないのですが、UINT
のunsigned int
をunsigned long
に置き換える方が、様々な処理がうまく動いてくれそうに思えたため、ULONG
を新たに定義することをやめて、以下のようにunsinged long
をUINT
として定義することにしました。
typedef unsigned long UINT;
typedef UINT VALUE;
typedef UINT ID;
また、本記事の最後の方でも触れますが、前回の記事でbignum
のテストを通すために変更した箇所を元に戻して、数値の取り扱いをまとめて修正している箇所があります。
コード等の編集について
以降、ソースコード等に加えた編集等に関して記載していきます。前回の記事の続きから実施しています。
拡張モジュール読み込み時のエラーの修正
sample/marshal.rb
などを実行すると、拡張もジュールを読み込むrequire "marshal"
でエラーが発生しました。cBignum
と呼ばれる名前のシンボルが定義されていないといったエラーとなります。
sample/marshal.rb:1:in `require': /usr/local/lib/ruby/marshal.so: undefined symbol: cBignum - /usr/local/lib/ruby/marshal.so
marshal.so
は、拡張モジュールとしてext/marshal/marshal.c
をコンパイルしたものです。marshal.c
の中でcBignum
は以下のようにして宣言されています。marshal.c
以外のファイルにあるcBignum
を参照することを意図しているようです。
extern VALUE cBignum, cStruct;
cBignum
を定義している箇所を探すと、bignum.c
に存在を確認することが可能です。グローバル変数として定義されています。(関連するソースコードを見ると、cBignumにはBignumのクラス定義自体が格納されていそうです。)
VALUE cBignum;
まずは、現状の状態でどこからシンボルの情報を取ってくることができそうか考えることにしました。取り扱うファイルの数を増やしたくないので、make install
でインストール済みのファイルを見て、ちょうどよいものがないか探します。
インストールされたファイルの一覧を以下に示します。
/usr/local/bin/ruby
-
/usr/local/lib/ruby/
etc.so
marshal.so
socket.so
- その他
require
で読み込み可能と思われる、.rb
ファイル (base64.rb, parseargs.rb
など)
(ここで、dbm.c
がコンパイルされていないことや、tkutil.c
が静的にリンクされていることに気が付きますが、後ほど取り扱います)
ruby
の実行ファイル自体からシンボルの情報を取得することができると良い気がします。調べていると、-rdynamic
オプションを使用すると、実行ファイルのシンボルを共有ライブラリから参照できそうという情報を見つけることができました。
試しにMakefile
のLIBS
のオプション指定に-rdynamic
を追加するときちんと動作するようになりました。(うまくいかない場合は、.o
や.so
や古い実行ファイルを削除して、再度ビルドすると良いかもしれません。)
$ ruby sample/marshal.rb
[25.6, #<struct point: x=10, y=10>, #<struct rectangle: origin=#<struct point: x=10, y=10>, corner=#<struct point: x=20, y=20>>, #<Object:0x632e9a60>, #<Object:0x632e9a60>, "fff"][25.6, #<struct point: x=10, y=10>, #<struct rectangle: origin=#<struct point: x=10, y=10>, corner=#<struct point: x=20, y=20>>, #<Object:0x632e9490>, #<Object:0x632e9490>, "fff"]
Makefile
は、./configure
が実行されると生成しなおされるため、Makefile
の生成に利用されていそうなMakefile.in
のLIBSに関する記述に、-rdynamic
の指定を追加します。
LDFLAGS = $(CFLAGS) @LDFLAGS@
LIBS = -lm -rdynamic @LIBS@ $(EXTLIBS)
MISSING = @LIBOBJS@ @ALLOCA@
静的リンクに関する補足
拡張モジュールのビルドに関しては、以下のようなREADME
中に説明があります。
(必要ならば)ext/Setupに静的にリンクする拡張モジュールを
指定するext/Setupに記述したモジュールは静的にリンクされます.
ruby 0.95は、初期状態では以下のような指定となっています。つまり、tkutilのみ静的にリンクされることが指定されているようです。
#option nodynamic
#dbm
#etc
#marshal
#socket
tkutil
試しに、#marshal
のコメントを解除してみると、先ほど追加した-rdynamic
のオプションを指定せずとも、正常にsample/marshal.rb
が動作しました。
ext/dbm
をビルドできるようにするためにgdbm
を導入
make install
でインストールされたファイルを確認したところ、ext/dbm
のビルドで得られるはずの共有ライブラリがないことに気が付きました。ログを見ていると、compiling dbm
のログ出力の後、特に何も実行されずにmarshal
のビルドに移行されていそうだということが分かりました。
処理を追っていると、ext/dbm/extconf.rb
を実行し、その結果Makefile
などが生成されず、ビルドがスキップされていそうだということが分かりました。
ext/dbm/extconf.rb
は以下のようになっています。have_library("dbm", "dbm_open")
の結果を確認するとnil
が返ってきており、READNE.EXT
内のライブラリが存在するとTRUE
を返すという記述から、必要なライブラリが存在していないことがわかりました。
have_library("dbm", "dbm_open")
if have_func("dbm_open")
create_makefile("dbm")
end
さらに処理を追っていくと、以下のコマンドをsystem
メソッドで実行し、コマンドの実行に失敗したため、処理がスキップされていたことがわかります。
gcc -o conftest -I../.. -g -O conftest.c -ldbm > /dev/null 2>&1
dbm
とは、Database Manager
の略で、オリジナルの実装の他にも複数の派生版が存在しているようです。キーとデータをペアにして格納して、キーによる高速な検索を行ってデータにアクセスするといった動きをし、比較的シンプルにデータを格納したい場合に用いられるデータベースのようです。
ext/dbm/dbm.c
では、#include <ndbm.h>
と指定されています。こちらは、派生版のndbm (New dbm)
を指すようです。
Ubuntu 24.04でndbm.h
を使えるようにする方法を調べていると、gdbm (GNU dbm)
と呼ばれるもののman
を見つけることができました。これによると、gdbm
はndbm
との互換性があるそうです。また、-lgdbm_compat
をコンパイル時に指定すると良さそうな記述を見つけたため、そちらを試してみることにしました。
以下のコマンドで関連するファイルをインストールすることができます。
sudo apt install libgdbm-dev libgdbm-compat-dev
また、ext/dbm/extconf.rb
を以下のように書き換えて、-lgdbm_compat
のオプションを付与したMakefile
の生成ができるようにします。
have_library("gdbm_compat", "dbm_open")
if have_func("dbm_open")
create_makefile("dbm")
end
これで再度make
を実行すると、dbm
に関するビルドの処理が走っているログを確認することができました。
また、sample/dbm.rb
を実行すると、以下のように正常に動作する様子を確認することができました。
$ ruby sample/dbm.rb # 保存 (test.dirとtest.pagが作成される)
$ ruby sample/dbm.rb # 読み込んで保存された値を表示
baz
foobar
quux
BZ
FB
QX
tkutil
を利用した際のSegmentation Fault
の解消
続いて、tkutil
を利用した際のエラーを解消していきます。sample/tkhello.rb
を実行すると、以下のようになります。0xf7c2dc50
がary
として渡されており、ary->len
でSegmentation Fault
が発生しています。これまで何回か登場したポインタの値が欠損する問題だと考えられます。
Program received signal SIGSEGV, Segmentation fault.
ary_push (ary=ary@entry=0xf7c2dc50, item=140737350130528) at array.c:171
171 astore(ary, ary->len, item);
(gdb) bt
#0 ary_push (ary=ary@entry=0xf7c2dc50, item=140737350130528)
at array.c:171
#1 0x0000555555564a49 in collect_i (i=<optimized out>, tmp=4156742736)
at enum.c:122
#2 0x000055555556ad9e in rb_yield_0 (val=140737350130912,
self=self@entry=0) at eval.c:1835
#3 0x000055555556ae1c in rb_yield (val=<optimized out>) at eval.c:1870
#4 0x000055555555e913 in ary_each (ary=0x7ffff7c2dc80) at array.c:484
#5 0x000055555556762d in rb_call (class=<optimized out>,
recv=recv@entry=140737350130816, mid=2992, argc=argc@entry=0,
argv=0x0, scope=scope@entry=1) at eval.c:2238
今回のエラー箇所を呼び出すことになった、大本のコードは、以下のext/tkutil/tkutil.c
の以下の箇所です。
static VALUE
tk_yield(obj)
VALUE obj;
{
rb_yield_0(obj, obj);
}
static VALUE
tk_s_new(argc, argv, class)
int argc;
VALUE *argv;
VALUE class;
{
VALUE obj = obj_alloc(class);
rb_funcall2(obj, rb_intern("initialize"), argc, argv);
if (iterator_p()) tk_yield(obj);
return obj;
}
eval.c
のrb_yield_0
を見ていくと、以下のような箇所にたどり着きます。このnd_cfnc
を実行する箇所で、collect_i
を呼び出すような形となります。
VALUE
rb_yield_0(val, self)
VALUE val, self;
{
// 省略
else if (nd_type(node) == NODE_CFUNC) {
result = (*node->nd_cfnc)(val,node->nd_argc);
}
// 省略
}
ここで利用するnode
は、enum.c
の以下の箇所で用意していると考えられます。collect_i
に渡すtmp
は、ary_mew()
返されるポインタです。この値が欠損してしまい、Segmentation Fault
を引き起こしています。
enum_collect
で呼び出している、eval.c
のrb_iterate
の内部で、NEW_CFUNC(f,c)
を使ってnode
を作成しています。node.h
に処理の内容が記載されており、enum_collect
のcollect_i
がNEW_CFUNC
のf
、tmp
がc
に対応しています。
// enum.c
static VALUE
enum_collect(obj)
VALUE obj;
{
VALUE tmp;
tmp = ary_new();
rb_iterate(rb_each, obj, collect_i, tmp);
return tmp;
}
// eval.c
VALUE
rb_iterate(it_proc, data1, bl_proc, data2)
VALUE (*it_proc)(), (*bl_proc)();
void *data1, *data2;
{
int state;
VALUE retval = Qnil;
NODE *node = NEW_CFUNC(bl_proc, data2);
// 省略
}
// node.h
#define NEW_CFUNC(f,c) newnode(NODE_CFUNC,f,c,0)
ポインタが欠損している原因となっているのは、先ほど示したrb_yield_0
関数中の、以下の箇所になります。NEW_CFUNC
で作成したNODE
をどこからか取得し、NODE
に格納されている関数のポインタと引数を用いて処理を実行しています。
// eval.c
result = (*node->nd_cfnc)(val,node->nd_argc);
nd_argc
はマクロで、それを展開するとnode->u2.argc
になります。u2.argc
はint
型(4 byte)なので、ここがポインタの値(8 byte)をキャストして欠損させている箇所となります。
typedef struct RNode {
// 省略
union {
// 省略
} u1;
union {
struct RNode *node;
ID id;
int argc;
} u2;
union {
// 省略
} u3;
} NODE;
u2
がunion
だったため、今回はeval.c
でnd_argc
を使用していた箇所を、以下のように変更しました。他の箇所に影響がないか心配ですが、少なくともmake test
は成功するようでした。
// eval.c
result = (*node->nd_cfnc)(val,node->u2.node);
以上のような変更を加えた後、sample/tkhello.rb
を実行すると、以下のようなGUIを持つアプリの画面が表示されます。helloのラベルがあるボタンを押すと、標準出力にhelloと出力されます。(WSLg
を利用しているため通常のUbuntuとは異なる表示かもしれません)
他のsample/tk*.rb
を実行していくと、tktimer.rb
は正常に動作し、それ以外は以下のようなエラーが発生しました。
- 起動したあと画面は表示されるが、数秒以内に画面が閉じる
-
require': No such file to load -- tkscrollbox
のエラー出力が表示される
以下は、sample/tktimer.rb
の画面です。(0.95で止められているのは、0.05秒(?)間隔で数字の更新が入るためです。)
tkutil
で作成されたGUIアプリが数秒後に終了するエラーの調査と対処
数秒以内に画面が閉じてしまうsample/tkline.rb
について、gdb
で実行中の様子を確かめれていると、以下のようなパイプに関するエラーのメッセージを確認することができました。
Program received signal SIGPIPE, Broken pipe.
また、printデバッグを実施すると、lib/tk.rb
の以下の箇所で例外が発生し、それがキャッチされてプログラムが正常終了していることが確認できました。
パイプが壊れたのが先か、例外を吐いたのが先かを調べると、原因をある程度絞ることができそうです。
def mainloop
begin
tk_write 'after idle {wm deiconify .}'
while TRUE
rf, wf = select(READABLE, WRITABLE)
for f in rf
READ_CMD[f].call(f) if READ_CMD[f] #.callの呼び出し時に例外が発生
tkutil.c
とtk.rb
、およびwish
について整理
ここで、今後のデバッグに向けて、tkutil.c
に関連する内容を調べることにしました。名称やビルドに必要な外部ライブラリなどから、GUIツールキットのTcl/Tk
に関連していると考えられます。
まずは、tkutil.c
からです。tkutil.c
は、C言語で実装された拡張モジュールのプログラムです。定数WISH_PATH
と拡張メソッドのeval_cmd
を持つTkUtil
モジュールと、Object
クラスを継承したTkKernel
クラスが実装されています。
ビルドの結果、tkutil.c
は共有ライブラリのtkutil.so
となり、/usr/local/lib/ruby/
以下に配置されます。このファイルはRubyのプログラムの中でrequire "tkutil"
が実行されると読み込まれます。(この読み込みの処理はeval.c
とdln.c
に実装されています。f_require
の中で共有ライブラリの読み込みと、Rubyのスクリプトファイルの読み込みの処理に分岐されており、dln.c
に実装されたdlopen
の処理によりtkutil.so
は読み込まれます。)
tk.rb
は、lib/
以下に配置されたRubyのスクリプトのファイルです。lib/
以下の.rb
のファイルは、make install
の時に/usr/local/lib/ruby/
以下のコピーされ、Rubyのプログラムの中でrequire "tk"
が実行されると読み込まれます。tk.rb
はtkutil
を利用して様々なモジュール、クラスを実装しています。Tcl/Tk
に関するライブラリの主な実装になると考えています。
その他、lib/tkcanvas.rb
、lib/tkclass.rb
、lib/tkentry.rb
、lib/tktext.rb
は、tk.rb
を利用してさらに便利なGUIの構成要素を実装したものです。
Tcl
はTool Command Language
の略で、様々な環境で動作するシンプルで柔軟性の高いスクリプト言語です。また、Tk
はTcl
向けに実装されたGUIツールキットで、単にTk
と呼ばれる場合は、多言語向けのツールキット、C言語向けライブラリ、wish
コマンド、Tcl
インタプリタに新しいコマンドを追加するTk
の拡張機能などを指すようです。今回取り扱うwish
コマンドは、Tk
のコマンドなどが拡張されたTcl
のインタプリタです。昔からTk
を利用するために使われているそうです。(Tclの参考資料, Tkの参考資料)
wish
コマンドを実行すると、プロンプトが%
に変わり、空のウィンドウが作成されます。プロンプトに対してスクリプトを入力していくと、ウィンドウにボタンなどのウィジェットを配置するなど、GUIを実装することが可能です。
tk.rb
のTk
モジュールの基本的な挙動
Tk
モジュールはwish
とのやり取りに用いるインターフェースを提供しているように見えます。また、モジュールの定義の最中にも様々な処理が実行されるようになっています。例えば、以下のような箇所です。
module Tk
include TkUtil
extend Tk
$0 =~ /\/(.*$)/
# パイプを使いつつwishコマンドを呼び出し、その入出力ストリームをPORTに保持する
PORT = open(format("|%s -n %s", WISH_PATH, $1), "w+");
# wishに文字列を入力するメソッド
def tk_write(*args)
printf PORT, *args;
PORT.print "\n"
PORT.flush
end
# withdrawでwishコマンド起動時に出現するウィンドウを非表示に
# wishでrb_outとkeepaliveを定義、tkerrorをオーバーライドして、エラーが発生した場合に終了するように
tk_write '\
wm withdraw .
proc rb_out args {
puts [format %%s $args]
flush stdout
}
proc tkerror args { exit }
proc keepalive {} { rb_out alive; after 120000 keepalive}
after 120000 keepalive' # 120000 ms後にkeepaliveを呼び出すように設定
その他、wishから出力された内容の読み込みや、書き込み内容を組み立てる処理、メインループなどが存在しています。
proc/lambda
に関する妥協)
エラーの調査とRubyで実装されたコードの修正(何となく関連情報を把握できたところで、エラーの詳細な調査に入ります。
まずは、先ほども提示したtkerror
をオーバーライドしている箇所を書き換え、エラー発生時にexit
で終了させずエラー内容を出力するように変更します。
tk_write '\
wm withdraw .
proc rb_out args {
puts [format %%s $args]
flush stdout
}
proc tkerror args {
puts stderr "tkerror: $args"
flush stderr
}
proc keepalive {} { rb_out alive; after 120000 keepalive}
after 120000 keepalive'
また、書き込みや読み込みの位置にも、その内容を標準出力に出力するような処理を追加します。
# 省略
def tk_write(*args)
printf *args # 追加
print "\n" # 追加
printf PORT, *args;
PORT.print "\n"
PORT.flush
end
# 省略
def tk_call(*args)
# 省略
while PORT.gets
print "[GETS]", $_, "\n" #追加
$_.chop!
if /^=(.*)@@$/
# 省略
この状態でtkline.rb
を実行すると、以下のような出力を確認することができます。after idle {wm deiconify .}
で再度ウィンドウが出現します。この状態でウィンドウをクリックしたりすると、tkerror: {invalid command name "150"}
の出力を確認することができます。
# 省略
after 120000 keepalive
if [catch {{canvas} {.w00001}} var] {puts "!$var"} {puts "=$var@@"};flush stdout
[GETS]=.w00001@@
if [catch {{pack} {.w00001}} var] {puts "!$var"} {puts "=$var@@"};flush stdout
[GETS]=@@
if [catch {{bind} {.w00001} {<1>} { %# %b %f %h %k %s %t %w %x %y %A %E %K %N %W %T %X %Y}} var] {puts "!$var"} {puts "=$var@@"};flush stdout
[GETS]=@@
if [catch {{bind} {.w00001} {<B1-Motion>} { %# %b %f %h %k %s %t %w %x %y %A %E %K %N %W %T %X %Y}} var] {puts "!$var"} {puts "=$var@@"};flush stdout
[GETS]=@@
if [catch {{bind} {.w00001} {<ButtonRelease-1>} { %# %b %f %h %k %s %t %w %x %y %A %E %K %N %W %T %X %Y}} var] {puts "!$var"} {puts "=$var@@"};flush stdout
[GETS]=@@
after idle {wm deiconify .}
tkerror: {invalid command name "150"}
tkerror: {invalid command name "151"}
tkerror: {invalid command name "152"}
tkerror: {invalid command name "153"}
# 省略
ここまでの情報で、invalid command name
のエラーが発生し、オーバーライドされたtkerror
でexit
が実行され、パイプを開いたままwish
が終了し、その結果パイプを利用して行うはずだった処理で例外が発生して、Rubyの実行が終了してしまったと考えることができます。まず、この方向性で原因を調査することにしました。
invalid command name
が発生しないようにするためには、おそらくwishに対して正しい名前でコマンドを登録する、もしくは呼び出すことが必要になると考えられます。
コマンドの登録に関しては、install_cmd
というメソッドを見つけることができ、これが関連すると考えました。また、install_cmd
はprivateメソッドになることが意図されているので、これを呼び出すpublicなメソッドを探しました。
$tk_cmdid = "c00000"
def install_cmd(cmd)
return '' if cmd == '' # uninstall cmd
id = $tk_cmdid
$tk_cmdid = $tk_cmdid.next
$tk_cmdtbl[id] = cmd
@cmdtbl = [] if not @cmdtbl
@cmdtbl.push id
return format('rb_out %s', id)
end
def uninstall_cmd(id)
$tk_cmdtbl[id] = nil
end
private :install_cmd, :uninstall_cmd
サンプルコードのtkline.rb
を見ていると、proc
(ブロックのオブジェクト)を引数としたbind
メソッドを呼び出していることが確認できます。これは、処理の登録に相当するように見えました。
$c.bind("1", proc{|e| do_press e.x,e.y})
$c.bind("B1-Motion", proc{|e| do_motion e.x,e.y})
$c.bind("ButtonRelease-1", proc{|e| do_release e.x,e.y})
Tk.mainloop
bind
メソッドを深掘りしていくと、install_cmd
を利用したinstall_bind
メソッドと、それを利用する_bind
メソッドにたどり着きました。
def install_bind(cmd)
id = install_cmd(proc{|args|
TkUtil.eval_cmd cmd, Event.new(*args)
})
id + " %# %b %f %h %k %s %t %w %x %y %A %E %K %N %W %T %X %Y"
end
def _bind(path, context, cmd)
begin
id = install_bind(cmd)
tk_call 'bind', path, "<#{context}>", id
rescue
$tk_cmdtbl[id] = nil
fail
end
end
次に、wish
と直接やり取りしている箇所を探し、登録している値が意図したものになっているか確認することにしました。今まで挙げたコードで、実際にwish
とやり取りをしているのは、_bind
メソッドのtk_call 'bind', path, "<#{context}>", id
の箇所です。tk_call
メソッドは以下のような実装になっています。
def tk_call(*args)
args = args.collect{|s|
# 省略
}
str = args.join(" ")
tk_write 'if [catch {%s} var] {puts "!$var"} {puts "=$var@@"};flush stdout', str
# 省略
tk_write 'if [catch {%s} var] {puts "!$var"} {puts "=$var@@"};flush stdout', str
が該当する箇所になります。おそらく、先ほど確認した入出力のログの以下の部分にあたると考えられます。
if [catch {{bind} {.w00001} {<1>} { %# %b %f %h %k %s %t %w %x %y %A %E %K %N %W %T %X %Y}} var] {puts "!$var"} {puts "=$var@@"};flush stdout
if [catch {{bind} {.w00001} {<B1-Motion>} { %# %b %f %h %k %s %t %w %x %y %A %E %K %N %W %T %X %Y}} var] {puts "!$var"} {puts "=$var@@"};flush stdout
if [catch {{bind} {.w00001} {<ButtonRelease-1>} { %# %b %f %h %k %s %t %w %x %y %A %E %K %N %W %T %X %Y}} var] {puts "!$var"} {puts "=$var@@"};flush stdout
install_bind
内で確認できる、%# %b %f %h %k %s %t %w %x %y %A %E %K %N %W %T %X %Y
と同じ文字列の並びを確認することができます。install_bind
では、install_cmd
で得たid
と結合した文字列を返していますが、入出力ログを見るとid
にあたりそうな箇所を確認することができませんでした。
次は、id
がどんな値となるのか確認することにしました。以下のように、install_bind
の中でid
を出力してみると、nil
といった値を確認することができました。おそらく一意の値を取得したい処理でnil
が返ってきてしまっているので、この箇所がエラーの原因である可能性が高いと考えました。
def install_bind(cmd)
id = install_cmd(proc{|args|
TkUtil.eval_cmd cmd, Event.new(*args)
})
print id, "\n" # nilと出力される
id + " %# %b %f %h %k %s %t %w %x %y %A %E %K %N %W %T %X %Y"
end
この後、install_cmd
など処理の内容を理解してデバッグしようとするなど、右往左往していました。install_cmd
をただ単に"hoge"
を返すように変更し、その場合でもnil
が返ってきたところでエラーの原因に近い挙動を確認することができ、現状のinstall_cmd
はメソッドの内容にかかわらずnil
を返すことを認識できました。
install_cmd
のメソッド呼び出しについて、今までの正常に動作してきたサンプルコードやmake test
と比較すると、proc{|args| func args}
のようなラムダ式に似たもの(以下、ブロックと呼びます)を直接引数に指定している点が差だと考えられます。そこで試しに以下のようなサンプルコードを実行していると、nil
が返る現象を再現することができました。少なくともid
がnil
として帰ってきているのは、以下のようなメソッドの呼び出しをしているためだとわからいました。
(Ruby 0.95においては、proc
メソッドとlambda
メソッドは、同じf_lambda
関数を呼び出すことになるため、同一の存在であると捉えています)
def hoge(cmd)
"hoge"
end
def fuga
ret = hoge(lambda{|args| "ret"})
return ret
end
print hoge(lambda{|args| "ret"}), "\n" # hogeが出力される
print fuga, "\n" # nilが出力される
この現象への対処のために、eval.c
の引数に関する処理や、goto
や、parse.y
の処理など、様々な内容を疑いましたが、結局原因はわからないままでした。
VALUE
をunsigned long
にした影響かどうか確かめるため、all-ruby
を利用して以下のような手順で動作を確認してみると、現在作業中の環境と同じ出力を確認することができました。
-
all-ruby
のDockerコンテナを建てる -
何らかの方法で、上記の動作確認に使ったスクリプトのファイルをコンテナ内に用意する
- 今回は、
/scripts/script.rb
として準備しました。
- 今回は、
-
ALL_RUBY_BINS='ruby-0.95' all-ruby /script/script.rb
を実行する -
以下が出力結果です
# ALL_RUBY_BINS='ruby-0.95' ./all-ruby /scripts/script.rb ruby-0.95 nil
-
バージョンを指定せずに、全バージョンでの実行結果を確認すると以下のようになります。
./all-ruby /scripts/script.rb ruby-0.49 /scripts/script.rb:6: syntax error /scripts/script.rb:6: syntax error /scripts/script.rb:6: syntax error /scripts/script.rb:8: syntax error exit 4 ruby-0.50 /scripts/script.rb:6: syntax error /scripts/script.rb:6: syntax error /scripts/script.rb:6: syntax error /scripts/script.rb:8: syntax error exit 4 ruby-0.51 nil ... ruby-1.1b9 nil ruby-1.1b9_01 hoge ... ruby-3.4.1 hoge
-
少なくとも私がWSLで用意できそうなLinuxの環境下のRuby 0.95では、正常にこのブロックを引数に指定するコードが動作しないことがわかりました。1995年当時のBSD(?)などの中に開発環境を用意するのは大変に思えたので、RubyのC言語の実装の修正はあきらめて、以下のコードのような回避策をtk.rb
やサンプルコードに適用していくことにしました。これにより、nil
が返ってくるという現象は回避することが可能です
(かなり確率が低いですが、もしpart3の記事を書く場合は、この箇所のきちんとした修正を取り扱うと思います)
def hoge(cmd)
"hoge"
end
def fuga_fix
funcobj = lambda{|args| "ret"} # 一度ブロックのオブジェクトを変数に格納する
ret = hoge(funcobj) # メソッドの引数にブロックのオブジェクトが格納された変数を指定する
return ret
end
print fuga_fix, "\n" # hogeと出力される
Rubyのスクリプトファイルの中から、proc
やlambda
で文字列の検索をして修正対象を探し、修正を行っていきます。
修正後、再度sample/tkline.rb
を実行すると、invalid command name
のエラーは発生せず、c00000 131 1 ?? ?? ?? 16 3347576 ?? 156 56 ?? 0 ?? ?? .w00001 4 313 171
のような文字列をwish
側に書き込めていることが確認できます。しかし、サンプルが意図したような、ウィンドウに対する線の描画が発生しません。
sample/tkline.rb
が直接用いているlib/tkcanvas.rb
のLine(TkcLine)
の実装を見ていると、create line
といったような文字列がwish
側に書き込まれることが正常な状態のようです。しかし、そのような書き込みが発生したような形跡が見られませんでした。
class TkcLine:TkcItem
def create_self(*args)
tk_call(@path, 'create', 'line', *args)
end
end
まずは、bind
メソッドで登録したメソッドが正常に呼び出されていないことを疑い、周辺を調査することにしました。
bind
の引数に指定したブロックのオブジェクトは、最終的には$tk_cmdtbl
という変数のハッシュに格納されます。ここからオブジェクトが読みだされて、ブロックが実行されるはずだと考えました。
$tk_cmdtbl
から値を読み取っているコードは、tk.rb
内部に3箇所存在します。そのうち、処理が実行され得るものはtk_tcl2ruby
メソッドと、dispatch
メソッドの2箇所です。試しにそれぞれのメソッドの先頭にprint "メソッド名\n"
のようなコードを差し込んで実行していみるといずれの出力も確認することができませんでした。これらのメソッドを呼び出す箇所も動作していないことが分かりました。
2つのメソッドに関する調査が必要だと考えられますが、tk_tcl2ruby
メソッドは、tk_split_list
メソッド内で呼び出されています。またtk_split_list
メソッドはdispatch
メソッドに呼び出されている状態となっています。そのため、先にdispatch
の呼び出しに関する問題を解消することが必要となります。
dispatch
メソッドが呼び出されるのは、tk_call
メソッド内と、file_readable
メソッドの引数に指定しているブロックの内部です。後者は以下のように、READ_CMD
という変数(定数?)に格納されたハッシュに格納されて、最終的にmainloop
メソッド内で呼び出されます。
def file_readable(port, cmd)
READABLE.push port
READ_CMD[port] = cmd
end
def mainloop
begin
tk_write 'after idle {wm deiconify .}'
while TRUE
rf, wf = select(READABLE, WRITABLE)
for f in rf
READ_CMD[f].call(f) if READ_CMD[f]
if f.closed?
READABLE.delete f
READ_CMD[f] = nil
end
end
# 省略
READ_CMD[f].call(f)
実行されないことに関しては、READ_CMD[f]
で代入済みのオブジェクトが見つからずにnil
が帰ってきたか、代入したオブジェクトがnil
になっていたかの2つが考えられます。
試しに、file_readable
でcmd
の内容を出力してみると、nil
になっていることが確認できました。つまり、以下のようなfile_readable
のメソッドの呼び出しに使われるブロックがnilとして取り扱われており、ブロックの部分がnilとして解釈されているということがわかりました。この問題を修正する必要がありそうです。
# 前述の修正により、ブロックを変数に格納している状態
file_readable_param_proc = proc {
exit if not PORT.gets
Tk.dispatch($_.chop!)
}
file_readable PORT, file_readable_param_proc
次に、上記の問題を再現するRubyのコードを実装して動作を確認することにしました。以下にそのコードを示します。
def test_func
multi_line_proc = proc {|args|
print("hello")
print(" multi line proc\n")
"ret"
}
print multi_line_proc, "\n" # nilが出力される
print multi_line_proc.call, "\n" # nilに対してcallメソッドを呼び出そうとするので、`test_func': undefined method `call' for nil(Nil)のエラーメッセージが出力
end
test_func
今回の挙動も、前述の問題と同根の原因であると考えられたため、実装の調査に入る前にall-ruby
で動作を確認しました。動作を確認したところ、こちらでも同じ出力やエラーの発生を確認できました。また、意図した通りに動作するバージョンに関しても、ruby-1.1b9_01
以降のバージョンになるため、先ほど修正をあきらめた問題と関連があると考えられます。そのため、こちら問題もC言語の実装を修正することを諦めて回避策を探すことにしました。
> ALL_RUBY_BINS='ruby-0.95' ./all-ruby /scripts/script2.rb
ruby-0.95 /scripts/script2.rb:8:in `test_func': undefined method `call' for nil(Nil)
nil
exit 1
様々な回避策が考えられると思いますが、今回はパーサーの気持ちになり、ブロックの後ろに別の解釈をする対象が続いていれば何か変わりそうだという直感に基づいて、以下のコードのように多重代入を利用することを試行しました。これで、今回の問題を回避することに成功したため、tk.rb
やサンプルコードに対しても多重代入を用いた回避策を適用することにしました。
def test_func_fix
multi_line_proc, _temp = proc {|args|
print("hello")
print(" multi line proc\n")
"ret"
}, "_"
print multi_line_proc, "\n" # #<Proc:0x1d44b5a0>と出力される
print multi_line_proc.call, "\n" # hello multi line procの出力の後、retと出力される
print _temp, "\n" # _と出力される
end
test_func_fix
ここまでの修正で、sample/tkline.rb
とsample/tkdialog.rb
とsample/tkfrom.rb
が動作するようになりました。sample/tkfrom.rb
はYou have no mail.
の表示の後、何も触らずとも終了してしまいますが、これはバグではなく Tk.after
で2秒後にexit
が実行されているためです。
No such file to load -- tkscrollbox
の解消
以下のメーリングリストのログにあるtkscrollbox.rb
の実装を新規に作成したファイルのlib/tkscrollbox.rb
に記載し、sudo make install
を実行することで解消することができます。
これにより、sample/tkbrowse.rb
とsample/tkbiff.rb
を実行した際に、No such file to load -- tkscrollbox
のエラーが発生しなくなります。
srand
のstack smashing detected
の解消
sample/rcs.rb
を実行すると、srand
メソッドの実行時に、*** stack smashing detected ***: terminated
が出力され、異常終了します。
今回も、part 1の時と同様に、ローカル変数に関連していると考えられたため、その点から調査することにしました。srand
は、random.c
のf_srand
関数に実装されており、以下のようになっています。
static VALUE
f_srand(argc, argv, obj)
int argc;
VALUE *argv;
VALUE obj;
{
int seed, old;
#ifdef HAVE_RANDOM
static int saved_seed;
#endif
if (rb_scan_args(argc, argv, "01", &seed) == 0) {
// 省略
}
// 省略
}
int seed
を&seed
で渡している箇所が怪しそうです。単純にint
をlong
に書き換えるだけで、この問題を解消することができました。ruby sample/rcs.rb sample/rcs.dat
を実行すると、正常に動作している様子を確認することができます。
その他、サンプル側の修正
sample/freq.rb
については、sub
をsub!
に置き換えて、文字列の置き換えができなくなった場合にnil
を返すようにして、無限ループにならないように変更しました。
sample/less.rb
については、ZCAT
やLESS
に格納されている、実行ファイルのパスを、現在の環境に合わせて書き換えました。
sample/io.rb
については、File.s
をFile.stat
に置き換え、sample/trojan.rb
に関しては、File.d
をFile.directory?
に、File.f
をFile.file?
に置き換えました。
ここまでの修正で、sample/cbreak.rb
を除いた、すべてのサンプルプログラムが動作するようになります。
sample/cbreak.rb
はファイル先頭に、# ioctl example works on Sun
と記載があるので、特に修正してまで動かそうとしませんでした。
数値の取り扱い周りの修正
前回のpart1の記事で、bignum.c
のint2big
、numeric.c
のfix_mul
、ruby.h
のINT2FIX
とFIX2INT
に修正を加え、bignum
のテストをパスするように変更を加えました。
しかし、実際にプログラムを書いていると数値の取り扱い周りでバグが見つかりました。そのため、前回の修正を元に戻し、今回の記事では数値の取り扱い周りをできる限り修正することにしました。
Fixnum
とBignum
の取り扱いの方針
実際の修正に入る前に、part 1であまり理解せず変更を行った、INT2FIX
やFIX2INT
に関して調べることにしました。
ruby.texi
で関連しそうな内容を調べると、Ruby 0.95では、Integer
と呼ばれる整数クラスがあり、その数値の大きさによって、サブクラスのFixnum
とBignum
が用いられる旨の記載がされていました。また、Rubyのスクリプトの上においては、Fixnum
とBignum
の変換は相互に自動で実施されるようです。
Fixnum
やBignum
に関しても、それぞれ説明があり、Fixnum
はマシンのlong
の長さ-1bitの整数のクラスで、それを超えるとBignum
(無限多倍長整数)に拡張されます。また、Fixnum
は他のクラスと違いポインタに値が格納されるため、一旦参照を挟まないことが特徴だとされています。
ここまでの情報を踏まえて、FIX2INT
およびINT2FIX
の役割を予測すると、C言語中の整数型の値とFixnum
を相互に変換するものだと考えられます。また、Ruby 2.4の拡張ライブラリのC言語実装に関するドキュメントにも、そのような記載があったため、この予測はおそらく正しいものであると考えられます。(正確には、FixnumをC言語の整数に変換するために、32bit環境ではFIX2INT
、64bit環境ではFIX2LONG
の利用を促すような記載がありました)
Fixnum
やBignum
の説明を考慮すると、今回の64bit版の実装では、long
が64bitのため、Fixnum
は63bitで表現できる値をサポートするように実装していくと良いだろうと考えました。
INT2FIX
やFIX2INT
などのマクロや関連する実装について
ここまでで、前提となる背景や、今後の方針を把握することができたため、関連するマクロを理解していくことにしました。
INT2FIX
などは、ruby.h
に定義されています。関連しそうなマクロや関数も含めて以下に示します。
#define FIXNUM_MAX (LONG_MAX>>1)
#define FIXNUM_MIN RSHIFT((long)LONG_MIN,1)
#define FIXNUM_FLAG 0x01
#define INT2FIX(i) (VALUE)(((int)(i))<<1 | FIXNUM_FLAG)
VALUE int2inum();
#if (-1==(((-1)<<1)&FIXNUM_FLAG)>>1) // 0と評価された
# define RSHIFT(x,y) ((x)>>y)
#else
# define RSHIFT(x,y) (((x)<0) ? ~((~(x))>>y) : (x)>>y)
#endif
#define FIX2INT(x) RSHIFT((int)x,1)
#define FIX2UINT(f) ((UINT)(f)>>1)
#define FIXNUM_P(f) (((long)(f))&FIXNUM_FLAG)
#define POSFIXABLE(f) ((f) <= FIXNUM_MAX)
#define NEGFIXABLE(f) ((f) >= FIXNUM_MIN)
#define FIXABLE(f) (POSFIXABLE(f) && NEGFIXABLE(f))
#define NUM2INT(x) (FIXNUM_P(x)?FIX2INT(x):num2int(x))
VALUE num2fix();
int num2int();
RSHIFT
まず、各所で使われているRSHIFT
から見ていくことにします。RSHIFT
は#if (-1==(((-1)<<1)&FIXNUM_FLAG)>>1)
の評価の結果に応じて、定義が変化します。もし、評価の結果が0
でなければ、C言語の>>
演算子を用いた単純な右シフト、0
の場合は三項分岐を含む複雑な右シフトの操作として定義されます。
// FIXNUM_FLAGは0x01
#if (-1==(((-1)<<1)&FIXNUM_FLAG)>>1) // 0と評価された
# define RSHIFT(x,y) ((x)>>y)
#else
# define RSHIFT(x,y) (((x)<0) ? ~((~(x))>>y) : (x)>>y)
#endif
後者のRSHIFT
の定義は、もしx
が負の値であれば一度ビットを反転させて右シフトを行い、再度反転させるといった動きになります。おそらく、シフト演算子が算術シフトにならない環境向けのマクロだったと考えられます。
(0以上の数値の場合は通常の論理シフトで、負の値の時はいったん反転させてから論理シフトで左を0埋めしてもらい、再度反転させて1に戻し、結果的に算術シフト相当の操作にしたいといった意図を勝手に感じています。)
今利用している環境ではどちらの分岐になっても算術シフトの右シフトになるなどの理由があり、今回は条件分岐部分を削除しつつ、前者のマクロを採用するように変更しました。
FIXNUM_MAX
/FIXNUM_MIN
/FIXABLE
/POSFIXABLE
/NEGFIXABLE
次は、Fixnum
として扱うか否かの閾値を示していそうな部分を見ていきます。
FIXNUN_MAX
はlong
の最大値を示すLONG_MAX
を右シフトしたもの。FIXNUM_MIN
はlong
の最小値を示すLONG_MIN
を右シフトしたものです。今回は8バイトのlong
なので、それに沿った値となっており、おそらくFixnum
の最大値と最小値を示しています。
LONG_MAX
やLONG_MIN
は、limit.h
があって定義されていればそれを利用し、そうでなければruby.h
中で2147483647
などの数値をベースに定義されているものを利用するようになっています。
POSFIXABLE
は値がFixnum
の最大値以下かどうか、NEGFIXABLE
は値がFixnum
の最小値以下かどうか確かめています。FIXABLE
はそれらの&
をとっているので、値がFixnum
の範囲に収まっているか否かを確かめることができます。
INT2FIX
/FIX2INT
/FIXNUM_P
INT2FIX
は数値をint
にキャストした後、左に1bitシフトして、LSBに1を立てるといったものになっています。FIX2INT
はただ単純に右に1bit分シフトしています。
FIXNUM_P
は、LSBに1が立っているかを&で取り出してくるものです。Ruby 2.4の拡張ライブラリのドキュメントやソースコード中の使われ方などからも、値がFixnum
かどうかを判定するために使われていると判断することができます。
#define FIXNUM_FLAG 0x01
#define INT2FIX(i) (VALUE)(((int)(i))<<1 | FIXNUM_FLAG)
#define FIX2INT(x) RSHIFT((int)x,1)
#define FIXNUM_P(f) (((long)(f))&FIXNUM_FLAG)
NUN2INT
/num2int
/num2fix
/int2inum
NUM2INT
は、値がFixnum
かどうか判定して、Fixnum
であればFIX2INT
で数値を整数値を取得し、そうでなければnum2int
で整数値を取得しているマクロです。
num2int
やnum2fix
、int2inum
は、Fixnum
かどうか、Float
やBignum
かどうかを確認しつつ、変換をするメソッドとなっています。
#define NUM2INT(x) (FIXNUM_P(x)?FIX2INT(x):num2int(x))
VALUE int2inum();
VALUE num2fix();
int num2int();
ここからは、これまでの内容を踏まえた実装の変更に戻ります。
数値・文字列を変換する処理を修正
これまでの修正内容では、例えば以下のようなプログラムが正常に動作しません。
print 2147483648, "\n" # 0と出力
print 2147483647, "\n" # -1と出力
print -2147483646, "\n" # 2と出力
print -2147483647, "\n" # 1と出力
2147483648
などのリテラルを数値に変換する仕組みを把握していないため、その関数を探すところから始めました。順当に考えると、Rubyのスクリプトを解釈していそうなparse.y
に何らかの実装が存在していそうです。
眺めていると start_num:
というラベルを発見しました。この付近をさらに読んでいくとdecode_num:
というラベルを見つけることができます。その下に記載されているコードは以下のようになっており、整数型の値を返しそうな雰囲気を感じます。
decode_num:
// 省略
yylval.val = str2inum(tok(), 10);
return INTEGER;
実際に、gdb
でstr2inum
にブレークポイントを貼って先ほどのプログラムを実行してみると、その箇所で実行が停止され、str2inum
の引数には、"2147483648"
が指定されていることが分かりました。この箇所で、整数のリテラルの解釈を行い、Rubyの内部で扱い数値の構造体を作っていると見て問題なさそうです。
Breakpoint 1, str2inum (str=0x5555555c18f0 "2147483648", base=base@entry=10) at bignum.c:172
172 {
引き続きstr2inum
の動きを追っていくと、以下のような処理にたどり着きます。
int result = strtoul(str, 0, base);
if (!sign) result = -result;
if (FIXABLE(result)) return INT2FIX(result);
return int2big(result);
}
len = (len/(sizeof(USHORT)*CHAR_BIT))+1;
stdlib.h
で定義されたstrtoul
関数を呼び出しており、ここで初めて文字列が数値に変換されています。strtoul
の返り値の型はunsigned long
のため、ここでunsigned
か否かとint
とlong
で型の不一致が発生しています。ただ、missing/strtoul.c
の実装を見ると、unsigned long
が返ってくるようになっているため、unsigned
かどうかは無視してよさそうに思えました。そのため、この箇所はint
型の変数をlong
型の変数に変更しました。
また、INT2FIX
や後続の処理で使われるはずのFIX2INT
などは、以下のように一度int
型へキャストした後の操作となっているため、その箇所もlong
に変更しておきます。
#define INT2FIX(i) (VALUE)(((int)(i))<<1 | FIXNUM_FLAG)
#define FIX2INT(x) RSHIFT((int)x,1)
いったんここまでの修正で、先ほど提示したRubyのスクリプトを実行してみると、以下のような結果を得ることができました。一番上のパターンのみ失敗しています。
print 2147483648, "\n" # -2147483648と出力
print 2147483647, "\n" # 2147483647と出力
print -2147483646, "\n" # -2147483646と出力
print -2147483647, "\n" # -2147483647と出力
print 2147483648
のみのコードでも同じような結果を得られることを確認することができました。様々な原因が考えられますが、先ほどとは逆の方向から見てみようということで、今回はprint
メソッドから順番に見ていくことにしました。
print
メソッドはio.c
のf_print
関数で実装されており、その中ではRubyの標準出力と関連づく変数を引数に指定しつつio_print
関数を呼び出しています。io_print
の中では条件分岐の結果次第で呼び出される処理が変わりますが、今回の場合は、rb_funcall(argv[i], id_print_on, 1, out)
の処理を呼び出していました。
rb_funcall
については、README.EXT
に簡単な説明があり、Rubyのメソッドを呼び出している処理だとわかります。また、周辺には文字列からメソッドのidを得るためには、rb_intern
を用いるとの記載もあります。
rb_funcall(argv[i], id_print_on, 1, out)
はid_print_on
に関連づいたRubyのメソッドを呼び出していると推測されます。Init_IO
でid_print_on = rb_intern("print_on")
とされていることから、いずれかの箇所で実装されたprint_on
メソッドを呼び出していることがわかりました。print_on
はio.c
の中ではf_print_on
関数として実装されています。(他にarray.c
のprint_on
もありますが、こちらは無視しました。)
さらに実装を追っていくと、f_print_on
関数はio_write
関数を呼び出していることがわかり、その中では以下のような実装を確認することができます。
if (TYPE(str) != T_STRING)
str = (struct RString*)obj_as_string(str);
if (str->len == 0) return INT2FIX(0);
n = fwrite(str->ptr, sizeof(char), str->len, f);
fwrite
時点のstr->ptr
を見ると、すでに"-2147483648"
になっていることを確認することができました。その手前の箇所でobj_as_string
を呼び出しており、この実装を見ると以下のような形で、pt_str
に紐づくメソッドを呼び出しているようです。pt_str
はInit_String
関数内でrb_intern("to_s")
の結果が格納されています。
VALUE
obj_as_string(obj)
VALUE obj;
{
VALUE str;
if (TYPE(obj) == T_STRING) {
return obj;
}
str = rb_funcall(obj, pr_str, 0);
if (TYPE(str) != T_STRING)
return krn_to_s(obj);
return str;
}
おそらく、rb_funcall
のobj (Fixnum)
のto_s
を実行してくれそうな雰囲気だったため、numeric.c
のrb_define_method
でFixnum
のto_s
として定義されている、fix_to_s
関数の実装を見に行きました。fix_to_s
はfix2str
関数を呼び出しており、その中でsprintf
で数値を文字列に変換している処理を見つけることができました。以下にその実装を示します。
VALUE
fix2str(x, base)
VALUE x;
int base;
{
char fmt[3], buf[12];
fmt[0] = '%', fmt[2] = '\0';
if (base == 10) fmt[1] = 'd';
else if (base == 16) fmt[1] = 'x';
else if (base == 8) fmt[1] = 'o';
else Fail("fixnum cannot treat base %d", base);
sprintf(buf, fmt, FIX2INT(x));
return str_new2(buf);
}
fmt
にフォーマットを指定する文字列が入り、その結果の文字列がbuf
に格納されるようです。基数が10の場合、"%d\0"
の文字列がフォーマットとして指定されるようになります。%d
はint
の範囲向けのものとなるため、VALUE
がunsigned int
(4 byte)からunsigned long
(8 byte)になったことに合わせて"%ld\0"
とする必要があります。fmt[4]
に変更して、fmt[1]
に'l'
を格納するように変更して、その他の後ろに一つずつずらして格納するように変更を加えました。
また、8byteまでの数値を文字列に変換することを許容するため、buf
のサイズをbuf[32]
に変更しました。(24で足りそうですが、念のため切りのよい32にしています。)
ここまでの修正で、print 2147483648
が正常に動作するようになりました。
FIX2INT
/INT2FIX
を利用する箇所の修正
先ほどの手順の中で、FIX2INT
とINT2FIX
のキャストについて、int
からlong
に変更していました。
このFIX2INTの結果を受け取る側や、INT2FIXに値を渡す側についても変更の必要があるため、周辺を見つつ修正を加えることにしました。
#define INT2FIX(i) (VALUE)(((long)(i))<<1 | FIXNUM_FLAG)
#define FIX2INT(x) RSHIFT((long)x,1)
FIX2INT
に関連して変更した箇所を以下に箇条書きで示します。
-
bignum.c
のint2big
の引数をint
からlong
に変更。また、extern
をつけてint2big
の関数プロトタイプを宣言している箇所も併せて変更 -
numeric.c
-
flo_div
関数のint
型の変数f_y
をlong
型の変数に変更 -
Fixnum
同士の比較の関数-
fix_cmp
関数のint
型の変数a, b
をlong
型の変数に変更 -
fix_gt
関数のint
型の変数a, b
をlong
型の変数に変更 -
fix_ge
関数のint
型の変数a, b
をlong
型の変数に変更 -
fix_lt
関数のint
型の変数a, b
をlong
型の変数に変更 -
fix_le
関数のint
型の変数a, b
をlong
型の変数に変更
-
-
fix_aref
関数のint
型の変数i
をlong
型の変数に変更 (※i
は位置指定のために使われるが、1<<i
がint
にならないように変更) -
fix_abs
関数のint
型の変数i
をlong
型に変更 -
fix_next
関数のint
型の変数i
をlong
型に変更
-
-
struct.c
のstruct_alloc
関数のint
型の変数n
をlong
型に変更 -
variable.c
のrb_class2name
関数のint
型の変数name
をlong
型に変更
INT2FIX
に関連して変更した箇所を以下に箇条書きで示します。
-
array.c
のary_hash
関数のint
型の変数h
をlong
型の変数に変更- 他の
hash
メソッドの結果を処理したものを格納するため
- 他の
-
hash.c
のhash_hash
関数のint
型の変数h
をlong
型の変数に変更 -
numeric.c
-
fix_plus
関数のint
型の変数a, b, c
をlong
型の変数に変更 -
fix_minus
関数のint
型の変数a, b, c
をlong
型の変数に変更 -
fix_mul
関数のint
型の変数a, b, c
をlong
型の変数に変更 -
fix_div
関数のint
型の変数i
をlong
型の変数に変更 -
fix_mod
関数のint
型の変数i
をlong
型の変数に変更 -
fix_pow
関数のint
型の変数a, b
をlong
型の変数に変更 -
flo_mul
関数のint
型へキャストしている箇所を、long
型へのキャストに変更 -
flo_hash
関数のint
型の変数h
をlong
型の変数に変更 -
flo_to_i
関数のint
型の変数val
をlong
型の変数に変更
-
-
string.c
のstr_hash_method
関数のint
型の変数key
をlong
型の変数に変更 -
struct.c
のstruct_hash
関数のint
型の変数h
をlong
型の変数に変更 -
time.c
のtime_hash
関数のint
型の変数h
をlong
型の変数に変更 -
marshal.c
のr_object
関数TYPE_FIXNUM
のラベル付近のint
型の変数i
をlong
型の変数に変更 -
socket.c
のsock_send
関数のint
型の変数n
をlong
型の変数に変更
その他の箇所に関しては、インデックスやカウント、ファイル記述子、文字(char
)を取り扱っているように見えたり、バグが発生しそうな場合でも正常に動作していたりしたため、置き換える必要がないと判断して置き換えませんでした。
fix_mul
の挙動を修正
オーバーフローの検出の仕方を変更して今までの変更でmake test
を実行すると、bignum
に関するテストで失敗してしまいます。前回、この問題に対して適当な変更を行ってテストが通るようにしましたが、懸念していた通り不適切な方法でした。
(具体的には、FIX2INT
でunsigned long
にキャストしたため、負の値に対する右シフト時に、MSBが0で埋まってしまうというものです)
以下のようなコードで、bignum
のテスト失敗の原因を再現することが可能でした。本来、計算結果が正の数となるべきですが、負の値となっています。
print 21 * 2432902008176640000, "\n" # -4249290049419214848 と出力される
数値同士の*
は、numeric.c
のfix_mul
関数か、bignum.c
のbig_mul
関数が対応していると考えられます。実際に両方の関数にブレークポイントを貼ってコードを実行するとfix_mul
のみが実行されていることがわかります。現時点でのfix_mul
の実装の一部を以下に示します。
long a = FIX2INT(x), b = FIX2INT(y);
long c = a * b;
VALUE r = INT2FIX(c);
if (FIX2INT(r) != c) {
r = big_mul(int2big(a), int2big(b));
}
return r;
まず、a * b
の結果がオーバーフローし、c
に格納されます。おそらくif (FIXNUM(r) != c)
でオーバーフローを検出して、Bignum
での計算に切り替えることが意図されているようですが、検出できずにそのままオーバーフローした値Fixnum
にしたものを返してしまっているようです。
具体的にbit単位で値を書いていくと、cが11000...00000
、rが10001...00001
になります。FIX2INT(r)
が11000...00000
で、cと同じ値になっています。仮にc
が101..
のような値であれば、rが01..
となり、FIX2INT(r)
が001...
で、オーバーフローを検出することが可能です。
この問題についてはRuby 0.95bのパッチで修正が提供されていました。しかし、私の環境では、これにより問題を解消することはできませんでした。c
がオーバーフローすることにより、c / a != b
が1
になると思われますが、実際にはそのようになりませんでした。
調査していくと、整数オーバーフロー時の挙動に関してはC言語の仕様として未定義な部分があり、また最適化も絡む問題のため、別の対策をする必要がありそうということがわかりました。対策を調べていくと、JPCERT/CCが公開しているセキュアなC言語の書き方の資料を見つけることができました。オーバーフローに関する言及があり、対策も一緒に記載がされているため、これをFixnum
向けに変更しつつ適用していくことにしました。
実際に行った変更を以下に示します。is_overflow
と呼ばれる変数を用意して、Fixnum
の最大値や最小値を超えそうな場合には、これを1
にするようにしています。1
になった場合は、Bignum
の乗算の処理に移るようになります。
この変更により、bignum
のテストが通るようになり、再度make test
が通るようになりました。他のfix_*
関数などにも、同様の変更を加えた方がよさそうですが、除算と剰余算はオーバーフローというよりも、ゼロ除算に気を使っているようで、似たような変更はfix_plus
とfix_minus
に対してのみ行う形となりました。
int is_overflow = 0;
long a, b, c;
VALUE r;
a = FIX2INT(x);
if (a == 0) { return x; }
b = FIX2INT(y);
c = a * b;
if (a > 0) {
if (b > 0) { // FIXNUM_MAXをbで割ったものよりaが大きい == a * bをするとFIXNUM_MAXを超える
is_overflow = a > (FIXNUM_MAX / b);
} else {
is_overflow = b < (FIXNUM_MIN / a);
}
} else {
if (b > 0) {
is_overflow = a < (FIXNUM_MIN / b);
} else {
is_overflow = (a != 0) && (b < (FIXNUM_MAX / a));
}
}
r = INT2FIX(c);
if (is_overflow) {
r = big_mul(int2big(a), int2big(b));
}
return r;
まとめ
今回のpart2の記事中の修正により、以下の2点を達成することができました。
- 拡張モジュールを正常に取り扱えるようにすること
- ほとんどのサンプルプログラムを正常に動作させること
また、前回の修正箇所を一部見直して修正を加え、再度make test
が通るようになることを確認しました。
しかし、以下の2点から、完全な64bit版Ruby 0.95にすることができたとは言い切れないと考えています。
-
lambda/proc
に関して、Rubyのスクリプト側の修正という逃げの一手を打った - すべての組み込みメソッドを利用してみた訳ではなく、まだ今回の修正に由来したバグが潜んでいる可能性がある
まだできることはありそうですが、今は別のことに力を割いた方がよさそうだったりしたため、一旦ここまでにし、気が向いた時に再度取り組んでみようと考えています。
記事の冒頭にも記載しておりますが、特に何もなければ今回の変更に関するパッチの公開しない予定です。何卒ご了承ください🙇♂️
(追記: なぜ公開しないかというとライセンス等に不安な点があるためです。今後その辺りに詳しくなり、明確に大丈夫だと分かった場合は公開します🙏 )
Discussion