だいたい下記の記事と同じなんだけれど、少しでも新しい何かがあれば。2020年現在、Ruby 2.7.1 を対象に。
デバッグしやすい Ruby をインストールする
rbenvで入れ直す場合
RUBY_CONFIGURE_OPTSで最適化を切るオプションを渡す- 国分さんの記事の通り
-gなどは渡す必要がないそう、デフォルトが-ggdb3なのでそれでOK
- 国分さんの記事の通り
-kでソースコードを残す
が留意点。こういう感じで。
$ RUBY_CONFIGURE_OPTS='optflags=-O0' rbenv install 2.7.1 -k
関数を探す
ソースコードを頑張って追いかける。あるいはとりあえずそれらしいものを readelf -s などで探しても良いかと思う。
$ readelf -s /home/vagrant/.rbenv/versions/2.7.1/bin/ruby
Symbol table '.dynsym' contains 24 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND ruby_run_node
2: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND ruby_init
4: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2)
5: 0000000000000000 0 FUNC GLOBAL DEFAULT UND ruby_options
6: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab
...
rb_ で始まる、Rubyのそれぞれのメソッドの実装部分は libruby.so 側にある。libruby.soを分けないオプションもあるのかもしれない(あれば教えてください)。
$ ldd /home/vagrant/.rbenv/versions/2.7.1/bin/ruby
linux-vdso.so.1 (0x00007ffd734e1000)
libruby.so.2.7 => /home/vagrant/.rbenv/versions/2.7.1/lib/libruby.so.2.7 (0x00007fc61f23f000)
$ $ readelf -s /home/vagrant/.rbenv/versions/2.7.1/lib/libruby.so.2.7 | grep rb_ | head -20
308: 000000000027f2a4 100 FUNC GLOBAL DEFAULT 12 rb_const_list
309: 000000000009c2ff 80 FUNC GLOBAL DEFAULT 12 rb_dir_getwd
310: 00000000002b0f93 50 FUNC GLOBAL DEFAULT 12 rb_frame_method_id_and_cl
311: 00000000000bfd58 217 FUNC GLOBAL DEFAULT 12 rb_loaderror_with_path
313: 000000000017a79e 190 FUNC GLOBAL DEFAULT 12 rb_inspect
314: 00000000000a24c3 52 FUNC GLOBAL DEFAULT 12 rb_enc_find
315: 0000000000612648 8 OBJECT GLOBAL DEFAULT 26 rb_cRational
316: 00000000002aa9fe 226 FUNC GLOBAL DEFAULT 12 rb_throw_obj
317: 00000000002b7647 309 FUNC GLOBAL DEFAULT 12 rb_profile_frame_classpat
319: 000000000027a090 18 FUNC GLOBAL DEFAULT 12 rb_gvar_val_getter
320: 000000000005f253 51 FUNC GLOBAL DEFAULT 12 rb_class_protected_instan
321: 00000000002a7d3b 370 FUNC GLOBAL DEFAULT 12 rb_apply
322: 00000000002703a0 136 FUNC GLOBAL DEFAULT 12 rb_str_encode
323: 00000000002548ae 28 FUNC GLOBAL DEFAULT 12 rb_thread_alone
324: 0000000000225eb1 53 FUNC GLOBAL DEFAULT 12 rb_str_conv_enc
325: 00000000002a0b50 374 FUNC GLOBAL DEFAULT 12 rb_clear_method_cache_by_
326: 00000000001c3f75 85 FUNC GLOBAL DEFAULT 12 rb_range_new
327: 000000000009934b 15 FUNC GLOBAL DEFAULT 12 rb_fiber_current
328: 0000000000612368 8 OBJECT GLOBAL DEFAULT 26 rb_cArray
329: 00000000006125d8 8 OBJECT GLOBAL DEFAULT 26 rb_cClass
やってみよう
$ gdb --args /home/vagrant/.rbenv/versions/2.7.1/bin/ruby -e 'p Object.new' GNU gdb (Ubuntu 8.1-0ubuntu3.2) 8.1.0.20180409-git Copyright (C) 2018 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
rb_inspect をつかまえたいんだけれど、この関数は読み込み前の libruby.so.2.7 の方にあるのでpendingがどうこう言われる。「y」を押す。
(gdb) b rb_inspect Function "rb_inspect" not defined. Make breakpoint pending on future shared library load? (y or [n]) y Breakpoint 1 (rb_inspect) pending.
run で動かすとちゃんと止まる。
(gdb) run Starting program: /home/vagrant/.rbenv/versions/2.7.1/bin/ruby -e p\ Object.new [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". Breakpoint 1, rb_inspect (obj=93824996526120) at object.c:680 680 VALUE str = rb_obj_as_string(rb_funcallv(obj, id_inspect, 0, 0));
list backtrace next などガチャガチャやって、飽きたら continue で再開。
(gdb) list
675 * the result must be ASCII only.
676 */
677 VALUE
678 rb_inspect(VALUE obj)
679 {
680 VALUE str = rb_obj_as_string(rb_funcallv(obj, id_inspect, 0, 0));
681
682 rb_encoding *enc = rb_default_internal_encoding();
683 if (enc == NULL) enc = rb_default_external_encoding();
684 if (!rb_enc_asciicompat(enc)) {
(gdb) bt
#0 rb_inspect (obj=93824996526120) at object.c:680
#1 0x00007ffff78c747e in rb_p (obj=93824996526120) at io.c:7801
#2 0x00007ffff78c75d7 in rb_f_p_internal (arg=140737488343280) at io.c:7827
#3 0x00007ffff7888555 in rb_ensure (b_proc=0x7ffff78c757e <rb_f_p_internal>, data1=140737488343280,
e_proc=0x7ffff77f7e6c <rb_ary_pop>, data2=93824994527560) at eval.c:1129
#4 0x00007ffff7a1acc8 in rb_uninterruptible (b_proc=0x7ffff78c757e <rb_f_p_internal>,
data=140737488343280) at thread.c:5563
...
(gdb) c Continuing. #<Object:0x000055555596c828> [Inferior 1 (process 26355) exited normally]
初期化処理を追いかける、みたいな場合は ruby_init() とか ruby_setup() などで止めるとそれっぽい気がする(いい関数があれば教えてください)。
Breakpoint 1, ruby_setup () at eval.c:57
57 {
(gdb) l
52 * @retval 0 if succeeded.
53 * @retval non-zero an error occurred.
54 */
55 int
56 ruby_setup(void)
57 {
58 enum ruby_tag_type state;
59
60 if (GET_VM())
61 return 0;
(gdb) n
60 if (GET_VM())
(gdb) n
63 ruby_init_stack((void *)&state);
(gdb) n
70 prctl(PR_SET_THP_DISABLE, 1, 0, 0, 0);
(gdb) n
72 Init_BareVM();
(gdb) n
73 Init_heap();
(gdb) n
74 rb_vm_encoded_insn_data_table_init();
Ruby の .gdbinit を使うには
色々まとまっているが、やっぱりこれもちょっと古く...(いま、Rubyのスレッドって一つだし)。Rubyレベル、Cレベルでのバックトレースの出し方や、 rp は役にたつと思う。
gdbinitは、 $RBENV_ROOT/sources/2.7.1/ruby-2.7.1/.gdbinit のようなところにある。読んでみると色々な関数が定義されていることがわかる。
ほか、何かあれば...という記事。