在Linux中追踪本地函数调用的工具

68

我正在寻找类似于ltracestrace的工具,可以追踪可执行文件中本地定义的函数。ltrace只能追踪动态库调用,而strace只能追踪系统调用。例如,给定以下C程序:

#include <stdio.h>

int triple ( int x )
{
  return 3 * x;
}

int main (void)
{
  printf("%d\n", triple(10));
  return 0;
}
使用ltrace运行程序将显示对printf的调用,因为它是标准库函数(在我的系统上是动态库),而strace将显示所有启动代码中使用的系统调用,用于实现printf的系统调用以及关闭代码,但我想要一些可以显示被调用的函数triple的工具。假设本地函数未被优化编译器内联且二进制文件未被剥离(符号已删除),是否有这样的工具?
编辑:
一些澄清事项:
- 如果该工具也提供非本地函数的跟踪信息,则可以接受。 - 我不想必须重新编译具有特定工具支持的程序,可执行文件中的符号信息应该足够。 - 如果我可以像使用 ltrace / strace 一样连接到现有进程,那就太好了。

1
你有尝试使用GDB进行跟踪吗?它曾经告诉我只适用于远程目标。也许你可以让GDB与远程目标一起工作并连接到本地主机?不确定,只是一些随机的想法。 - Johannes Schaub - litb
我不想打断程序的流程,如果gdb能像ltrace一样不引人注目地跟踪程序,我愿意尝试一下,如果有人告诉我如何做。 - Robert Gamble
特别是对于GDB:https://dev59.com/questions/bGkw5IYBdhLWcg3w8O-o#31814519 - Ciro Santilli OurBigBook.com
14个回答

56

假设你只想接收特定功能的通知,你可以像这样操作:

编译时包含调试信息(由于您已经具有符号信息,因此您可能已经具有足够的调试信息)

给定

#include <iostream>

int fac(int n) {
    if(n == 0)
        return 1;
    return n * fac(n-1);
}

int main()
{
    for(int i=0;i<4;i++)
        std::cout << fac(i) << std::endl;
}

使用gdb进行跟踪:

[js@HOST2 cpp]$ g++ -g3 test.cpp
[js@HOST2 cpp]$ gdb ./a.out
(gdb) b fac
Breakpoint 1 at 0x804866a: file test.cpp, line 4.
(gdb) commands 1
Type commands for when breakpoint 1 is hit, one per line.
End with a line saying just "end".
>silent
>bt 1
>c
>end
(gdb) run
Starting program: /home/js/cpp/a.out
#0  fac (n=0) at test.cpp:4
1
#0  fac (n=1) at test.cpp:4
#0  fac (n=0) at test.cpp:4
1
#0  fac (n=2) at test.cpp:4
#0  fac (n=1) at test.cpp:4
#0  fac (n=0) at test.cpp:4
2
#0  fac (n=3) at test.cpp:4
#0  fac (n=2) at test.cpp:4
#0  fac (n=1) at test.cpp:4
#0  fac (n=0) at test.cpp:4
6

Program exited normally.
(gdb)

这是我收集所有函数地址的方法:

tmp=$(mktemp)
readelf -s ./a.out | gawk '
{ 
  if($4 == "FUNC" && $2 != 0) { 
    print "# code for " $NF; 
    print "b *0x" $2; 
    print "commands"; 
    print "silent"; 
    print "bt 1"; 
    print "c"; 
    print "end"; 
    print ""; 
  } 
}' > $tmp; 
gdb --command=$tmp ./a.out; 
rm -f $tmp

请注意,与其只打印当前帧(bt 1),您可以做任何您喜欢的事情,例如打印某个全局变量的值、执行一些shell命令或者在触发 fatal_bomb_exploded 函数时发送电子邮件 :) 不幸的是,gcc会在其中输出一些"当前语言已更改"的消息。但这很容易被grep掉。没什么大不了的。


