No New Messages
author avatar
Syed Reza
1716104114603 NOTE

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 }