#!/usr/bin/perl # This script processes strace -f output. It displays a graph of invoked # subprocesses, and is useful for finding out what complex commands do. # You will probably want to invoke strace with -q as well, and with # -s 100 to get complete filenames. # The script can also handle the output with strace -t, -tt, or -ttt. # It will add elapsed time for each process in that case. # Copyright (c) 1998 by Richard Braakman <dark@xs4all.nl>. # Copyright (c) 1998-2017 The strace developers. # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # 3. The name of the author may not be used to endorse or promote products # derived from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. use strict; use warnings; my %unfinished; my $floatform; # Scales for strace slowdown. Make configurable! my $scale_factor = 3.5; my %running_fqname; while (<>) { my ($pid, $call, $args, $result, $time, $time_spent); chop; $floatform = 0; s/^(\d+)\s+//; $pid = $1; if (s/^(\d\d):(\d\d):(\d\d)(?:\.(\d\d\d\d\d\d))? //) { $time = $1 * 3600 + $2 * 60 + $3; if (defined $4) { $time = $time + $4 / 1000000; $floatform = 1; } } elsif (s/^(\d+)\.(\d\d\d\d\d\d) //) { $time = $1 + ($2 / 1000000); $floatform = 1; } if (s/ <unfinished ...>$//) { $unfinished{$pid} = $_; next; } if (s/^<... \S+ resumed> //) { unless (exists $unfinished{$pid}) { print STDERR "$0: $ARGV: cannot find start of resumed call on line $."; next; } $_ = $unfinished{$pid} . $_; delete $unfinished{$pid}; } if (/^--- SIG(\S+) (.*) ---$/) { # $pid received signal $1 # currently we don't do anything with this next; } if (/^\+\+\+ killed by SIG(\S+) \+\+\+$/) { # $pid received signal $1 handle_killed($pid, $time); next; } if (/^\+\+\+ exited with (\d+) \+\+\+$/) { # $pid exited $1 # currently we don't do anything with this next; } ($call, $args, $result) = /(\S+)\((.*)\)\s+= (.*)$/; if ($result =~ /^(.*) <([0-9.]*)>$/) { ($result, $time_spent) = ($1, $2); } unless (defined $result) { print STDERR "$0: $ARGV: $.: cannot parse line.\n"; next; } handle_trace($pid, $call, $args, $result, $time); } display_trace(); exit 0; sub parse_str { my ($in) = @_; my $result = ""; while (1) { if ($in =~ s/^\\(.)//) { $result .= $1; } elsif ($in =~ s/^\"//) { if ($in =~ s/^\.\.\.//) { return ("$result...", $in); } return ($result, $in); } elsif ($in =~ s/([^\\\"]*)//) { $result .= $1; } else { return (undef, $in); } } } sub parse_one { my ($in) = @_; if ($in =~ s/^\"//) { my $tmp; ($tmp, $in) = parse_str($in); if (not defined $tmp) { print STDERR "$0: $ARGV: $.: cannot parse string.\n"; return (undef, $in); } return ($tmp, $in); } elsif ($in =~ s/^0x([[:xdigit:]]+)//) { return (hex $1, $in); } elsif ($in =~ s/^(\d+)//) { return (int $1, $in); } else { print STDERR "$0: $ARGV: $.: unrecognized element.\n"; return (undef, $in); } } sub parseargs { my ($in) = @_; my @args = (); my $tmp; while (length $in) { if ($in =~ s/^\[//) { my @subarr = (); if ($in =~ s,^/\* (\d+) vars \*/\],,) { push @args, $1; } else { while ($in !~ s/^\]//) { ($tmp, $in) = parse_one($in); defined $tmp or return undef; push @subarr, $tmp; unless ($in =~ /^\]/ or $in =~ s/^, //) { print STDERR "$0: $ARGV: $.: missing comma in array.\n"; return undef; } if ($in =~ s/^\.\.\.//) { push @subarr, "..."; } } push @args, \@subarr; } } elsif ($in =~ s/^\{//) { my %subhash = (); while ($in !~ s/^\}//) { my $key; unless ($in =~ s/^(\w+)=//) { print STDERR "$0: $ARGV: $.: struct field expected.\n"; return undef; } $key = $1; ($tmp, $in) = parse_one($in); defined $tmp or return undef; $subhash{$key} = $tmp; unless ($in =~ s/, //) { print STDERR "$0: $ARGV: $.: missing comma in struct.\n"; return undef; } } push @args, \%subhash; } else { ($tmp, $in) = parse_one($in); defined $tmp or return undef; push @args, $tmp; } unless (length($in) == 0 or $in =~ s/^, //) { print STDERR "$0: $ARGV: $.: missing comma.\n"; return undef; } } return @args; } my $depth = ""; # process info, indexed by pid. # fields: # parent pid number # seq clones, forks and execs for this pid, in sequence (array) # filename and argv (from latest exec) # basename (derived from filename) # argv[0] is modified to add the basename if it differs from the 0th argument. my %pr; sub handle_trace { my ($pid, $call, $args, $result, $time) = @_; my $pid_fqname = $pid . "-" . $time; if (defined $time and not defined $running_fqname{$pid}) { $pr{$pid_fqname}{start} = $time; $running_fqname{$pid} = $pid_fqname; } $pid_fqname = $running_fqname{$pid}; if ($call eq 'execve') { return if $result ne '0'; my ($filename, $argv) = parseargs($args); my ($basename) = $filename =~ m/([^\/]*)$/; if ($basename ne $$argv[0]) { $$argv[0] = "$basename($$argv[0])"; } my $seq = $pr{$pid_fqname}{seq}; $seq = [] if not defined $seq; push @$seq, ['EXEC', $filename, $argv]; $pr{$pid_fqname}{seq} = $seq; } elsif ($call eq 'fork' || $call eq 'clone' || $call eq 'vfork') { return if $result == 0; my $seq = $pr{$pid_fqname}{seq}; my $result_fqname= $result . "-" . $time; $seq = [] if not defined $seq; push @$seq, ['FORK', $result_fqname]; $pr{$pid_fqname}{seq} = $seq; $pr{$result_fqname}{start} = $time; $pr{$result_fqname}{parent} = $pid_fqname; $pr{$result_fqname}{seq} = []; $running_fqname{$result} = $result_fqname; } elsif ($call eq '_exit' || $call eq 'exit_group') { $pr{$running_fqname{$pid}}{end} = $time if defined $time and not defined $pr{$running_fqname{$pid}}{end}; delete $running_fqname{$pid}; } } sub handle_killed { my ($pid, $time) = @_; $pr{$pid}{end} = $time if defined $time and not defined $pr{$pid}{end}; } sub straight_seq { my ($pid) = @_; my $seq = $pr{$pid}{seq}; for my $elem (@$seq) { if ($$elem[0] eq 'EXEC') { my $argv = $$elem[2]; print "$$elem[0] $$elem[1] @$argv\n"; } elsif ($$elem[0] eq 'FORK') { print "$$elem[0] $$elem[1]\n"; } else { print "$$elem[0]\n"; } } } sub first_exec { my ($pid) = @_; my $seq = $pr{$pid}{seq}; for my $elem (@$seq) { if ($$elem[0] eq 'EXEC') { return $elem; } } return undef; } sub display_pid_trace { my ($pid, $lead) = @_; my $i = 0; my @seq = @{$pr{$pid}{seq}}; my $elapsed; if (not defined first_exec($pid)) { unshift @seq, ['EXEC', '', ['(anon)'] ]; } if (defined $pr{$pid}{start} and defined $pr{$pid}{end}) { $elapsed = $pr{$pid}{end} - $pr{$pid}{start}; $elapsed /= $scale_factor; if ($floatform) { $elapsed = sprintf("%0.02f", $elapsed); } else { $elapsed = int $elapsed; } } for my $elem (@seq) { $i++; if ($$elem[0] eq 'EXEC') { my $argv = $$elem[2]; if (defined $elapsed) { print "$lead [$elapsed] $pid @$argv\n"; undef $elapsed; } else { print "$lead $pid @$argv\n"; } } elsif ($$elem[0] eq 'FORK') { if ($i == 1) { if ($lead =~ /-$/) { display_pid_trace($$elem[1], "$lead--+--"); } else { display_pid_trace($$elem[1], "$lead +--"); } } elsif ($i == @seq) { display_pid_trace($$elem[1], "$lead `--"); } else { display_pid_trace($$elem[1], "$lead +--"); } } if ($i == 1) { $lead =~ s/\`--/ /g; $lead =~ s/-/ /g; $lead =~ s/\+/|/g; } } } sub display_trace { my ($startpid) = @_; $startpid = (keys %pr)[0]; while ($pr{$startpid}{parent}) { $startpid = $pr{$startpid}{parent}; } display_pid_trace($startpid, ""); }