1
你可以使用 objdump 来获取函数及其地址,然后使用 --command 参数将 gdb 指向一个生成的文件,该文件会自动设置断点。 - Johannes Schaub - litb
@litb,是的,这正是我现在正在尝试做的事情,这可能会起作用,感谢您的见解。 - Robert Gamble
3
@litb,我需要这个功能,所以我编写了一个Python脚本来完成你建议的任务,可以生成OpenGrok和GraphViz dot的输出。如果有人感兴趣,可以在https://github.com/EmmetCaulfield/ftrace上获取它。它满足我的需求,但我怀疑它是否非常稳定。结果可能因人而异。 - Emmet
我不得不在$tmp的末尾添加一个只有'c'的新行,并像这样运行gdb以完全自动化跟踪正在运行的进程:echo c >> $tmp接着是gdb -batch --command=$tmp -p 15414 < /dev/null &> /tmp/gdb.log & - Gurjeet Singh
现在我们有了 rbreak,所以答案可以更新了吗? - bam
显示剩余3条评论

23

System Tap 可以在现代 Linux 系统上使用(如 Fedora 10、RHEL 5 等)。

首先下载 para-callgraph.stp 脚本。

然后运行:

$ sudo stap para-callgraph.stp 'process("/bin/ls").function("*")' -c /bin/ls
0    ls(12631):->main argc=0x1 argv=0x7fff1ec3b038
276  ls(12631): ->human_options spec=0x0 opts=0x61a28c block_size=0x61a290
365  ls(12631): <-human_options return=0x0
496  ls(12631): ->clone_quoting_options o=0x0
657  ls(12631):  ->xmemdup p=0x61a600 s=0x28
815  ls(12631):   ->xmalloc n=0x28
908  ls(12631):   <-xmalloc return=0x1efe540
950  ls(12631):  <-xmemdup return=0x1efe540
990  ls(12631): <-clone_quoting_options return=0x1efe540
1030 ls(12631): ->get_quoting_style o=0x1efe540

另请参阅:Observe、systemtap 和 oprofile 更新


3
只是想指出,这可能取决于内核编译选项;例如,我得到了相同命令的以下输出:"semantic error: process probes not available without kernel CONFIG_UTRACE while resolving probe point process("/bin/ls").function("*").call"。 - sdaau
这在我的Ubuntu 14.04上无法工作,出现了“语义错误:在解析探测点时:在a.stp的第23行第7列找不到标识符'process'”。System Tap的工作原理是什么? - Ciro Santilli OurBigBook.com
不需要将完整路径作为参数传递给 process()sudo stap para-callgraph.stp 'process.function("*")' -c /bin/ls同样有效。为了减少没有调试符号的库函数产生的噪音,您可以使用:'process.function("*@*")' - ack

11

使用 Uprobes (自 Linux 3.5 起)

假设您想要跟踪调用带参数 -l ~/Desktop/datalog-2.2/add.lua ~/Desktop/datalog-2.2/test.dl~/Desktop/datalog-2.2/datalog 中的所有函数。

  1. cd /usr/src/linux-`uname -r`/tools/perf
  2. for i in `./perf probe -F -x ~/Desktop/datalog-2.2/datalog`; do sudo ./perf probe -x ~/Desktop/datalog-2.2/datalog $i; done
  3. sudo ./perf record -agR $(for j in $(sudo ./perf probe -l | cut -d' ' -f3); do echo "-e $j"; done) ~/Desktop/datalog-2.2/datalog -l ~/Desktop/datalog-2.2/add.lua ~/Desktop/datalog-2.2/test.dl
  4. sudo ./perf report -G

list of functions in datalog binary call tree when selecting dl_pushlstring, showing how main called loadfile called dl_load called program called rule which called literal which in turn called other functions that ended up calling dl_pushlstring, scan (parent: program, that is, the third scan from the top) which called dl_pushstring and so on


