strace/strace-graph
Dmitry V. Levin b93d52fe3d Change the license of strace to LGPL-2.1-or-later
strace is now provided under the terms of the GNU Lesser General
Public License version 2.1 or later, see COPYING for more details.

strace test suite is now provided under the terms of the GNU General
Public License version 2 or later, see tests/COPYING for more details.
2018-12-10 00:00:00 +00:00

342 lines
7.6 KiB
Perl
Executable File

#!/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.
# SPDX-License-Identifier: LGPL-2.1-or-later
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, "");
}