RbBCCというツールをまたいじり始めたんですが。こいつは全面的にRubyの標準ライブラリfiddleを用いています。
今回、早速、fiddle周りについてRuby 2.7で動かなかった箇所を直しました。
しかし、これは多分潜在的には2.6でも「たまたま動いてた」に等しいんだろうなあ... ということがわかったので、その辺のノウハウを共有・シェアします。
(2020/09/14 コメントのご指摘に合わせて追記していますので、必ずお読みください)
現象
RbBCCが使うlibbccにはこんな定義の関数があります。fiddleのexternで表現します:
module Clib dlload "libbcc.so" extern 'void * bpf_module_create_c_from_string(char *, unsigned int, char **, int, long)' end
使う際、これに例えばこういう値を渡します。2.6では動いていたのですが、2.7ではほぼ毎回segfaultになりました。
Clib.bpf_module_create_c_from_string( "int foo (void) { ... }", 0, [].pack("p*"), 0, 0 )
そもそも pack って何?
正味な話、私もRbBCCに携わるまでは、base64な文字列を作るときぐらいしか Array#pack/String#unpack
を使ってきませんでした。しかしこれは(本来?)C言語レベルで使える構造体・配列などのデータ生成や、C言語のデータをRubyのオブジェクトに展開する際に利用できるメソッドです。
fiddleでもこのArray#packを用いて、文字列の配列が必要な引数にはRuby側で [...].pack("p*")
とパックして渡すべき、とされています(るりまより。以下参照)。
こういうコードを書くとわかりやすいかもしれません。これはCの世界で「ヒープにある文字列の配列」として解釈される雰囲気が出てるのがわかります。
# charポインタの配列表現を、ulongの配列表現に変換し、16進数に直すと、 # ヒープの範囲のアドレスの配列になっている。 ['Abc', 'Def', 'Ghi'].pack('p*').unpack('J*').map{|ulong| "0x%x" % ulong} #=> ["0x55908ab14058", "0x55908ab14030", "0x55908ab169c0"]
ところで空の配列のパック結果、 [].pack("p*")
は空文字 ""
になります。pack/unpackとしてはこういうものと定義すればいいのですが、fiddle(ffi)としては少し困ったことになるようです。
検証コード
検証環境は x86_64 Ubuntu 18.04 で、各種ツールやライブラリは標準的なパッケージマネージャ経由のもので、Rubyはrbenv経由の2.6.4(たまたま入ってた)です。
まず、共有ライブラリを作ります。Cでこういうコードを書きました。
#include <stdlib.h> struct foo_t { int bar; }; extern void * ffitest_call_foo(int x, char **data) { int i, j; struct foo_t *ret; if(!data) return NULL; for(i = 0; data[i] != NULL; i++) { char *row = data[i]; for(j = 0; row[j] != '\0'; j++) { if(row[j] == 'a') x++; } } ret = malloc(sizeof(struct foo_t)); ret->bar = x; return ret; }
共有ライブラリにします。デバッグに便利なオプションでコンパイルすること。
$ gcc -g -c -fPIC -Wall -g -O0 myffi.c $ gcc -g -shared -o libmyffi.so myffi.o
Rubyのコードです。
require 'fiddle/import' module DoFFI extend Fiddle::Importer dlload './libmyffi.so' extern 'void *ffitest_call_foo(int, char **)' end ret = DoFFI.ffitest_call_foo(0, ["aaa", "bab", "aaazzzaaa"].pack("p*")) p ret[0, 4].unpack("i!") ret2 = DoFFI.ffitest_call_foo(0, nil) p ret2.null? ? "<Null Pointer>" : ret3[0, 4].unpack("i!") ret3 = DoFFI.ffitest_call_foo(0, [].pack("p*")) p ret3[0, 4].unpack("i!")
libmyffi.so
と同じディレクトリで実行します。何回も回すとたまにret3の呼び出しでsegfaultします。
$ ruby runload.rb [10] "<Null Pointer>" runload.rb:6: [BUG] Segmentation fault at 0x0000000400000054 ruby 2.6.4p104 (2019-08-28 revision 67798) [x86_64-linux] -- Control frame information ----------------------------------------------- c:0004 p:---- s:0021 e:000020 CFUNC :call c:0003 p:0018 s:0015 e:000014 METHOD runload.rb:6 c:0002 p:0122 s:0009 E:000c98 EVAL runload.rb:14 [FINISH] c:0001 p:0000 s:0003 E:000f10 (none) [FINISH] -- Ruby level backtrace information ---------------------------------------- runload.rb:14:in `<main>' runload.rb:6:in `ffitest_call_foo' runload.rb:6:in `call' -- Machine register context ------------------------------------------------ RIP: 0x00007f133f730690 RBP: 0x00007fff3cf660b0 RSP: 0x00007fff3cf66080 RAX: 0x0000000400000054 RBX: 0x0000000000000010 RCX: 0x0000000000000000 RDX: 0x0000000000000000 RDI: 0x0000000000000000 RSI: 0x000055a566e23fe0 R8: 0x0000000000000000 R9: 0x0000000000000000 R10: 0x00007fff3cf66010 R11: 0x00007f133f73061a R12: 0x00007fff3cf66160 R13: 0x0000000000000000 R14: 0x0000000000000000 R15: 0x00007fff3cf660c0 EFL: 0x0000000000010202 -- C level backtrace information ------------------------------------------- /home/vagrant/.rbenv/versions/2.6.4/bin/ruby(rb_vm_bugreport+0x7d3) [0x55a564794b03] vm_dump.c:715 /home/vagrant/.rbenv/versions/2.6.4/bin/ruby(rb_bug_context+0xe4) [0x55a564787b64] error.c:609 /home/vagrant/.rbenv/versions/2.6.4/bin/ruby(sigsegv+0x42) [0x55a564657182] signal.c:998 /lib/x86_64-linux-gnu/libpthread.so.0(__restore_rt+0x0) [0x7f13434c58a0] ./libmyffi.so(ffitest_call_foo+0x76) [0x7f133f730690] myffi.c:16 ./libmyffi.so(ffitest_call_foo) (null):0 /usr/lib/x86_64-linux-gnu/libffi.so.6(ffi_call_unix64+0x4c) [0x7f133f937dae] /usr/lib/x86_64-linux-gnu/libffi.so.6(ffi_call+0x22f) [0x7f133f93771f] ...
値を覗く
gdbでどういう値になっちゃってるか見てみます。先ほどのCのコードで、 ffitest_call_foo()
に入った直後で止まればいいでしょう。
$ gdb -ex 'b myffi.c:10' -ex 'run ./runload.rb' /home/vagrant/.rbenv/versions/2.6.4/bin/ruby
1回目の値は、ちゃんと正しい文字列の配列、なおかつ配列の最後はNULLが詰められている、扱いやすいものが渡ります。
Breakpoint 1, ffitest_call_foo (x=0, data=0x555555f50940) at myffi.c:11 11 if(!data) (gdb) p data $1 = (char **) 0x555555f50940 (gdb) p data[0] $2 = 0x555555f61388 "aaa" (gdb) p data[1] $3 = 0x555555f61360 "bab" (gdb) p data[2] $4 = 0x555555f61338 "aaazzzaaa" (gdb) p data[3] $5 = 0x0
2回目の値はNULLポインタです、定義上、きちんと検査すべきなので、問題ないでしょう。
Breakpoint 1, ffitest_call_foo (x=0, data=0x0) at myffi.c:11 11 if(!data) (gdb) p data $6 = (char **) 0x0
3回目の値はちょっとおかしいですね...。
(gdb) p data[0] $7 = 0x7ffff6d30c00 <_IO_wide_data_0+288> "" (gdb) p data[1] $8 = 0x7ffff6d30ca0 <main_arena+96> "" (gdb) p data[2] $9 = 0x0
0x7ffff6d30c00
や 0x7ffff6d30ca0
はこの環境ではどうやら共有ライブラリの中で、そんなところのポインタを示しているということは、壊れたデータであると言えそうです。
(gdb) call (pid_t)getpid() $10 = 24548 (gdb) call system("cat /proc/24548/maps") 555555554000-5555558d1000 r-xp 00000000 08:01 183573 /home/vagrant/.rbenv/versions/2.6.4/bin/ruby 555555ad0000-555555ad6000 r--p 0037c000 08:01 183573 /home/vagrant/.rbenv/versions/2.6.4/bin/ruby 555555ad6000-555555ad7000 rw-p 00382000 08:01 183573 /home/vagrant/.rbenv/versions/2.6.4/bin/ruby 555555ad7000-555555f74000 rw-p 00000000 00:00 0 [heap] ... 7ffff483b000-7ffff6945000 rw-p 00000000 00:00 0 7ffff6945000-7ffff6b2c000 r-xp 00000000 08:01 29088 /lib/x86_64-linux-gnu/libc-2.27.so 7ffff6b2c000-7ffff6d2c000 ---p 001e7000 08:01 29088 /lib/x86_64-linux-gnu/libc-2.27.so 7ffff6d2c000-7ffff6d30000 r--p 001e7000 08:01 29088 /lib/x86_64-linux-gnu/libc-2.27.so 7ffff6d30000-7ffff6d32000 rw-p 001eb000 08:01 29088 /lib/x86_64-linux-gnu/libc-2.27.so <- ⭐️ここ? 7ffff6d32000-7ffff6d36000 rw-p 00000000 00:00 0 7ffff6d36000-7ffff6ed3000 r-xp 00000000 08:01 29092 /lib/x86_64-linux-gnu/libm-2.27.so 7ffff6ed3000-7ffff70d2000 ---p 0019d000 08:01 29092 /lib/x86_64-linux-gnu/libm-2.27.so 7ffff70d2000-7ffff70d3000 r--p 0019c000 08:01 29092 /lib/x86_64-linux-gnu/libm-2.27.so 7ffff70d3000-7ffff70d4000 rw-p 0019d000 08:01 29092 /lib/x86_64-linux-gnu/libm-2.27.so 7ffff70d4000-7ffff70dd000 r-xp 00000000 08:01 29090 /lib/x86_64-linux-gnu/libcrypt-2.27.so 7ffff70dd000-7ffff72dc000 ---p 00009000 08:01 29090 /lib/x86_64-linux-gnu/libcrypt-2.27.so 7ffff72dc000-7ffff72dd000 r--p 00008000 08:01 29090 /lib/x86_64-linux-gnu/libcrypt-2.27.so 7ffff72dd000-7ffff72de000 rw-p 00009000 08:01 29090 /lib/x86_64-linux-gnu/libcrypt-2.27.so ...
この起動時はたまたま「一応読める」範囲なので動作していますが、状況によりsegfaultするのも納得です。
ということで、ffi経由で char **
の値に空配列のpackの結果そのまま、空文字列を渡すと未定義っぽい動作になります。「空の配列」相当を渡したいなら nil
を用いましょう、という結論になりました。RbBCCで直した時のdiffはこんな感じ:
diff --git a/lib/rbbcc/bcc.rb b/lib/rbbcc/bcc.rb index b4e5a6e..9e963f0 100644 --- a/lib/rbbcc/bcc.rb +++ b/lib/rbbcc/bcc.rb @@ -219,10 +219,16 @@ def initialize(text: "", src_file: nil, hdr_file: nil, debug: 0, cflags: [], usd end # Util.debug text + cflags_p = if cflags.empty? + nil + else + cflags.pack('p*') + end + @module = Clib.bpf_module_create_c_from_string( text, debug, - cflags.pack('p*'), + cflags_p, cflags.size, allow_rlimit )
これはライブラリの利用側で注意すべき問題のように思います。
2020/09/14 追記
コメントいただいている通り、(Cの配列では当然そうするように) cflags
の最後尾を nil
で終端するのが適切です。
具体的に rbbcc 0.6.1 で修正しましたので、パッチは下記のP/Rをご参照ください。
nil(NULL)
をC側でいう char **
な引数として渡すのは、たまたま動く場合もあるかもしれませんが、今回使う bpf_module_create_c_from_string()
関数は使う際のマニュアルなどが公開されておらず、空の配列を渡す方が安全だと思います。