Ruby 使用 MinGW + clang 编译 CRuby 时遇到的问题及其解决

kirh_036 · 2022年06月09日 · 227 次阅读

注:本文提到的 ruby 均为 ruby 3.1.2 版本,源代码的行数以该版本为准。

最近,当我试图用 msys2 的 clang64 工具链编译 ruby 时,出现了 Segmentation fault:

发生了什么事?我试着用 ucrt64 工具链代替。结果,编译过程很顺利。下面是 make check 的执行结果。

Finished tests in 195.106628s, 1.5581 tests/s, 8.6619 assertions/s.
304 tests, 1690 assertions, 17 failures, 0 errors, 11 skips

ruby -v: ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) [x64-mingw-ucrt]

接下来,我尝试在 ucrt64 环境下使用 clang 而不是 gcc 作为编译器。结果,像 clang64 工具链一样,出现了一个 Segmentation fault。这表明,用 clang 编译 ruby 可能会引起一些问题。我试图在 GitHub 上搜索相关问题,但一无所获(不排除我信息查找能力差)。

我不服气,我开始寻找问题的根源。通过 make -n ,我找到了发生崩溃的命令行:

./miniruby.exe -I/k/Download/ruby-3.1.2/lib -I. -I.ext/common  /k/Download/ruby-3.1.2/ext/extmk.rb --make='make' \
        --command-output=ext/-test-/exts.mk --dest-dir="" --extout=".ext" --ext-build-dir="./ext" --mflags="" --make-flags="" --gnumake=yes --extflags="" --make-flags="MINIRUBY='./miniruby.exe -I/k/Download/ruby-3.1.2/lib -I. -I.ext/common '" --extstatic  \
        -- configure ext/-test-
make[1]: *** [ext/configure-ext.mk:20: ext/-test-/exts.mk] Segmentation fault

似乎在 miniruby 执行的时候发生了什么。那么传统艺能,我把优化参数改为 -Og (当然,并没有解决崩溃问题),并分别用 lldb 和 gdb 进行调试。

lldb:

