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