Overview
Crash dump analysis examines the state of a program at the moment of failure by inspecting memory snapshots, stack traces, and system state information. When a program terminates unexpectedly, the operating system can capture a snapshot of the process memory, register states, and execution context. This snapshot, called a crash dump or core dump, preserves the exact state that led to the failure.
The practice originated in mainframe computing where operators would print memory contents to paper for analysis. Modern crash dumps contain thread stacks, heap allocations, loaded libraries, register values, and file descriptors. Analysis reconstructs the sequence of events leading to failure without requiring the ability to reproduce the crash.
Crash dumps serve multiple purposes in software development. They enable debugging of issues that occur in production environments where debuggers cannot run. They capture failures that occur intermittently or under specific conditions that are difficult to reproduce. They provide forensic evidence for security incidents and help identify memory corruption, race conditions, and resource exhaustion problems.
The analysis process differs significantly from interactive debugging. Interactive debuggers allow stepping through code execution and examining state dynamically. Crash dump analysis works with a static snapshot, requiring different techniques to understand program flow and identify root causes. Analysts must infer execution history from memory contents and stack traces without the ability to execute code or evaluate expressions in the original context.
# Basic crash dump generation in Ruby
Signal.trap("SEGV") do
File.write("/tmp/crash_info.txt", "Segfault occurred at #{Time.now}")
Thread.list.each do |thread|
File.open("/tmp/crash_info.txt", "a") do |f|
f.puts "\nThread: #{thread.inspect}"
f.puts thread.backtrace.join("\n") if thread.backtrace
end
end
exit(1)
end
Key Principles
Crash dumps contain multiple types of information critical for failure analysis. The stack trace shows the call chain at the time of crash, revealing which functions were executing and how they were invoked. Each stack frame contains local variables, function parameters, and the instruction pointer showing the exact location in code where execution stopped. The heap section contains dynamically allocated objects with their current values, revealing memory corruption or invalid references.
Memory layout information shows how the process address space was organized at crash time. This includes the locations of code segments, data segments, heap regions, and thread stacks. Understanding memory layout helps identify buffer overflows, use-after-free errors, and heap corruption. The dump also contains register values, which are critical for understanding the precise CPU state during the crash.
Thread state information captures what each thread was doing at crash time. This includes thread IDs, priority levels, synchronization state, and per-thread stacks. Multi-threaded crashes often involve race conditions or deadlocks, making thread state analysis essential. The dump shows which threads were running, blocked, or waiting on synchronization primitives.
Signal and exception information records what caused the crash. On Unix systems, signals like SIGSEGV indicate memory access violations, while SIGABRT indicates deliberate termination. On Windows, structured exceptions provide similar information. The signal context includes the faulting address for memory violations and the exception code for other failures.
File descriptors and system resources show what external resources the program was using. Open files, network connections, and inter-process communication channels all appear in the dump. This information helps identify resource leaks or improper resource handling that contributed to the crash.
The concept of dump completeness varies by platform and configuration. A minidump contains minimal information like stack traces and basic thread state, suitable for quick analysis or privacy-sensitive environments. A full dump captures the entire process address space, enabling deep analysis but consuming significant storage. Heap dumps focus specifically on memory allocations, used primarily for memory leak analysis.
# Capturing comprehensive crash context
class CrashHandler
def self.install
Signal.trap("SEGV") { handle_crash("SIGSEGV") }
Signal.trap("ABRT") { handle_crash("SIGABRT") }
Signal.trap("ILL") { handle_crash("SIGILL") }
end
def self.handle_crash(signal)
context = {
signal: signal,
timestamp: Time.now,
pid: Process.pid,
threads: capture_threads,
memory: capture_memory_info,
environment: ENV.to_h
}
File.write("/var/crash/dump_#{Process.pid}.json", JSON.pretty_generate(context))
abort
end
def self.capture_threads
Thread.list.map do |t|
{
id: t.object_id,
status: t.status,
backtrace: t.backtrace || []
}
end
end
def self.capture_memory_info
if RUBY_PLATFORM =~ /linux/
File.read("/proc/#{Process.pid}/status")
else
"Memory info not available on this platform"
end
end
end
Ruby Implementation
Ruby's crash handling differs from lower-level languages because Ruby runs in a virtual machine. When a Ruby process crashes, the dump might reflect issues in either the Ruby interpreter itself or in C extensions. Ruby provides signal handling mechanisms through the Signal module, allowing programs to intercept signals before they terminate the process.
The built-in signal handlers can capture information about the Ruby execution state at crash time. This includes the current exception if one was being raised, the backtrace of all threads, and Ruby's internal object state. However, Ruby signal handlers run within the Ruby VM, so they cannot capture information if the VM itself is corrupted.
# Ruby-specific crash dump with VM state
require 'json'
module RubyCrashDumper
def self.install
%w[SEGV ABRT ILL BUS FPE].each do |sig|
Signal.trap(sig) { dump_ruby_state(sig) }
end
end
def self.dump_ruby_state(signal)
dump_data = {
signal: signal,
timestamp: Time.now.iso8601,
ruby_version: RUBY_VERSION,
platform: RUBY_PLATFORM,
exception: capture_current_exception,
threads: capture_all_threads,
objects: capture_object_counts,
gc_stats: GC.stat,
loaded_features: $LOADED_FEATURES.size
}
dump_file = "/tmp/ruby_crash_#{Process.pid}_#{Time.now.to_i}.json"
File.write(dump_file, JSON.pretty_generate(dump_data))
STDERR.puts "Crash dump written to #{dump_file}"
exit!(1)
end
def self.capture_current_exception
return nil unless $!
{
class: $!.class.name,
message: $!.message,
backtrace: $!.backtrace
}
end
def self.capture_all_threads
Thread.list.map do |thread|
{
object_id: thread.object_id,
status: thread.status,
priority: thread.priority,
backtrace: thread.backtrace,
variables: capture_thread_locals(thread)
}
end
end
def self.capture_thread_locals(thread)
locals = {}
thread.thread_variables.each do |var|
locals[var] = thread.thread_variable_get(var).inspect rescue "Error capturing"
end
locals
end
def self.capture_object_counts
counts = Hash.new(0)
ObjectSpace.each_object { |obj| counts[obj.class.name] += 1 }
counts.sort_by { |_, count| -count }.take(20).to_h
end
end
RubyCrashDumper.install
Ruby on Unix systems can trigger core dumps through the operating system. Setting Process.setrlimit(Process::RLIMIT_CORE, Process::RLIM_INFINITY) enables core dump generation. The resulting core file can be analyzed with debuggers like gdb or lldb, though interpreting Ruby VM internals requires understanding of Ruby's C implementation.
C extensions in Ruby applications introduce additional complexity. When a C extension crashes, the Ruby VM may not have an opportunity to run crash handlers. The operating system generates a core dump directly, capturing both the Ruby VM state and the C extension's state. Analyzing these dumps requires tools that understand both Ruby internals and C memory layout.
# Enabling system core dumps
Process.setrlimit(Process::RLIMIT_CORE, Process::RLIM_INFINITY)
# Verify core dump settings
core_limit = Process.getrlimit(Process::RLIMIT_CORE)
puts "Core dump size limit: #{core_limit[0]} bytes"
# Core pattern can be checked on Linux
if File.exist?("/proc/sys/kernel/core_pattern")
pattern = File.read("/proc/sys/kernel/core_pattern").strip
puts "Core dump pattern: #{pattern}"
end
The Ruby garbage collector's state provides valuable debugging information. When analyzing crashes related to memory issues, GC statistics reveal memory pressure, allocation rates, and collection frequency. This information helps distinguish between crashes caused by memory exhaustion versus other issues.
# Comprehensive GC state capture
class GCDumper
def self.capture_state
{
stat: GC.stat,
latest_gc_info: GC.latest_gc_info,
stress: GC.stress,
count: GC.count,
time: GC.total_time,
profiler: capture_gc_profiler
}
end
def self.capture_gc_profiler
GC::Profiler.enable
report = GC::Profiler.result
GC::Profiler.clear
report
end
end
Ruby's Exception class provides programmatic access to backtraces, which form the foundation of crash analysis. The backtrace_locations method returns structured information about each stack frame, including file paths, line numbers, and method names. This structured data allows automated analysis of crash patterns.
Practical Examples
Analyzing a segmentation fault in a Ruby application requires examining both Ruby-level and system-level information. The process begins by identifying the crashing thread and its stack trace, then examining the state of objects being accessed when the crash occurred.
# Scenario: Analyzing a crash from a C extension
require 'json'
class CrashAnalyzer
def analyze_dump(dump_file)
data = JSON.parse(File.read(dump_file))
puts "=== Crash Analysis Report ==="
puts "Signal: #{data['signal']}"
puts "Time: #{data['timestamp']}"
puts "Ruby: #{data['ruby_version']} on #{data['platform']}"
puts
identify_crashing_thread(data['threads'])
analyze_object_state(data['objects'])
check_memory_pressure(data['gc_stats'])
if data['exception']
puts "\n=== Active Exception ==="
puts "#{data['exception']['class']}: #{data['exception']['message']}"
puts data['exception']['backtrace'].first(10).join("\n")
end
end
def identify_crashing_thread(threads)
puts "=== Thread Analysis ==="
puts "Active threads: #{threads.size}"
threads.each_with_index do |thread, idx|
next unless thread['backtrace']
puts "\nThread #{idx} (#{thread['status']}):"
suspicious = detect_suspicious_frames(thread['backtrace'])
if suspicious.any?
puts " SUSPICIOUS FRAMES DETECTED:"
suspicious.each { |frame| puts " #{frame}" }
else
puts " Top frame: #{thread['backtrace'].first}"
end
end
end
def detect_suspicious_frames(backtrace)
suspicious = []
backtrace.each do |frame|
suspicious << frame if frame.include?('.so') # C extension
suspicious << frame if frame.include?('native') # Native code
suspicious << frame if frame.include?('malloc') || frame.include?('free')
end
suspicious
end
def analyze_object_state(objects)
puts "\n=== Object Analysis ==="
puts "Top object types:"
objects.take(10).each do |type, count|
puts " #{type}: #{count}"
end
# Check for memory leak indicators
if objects['String'] && objects['String'] > 1_000_000
puts " WARNING: Excessive String objects may indicate memory leak"
end
if objects['Array'] && objects['Array'] > 500_000
puts " WARNING: Excessive Array objects may indicate memory leak"
end
end
def check_memory_pressure(gc_stats)
puts "\n=== Memory Pressure Analysis ==="
puts "GC count: #{gc_stats['count']}"
puts "Heap allocated pages: #{gc_stats['heap_allocated_pages']}"
puts "Heap free slots: #{gc_stats['heap_free_slots']}"
puts "Old objects: #{gc_stats['old_objects']}"
if gc_stats['heap_free_slots'] < 1000
puts " WARNING: Very low free slots, memory exhaustion likely"
end
if gc_stats['count'] > 10000
puts " WARNING: High GC count may indicate memory pressure"
end
end
end
# Usage
analyzer = CrashAnalyzer.new
analyzer.analyze_dump("/tmp/ruby_crash_12345_1640000000.json")
A deadlock scenario demonstrates the importance of thread state analysis. When multiple threads block waiting for each other, the crash dump shows threads in waiting states, often with synchronization primitives visible in stack traces.
# Scenario: Detecting deadlock from crash dump
class DeadlockDetector
def analyze_for_deadlock(dump_file)
data = JSON.parse(File.read(dump_file))
threads = data['threads']
waiting_threads = threads.select { |t| t['status'] == 'sleep' }
if waiting_threads.size == threads.size && threads.size > 1
puts "DEADLOCK SUSPECTED: All threads are waiting"
# Analyze what each thread is waiting on
waiting_threads.each_with_index do |thread, idx|
puts "\nThread #{idx}:"
mutex_frames = thread['backtrace'].select { |f| f.include?('Mutex') || f.include?('lock') }
mutex_frames.each { |frame| puts " Waiting: #{frame}" }
end
puts "\n=== Deadlock Resolution Suggestions ==="
puts "1. Check for circular lock dependencies"
puts "2. Review lock acquisition order"
puts "3. Consider using timeout on lock acquisition"
else
puts "No deadlock pattern detected"
end
end
end
Memory corruption issues often manifest as crashes in seemingly unrelated code. The corruption occurs at one point, but the program doesn't crash until it accesses the corrupted memory later. Crash dump analysis must work backwards from the crash site to find the corruption source.
# Scenario: Tracking memory corruption patterns
class CorruptionAnalyzer
def analyze_heap_corruption(dump_file)
data = JSON.parse(File.read(dump_file))
# Look for corruption indicators
indicators = []
# Check for double-free patterns
data['threads'].each do |thread|
thread['backtrace']&.each do |frame|
if frame.include?('free') || frame.include?('gc_')
indicators << {
type: 'potential_double_free',
thread: thread['object_id'],
frame: frame
}
end
end
end
# Check for buffer overflow patterns
if data['signal'] == 'SIGSEGV'
indicators << {
type: 'memory_access_violation',
details: 'Segmentation fault suggests invalid memory access'
}
end
# Analyze object counts for anomalies
if data['objects']
data['objects'].each do |type, count|
if count > 10_000_000
indicators << {
type: 'excessive_allocation',
class: type,
count: count
}
end
end
end
report_findings(indicators)
end
def report_findings(indicators)
puts "=== Memory Corruption Analysis ==="
if indicators.empty?
puts "No clear corruption indicators found"
else
indicators.each do |indicator|
puts "\n#{indicator[:type].upcase}:"
indicator.each { |k, v| puts " #{k}: #{v}" unless k == :type }
end
end
end
end
Tools & Ecosystem
Ruby developers use multiple tools for crash dump analysis, each serving different purposes. The standard Ruby interpreter provides built-in crash reporting through signal handlers and exception mechanisms. When the Ruby VM crashes due to internal errors, it writes a detailed error report showing the C-level backtrace, Ruby-level backtrace, and system information.
Third-party gems extend crash analysis capabilities. The rbtrace gem attaches to running Ruby processes to inspect their state without stopping execution. While not a crash dump analyzer per se, it helps understand process state before crashes occur. The heapy gem analyzes heap dumps generated by Ruby's heap dump facility, identifying memory leaks and retention issues.
# Using Ruby's built-in heap dump
require 'objspace'
# Generate heap dump before potential crash
ObjectSpace.trace_object_allocations_start
# Simulate some work
1000.times { Array.new(1000) { "data" } }
# Dump heap to file
File.open('/tmp/heap_dump.json', 'w') do |f|
ObjectSpace.dump_all(output: f)
end
ObjectSpace.trace_object_allocations_stop
System-level debugging tools analyze core dumps generated by the operating system. On Linux, gdb (GNU Debugger) opens core files and inspects the crashed process's memory. For Ruby applications, gdb shows the C-level stack trace of the Ruby VM, which is less useful without Ruby-specific extensions.
# Analyzing a Ruby core dump with gdb
# gdb /usr/bin/ruby core.12345
# (gdb) bt # C-level backtrace
# (gdb) info threads # Show all threads
# (gdb) thread apply all bt # Backtrace for all threads
The ruby-gdb.py helper script, included with Ruby source, adds Ruby-aware commands to gdb. These commands display Ruby stack traces, inspect Ruby objects, and navigate Ruby's internal structures. Installing these helpers transforms gdb from a C debugger into a Ruby-aware debugging tool.
Platform-specific tools provide additional capabilities. On macOS, lldb serves as the primary debugger with similar capabilities to gdb. The Console application displays crash reports automatically generated by the system. On Windows, WinDbg analyzes crash dumps with extensions that understand Ruby VM internals.
Profiling tools help prevent crashes by identifying issues before they cause failures. The stackprof gem performs sampling-based profiling to find hot spots and deep recursion. The memory_profiler gem tracks object allocations to identify memory leaks. The derailed_benchmarks gem specializes in Rails application memory analysis.
# Using memory_profiler to prevent future crashes
require 'memory_profiler'
report = MemoryProfiler.report do
# Code that might be leaking memory
@cache = {}
10_000.times do |i|
@cache[i] = "data" * 1000
end
end
report.pretty_print
# Check for retained memory
if report.total_retained_memsize > 100_000_000
puts "WARNING: High retained memory detected"
File.write('/tmp/memory_report.txt', report.pretty_print)
end
Application Performance Monitoring (APM) services like New Relic, Datadog, and Scout provide crash tracking and analysis in production. These services capture exceptions, collect performance metrics, and aggregate crash reports across deployments. They offer dashboards showing crash frequency, affected users, and trends over time.
Crash reporting services specialize in collecting and analyzing crashes from production environments. Services like Sentry, Bugsnag, and Rollbar capture exceptions with full context including environment variables, request parameters, and user information. They group similar crashes together and track resolution status.
# Integrating with Sentry for crash tracking
require 'sentry-ruby'
Sentry.init do |config|
config.dsn = ENV['SENTRY_DSN']
config.breadcrumbs_logger = [:active_support_logger, :http_logger]
config.traces_sample_rate = 0.1
# Add custom context to crashes
config.before_send = lambda do |event, hint|
event.contexts[:runtime] = {
name: 'ruby',
version: RUBY_VERSION,
gc_stats: GC.stat
}
event
end
end
# Crashes are automatically reported
begin
risky_operation
rescue => e
Sentry.capture_exception(e)
raise
end
Error Handling & Edge Cases
Signal handling during crash dump generation introduces complexity. Signal handlers run in an async-signal-safe context with significant restrictions. They cannot allocate memory, cannot call non-reentrant functions, and must avoid most Ruby operations. Violating these restrictions causes undefined behavior, potentially preventing crash dump generation entirely.
# Signal handler with safety considerations
Signal.trap("SEGV") do
# UNSAFE: These operations might fail in signal context
# File.write - might allocate memory
# JSON.generate - definitely allocates memory
# Complex Ruby operations - might trigger GC
# SAFER: Write minimal information with preallocated resources
fd = IO.sysopen('/tmp/crash.log', 'a')
IO.write(fd, "SEGV at #{Time.now.to_i}\n")
IO.close(fd)
exit!(1) # exit! is async-signal-safe, exit is not
end
Recursive crash handling occurs when the crash dump code itself crashes. This happens when examining corrupted memory structures or when signal handlers trigger additional signals. Proper crash handlers include recursion detection to prevent infinite loops.
# Crash handler with recursion protection
module SafeCrashHandler
@in_handler = false
def self.handle_crash(signal)
if @in_handler
# Already handling a crash, exit immediately
exit!(1)
end
@in_handler = true
begin
write_crash_dump(signal)
rescue => e
# Even crash handling failed, write minimal info
STDERR.puts "Crash handler failed: #{e.class}"
ensure
exit!(1)
end
end
def self.write_crash_dump(signal)
# Crash dump logic here
end
end
Insufficient disk space prevents writing crash dumps. Crash handlers should check available space before writing large dumps and fall back to minimal dumps when space is limited. Preallocating space for crash dumps ensures that space is available when needed.
# Checking available disk space before dump
require 'sys/filesystem'
def write_crash_dump_safe(data, path)
stat = Sys::Filesystem.stat(File.dirname(path))
available_mb = stat.bytes_available / 1024 / 1024
if available_mb < 100
# Write minimal dump
File.write("#{path}.minimal", data[:summary].to_json)
else
# Write full dump
File.write(path, data.to_json)
end
rescue => e
# Fall back to STDERR if file writing fails
STDERR.puts "Crash dump write failed: #{e}"
STDERR.puts data[:summary].inspect
end
Permission issues prevent writing dumps to protected directories. Production systems often run with restricted permissions, making common dump locations like /var/crash inaccessible. Crash handlers need fallback locations like /tmp or the application's working directory.
Thread-safety issues arise when multiple threads crash simultaneously or when crash handlers access shared state. Multiple threads might trigger signal handlers concurrently, causing race conditions in dump generation. Using thread-local storage and atomic operations prevents these races.
# Thread-safe crash dump counter
require 'concurrent'
module CrashDumper
@dump_counter = Concurrent::AtomicFixnum.new(0)
def self.generate_dump
# Each thread gets a unique dump number
dump_num = @dump_counter.increment
dump_file = "/tmp/crash_#{Process.pid}_#{dump_num}.json"
# Continue with dump generation
end
end
Large process crashes generate enormous dumps. A Ruby process with 10GB of heap memory produces a 10GB+ core dump. These dumps are impractical to transfer, store, or analyze. Configuring the kernel to truncate dumps or using minidumps addresses this issue.
# Generating a minidump instead of full dump
class MinidumpGenerator
def self.generate
{
timestamp: Time.now.iso8601,
pid: Process.pid,
# Only capture summary information
thread_count: Thread.list.size,
top_backtrace: Thread.main.backtrace&.first(20),
memory_summary: {
rss_mb: get_rss_mb,
objects: ObjectSpace.count_objects.slice(:TOTAL, :FREE, :T_STRING, :T_ARRAY, :T_HASH)
}
}
end
def self.get_rss_mb
return nil unless File.exist?("/proc/#{Process.pid}/status")
status = File.read("/proc/#{Process.pid}/status")
if match = status.match(/VmRSS:\s+(\d+)\s+kB/)
match[1].to_i / 1024
end
end
end
Common Pitfalls
Assuming crash dumps contain current code leads to misanalysis. The code in memory might differ from the current source code if the process has been running since before a deployment. Always verify the code version present in the dump matches expectations.
Ignoring load time versus crash time causes confusion. Stack traces show where code was executing at crash time, not necessarily where the bug was introduced. A null pointer exception might occur in generic container code, while the actual bug is where null was inserted earlier.
Focusing solely on the crashing thread misses the root cause. The crashing thread might be a victim of corruption caused by another thread. Multi-threaded crashes require analyzing all threads to understand the full picture. A crash in thread A might result from memory corruption by thread B.
Over-interpreting optimized code stack traces causes errors. Optimized builds inline functions, eliminate stack frames, and reorder operations. Stack traces from optimized code show incomplete or confusing call chains. Production builds often enable optimizations, making their crash dumps harder to analyze than debug builds.
Treating core dumps as small data files ignores resource constraints. A core dump of a large Ruby process might be tens of gigabytes. Transferring, storing, and analyzing these dumps requires appropriate infrastructure. Network bandwidth, disk space, and analysis tool memory limits all become relevant.
Modifying the process during dump generation corrupts the dump. Some debugging approaches attach to the crashed process and modify its memory. These modifications invalidate the crash state, making the dump useless for understanding the original failure.
# Common mistake: Complex operations in signal handler
Signal.trap("SEGV") do
# WRONG: These operations are not async-signal-safe
logger = Logger.new('/tmp/crash.log')
logger.error("Crashed: #{$!.inspect}") # Might trigger GC
# WRONG: Network operations in signal handler
HTTP.post('https://api.example.com/crashes', {
error: $!.message,
backtrace: caller
})
exit(1)
end
# BETTER: Minimal operations only
Signal.trap("SEGV") do
STDERR.puts "CRASH"
exit!(1) # Use exit! not exit
end
Analyzing crashes without symbol information makes stack traces cryptic. Stack traces show memory addresses instead of function names without symbols. Production deployments should preserve symbol files or debug symbols to make crash analysis feasible.
Expecting complete information from minidumps leads to frustration. Minidumps sacrifice completeness for size. They capture stack traces but not heap contents, making certain types of analysis impossible. Choose dump type based on analysis needs versus resource constraints.
Dumping sensitive data to crashes creates security issues. Crash dumps contain process memory, including passwords, API keys, and user data. Storing these dumps insecurely or sending them to external services exposes sensitive information. Scrub sensitive data or encrypt dumps before storage.
Reference
Crash Dump Types
| Type | Size | Contents | Use Case |
|---|---|---|---|
| Full Dump | Large (GB+) | Complete process memory | Deep analysis, memory corruption |
| Minidump | Small (MB) | Stacks and basic context | Quick diagnosis, production |
| Heap Dump | Medium (100s MB) | Object allocations only | Memory leak analysis |
| Thread Dump | Tiny (KB) | Stack traces only | Deadlock detection |
Signal Types (Unix)
| Signal | Numeric | Cause | Dump Core |
|---|---|---|---|
| SIGSEGV | 11 | Invalid memory access | Yes |
| SIGABRT | 6 | Abort signal from process | Yes |
| SIGILL | 4 | Illegal instruction | Yes |
| SIGFPE | 8 | Floating point exception | Yes |
| SIGBUS | 7 | Bus error (alignment) | Yes |
| SIGTERM | 15 | Termination request | No |
| SIGKILL | 9 | Force kill | No |
Ruby Crash Dump Methods
| Method | Purpose | Output |
|---|---|---|
| Signal.trap | Handle crash signals | Custom crash handler |
| ObjectSpace.dump_all | Heap dump | JSON object graph |
| GC.stat | GC statistics | Hash of GC metrics |
| Thread.list | Active threads | Array of threads |
| caller | Stack trace | Array of frame strings |
| Process.setrlimit | Configure core dumps | System core dump |
GC Stat Keys
| Key | Description | High Value Indicates |
|---|---|---|
| count | GC run count | Memory pressure |
| heap_allocated_pages | Total heap pages | Memory usage |
| heap_free_slots | Available slots | Memory available |
| old_objects | Tenured objects | Long-lived data |
| malloc_increase_bytes | C allocations | Native memory use |
Crash Analysis Commands
| Tool | Command | Purpose |
|---|---|---|
| gdb | bt | C backtrace |
| gdb | info threads | List threads |
| gdb | thread apply all bt | All backtraces |
| lldb | bt all | All backtraces (macOS) |
| ruby | ruby --dump=crash | VM crash dump |
Ruby Signal Handler Safety
| Operation | Safe in Handler | Alternative |
|---|---|---|
| exit | No | exit! |
| File.write | No | IO.sysopen + IO.write |
| JSON.generate | No | Preformat string |
| Logger | No | Direct STDERR |
| Complex Ruby | No | Keep minimal |
| Memory allocation | No | Preallocate |
Common Crash Patterns
| Pattern | Indicator | Likely Cause |
|---|---|---|
| SIGSEGV in malloc | Memory operation frames | Heap corruption |
| All threads sleeping | Every thread waiting | Deadlock |
| High GC count | GC stat count | Memory exhaustion |
| Native extension frame | .so in backtrace | C extension bug |
| Low free slots | heap_free_slots near zero | Out of memory |
Analysis Checklist
| Check | Location | What to Look For |
|---|---|---|
| Signal type | Header | SIGSEGV vs SIGABRT vs other |
| Crashing thread | Thread list | Main thread or worker |
| Stack depth | Backtrace | Stack overflow if very deep |
| Memory stats | GC stats | Free slots, allocation rate |
| Object counts | ObjectSpace | Excessive counts by type |
| Exception state | $! variable | Active exception if any |
| Thread states | Thread.status | Blocked or sleeping threads |
| Recent GC | latest_gc_info | Time since last collection |