(lldb) r
Process 6480 launched: 'K:\Download\ruby-3.1\miniruby.exe' (x86_64)
Process 6480 stopped
* thread #1, stop reason = Exception 0xc0000005 encountered at address 0x7ff6c3b3fd93: Access violation reading location 0x00000017
    frame #0: 0x00007ff6c3b3fd93 miniruby.exe`rb_vm_exec [inlined] rb_ec_tag_state(ec=0xffffffffffffffff) at eval_intern.h:146:33
   143  static inline int
   144  rb_ec_tag_state(const rb_execution_context_t *ec)
   145  {
-> 146      struct rb_vm_tag *tag = ec->tag;
                                        ^
   147      enum ruby_tag_type state = tag->state;
   148      tag->state = TAG_NONE;
   149      rb_ec_vm_lock_rec_check(ec, tag->lock_rec);
(lldb) bt
* thread #1, stop reason = Exception 0xc0000005 encountered at address 0x7ff6c3b3fd93: Access violation reading location 0x00000017
  * frame #0: 0x00007ff6c3b3fd93 miniruby.exe`rb_vm_exec [inlined] rb_ec_tag_state(ec=0xffffffffffffffff) at eval_intern.h:146:33
    frame #1: 0x00007ff6c3b3fd93 miniruby.exe`rb_vm_exec(ec=0x0000000012070015, mjit_enable_p=false) at vm.c:2209:18

gdb:

(gdb) r
Starting program: /k/Download/ruby-3.1/miniruby.exe -I/k/Download/ruby-3.1.2/lib -I. -I.ext/common /k/Download/ruby-3.1.2/ext/extmk.rb --make=make --command-output=ext/-test-/exts.mk --dest-dir= --extout=.ext --ext-build-dir=./ext --mflags= --make-flags= --gnumake=yes --extflags= --make-flags=MINIRUBY=\'./miniruby.exe\ -I/k/Download/ruby-3.1.2/lib\ -I.\ -I.ext/common\ \' --extstatic -- configure ext/-test-
[New Thread 11476.0x63c]
[New Thread 11476.0xb7c]
[New Thread 11476.0x9c8]
[New Thread 11476.0x698]

Thread 1 received signal SIGSEGV, Segmentation fault.
ruby_options (argc=32758, argv=0x17) at K:/Download/ruby-3.1.2/eval.c:117
117     K:/Download/ruby-3.1.2/eval.c: No such file or directory.
(gdb) bt
#0  ruby_options (argc=32758, argv=0x17) at K:/Download/ruby-3.1.2/eval.c:117
#1  0x0000000000000000 in ?? ()
Backtrace stopped: previous frame inner to this frame (corrupt stack?)

其中,gdb 指向的 eval.c:117 的内容是:

if ((state = EC_EXEC_TAG()) == TAG_NONE) {

其中 EC_EXEC_TAG 宏的定义在 eval_intern.h:165

#define EC_EXEC_TAG() \
    (ruby_setjmp(_tag.buf) ? rb_ec_tag_state(VAR_FROM_MEMORY(_ec)) : (EC_REPUSH_TAG(), 0))

不难发现,两个调试器的矛头都指向 rb_ec_tag_state 这个函数。根据 lldb 的显示结果,其崩溃的原因似乎是 ec 是一个无效的指针(甚至不知道为什么是 0xffffffffffffffff),在获取结构成员时由于访问无效内存而崩溃。但是为什么 ec 是一个无效的指针?为什么 gcc 的编译没有这个问题?事情开始玄学了起来。

但问题最终还是解决了。经过全局搜索,我发现调用 rb_ec_tag_state 只存在于 EC_EXEC_TAG 宏中。有没有一种可能,是调用 rb_ec_tag_state 前的判断语句————具体的说是 ruby_setjmp 的返回值,出问题了呢?

我开始追踪 ruby_setjmp 函数,首先在 eval_intern.h:58 处定位到:

#define ruby_setjmp(env) RUBY_SETJMP(env)

然后在 include/x64-mingw-ucrt/ruby/config.h (编译前生成的一个头文件)中定位到:

#define RUBY_SETJMP(env) __builtin_setjmp((void **)(env))

值得注意的是 __builtin_ 前缀。百度结果显示,带有这个前缀的函数是 gcc 的内置函数(clang 也支持),是相应标准库函数的优化版本。我怀疑这个函数的行为 clang 与 gcc 的不同,这导致了我在本文开头遇到的问题。

解决方案:果然用标准库函数才是最稳的。我把 include/x64-mingw-ucrt/ruby/config.h 中的:

#define RUBY_SETJMP(env) __builtin_setjmp((void **)(env))
#define RUBY_LONGJMP(env,val) __builtin_longjmp((void **)(env),val)

替换成:

#define RUBY_SETJMP(env) setjmp(env)
#define RUBY_LONGJMP(env,val) longjmp(env,val)
#define RUBY_JMP_BUF jmp_buf

并使用 clang64 工具链进行编译。结果,编译过程很顺利。以下是 make check 的执行结果:

Finished tests in 194.228169s, 1.5652 tests/s, 8.7011 assertions/s.
304 tests, 1690 assertions, 17 failures, 0 errors, 11 skips

ruby -v: ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) [x64-mingw-ucrt]

顺便一提,在 ucrt64 环境下,clang 编译也是成功的,但 make check 的结果有微妙的不同。

Finished tests in 194.135740s, 1.5659 tests/s, 8.7001 assertions/s.
304 tests, 1689 assertions, 18 failures, 0 errors, 11 skips

ruby -v: ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) [x64-mingw-ucrt]

最后,问题虽然解决,但实际上我并不知其所以然。因为我今后多半不会用到 setjmp 函数,而且最近忙于毕业找工作,就没有探究导致这个问题的根本原因。希望有能大佬能找出答案。

暂无回复。
需要 登录 后方可回复, 如果你还没有账号请 注册新账号