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.somarshal.sosocket.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から出力された内容の読み込みや、書き込み内容を組み立てる処理、メインループなどが存在しています。
エラーの調査とRubyで実装されたコードの修正(proc/lambdaに関する妥協)
何となく関連情報を把握できたところで、エラーの詳細な調査に入ります。
まずは、先ほども提示した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