ローファイ日記

出てくるコード片、ぼくが書いたものは断りがない場合 MIT License としています http://udzura.mit-license.org/

fiddle(ffi)で char ** な定義の引数に値を渡す際の留意点

RbBCCというツールをまたいじり始めたんですが。こいつは全面的にRubyの標準ライブラリfiddleを用いています。

github.com

今回、早速、fiddle周りについてRuby 2.7で動かなかった箇所を直しました。

github.com

しかし、これは多分潜在的には2.6でも「たまたま動いてた」に等しいんだろうなあ... ということがわかったので、その辺のノウハウを共有・シェアします。

現象

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*") とパックして渡すべき、とされています(るりまより。以下参照)。

docs.ruby-lang.org

こういうコードを書くとわかりやすいかもしれません。これは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

0x7ffff6d30c000x7ffff6d30ca0 はこの環境ではどうやら共有ライブラリの中で、そんなところのポインタを示しているということは、壊れたデータであると言えそうです。

(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
         )

これはライブラリの利用側で注意すべき問題のように思います。