In Progress
Unit 1, Lesson 1
In Progress

ltrace

As we’ve explored recently, strace is a powerful tool for understanding how a program interacts with the operating system. But it’s only for system calls. In today’s episode, learn how to use ltrace to get the rest of the story!

Video transcript & code

Note: this was a live-style episode. Unlike previous live-style episodes, I'm shipping an actual transcript! ...but it's a transcript, so not quite as nice as a script.

So the other day I did an episode on how I was trying to get a deeper understanding of how Ruby's bundle command works. I wanted to see all of the files that it tries to interact with. And if you saw that episode, you might recall that I wound up using the strace utility to find out exactly what files this command opens or tries to open, or even just looks at.

strace -e %file bundle

which is quite a lot. but then also in that episode, I narrowed it down a little bit with some filtering. But anyway, the point is, this utility is, was turned out to be fantastic for digging into exactly how this program was interacting with the operating system. and as I dug into it more, I realized I also wanted to understand how bundler uses environment variables.

What environment variables does it use? Does it use any that say, aren't documented in the documentation that I could find? I thought, hey, I could totally do that with, strace, right?

I happen to know that getenv is the name or should be the name of the C function that gets an environment variable. That was not the response that I was expecting. at least the first time that this happened, that was not the response I was expecting. I was expecting it to trace, calls to getenv, did not tell me that getenv is an invalid system call.

# strace -e getenv bundle
strace: invalid system call 'getenv'

So, fast forward a little bit. It turns out that getenv is the library call, not a system call. strace only traces system calls. Fortunately, there is another tool for tracing library calls.

It is called, not surprisingly ltrace. So let's try doing this again. Only this time with ltrace.

# ltrace -e getenv bundle
"/usr/local/bin/bundle" is not an ELF file

So the first problem we run into is that it says user local bin bundle is not an an ELF file. So this is basically saying, I was looking for a binary and what I found was not a binary. So let's take a look at what that actually is. we'll say cat /usr/local/bin/bundle. This is a script file, and it's a little stub that's generated by Rubygems.

It says #!/usr/local/bin/ruby. So run this with /usr/local/bin/ruby and then load up the appropriate library and execute the command. so yeah, this is not a binary. ltrace wants to work with binaries, not a problem. The Ruby executable is a binary, so all we really need to do here is just modify our command to be Ruby, followed by /usr/local/bin/bundle.