9
假设您可以使用gcc选项-finstrument-functions重新编译(无需更改源代码)要跟踪的代码,您可以使用etrace来获取函数调用图。
以下是输出样例:
\-- main
|   \-- Crumble_make_apple_crumble
|   |   \-- Crumble_buy_stuff
|   |   |   \-- Crumble_buy
|   |   |   \-- Crumble_buy
|   |   |   \-- Crumble_buy
|   |   |   \-- Crumble_buy
|   |   |   \-- Crumble_buy
|   |   \-- Crumble_prepare_apples
|   |   |   \-- Crumble_skin_and_dice
|   |   \-- Crumble_mix
|   |   \-- Crumble_finalize
|   |   |   \-- Crumble_put
|   |   |   \-- Crumble_put
|   |   \-- Crumble_cook
|   |   |   \-- Crumble_put
|   |   |   \-- Crumble_bake

在Solaris系统中,truss(类似于strace的工具)可以过滤要跟踪的库。当我发现strace没有这样的功能时,我感到很惊讶。


1
你不需要编译和链接ptrace.c到你的代码中才能使其工作吗?当你有一个庞大的代码库和巨大的make文件时,这并不总是一个合理的任务 :) - ljs
@philant 我忘记了那个选项。真的很不错。 - user877329

5

KcacheGrind(缓存磨合)

https://kcachegrind.github.io/html/Home.html

测试程序:

int f2(int i) { return i + 2; }
int f1(int i) { return f2(2) + i + 1; }
int f0(int i) { return f1(1) + f2(2); }
int pointed(int i) { return i; }
int not_called(int i) { return 0; }

int main(int argc, char **argv) {
    int (*f)(int);
    f0(1);
    f1(1);
    f = pointed;
    if (argc == 1)
        f(1);
    if (argc == 2)
        not_called(1);
    return 0;
}

使用方法:

sudo apt-get install -y kcachegrind valgrind

# Compile the program as usual, no special flags.
gcc -ggdb3 -O0 -o main -std=c99 main.c

# Generate a callgrind.out.<PID> file.
valgrind --tool=callgrind ./main

# Open a GUI tool to visualize callgrind data.
kcachegrind callgrind.out.1234

现在你处于一个包含大量有趣性能数据的GUI程序中。

在右下角,选择“调用图”选项卡。这将显示一个交互式调用图,当你点击函数时,它与其他窗口中的性能指标相关联。

要导出图形,请右键单击它并选择“导出图形”。导出的PNG文件如下所示:

从中我们可以看到:

  • 根节点是_start,它是实际的ELF入口点,并包含glibc初始化样板。
  • f0f1f2按预期相互调用。
  • pointed也显示出来,即使我们使用函数指针调用它。如果我们传递了命令行参数,它可能没有被调用。
  • not_called未显示,因为它在运行中没有被调用,因为我们没有传递额外的命令行参数。

valgrind的好处是它不需要任何特殊的编译选项。

因此,即使你只有可执行文件而没有源代码,你也可以使用它。

valgrind通过运行你的代码通过一个轻量级“虚拟机”来实现这一点。

在Ubuntu 18.04上测试过。


4
$ sudo yum install frysk
$ ftrace -sym:'*' -- ./a.out

More: ftrace.1


从手册上看,我不确定这个功能是否符合我的要求,但是这个项目似乎处于测试阶段,并且除了 Fedora 之外没有任何平台提供良好的支持。我使用多个发行版,其中没有一个是 Fedora,而且尝试让它在任何一个发行版上运行似乎都会很具有挑战性。 - Robert Gamble

