A common refrain for tracking down issues on computer systems running open source software is "Use the source, Luke." Reviewing the source code can be helpful in understanding how the code works, but the static view may not give you a complete picture of how things work (or are broken) in the code. The paths taken through code are heavily data dependent. Without knowledge about specific values at key locations in code, you can easily miss what is happening. Dynamic instrumentation tools, such as SystemTap, that trace and instrument the software can help provide a more complete understanding of what the code is actually doing
I have wanted to better understand how the Ruby interpreter works. This is an opportunity to use SystemTap to investigate Ruby MRI internals on Red Hat Enterprise Linux 7. The article What is SystemTap and how to use it? has more information about installing SystemTap. The x86_64 RHEL 7 machine has ruby-2.0.0648-33.el7_4.x86_64.rpm
installed, so the matching debuginfo
RPM is installed to provide SystemTap with information about function parameters and to provide me with human-readable source code. The debuginfo
RPM is installed by running the following command as root:
# debuginfo-install ruby -y
The first thing I notice is the Ruby command /usr/bin/ruby
is very small, less than 8K bytes in size:
$ ls -l /usr/bin/ruby
-rwxr-xr-x. 1 root root 7184 Feb 19 07:12 /usr/bin/ruby
The actual interpreter must be located elsewhere. I look through the shared libraries used by /usr/bin/ruby
and find libruby.so.2.0
as a likely home of the Ruby interpreter:
$ ldd /usr/bin/ruby
linux-vdso.so.1 => (0x00007ffc11076000)
libruby.so.2.0 => /lib64/libruby.so.2.0 (0x00007f8a62a1a000)
libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f8a627fe000)
librt.so.1 => /lib64/librt.so.1 (0x00007f8a625f6000)
libdl.so.2 => /lib64/libdl.so.2 (0x00007f8a623f2000)
libcrypt.so.1 => /lib64/libcrypt.so.1 (0x00007f8a621bb000)
libm.so.6 => /lib64/libm.so.6 (0x00007f8a61eb9000)
libc.so.6 => /lib64/libc.so.6 (0x00007f8a61aec000)
/lib64/ld-linux-x86-64.so.2 (0x00007f8a62e77000)
libfreebl3.so => /lib64/libfreebl3.so (0x00007f8a618e9000)
We can list out the all the functions in the shared library that SystemTap can probe using the wildcard "*"
to match all function names:
$ stap -l 'process("/lib64/libruby.so.2.0").function("*")'
There are over 5600 functions visible to SystemTap in the Ruby MRI interpreter. Instrumenting all of these functions is probably going to give us information overload. Also, SystemTap checks to see how much overhead its instrumentation introduces into the code and will stop if it is causing too much overhead. Let's reduce the scope and just look at how Ruby's arbitrary precision arithmetic works. The following shows the 125 various functions associated with bignum
:
$ stap -l 'process("/lib64/libruby.so.2.0").function("*")'|grep "bignum"
Below, is a single line of the output from the stap -l
command above for the function rb_cstr_to_inum
in the Ruby MRI shared library /usr/lib64/libruby.so.2.0.0
. This line has additional information following the function name after the @
sign: the source file and line number for this function.
process("/usr/lib64/libruby.so.2.0.0").function("rb_cstr_to_inum@/usr/src/debug/ruby-2.0.0-p648/bignum.c:579")
As mentioned earlier, the Ruby debuginfo
provides the bignum.c
source file. Loading that file in the editor and going to line 579 shows the start of that function:
VALUE
rb_cstr_to_inum(const char *str, int base, int badcheck)
{
const char *s = str;
char *end;
char sign = 1, nondigit = 0;
int c;
...
The SystemTap wildcard can be specified to limit the matching to just functions in the bignum.c
file, as shown below:
$ stap -l 'process("/lib64/libruby.so.2.0").function("*@*/bignum.c")'
Now, with armed with this collection of probe-able functions, we can look at how the arbitrary precision arithmetic operates using the SystemTap example script para-callgraph.stp
in /usr/share/systemtap/examples/general/para-callgraph.stp
. Ruby source code has a sample program, pi.rb
, that computes the digits of pi using the arbitrary precision arithmetic.
$ more pi.rb
#!/usr/local/bin/ruby
k, a, b, a1, b1 = 2, 4, 1, 12, 4
loop do
# Next approximation
p, q, k = k*k, 2*k+1, k+1
a, b, a1, b1 = a1, b1, p*a+q*a1, p*b+q*b1
# Print common digits
d = a / b
d1 = a1 / b1
while d == d1
print d
$stdout.flush
a, a1 = 10*(a%b), 10*(a1%b1)
d, d1 = a/b, a1/b1
end
end
It will run, printing out the never-ending stream of digits of pi:
$ ruby pi.rb
3141592653589793238462643383279502884197169399375105820974944592
3078164062862089986280348253421170679821480865132823066470938446
0955058223172535940812848111745028410270193852110555964462294895
4930381964428810975665933446128475648233786783...
Now we can see how the arbitrary arithmetic functions operate during the program execution. The first line of the command below invokes SystemTap with the para-callgraph.stp
script. The second line provides the script with the information about which functions to probe. In this case, all the functions from the bignum.c
source file are traced. The last line of the command makes SystemTap start the ruby
program once all the SystemTap instrumentation has been created. The pi.rb
program will run endlessly. Once we have enough trace data, we can stop the program and tracing by pressing Ctrl-C.
stap /usr/share/systemtap/examples/general/para-callgraph.stp \
'process("/lib64/libruby.so.2.0").function("*@*/bignum.c")' \
-c "ruby pi.rb"
Below is the start of the output. Approximately 25 digits are printed out before we see para-callgraph.stp
output for the call to rb_big_resize_big
. This might be a side effect of buffering output rather than the actual order in which things occurred. Each line of the para-callgraph.stp
starts with the number of microseconds elapsed from the previous line, the process name, and the PID. The ->
indicates entering a function function with its list of argument values. The <-
is the return from the from the function with a return value, if it has one. Nested function calls are indented.
At the very beginning of the program trace, we can see some initialization including a call to Init_Bignum
. The source for the Init_Bignum
function show a series of calls to set up the various methods in the class using the rb_define_method
function, which is in a file other than bignum.c
and is not instrumented. Thus, there is no tracing of the rb_define_method
function. However, there is a call to the rb_uint2big
function at the end of Init_Bignum
and this is seen in the trace. The rb_uint2big
source code has bignew
in it with two arguments in the code, but in the trace there is bignew_1
invoked with three arguments. Searching through the bignum.c
shows a define
that converts bignew
into a call to bignew_1
with three arguments.
We also see a conversion of a string with the first rb_cstr_to_inum
that is base 8 (0x8
) and returns 0x5
. The next conversion by rb_cstr_to_inum
from strings to number is base 10 (0xa
) and returns 0x3
. The source code shows that rb_cstr_to_inum
returns the value from bignorm
, but there is no trace of bignorm
in the trace. The compiler optimized the code by inlining bignorm
into rb_cstr_to_inum
to eliminate the cost of the function call. The values returned by bignorm
(and rb_cstr_to_inum
) use the low bit to indicate the object type (How Big is a Bignum?). In rb_cstr_to_inum
, the objects have the least-significant bit set indicating the values are fixed numbers.
31415926535897932384626433 0 ruby(6049):->rb_big_resize big=0x201bfb0 len=0x5
5 ruby(6049):rb_big_norm x=0x201bfb0
2 ruby(6049): ->bignorm x=0x201bfb0
4 ruby(6049): ->rb_big_resize big=0x201bfb0 len=0x4
7 ruby(6049): <-rb_big_resize
8 ruby(6049): <-bignorm return=0x201bfb0
9 ruby(6049):Init_Bignum
25 ruby(6049): ->rb_uint2big n=0x3
28 ruby(6049): ->bignew_1 klass=0x20168a8 len=0x2 sign=0x1
31 ruby(6049): <-bignew_1 return=0x2016858
33 ruby(6049): <-rb_uint2big return=0x2016858
36 ruby(6049):rb_uint2big n=0xffffffffffffffff
4 ruby(6049): ->bignew_1 klass=0x20168a8 len=0x2 sign=0x1
6 ruby(6049): <-bignew_1 return=0x200b4a8
8 ruby(6049):rb_cstr_to_inum str=0x2122390 base=0x8 badcheck=0x0
6 ruby(6049):rb_cstr_to_inum str=0x2122390 base=0xa badcheck=0x0
3 ruby(6049):<-rb_cstr_to_inum return=0x3
Earlier examination of the Init_Bignum
function code showed that the /
(divide) method is performed by the rb_big_div
function. Since pi.rb
performs division, there should be calls to rb_big_div
. A bit later in the trace output, there is an example of a divide operation on arbitrary precision arithmetic by the rb_big_div
function. It calls rb_big_divide
with the argument op=0x2f
, an ASCII character /
. The trace shows an argument needed to be converted to the proper class with a call to rb_int2big
before passing the values to bigdivmod
. The bigdivrem
and bigdivrem1
functions do the actual division. Once the divide operation is done, another normalization is done by the bignorm
function to determine how large the result is and size the storage appropriately. In this particular case, the value (0xb
) has the low bit set indicating the value is a fixed number with the numerical value coded in the remaining bits of the return value.
0 ruby(6049):->rb_big_div x=0x238abd8 y=0x63c25f1981ac4201
3 ruby(6049): ->rb_big_divide x=0x238abd8 y=0x63c25f1981ac4201 op=0x2f
6 ruby(6049): ->rb_int2big n=0x31e12f8cc0d62100
8 ruby(6049): ->rb_uint2big n=0x31e12f8cc0d62100
11 ruby(6049): ->bignew_1 klass=0x20168a8 len=0x2 sign=0x1
13 ruby(6049): <-bignew_1 return=0x238abb0
15 ruby(6049): <-rb_uint2big return=0x238abb0
16 ruby(6049): bigdivmod x=0x238abd8 y=0x238abb0 divp=0x7fff57542938 modp=0x0
22 ruby(6049): ->bigdivrem x=0x238abd8 y=0x238abb0 divp=0x7fff57542938 modp=0x7fff575428e8
24 ruby(6049): ->bignew_1 klass=0x20168a8 len=0x4 sign=0x1
27 ruby(6049): rb_big_clone x=0x238abb0
31 ruby(6049): ->bignew_1 klass=0x20168a8 len=0x2 sign=0x1
34 ruby(6049): <-bignew_1 return=0x238ab60
36 ruby(6049): bigdivrem1 ptr=0x7fff57542860
41 ruby(6049): rb_big_clone x=0x238ab88
45 ruby(6049): ->bignew_1 klass=0x20168a8 len=0x4 sign=0x1
47 ruby(6049): <-bignew_1 return=0x238ab38
48 ruby(6049): rb_big_clone x=0x238ab88
62 ruby(6049): ->bignew_1 klass=0x20168a8 len=0x4 sign=0x1
64 ruby(6049): <-bignew_1 return=0x238ab10
66 ruby(6049): <-rb_big_clone return=0x238ab10
67 ruby(6049): <-bigdivrem return=0x238ab88
69 ruby(6049): bignorm x=0x238ab38
82 ruby(6049): <-bignorm return=0xb
84 ruby(6049): <-rb_big_divide return=0xb
85 ruby(6049):<-rb_big_div return=0xb
As a result of this little exercise, I have a bit better understanding of how the Ruby interpreter handles arbitrary arithmetic. This same technique can be used to trace out the execution of other parts of Ruby MRI (or any other complicated piece of software) to better understand how it operates.
Last updated: May 10, 2018