何がしたいか
実行時の関数呼び出しをログにとって、自動でシーケンス図を作りたい。
できたもの
とりあえず、githubにあげる。(8/14更新)
(gistにしようとしたら、フォルダは作るなと怒られた)
pluntumlを入れる。mac osでは
$ brew install pluntuml
でいけるはず
上記、コードをcloneして
$ git clone https://github.com/tksmiura/simple_trace.git
ビルドして実行までmakeで
$ cd simple_trace
$ make trace
で画像ファイル(*.svg)ができるので、
open main_trace.svg
あたりでsafariで見ることができます。(mac only)
PNGが欲しい場合、
make PIC=PNG trace
でできます。(上記画像はこちら)
技術的な話
- gccのオプション(-finstrument-functions)で関数呼び出しと戻りのタイミングでフック関数を呼ばれるようにする。フック関数でIN/OUTの時に関数のアドレスを出力。
- perlスクリプトでmapファイルで上記アドレスを関数名に変換。(dlopenを使う例がネットでは多いですが、組込み用途では難しいのでこちらで)注意点としては、mapファイルのアドレスと実行時のアドレスは異なるため、どこかわかっている関数のアドレスからオフセットを計算する必要がある。(__cyg_profile_func_enterで初回に自身のアドレスをログに残しています)
- 後はperlでplantumlの形式に変換してplantumlで画像化する
補足
- マクロTRACE_EVENTとTRACE_NOTEを追加しました。これで、シーケンス図のイベント(非同期の意味で矢印の形が違う)と四角形のノートが記載できます。ただし、元コードに埋め込みが必要です。
- mac osがclangですが、homebrewで入れたgcc(ver 7.1.0)でも動作しました。なのでmapファイルの形式がどっちも同じ。組込みのgccはもっと古いバージョンを使っていそうですが、、、手元に環境がないので、諦め。
simple_trace.c
# include <stdio.h>
# include <string.h>
__attribute__((no_instrument_function))
void __cyg_profile_func_enter(void* func_address, void* call_site);
__attribute__((no_instrument_function))
void __cyg_profile_func_exit(void* func_address, void* call_site);
__attribute__((no_instrument_function))
void __cyg_profile_func_enter(void* func_address, void* call_site) {
static int f = 0;
if (! f) {
printf("offset __cyg_profile_func_enter = %lx\n",
(unsigned long)__cyg_profile_func_enter);
f = 1;
}
printf("in %lx -> %lx\n",
(unsigned long)call_site,
(unsigned long)func_address);
}
__attribute__((no_instrument_function))
void __cyg_profile_func_exit(void* func_address, void* call_site) {
printf("out %lx <- %lx\n",
(unsigned long)call_site,
(unsigned long)func_address);
}
seq_trace.pl
# !/usr/bin/perl
%file_map = {};
@start = ();
@end = ();
@fid = ();
@func = ();
# read map.txt for MAC OS X(CLang) or gcc-7 (Homebrew GCC 7.1.0)
open(MAP, "<", "map.txt") || die "Can't open map file";
while ($line = <MAP>) {
#[ 0] linker synthesized
if ($line =~ /^\[([^\]])+\](.*)/) {
my ($fid) = $1;
my ($fname) = $2;
$file_map{$fid} = $fname;
#0x100000D90 0x00000060 [ 1] _func1
} elsif ($line =~ /^0x([0-9A-Fa-f]+)\s+0x([0-9A-Fa-f]+)\s+\[([^\]])+\](.*)/) {
my($fn) = $4;
push(@start, hex($1));
push(@end, hex($1) + hex($2));
push(@fid, $3);
$fn =~ s/^\s*\_//;
push(@func, $fn);
}
}
close(MAP);
if ($ARGV[0]) {
$dest_file = $ARGV[0];
} else {
$dest_file = "tarce.png";
}
print STDOUT "\@startuml $dest_file\n";
print STDOUT "hide footbox\n\n";
while ($line = <STDIN>) {
if ($line =~ /offset\s+([_a-zA-Z0-9]+)\s*\=\s*([0-9A-Fa-f]+)/) {
my ($base_func) = $1;
my ($base_func_addr) = hex($2);
my ($i);
for($i = 0; $i < $#func; $i++) {
if ($func[$i] eq $base_func) {
$offset = $base_func_addr - $start[$i];
last;
}
}
} elsif ($line =~ /in\s+([0-9A-Fa-f]+)\s+\-\>\s+([0-9A-Fa-f]+)/) {
my ($cf, $cm) = &func(hex($1) - $offset);
my ($f, $m) = &func(hex($2) - $offset);
if ($delay_out) {
print STDOUT $delay_out;
$delay_out = 0;
}
if ($cm ne "unknown" && $m ne "unknown") {
print STDOUT "$cm -> $m : $f\n";
$delay_out = "activate $m\n";
} elsif ($cm ne "unknown" && $m eq "unknown") {
print STDOUT "$cm ->] : $f\n";
} elsif ($cm eq "unknown" && $m ne "unknown") {
print STDOUT "[-> $m : $f\n";
$delay_out = "activate $m\n";
} else {
# no output
}
} elsif ($line =~ /out\s+([0-9A-Fa-f]+)\s+\<\-\s+([0-9A-Fa-f]+)/) {
my ($cf, $cm) = &func(hex($1) - $offset);
my ($f, $m) = &func(hex($2) - $offset);
if ($cm ne $m) {
if ($cm ne "unknown" && $m ne "unknown") {
if (! $delay_out) {
print STDOUT "$cm <-- $m\n";
}
} elsif ($cm ne "unknown" && $m eq "unknown") {
print STDOUT "$cm <--]\n";
} elsif ($cm eq "unknown" && $m ne "unknown") {
print STDOUT "[<-- $m\n";
} else {
# no output
}
}
if (! $delay_out) {
print STDOUT "deactivate $m\n";
}
$delay_out = 0;
} elsif ($line =~ /EVENT\s*([^\s]+)\s+\-\>\>\s*([^\:]*)\s*\:(.*)$/) {
my $from, $to, $c;
$from = $1;
$to = $2;
$c = $3;
$from =~ s/.c$//;
$to =~ s/.c$//;
if ($delay_out) {
print STDOUT $delay_out;
$delay_out = 0;
}
print STDOUT "$from ->> $to : $c\n";
} elsif ($line =~ /NOTE\s*([^\:]+)\:(.*)$/) {
my $m, $c;
$m = $1;
$c = $2;
$m =~ s/.c$//;
if ($delay_out) {
print STDOUT $delay_out;
$delay_out = 0;
}
print STDOUT "rnote over $m\n$c\nendrnote\n";
}
}
print STDOUT "\n\@enduml\n";
close(STDOUT);
close(STDIN);
exit 0;
sub func {
my ($addr) = @_;
my ($i);
my ($m) = "unknown";
my ($f) = sprintf("(%x)", $addr);
for($i = 0; $i < $#func; $i++) {
if ($addr >= $start[$i] && $addr < $end[$i]) {
$f = $func[$i];
$m = $file_map{$fid[$i]};
$m =~ s/\.trace_o$//;
last;
}
}
return ($f, $m);
}
