As Ruby developers, we normally think execution is simple:
ruby app.rb
Ruby runs the file.
But what exactly is ruby?
Does the CPU execute Ruby code directly?
What is the Ruby interpreter?
Where does bytecode come into the picture?
What exactly is the runtime?
And where do C, machine code and the operating system enter the story?
For a developer who wants to understand Ruby beyond the language syntax, these are important questions.
This article follows a small Ruby program from source code all the way down to CPU execution.
Note: The discussion here focuses on CRuby/MRI- the standard Ruby implementation. Details differ in JRuby, TruffleRuby and other implementations. Ruby’s
RubyVMAPIs are explicitly MRI-specific. (docs.ruby-lang.org)
1. Start with a simple Ruby class
Consider this file:
# person.rb
class Person
def initialize(name)
@name = name
end
def greet
"Hello, #{@name}"
end
end
person = Person.new("Ruby")
puts person.greet
We execute it:
ruby person.rb
So what happens after we press Enter?
2. ruby is an executable program
When we type:
ruby person.rb
the shell does not understand Ruby syntax.
It finds the ruby executable in your PATH.
For example:
which ruby
might return:
/usr/bin/ruby
or perhaps a version-manager path such as:
/Users/me/.rbenv/shims/ruby
That executable is a compiled native program.
This is a crucial distinction:
Ruby source code is not itself executed by the operating system. The operating system starts the Ruby executable, and that program executes your Ruby program.
The flow initially looks like this:
Terminal
│
│ ruby person.rb
▼
Shell
│
│ locate executable
▼
Ruby executable
│
▼
Operating System creates process
The ruby process is now running.
3. The Ruby interpreter is inside that process
People often say:
“Ruby interprets my code.”
This is useful shorthand, but the reality is more interesting.
The Ruby executable contains the runtime machinery necessary to:
- read Ruby source
- parse it
- compile it
- create internal structures
- execute VM instructions
- manage Ruby objects
- run garbage collection
- perform method calls
- interact with the operating system
So we can think of:
ruby executable
│
├── parser
├── compiler
├── VM
├── garbage collector
├── object system
└── runtime libraries
This collection of mechanisms is what we generally mean by the Ruby runtime.
4. Source code is first parsed
Our source:
person = Person.new("Ruby")
is not immediately converted into CPU instructions.
Ruby first needs to understand its structure.
The parser turns the source into an internal representation of the program.
Conceptually:
Ruby source
│
▼
Tokenizer / Parser
│
▼
Internal syntax representation
For example, Ruby has to understand:
Person.new("Ruby")
as roughly:
receiver: Person
method: new
argument: "Ruby"
The exact internal representation is an implementation detail, but the important point is:
Ruby must understand the program before it can execute it.
5. Ruby then compiles the code into VM instructions
This is the part many Ruby developers don’t realize.
CRuby does not normally execute the original Ruby source line-by-line.
The code is compiled into instructions for Ruby’s virtual machine.
These are commonly referred to as YARV instructions or Ruby bytecode.
Ruby exposes this machinery through:
RubyVM::InstructionSequence
For example:
puts RubyVM::InstructionSequence.compile(
'puts "Hello"'
).disasm
You will see VM instructions rather than Ruby source.
The exact output changes between Ruby versions because the instruction set and compiler details are implementation-specific. Ruby documents InstructionSequence specifically as a way to inspect the VM’s compiled instructions.
So our pipeline becomes:
person.rb
│
▼
Parser
│
▼
Ruby internal representation
│
▼
Compiler
│
▼
YARV bytecode / InstructionSequence
6. What is bytecode?
Bytecode is an intermediate instruction format designed for a virtual machine.
It is not CPU machine code.
Think of this distinction:
Ruby source
↓
Ruby VM bytecode
↓
CPU machine code
Bytecode might conceptually contain operations such as:
putself
putobject
send
setlocal
getinstancevariable
leave
These aren’t x86 instructions.
They are instructions understood by the Ruby VM.
Ruby’s documentation exposes the compiled instruction sequence and its bytecode specifically for inspecting how YARV works. (docs.ruby-lang.org)
7. Enter the virtual machine
Now we have something like:
Ruby source
↓
Compiler
↓
YARV bytecode
↓
Ruby VM
The VM executes those instructions.
You can think of it as a machine built inside the Ruby process:
Ruby Process
┌──────────────────────────────────────┐
│ │
│ Ruby VM │
│ │
│ ┌──────────────────────────────┐ │
│ │ YARV instructions │ │
│ │ │ │
│ │ putobject │ │
│ │ send │ │
│ │ getinstancevariable │ │
│ │ leave │ │
│ └──────────────┬───────────────┘ │
│ │ │
│ ▼ │
│ VM execution │
│ │
└──────────────────────────────────────┘
CRuby’s interpreter loop and instruction definitions are implemented in the Ruby source tree; the Ruby documentation points to insns.def and vm_exec.c as core pieces of this machinery. (docs.ruby-lang.org)
8. But the VM itself is native code
Here is the important connection to C.
The Ruby VM isn’t written in Ruby.
CRuby itself is implemented primarily in C, with some components implemented in other languages.
So conceptually:
Your Ruby code
↓
Ruby bytecode
↓
CRuby VM
↓
C code
↓
Machine instructions
↓
CPU
This is where learning C becomes incredibly useful for a Ruby developer.
Ruby is high-level.
The Ruby runtime is much closer to the machine.
9. What happens with our Person class?
Take:
class Person
def greet
"Hello, #{@name}"
end
end
Ruby compiles the class and its methods into VM instruction sequences.
There isn’t simply one giant sequence representing the entire application.
Different constructs can have different instruction sequences.
Ruby’s InstructionSequence#type can identify sequences such as:
:class
:method
:block
:rescue
:ensure
:top
among others. (docs.ruby-lang.org)
Conceptually:
Person class
│
├── class instruction sequence
│
├── initialize method sequence
│
└── greet method sequence
When:
person.greet
executes, the VM needs to resolve the method call and execute the corresponding instruction sequence.
10. Method calls become VM work
This Ruby:
person.greet
looks tiny.
Internally, Ruby has to determine:
1. What object is `person`?2. What class does it belong to?3. Which method is `greet`?4. Is the method overridden?5. What arguments are involved?6. What execution frame should be created?7. Which instructions should run?
Conceptually:
person.greet
│
▼
VM method dispatch
│
▼
Find `greet`
│
▼
Create/enter execution frame
│
▼
Execute method instructions
The exact internals are sophisticated, including method caches and object-shape optimizations, but the important thing is that the VM – not your operating system- understands the Ruby method call.
11. Where does the operating system come in?
Eventually, everything has to reach the real machine.
The operating system created the Ruby process.
It provides things such as:
virtual memory
threads
file descriptors
sockets
timers
process scheduling
system calls
When Ruby needs to write:
puts "Hello"
the operation eventually crosses from Ruby runtime code into OS facilities for output.
Conceptually:
puts
↓
Ruby implementation
↓
C runtime / OS interface
↓
system call
↓
Operating System
↓
terminal / file / pipe
The exact path can vary by platform and implementation, but this is the important architectural boundary.
12. Where does the CPU actually execute instructions?
Here is the complete picture:
┌──────────────────────────────┐
│ Ruby Source │
│ │
│ person.greet │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ Parser / Compiler │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ YARV Bytecode │
│ Ruby VM instructions │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ CRuby VM │
│ Native runtime implementation│
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ Native Machine Instructions │
│ x86-64 / ARM64 / etc. │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ CPU │
└──────────────────────────────┘
That is the mental model I want to keep as a Ruby developer.
13. And then there is JIT
The previous diagram describes the interpreter path well, but modern Ruby can go further.
CRuby includes YJIT, a Just-In-Time compiler.
Instead of always executing VM bytecode through the interpreter, frequently executed code can be compiled into native machine code.
Conceptually:
Ruby source
↓
VM bytecode
↓
┌───────┴────────┐
│ │
▼ ▼
Interpreter YJIT
│ │
▼ ▼
VM execution Native code
│ │
└───────┬────────┘
▼
CPU
YJIT became production-ready in Ruby 3.2, and Ruby’s documentation describes the interpreter and YJIT as different execution paths around the VM. (Ruby)
This is an important distinction:
Ruby bytecode is not necessarily the final form of execution.
Depending on how Ruby is running and whether JIT is enabled, execution can involve interpreted VM instructions, JIT-generated native code, or transitions between them.
14. Try it yourself
Check your Ruby implementation:
ruby -v
Check where the executable comes from:
which ruby
Inspect VM instructions:
ruby -e 'p RubyVM::InstructionSequence.compile("1 + 2").disasm'
Try a method:
ruby -e '
class Person
def greet
"hello"
end
end
puts RubyVM::InstructionSequence.compile(
"Person.new.greet"
).disasm
'
You will see that Ruby source code has already been transformed into a lower-level instruction sequence before execution.
The exact instructions will depend on your Ruby version, so don’t treat a particular disassembly listing as universal. Ruby explicitly warns that instruction sequences are version-dependent. (docs.ruby-lang.org)
15. The complete mental model
As a senior Ruby developer, I find this model much more useful than simply saying “Ruby is interpreted.”
Ruby Program
│
▼
Ruby Executable
│
▼
Parser
│
▼
Compiler
│
▼
YARV Bytecode
│
▼
┌──────────────────┐
│ CRuby VM │
└────────┬─────────┘
│
┌────────┴────────┐
│ │
▼ ▼
Interpreter YJIT
│ │
▼ ▼
Native runtime Native machine code
│ │
└────────┬────────┘
▼
CPU executes
│
▼
Memory / OS / I/O
So when I run:
ruby person.rb
the CPU isn’t magically executing Ruby syntax.
The operating system starts a native Ruby process.
That process parses my Ruby source, compiles it into VM instructions, and the CRuby runtime executes those instructions – potentially compiling hot code to native machine code through JIT.
And that brings us right back to why learning C is so valuable.
When you understand C, pointers, memory, functions, stacks, machine instructions, and system calls, the Ruby runtime stops looking like a black box.
It becomes another program.
A very sophisticated program – but still a program running on a machine.
And that is exactly where I want to go next: inside the Ruby object model itself – VALUE, RBasic, object headers, heap allocation, and how a simple Person.new becomes a real object in memory.
The natural next article is “What does Person.new actually create inside CRuby?“—connecting the Ruby object model to C structs, VALUE, object headers, heap slots, and garbage collection.
Happy Rubying! ~