2
有一个用于自动跟踪带有gdb的函数调用的Shell脚本。但它无法附加到运行中的进程。 博客页面-blog.superadditive.com/2007/12/01/call-graphs-using-the-gnu-project-debugger/
该工具的副本-callgraph.tar.gz http://web.archive.org/web/20090317091725/http://superadditive.com/software/callgraph.tar.gz 它会转储程序中的所有函数,并生成一个带有每个函数断点的GDB命令文件。在每个断点处,执行"backtrace 2"和"continue"。
这个脚本在大型项目(~数千个函数)上运行相当缓慢,因此我添加了一个过滤器以对函数列表进行筛选(通过egrep)。非常容易,我几乎每天都使用此脚本。

您提供的链接现在已经失效。 - Alex Reinking
Alex Reinking,谢谢,已更新为存档版本。 - osgx
我使用GDB Python脚本+Graphviz在Python中编写了一个类似的工具:https://github.com/tarun27sh/Python_gdb_networkx_graphs - brokenfoot

2
如果将该函数外部化为一个外部库,您还应该能够看到它被调用(通过ltrace)。
这样做的原因是因为ltrace将自己置于应用程序和库之间,当所有代码都与一个文件内部化时,它无法拦截调用。
即:ltrace xterm 会从X库中喷出东西,而X几乎不是系统级别的。
除此之外,唯一真正的方法是通过编译时拦截,使用prof标志或调试符号。
我刚刚测试了这个应用程序,看起来很有趣:

http://www.gnu.org/software/cflow/

但我认为那不是你想要的。

1
我理解为什么ltrace能够做到它所做的事情,以及跟踪本地函数更加困难,但如果有一个工具可以附加到进程并自动在所有本地函数上设置断点以跟踪它们,那将是很好的。 - Robert Gamble

2
如果这些功能没有被内联,您甚至可以尝试使用objdump -d <program>。例如,让我们看一下GCC 4.3.2的main例程开头部分:
$ objdump `which gcc` -d | grep '\(call\|main\)' 

08053270 <main>:
8053270:    8d 4c 24 04             lea    0x4(%esp),%ecx
--
8053299:    89 1c 24                mov    %ebx,(%esp)
805329c:    e8 8f 60 ff ff          call   8049330 <strlen@plt>
80532a1:    8d 04 03                lea    (%ebx,%eax,1),%eax
--
80532cf:    89 04 24                mov    %eax,(%esp)
80532d2:    e8 b9 c9 00 00          call   805fc90 <xmalloc_set_program_name>
80532d7:    8b 5d 9c                mov    0xffffff9c(%ebp),%ebx
--
80532e4:    89 04 24                mov    %eax,(%esp)
80532e7:    e8 b4 a7 00 00          call   805daa0 <expandargv>
80532ec:    8b 55 9c                mov    0xffffff9c(%ebp),%edx
--
8053302:    89 0c 24                mov    %ecx,(%esp)
8053305:    e8 d6 2a 00 00          call   8055de0 <prune_options>
805330a:    e8 71 ac 00 00          call   805df80 <unlock_std_streams>
805330f:    e8 4c 2f 00 00          call   8056260 <gcc_init_libintl>
8053314:    c7 44 24 04 01 00 00    movl   $0x1,0x4(%esp)
--
805331c:    c7 04 24 02 00 00 00    movl   $0x2,(%esp)
8053323:    e8 78 5e ff ff          call   80491a0 <signal@plt>
8053328:    83 e8 01                sub    $0x1,%eax

需要花费一些精力才能浏览所有的汇编代码,但是你可以看到给定函数的所有可能调用。虽然它不像 gprof 或其他一些工具那样易于使用,但它有几个明显的优点:

  • 通常情况下,您无需重新编译应用程序即可使用它
  • 它显示所有可能的函数调用,而像 gprof 这样的工具只会显示已执行的函数调用。

1

Gprof 可能是你想要的


3
我不想对代码进行分析,只想追踪它。我想知道每次调用本地函数时传入了哪些参数,以及返回值是什么。另外,我也不想为了使用特定工具而重新编译程序,就像 gprof 需要的那样。 - Robert Gamble

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接