# ltrace -e getenv ruby /usr/local/bin/bundle
libruby.so.2.7->getenv("RUBY_THREAD_VM_STACK_SIZE") = nil
libruby.so.2.7->getenv("RUBY_THREAD_MACHINE_STACK_SIZE") = nil
libruby.so.2.7->getenv("RUBY_FIBER_VM_STACK_SIZE") = nil
libruby.so.2.7->getenv("RUBY_FIBER_MACHINE_STACK_SIZE") = nil
libruby.so.2.7->getenv("RUBY_GLOBAL_METHOD_CACHE_SIZE") = nil
libruby.so.2.7->getenv("RUBY_SHARED_FIBER_POOL_FREE_STAC"...) = nil
libruby.so.2.7->getenv("RUBYOPT") = nil
libruby.so.2.7->getenv("RUBY_GC_HEAP_FREE_SLOTS") = nil
libruby.so.2.7->getenv("RUBY_FREE_MIN") = nil
libruby.so.2.7->getenv("RUBY_GC_HEAP_INIT_SLOTS") = nil
libruby.so.2.7->getenv("RUBY_HEAP_MIN_SLOTS") = nil
libruby.so.2.7->getenv("RUBY_GC_HEAP_GROWTH_FACTOR") = nil
libruby.so.2.7->getenv("RUBY_GC_HEAP_GROWTH_MAX_SLOTS") = nil
libruby.so.2.7->getenv("RUBY_GC_HEAP_FREE_SLOTS_MIN_RATI"...) = nil
libruby.so.2.7->getenv("RUBY_GC_HEAP_FREE_SLOTS_MAX_RATI"...) = nil
libruby.so.2.7->getenv("RUBY_GC_HEAP_FREE_SLOTS_GOAL_RAT"...) = nil
libruby.so.2.7->getenv("RUBY_GC_HEAP_OLDOBJECT_LIMIT_FAC"...) = nil
libruby.so.2.7->getenv("RUBY_GC_MALLOC_LIMIT") = nil
libruby.so.2.7->getenv("RUBY_GC_MALLOC_LIMIT_MAX") = nil
libruby.so.2.7->getenv("RUBY_GC_MALLOC_LIMIT_GROWTH_FACT"...) = nil
libruby.so.2.7->getenv("RUBY_GC_OLDMALLOC_LIMIT") = nil
libruby.so.2.7->getenv("RUBY_GC_OLDMALLOC_LIMIT_MAX") = nil
libruby.so.2.7->getenv("RUBY_GC_OLDMALLOC_LIMIT_GROWTH_F"...) = nil
libruby.so.2.7->getenv("RUBYLIB") = nil
libruby.so.2.7->getenv("RUBYGEMS_GEMDEPS") = nil
libruby.so.2.7->getenv("GEM_SKIP") = nil
libruby.so.2.7->getenv("GEM_REQUIREMENT_DID_YOU_MEAN") = nil
libruby.so.2.7->getenv("GEM_HOME") = "/usr/local/bundle"
libruby.so.2.7->getenv("GEM_PATH") = nil
libruby.so.2.7->getenv("HOME") = "/root"
libruby.so.2.7->getenv("GEM_VENDOR") = nil
libruby.so.2.7->getenv("GEM_VENDOR") = nil
libruby.so.2.7->getenv("GEM_SPEC_CACHE") = nil
libruby.so.2.7->getenv("GEM_REQUIREMENT_BUNDLER") = nil
libruby.so.2.7->getenv("BUNDLER_VERSION") = nil
libruby.so.2.7->getenv("BUNDLE_GEMFILE") = nil
libruby.so.2.7->getenv("DEBUG_RESOLVER") = nil
libruby.so.2.7->getenv("GEM_SKIP") = nil
libruby.so.2.7->getenv("GEM_REQUIREMENT_URI") = nil
libruby.so.2.7->getenv("BUNDLER_VERSION") = nil
libruby.so.2.7->getenv("BUNDLE_GEMFILE") = nil
libruby.so.2.7->getenv("BUNDLE_APP_CONFIG") = "/usr/local/bundle"
libruby.so.2.7->getenv("BUNDLE_GEMFILE") = nil
libruby.so.2.7->getenv("BUNDLE_SPEC_RUN") = nil
libruby.so.2.7->getenv("BUNDLE_SPEC_RUN") = nil
libruby.so.2.7->getenv("BUNDLE_SPEC_RUN") = nil
libruby.so.2.7->getenv("BUNDLE_SPEC_RUN") = nil
libruby.so.2.7->getenv("BUNDLE_IGNORE_CONFIG") = nil
libruby.so.2.7->getenv("BUNDLE_CONFIG") = nil
libruby.so.2.7->getenv("BUNDLE_USER_CONFIG") = nil
libruby.so.2.7->getenv("BUNDLE_USER_HOME") = nil
libruby.so.2.7->getenv("BUNDLE_IGNORE_CONFIG") = nil
libruby.so.2.7->getenv("BUNDLE_DEFAULT_CLI_COMMAND") = nil
libruby.so.2.7->getenv("BUNDLE_FORGET_CLI_OPTIONS") = nil
libruby.so.2.7->getenv("BUNDLE_FORGET_CLI_OPTIONS") = nil
libruby.so.2.7->getenv("BUNDLE_FORGET_CLI_OPTIONS") = nil
libruby.so.2.7->getenv("BUNDLE_CACHE_ALL") = nil
libruby.so.2.7->getenv("BUNDLE_FORGET_CLI_OPTIONS") = nil
libruby.so.2.7->getenv("BUNDLER_EDITOR") = nil
libruby.so.2.7->getenv("VISUAL") = nil
libruby.so.2.7->getenv("EDITOR") = nil
libruby.so.2.7->getenv("BUNDLE_PLUGINS") = nil
libruby.so.2.7->getenv("THOR_SHELL") = nil
libruby.so.2.7->getenv("BUNDLE_GEMFILE") = nil
libruby.so.2.7->getenv("THOR_SHELL") = nil
libruby.so.2.7->getenv("DEBUG") = nil
libruby.so.2.7->getenv("THOR_SHELL") = nil
libruby.so.2.7->getenv("DEBUG") = nil
libruby.so.2.7->getenv("RUBYGEMS_GEMDEPS") = nil
libruby.so.2.7->getenv("BUNDLE_DISABLE_VERSION_CHECK") = nil
libruby.so.2.7->getenv("BUNDLE_SILENCE_ROOT_WARNING") = "1"
libruby.so.2.7->getenv("BUNDLE_WITH") = nil
libruby.so.2.7->getenv("BUNDLE_WITHOUT") = nil
libruby.so.2.7->getenv("GEM_SKIP") = nil
libruby.so.2.7->getenv("GEM_REQUIREMENT_FILEUTILS") = nil
libruby.so.2.7->getenv("GEM_SKIP") = nil
libruby.so.2.7->getenv("GEM_REQUIREMENT_OPENSSL") = nil
libruby.so.2.7->getenv("GEM_REQUIREMENT_IPADDR") = nil
libcrypto.so.1.1->getenv("OPENSSL_ia32cap") = nil
libruby.so.2.7->getenv("GEM_SKIP") = nil
libruby.so.2.7->getenv("GEM_REQUIREMENT_STRINGIO") = nil
libruby.so.2.7->getenv("BUNDLE_DEPLOYMENT") = nil
libruby.so.2.7->getenv("BUNDLE_DEPLOYMENT_MEANS_FROZEN") = nil
libruby.so.2.7->getenv("BUNDLE_FROZEN") = nil
libruby.so.2.7->getenv("BUNDLE_DEPLOYMENT") = nil
libruby.so.2.7->getenv("BUNDLE_WITHOUT") = nil
libruby.so.2.7->getenv("BUNDLE_WITH") = nil
libruby.so.2.7->getenv("GEM_SKIP") = nil
libruby.so.2.7->getenv("GEM_REQUIREMENT_TIMEOUT") = nil
libruby.so.2.7->getenv("GEM_SKIP") = nil
libruby.so.2.7->getenv("GEM_REQUIREMENT_ZLIB") = nil
libruby.so.2.7->getenv("GEM_SKIP") = nil
libruby.so.2.7->getenv("GEM_REQUIREMENT_CGI") = nil
libruby.so.2.7->getenv("GEM_SKIP") = nil
libruby.so.2.7->getenv("GEM_REQUIREMENT_DATE") = nil
libruby.so.2.7->getenv("BUNDLE_REDIRECT") = nil
libruby.so.2.7->getenv("BUNDLE_TIMEOUT") = nil
libruby.so.2.7->getenv("BUNDLE_RETRY") = nil
libruby.so.2.7->getenv("BUNDLE_PLUGINS") = nil
libruby.so.2.7->getenv("BUNDLE_GEMFILE") = nil
libruby.so.2.7->getenv("BUNDLE_SPEC_RUN") = nil
libruby.so.2.7->getenv("BUNDLE_SPEC_RUN") = nil
libruby.so.2.7->getenv("BUNDLE_SPEC_RUN") = nil
libruby.so.2.7->getenv("BUNDLE_SPEC_RUN") = nil
libruby.so.2.7->getenv("THOR_COLUMNS") = nil
--- SIGCHLD (Child exited) ---
libruby.so.2.7->getenv("NO_COLOR") = nil
Could not locate Gemfile
+++ exited (status 10) +++

 

And there we go. A complete listing of every call to getenv that was executed by Ruby running the bundle program. And we can see in here, I mean, we can do some extra filtering if we wanted to. We could do some grepping. We could look up just the ones that are, that are like BUNDLE_. The point is that is how we can trace a programs library invocations.

And it's sometimes not obvious whether something is going to be a system call or a library call. In this case, one of the things that I noted in this output is that this is all traced to libruby.so, so that's the shared library that it's finding getenv in. That's not what I would've predicted.

If you would have asked me to say what library I expect to find getenv in. It wouldn't have been that one. I think now that I think about it, I think that's because a Ruby is, it's being compiled or rather the libruby library is being compiled with the C standard library, which includes getenv.

Then the Ruby executable is simply linking in libruby and using the functionality from libruby. I guess one of the lessons here is don't make too many assumptions about where functions are defined. Anyway, that's all I have for today. Thanks for watching and happy hacking!

Responses