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

2024-10-11 08:36:00
admin
原创
90
摘要:问题描述:我正在寻找一种像ltrace或strace这样的工具,它可以跟踪可执行文件中本地定义的函数。ltrace 仅跟踪动态库调用,而 strace 仅跟踪系统调用。例如,给定以下 C 程序:#include <stdio.h> int triple ( int x ) { ret...

问题描述:

我正在寻找一种像ltrace或strace这样的工具,它可以跟踪可执行文件中本地定义的函数。ltrace 仅跟踪动态库调用,而 strace 仅跟踪系统调用。例如,给定以下 C 程序:

#include <stdio.h>

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

int main (void)
{
  printf("%d
", triple(10));
  return 0;
}

使用 运行程序ltrace将显示对 的调用,printf因为这是一个标准库函数(在我的系统上是一个动态库),并将strace显示启动代码中的所有系统调用、用于实现 printf 的系统调用以及关闭代码,但我想要一些可以显示该函数triple被调用的东西。假设本地函数尚未被优化编译器内联,并且二进制文件尚未被剥离(删除符号),是否有可以执行此操作的工具?

编辑

需要澄清几点:

  • 如果该工具还提供非本地函数的跟踪信息则没问题。

  • 我不想重新编译支持特定工具的程序,可执行文件中的符号信息就足够了。

  • 如果我能使用该工具附加到现有进程,就像使用 ltrace/strace 一样,那就太好了。


解决方案 1:

假设您只想收到特定功能的通知,您可以这样做:

使用调试信息进行编译(因为您已经有符号信息,所以您可能也有足够的调试)

给定

#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 删除。没什么大不了的。

解决方案 2:

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:

使用Uprobes(自 Linux 3.5 起)

假设你想在~/Desktop/datalog-2.2/datalog使用参数调用它时跟踪所有函数-l ~/Desktop/datalog-2.2/add.lua ~/Desktop/datalog-2.2/test.dl

  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

数据记录二进制中的函数列表
选择 dl_pushlstring 时的调用树,显示了 main 如何调用 loadfile,如何调用 dl_load,如何调用 program,如何调用 rule,如何调用 literal,如何依次调用其他函数,最终调用 dl_pushlstring,scan(父级:program,即从顶部开始的第三个 scan)如何调用 dl_pushstring 等等

解决方案 4:

假设您可以使用 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 没有这样的功能时,我感到很惊讶。

解决方案 5:

缓存研磨

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 初始化样板

  • f0f1并按f2预期互相调用

  • pointed也会显示,尽管我们用函数指针调用了它。如果我们传递了命令行参数,它可能不会被调用。

  • not_called没有显示,因为它在运行中没有被调用,因为我们没有传递额外的命令行参数。

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

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

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

在 Ubuntu 18.04 上测试。

解决方案 6:

由于这个问题在搜索结果中非常突出,我将添加另一种方法,15 年后,这种方法比这里列出的其他方法麻烦更少:uftrace

它需要编译相应的应用程序-pg -g(或者-finstrument-functions如果您只对函数名称感兴趣,而不关心参数和返回值)。然后您可以以交互方式运行该命令:

uftrace -a --no-libcall -f none <cmd>

或者,也可以先记录跟踪数据,然后单独输出。

uftrace record -a --no-libcall -f none <cmd>
uftrace replay

后者也可以跨平台工作,例如,您可以在系统 A 上运行记录阶段,将跟踪数据(目录uftrace.data)传输到系统 B,然后在机器 B 上重放。

使用的选项:

  • -a启用所有参数和返回值的输出

  • --no-libcall隐藏所有标准 libc 函数

  • -f none隐藏通常打印在前面的持续时间和线程 ID 列

另一个有用的选项是-N,例如,-N log_*过滤掉所有以 开头的函数调用log_。有关更多信息,请参阅 的联机帮助页uftrace-replay

如果输出缺少返回值,请禁用链接时优化后重试。由于某种原因,它们在我的测试中被隐藏了。

使用原始选项以交互方式运行 OP 示例(注意:30 是程序到 stdout 的输出):

# uftrace a.out 
30
# DURATION     TID     FUNCTION
   0.671 us [802533] | __monstartup();
   0.421 us [802533] | __cxa_atexit();
            [802533] | main() {
   0.060 us [802533] |   triple();
  11.882 us [802533] |   printf();
  12.263 us [802533] | } /* main */

C++ fac 示例仅展示要点:

# uftrace --no-libcall -a a.out
1
1
2
6
# DURATION     TID     FUNCTION
            [803250] | _GLOBAL__sub_I_fac() {
 107.551 us [803250] |   __static_initialization_and_destruction_0(1, 65535);
 108.473 us [803250] | } /* _GLOBAL__sub_I_fac */
            [803250] | main() {
   0.211 us [803250] |   fac(0) = 1;
            [803250] |   fac(1) {
   0.080 us [803250] |     fac(0) = 1;
   0.491 us [803250] |   } = 1; /* fac */
            [803250] |   fac(2) {
            [803250] |     fac(1) {
   0.070 us [803250] |       fac(0) = 1;
   0.340 us [803250] |     } = 1; /* fac */
   0.541 us [803250] |   } = 2; /* fac */
            [803250] |   fac(3) {
            [803250] |     fac(2) {
            [803250] |       fac(1) {
   2.464 us [803250] |         fac(0) = 1;
   2.725 us [803250] |       } = 1; /* fac */
   2.916 us [803250] |     } = 2; /* fac */
   3.086 us [803250] |   } = 6; /* fac */
  33.463 us [803250] | } = 0; /* main */

解决方案 7:

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

更多内容:ftrace.1

解决方案 8:

如果将该函数外部化到外部库中,您还应该能够看到它被调用(使用 ltrace)。

这种方法之所以有效,是因为 ltrace 将自身置于您的应用程序和库之间,并且当所有代码都与一个文件内部化时,它无法拦截调用。

例如:ltrace xterm

从 X 库中喷涌出东西,而 X 几乎不是系统。

除此之外,唯一真正可行的方法是通过 prof 标志或调试符号进行编译时拦截。

我刚刚运行了这个应用程序,它看起来很有趣:

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

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

解决方案 9:

如果函数不是内联的,您甚至可能幸运地使用objdump -d <program>

举个例子,我们来看看 GCC 4.3.2 例程开头的 loot 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只会显示已执行的函数调用。

解决方案 10:

有一个 shell 脚本可以使用 gdb 自动跟踪函数调用。但是它无法附加到正在运行的进程。

blog.superadditive.com/2007/12/01/call-graphs-using-the-gnu-project-debugger/

页面副本 - http://web.archive.org/web/20090317091725/http://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)。这很容易,我几乎每天都使用这个脚本。

解决方案 11:

Gprof可能是你想要的

解决方案 12:

查看 traces,一个用于 Linux C/C++ 应用程序的跟踪框架:
https://github.com/baruch/traces#readme

它需要用其工具重新编译您的代码,但会提供所有函数及其参数和返回值的列表。有一个交互式界面,可以轻松浏览大数据样本。

解决方案 13:

希望Valgrind的callgrind 或 cachegrind 工具能够为您提供您所寻求的信息。

解决方案 14:

注意:这不是基于 Linux 内核的 ftrace,而是我最近设计的一个用于完成本地函数跟踪和控制流的工具。Linux ELF x86_64/x86_32 公开支持。

https://github.com/leviathansecurity/ftrace

相关推荐
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   601  
  华为IPD与传统研发模式的8大差异在快速变化的商业环境中,产品研发模式的选择直接决定了企业的市场响应速度和竞争力。华为作为全球领先的通信技术解决方案供应商,其成功在很大程度上得益于对产品研发模式的持续创新。华为引入并深度定制的集成产品开发(IPD)体系,相较于传统的研发模式,展现出了显著的差异和优势。本文将详细探讨华为...
IPD流程是谁发明的   7  
  如何通过IPD流程缩短产品上市时间?在快速变化的市场环境中,产品上市时间成为企业竞争力的关键因素之一。集成产品开发(IPD, Integrated Product Development)作为一种先进的产品研发管理方法,通过其结构化的流程设计和跨部门协作机制,显著缩短了产品上市时间,提高了市场响应速度。本文将深入探讨如...
华为IPD流程   9  
  在项目管理领域,IPD(Integrated Product Development,集成产品开发)流程图是连接创意、设计与市场成功的桥梁。它不仅是一个视觉工具,更是一种战略思维方式的体现,帮助团队高效协同,确保产品按时、按质、按量推向市场。尽管IPD流程图可能初看之下显得错综复杂,但只需掌握几个关键点,你便能轻松驾驭...
IPD开发流程管理   8  
  在项目管理领域,集成产品开发(IPD)流程被视为提升产品上市速度、增强团队协作与创新能力的重要工具。然而,尽管IPD流程拥有诸多优势,其实施过程中仍可能遭遇多种挑战,导致项目失败。本文旨在深入探讨八个常见的IPD流程失败原因,并提出相应的解决方法,以帮助项目管理者规避风险,确保项目成功。缺乏明确的项目目标与战略对齐IP...
IPD流程图   8  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用