mirror of
https://github.com/ruby/ruby.git
synced 2026-11-09 00:42:18 +00:00
182 lines
8.6 KiB
Markdown
182 lines
8.6 KiB
Markdown
# Debugging Ruby
|
|
|
|
You can use either lldb or gdb for debugging. Before debugging, you need to
|
|
create a `test.rb` with the Ruby script you'd like to run. You can use the
|
|
following make targets:
|
|
|
|
* `make run`: Runs `test.rb` using Miniruby
|
|
* `make lldb`: Runs `test.rb` using Miniruby in lldb
|
|
* `make gdb`: Runs `test.rb` using Miniruby in gdb
|
|
* `make runruby`: Runs `test.rb` using Ruby
|
|
* `make lldb-ruby`: Runs `test.rb` using Ruby in lldb
|
|
* `make gdb-ruby`: Runs `test.rb` using Ruby in gdb
|
|
|
|
For VS Code users, you can set up editor-based debugging experience by running:
|
|
|
|
```shell
|
|
cp -r misc/.vscode .vscode
|
|
```
|
|
|
|
This will add launch configurations for debugging Ruby itself by running `test.rb` with `lldb`.
|
|
|
|
**Note**: if you build Ruby under the `./build` folder, you'll need to update `.vscode/launch.json`'s program entry accordingly to: `"${workspaceFolder}/build/ruby"`
|
|
|
|
## Compiling for Debugging
|
|
|
|
You can compile Ruby with the `RUBY_DEBUG` macro to enable debugging on some
|
|
features. One example is debugging object shapes in Ruby with
|
|
`RubyVM::Shape.of(object)`.
|
|
|
|
Additionally Ruby can be compiled to support the `RUBY_DEBUG` environment
|
|
variable to enable debugging on some features. An example is using
|
|
`RUBY_DEBUG=gc_stress` to debug GC-related issues.
|
|
|
|
There is also support for the `RUBY_DEBUG_LOG` environment variable to log a
|
|
lot of information about what the VM is doing, via the `USE_RUBY_DEBUG_LOG`
|
|
macro.
|
|
|
|
You should also configure Ruby without optimization and other flags that may
|
|
interfere with debugging by changing the optimization flags.
|
|
|
|
Bringing it all together:
|
|
|
|
```sh
|
|
./configure cppflags="-DRUBY_DEBUG=1 -DUSE_RUBY_DEBUG_LOG=1" --enable-debug-env optflags="-O0 -fno-omit-frame-pointer"
|
|
```
|
|
|
|
## Building with Address Sanitizer
|
|
|
|
Using the address sanitizer (ASAN) is a great way to detect memory issues. It
|
|
can detect memory safety issues in Ruby itself, and also in any C extensions
|
|
compiled with and loaded into a Ruby compiled with ASAN.
|
|
|
|
```sh
|
|
./autogen.sh
|
|
mkdir build && cd build
|
|
../configure CC=clang-18 cflags="-fsanitize=address -fno-omit-frame-pointer -DUSE_MN_THREADS=0" # and any other options you might like
|
|
make
|
|
```
|
|
|
|
The compiled Ruby will now automatically crash with a report and a backtrace
|
|
if ASAN detects a memory safety issue. To run Ruby's test suite under ASAN,
|
|
issue the following command. Note that this will take quite a long time (over
|
|
two hours on my laptop); the `RUBY_TEST_TIMEOUT_SCALE` and
|
|
`SYNTAX_SUGGEST_TIMEOUT` variables are required to make sure tests don't
|
|
spuriously fail with timeouts when in fact they're just slow.
|
|
|
|
```sh
|
|
RUBY_TEST_TIMEOUT_SCALE=5 SYNTAX_SUGGEST_TIMEOUT=600 make check
|
|
```
|
|
|
|
Please note, however, the following caveats!
|
|
|
|
* Due to [Bug #20243], Clang generates code for threadlocal variables which
|
|
doesn't work with M:N threading. Thus, it's necessary to disable M:N
|
|
threading support at build time for now (with the `-DUSE_MN_THREADS=0`
|
|
configure argument).
|
|
* ASAN will only work when using Clang version 18 or later - it requires
|
|
[llvm/llvm-project#75290] related to multithreaded `fork`.
|
|
* ASAN has only been tested so far with Clang on Linux. It may or may not work
|
|
with other compilers or on other platforms - please file an issue on
|
|
[Ruby Issue Tracking System] if you run into problems with such configurations
|
|
(or, to report that they actually work properly!)
|
|
* In particular, although I have not yet tried it, I have reason to believe
|
|
ASAN will _not_ work properly on macOS yet - the fix for the multithreaded
|
|
fork issue was actually reverted for macOS (see [llvm/llvm-project#75659]).
|
|
Please open an issue on [Ruby Issue Tracking System] if this is a problem for
|
|
you.
|
|
|
|
[Revision 9d0a5148]: https://bugs.ruby-lang.org/projects/ruby-master/repository/git/revisions/9d0a5148ae062a0481a4a18fbeb9cfd01dc10428
|
|
[Bug #20243]: https://bugs.ruby-lang.org/issues/20243
|
|
[llvm/llvm-project#75290]: https://github.com/llvm/llvm-project/pull/75290
|
|
[llvm/llvm-project#75659]: https://github.com/llvm/llvm-project/pull/75659#issuecomment-1861584777
|
|
[Ruby Issue Tracking System]: https://bugs.ruby-lang.org
|
|
|
|
## Crash dumps
|
|
|
|
A `[BUG]` report shows the backtrace of the crashing thread, but not the other threads or the memory, and some crashes leave no report at all, such as one in a thread other than the main one on Windows. A crash dump keeps that state for a debugger.
|
|
|
|
### Getting a dump
|
|
|
|
#### Locally
|
|
|
|
On Linux, the `abort()` after a `[BUG]` report writes a core file when `ulimit -c` allows. `/proc/sys/kernel/core_pattern` decides where it goes, and `coredumpctl` retrieves it if the pattern pipes to systemd-coredump. To write it into the current directory, as the CI does:
|
|
|
|
```sh
|
|
ulimit -c unlimited
|
|
echo "$PWD/core.%p" | sudo tee /proc/sys/kernel/core_pattern
|
|
```
|
|
|
|
On macOS, the kernel writes no core for the ad-hoc signed `ruby`. Instead, lldb can attach and save one through `RUBY_ON_BUG`, a command that Ruby runs with its pid appended when it reaches `[BUG]` (`make runruby` uses it for gdb):
|
|
|
|
```sh
|
|
RUBY_ON_BUG='lldb --batch -o "process save-core -s modified-memory /tmp/ruby.core" -p' ./ruby test.rb
|
|
```
|
|
|
|
On Windows, `RUBY_DEBUG=wer` leaves the crashes that bypass the `[BUG]` report to Windows Error Reporting, which writes a dump once `LocalDumps` is set in the registry as an administrator. `RUBY_DEBUG` needs `--enable-debug-env` at configure:
|
|
|
|
```
|
|
reg add "HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps" /v DumpFolder /t REG_EXPAND_SZ /d C:\dumps /f
|
|
set RUBY_DEBUG=wer
|
|
```
|
|
|
|
#### From CI
|
|
|
|
When a Ruby process crashes in the tests on GitHub Actions, these workflows upload an artifact with the dumps and what is needed to read them:
|
|
|
|
* Windows (`windows.yml`), as `crashdumps-OS-TASK`, or `crashdumps-OS-x86-TASK` for the x86 job
|
|
* Ubuntu (`ubuntu.yml`, except the ppc64le and s390x jobs), YJIT Ubuntu (`yjit-ubuntu.yml`) and ZJIT Ubuntu (`zjit-ubuntu.yml`), as `crashdumps-JOB-INDEX`
|
|
* macOS (`macos.yml`), YJIT macOS (`yjit-macos.yml`) and ZJIT macOS (`zjit-macos.yml`), as `crashdumps-JOB-INDEX`
|
|
|
|
`JOB-INDEX` is the job name and its position in the matrix, such as `crashdumps-make-3`. The other workflows collect no dumps. The artifact is listed under "Artifacts" on the run's summary page for 7 days, and each dump is named after the pid in the `pid N killed by SIG...` line of the test output.
|
|
|
|
```sh
|
|
gh run download RUN_ID -R ruby/ruby -n ARTIFACT_NAME -D artifact
|
|
cd artifact
|
|
```
|
|
|
|
### Reading a dump
|
|
|
|
Below, `~/src/ruby` is a checkout of the commit that crashed.
|
|
|
|
#### Linux and macOS
|
|
|
|
A local dump opens with the binary that wrote it, as in `gdb ./ruby core.PID` or `lldb -c /tmp/ruby.core ./ruby`.
|
|
|
|
For a Linux dump from CI, `file core.PID` tells which executable crashed, and every file the process mapped is under `sysroot/` at its original path. gdb finds the libraries there by their sonames:
|
|
|
|
```sh
|
|
gdb -q -ex 'set sysroot sysroot' -ex "set solib-search-path $(find sysroot/usr -name libc.so.6 -printf '%h')" -ex 'set substitute-path /home/runner/work/ruby/ruby/src ~/src/ruby' -ex 'file sysroot/home/runner/work/ruby/ruby/build/ruby' -ex 'core-file core.PID'
|
|
```
|
|
|
|
lldb on macOS reads these cores too, with `target.exec-search-paths` set before `target create` (`aarch64-linux-gnu` for an arm64 job):
|
|
|
|
```sh
|
|
lldb -o 'settings set target.exec-search-paths sysroot/usr/lib/x86_64-linux-gnu' -o 'platform select remote-linux --sysroot sysroot' -o 'target create --core core.PID sysroot/home/runner/work/ruby/ruby/build/ruby'
|
|
```
|
|
|
|
A macOS dump from CI comes with `core.PID.lldb`, which loads the binaries and dSYMs under `sysroot/` first. If a dump is missing, see `lldb.PID.log`.
|
|
|
|
```sh
|
|
lldb -s core.PID.lldb -o 'settings set target.source-map /Users/runner/work/ruby/ruby/src ~/src/ruby' -o 'bt all'
|
|
```
|
|
|
|
The backtrace starts in the signal handler. The faulting instruction is found from the `sigsegv` frame, with `__rip` for `__pc` on x86_64:
|
|
|
|
```
|
|
image lookup -a `((ucontext_t *)ctx)->uc_mcontext->__ss.__pc`
|
|
```
|
|
|
|
#### Windows
|
|
|
|
In an artifact from CI, the dumps `ruby.exe.PID.dmp` are under `crashdumps/`, the binaries and PDBs under `build/` (those of extension libraries under `build/ext/NAME/`), and the vcpkg libraries under `src/vcpkg_installed/TRIPLET/bin/`. WinDbg reads a dump with these directories in the symbol path.
|
|
|
|
Elsewhere, lldb prints the frames as module offsets, which `llvm-symbolizer` resolves with the PDB:
|
|
|
|
```sh
|
|
lldb --batch -c crashdumps/ruby.exe.PID.dmp -o 'image list' -o 'bt all'
|
|
llvm-symbolizer --obj=build/arm64-vcruntime140-ruby410.dll --relative-address --inlining 0x15d7c
|
|
```
|
|
|
|
The offset is the frame address minus the module base in `image list`. `llvm-symbolizer` looks for a PDB only next to the binary, so link those of extension and vcpkg libraries there first.
|