Print Sharedlibs of a Process
Here is my simple Ruby script for listing the shared libraries loaded by a process. It does so by reading the /proc/(id|self)/maps exposed by the kernel, thanks to the /proc Filesystem.
#!/usr/bin/env ruby
require 'json'
$allowed_columns = ['mimetype', 'sha256sum']
def mimetype(p)
`file --mime-type #{p}`.chomp.split(': ')[1]
end
def sha256sum(p)
`sha256sum #{p}`.split(/\s+/)[0]
end
def die_with_usage(err=nil, ecode=nil)
puts <<~EOS
Usage: #{File.basename($0)} [OPTIONS]"
Prints all shared libraries loaded in the /proc/(self|pid)/maps of the
specified PID.
When PID is not specified it prints its own shared libraries.
--help, -h
Print this helpful message.
--pid, -p
Scan the /prod/<pid>/maps file for this pid.
DEFAULT=self
--json, -j
Output all sharedlibs as JSON.
Ignores all '--column' options.
--column, -c
When printing include these additional columns of data.
Must be one of: #{$allowed_columns}
EOS
if err
puts "ERROR: #{err}"
ecode ||= 1
else
ecode ||= 0
end
exit ecode
end
#--- main
require 'getoptlong'
opts = GetoptLong.new(
['--help', '-h', GetoptLong::NO_ARGUMENT ],
['--pid', '-p', GetoptLong::REQUIRED_ARGUMENT ],
['--json', '-j', GetoptLong::NO_ARGUMENT ],
['--column', '-c', GetoptLong::REQUIRED_ARGUMENT ],
['--pgrep', '-g', GetoptLong::REQUIRED_ARGUMENT ],
)
json_mode = false
extra_columns = []
target_pid = nil
target_path = "/proc/self/maps"
opts.each do |opt, arg|
case opt
when '--help'
die_with_usage()
when '--pid'
target_pid = arg.to_i
target_path = "/proc/#{target_pid}/maps"
when '--json'
json_mode = true
when '--column'
unless $allowed_columns.member?(arg)
die_with_usage("column must be one of: #{$allowed_columns}")
end
extra_columns << arg
when '--pgrep'
pids = `pgrep #{arg}`.chomp.split("\n").map(&:to_i)
if pids.size == 0
die_with_usage("No pid found for: #{arg}")
end
if pids.size > 1
die_with_usage("Too many pids for: #{arg}")
end
target_pid = pids.first
target_path = "/proc/#{target_pid}/maps"
end
end
rawdata = []
unless File.exist?(target_path)
die_with_usage("No such program at pid=#{target_pid}")
end
f = File.open(target_path)
rawdata = f.each_line.
map{
|e| e.chomp.split(/\s+/)
}.
select {
|e| e.size == 6
}.
map {|e|
{
:memrange => e[0],
:perm => e[1],
:fpath => e[5],
}
}.
select {|e|
File.exist?(e[:fpath])
}.
map {|e|
e[:fmimetype] = mimetype(e[:fpath])
e[:fsha256sum] = sha256sum(e[:fpath])
e
}.
select {|e| e[:fmimetype] == 'application/x-sharedlib' }
f.close
# puts rawdata
sharedlibs = {}
rawdata.each do |data|
libdata = sharedlibs[data[:fpath]] ||= {}
libdata[:memranges] ||= []
libdata[:memranges] << data[:memrange]
libdata[:perm] = data[:perm]
libdata[:sha256sum] = data[:fsha256sum]
libdata[:mimetype] = data[:fmimetype]
end
if json_mode
puts JSON.pretty_generate(sharedlibs)
else
max_widths = {}
sharedlibs.keys.each do |key|
max_widths[:key] ||= 0
max_widths[:key] = [max_widths[:key], key.size].max
libdata = sharedlibs[key]
libdata.each do |prop, val|
max_widths[prop] ||= 0
max_widths[prop] = [max_widths[prop], val.size].max
end
end
sharedlibs.keys.sort.each do |key|
libdata = sharedlibs[key]
printf("%-#{max_widths[:key]}s", key, libdata[:mimetype])
extra_columns.each do |prop|
prop = prop.to_sym
printf("\t%-#{max_widths[prop]}s", libdata[prop])
end
printf("\n")
end
end
It's a bit quick-and-dirty and shells out in a number of places which is easy enough to cleanup, but it works. By default it will print the loaded sharedlibs of itself.
$ ./print-sharedlibs.rb
/home/syed/.rvm/rubies/ruby-3.2.1/lib/libruby.so.3.2.1
/home/syed/.rvm/rubies/ruby-3.2.1/lib/ruby/3.2.0/x86_64-linux/enc/encdb.so
/home/syed/.rvm/rubies/ruby-3.2.1/lib/ruby/3.2.0/x86_64-linux/enc/trans/transdb.so
/home/syed/.rvm/rubies/ruby-3.2.1/lib/ruby/3.2.0/x86_64-linux/json/ext/generator.so
/home/syed/.rvm/rubies/ruby-3.2.1/lib/ruby/3.2.0/x86_64-linux/json/ext/parser.so
/home/syed/.rvm/rubies/ruby-3.2.1/lib/ruby/3.2.0/x86_64-linux/monitor.so
/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
/usr/lib/x86_64-linux-gnu/libc.so.6
/usr/lib/x86_64-linux-gnu/libcrypt.so.1.1.0
/usr/lib/x86_64-linux-gnu/libgmp.so.10.4.1
/usr/lib/x86_64-linux-gnu/libm.so.6
/usr/lib/x86_64-linux-gnu/libz.so.1.2.13
You can print extra columns like so with the '--column' option, currently supported columns are:
- sha256sum -- print the sha256sum of each library
- mimetype -- print the mimetype of each library file, we expect 'application/x-sharedlib'
Here are the help docs:
Usage: print-sharedlibs.rb [OPTIONS]
Prints all shared libraries loaded in the /proc/(self|pid)/maps of the
specified PID.
When PID is not specified it prints its own shared libraries.
--help, -h
Print this helpful message.
--pid, -p
Scan the /prod/<pid>/maps file for this pid.
DEFAULT=self
--pgrep, -g
Call pgrep to find a specified pid and use that pid
if and only if 1 pid is found.
--json, -j
Output all sharedlibs as JSON.
Ignores all '--column' options.
--column, -c
When printing include these additional columns of data.
Must be one of: ["mimetype", "sha256sum"]
It can take a --pid
option which targets a specified pid and even supports a --pgrep
option
which will call pgrep
for you, and if exactly 1 pid is found, it will operate on that pid.
It also supports raw JSON output, which gives more information about the memory ranges.
% ./print-sharedlibs.rb --pgrep nano --json
{
"/usr/lib/x86_64-linux-gnu/libc.so.6": {
"memranges": [
"7f5fd9600000-7f5fd9622000",
"7f5fd9622000-7f5fd979a000",
"7f5fd979a000-7f5fd97f2000",
"7f5fd97f2000-7f5fd97f6000",
"7f5fd97f6000-7f5fd97f8000"
],
"perm": "rw-p",
"sha256sum": "c3a14ee6eb14cdb81f6bbd0ab94ca138597db93d5c8e7bafb5609d2f94ee0068",
"mimetype": "application/x-sharedlib"
},
"/usr/lib/x86_64-linux-gnu/libtinfo.so.6.4": {
"memranges": [
"7f5fd99c0000-7f5fd99ce000",
"7f5fd99ce000-7f5fd99df000",
"7f5fd99df000-7f5fd99ed000",
"7f5fd99ed000-7f5fd99f1000",
"7f5fd99f1000-7f5fd99f2000"
],
"perm": "rw-p",
"sha256sum": "26ca680bd08f516a482b9eacdc68c99f2d9a6e850dee63138b1527d92aef4a78",
"mimetype": "application/x-sharedlib"
},
"/usr/lib/x86_64-linux-gnu/libncursesw.so.6.4": {
"memranges": [
"7f5fd99f2000-7f5fd99fa000",
"7f5fd99fa000-7f5fd9a22000",
"7f5fd9a22000-7f5fd9a2a000",
"7f5fd9a2a000-7f5fd9a2b000",
"7f5fd9a2b000-7f5fd9a2c000"
],
"perm": "rw-p",
"sha256sum": "99b5d5a9aa1231d16eea1c84528f76a84a0f955b0c804341af34a21873b12be8",
"mimetype": "application/x-sharedlib"
},
"/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2": {
"memranges": [
"7f5fd9a5e000-7f5fd9a5f000",
"7f5fd9a5f000-7f5fd9a87000",
"7f5fd9a87000-7f5fd9a91000",
"7f5fd9a91000-7f5fd9a93000",
"7f5fd9a93000-7f5fd9a95000"
],
"perm": "rw-p",
"sha256sum": "db61dfe5ac2fb5522cc111df698146d187b13cbfb73684f190f58217b8dbeec4",
"mimetype": "application/x-sharedlib"
}
}
Simple Two-Liner
Here's my original two-liner which captures the essence of what this is doing without all of the fluff.
def mimetype(p); `file --mime-type #{p}`.chomp.split(': ')[1]; end;
File.open('/proc/self/maps').each_line.to_a.map{|e| e.chomp.split(/\s+/) }.select {|e| e.size == 6 }.map {|e| {:memrange => e[0], :perm => e[1], :fpath => e[5] } }.select {|e| File.exist?(e[:fpath]) }.map {|e| e[:fmimetype] = mimetype(e[:fpath]); e }.select {|e| e[:fmime] == 'application/x-sharedlib'; e }