注:本文提到的 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
函数,而且最近忙于毕业找工作,就没有探究导致这个问题的根本原因。希望有能大佬能找出答案。