void __cyg_profile_func_enter (void *this_fn, void *call_site); void __cyg_profile_func_exit (void *this_fn, void *call_site);The first argument is the address of the start of the current function, which may be looked up exactly in the symbol table.
#include <stdio.h> #define DUMP(func, call) / printf("%s: func = %p, called by = %p/n", __FUNCTION__, func, call) void __attribute__((__no_instrument_function__)) __cyg_profile_func_enter(void *this_func, void *call_site) { DUMP(this_func, call_site); } void __attribute__((__no_instrument_function__)) __cyg_profile_func_exit(void *this_func, void *call_site) { DUMP(this_func, call_site); } main() { puts("Hello World!"); return 0; }編譯與執行:
$ gcc -finstrument-functions hello.c -o hello $ ./hello __cyg_profile_func_enter: func = 0x8048468, called by = 0xb7e36ebc Hello World! __cyg_profile_func_exit: func = 0x8048468, called by = 0xb7e36ebc看到 "__attribute__" 就知道一定是 GNU extension,而前述的 man page 也提到 -finstrument-functions 會在每次進入與退出函式前呼叫 "__cyg_profile_func_enter" 與 "__cyg_profile_func_exit" 這兩個 hook function。等等,「進入」與「退出」是又何解?C Programming Language 最經典之處在於,雖然沒有定義語言實做的方式,但實際上 function call 皆以 stack frame 的形式存在,去年在「深入淺出 Hello World」Part II 有提過。所以上述那一大段英文就是說,如果我們不透過 GCC 內建函式 "__builtin_return_address" 取得 caller 與 callee 相關的動態位址,那麼仍可透過 -finstrument-functions,讓 GCC 合成相關的處理指令,讓我們得以追蹤。而看到 __cyg 開頭的函式,就知道是來自 Cygnus 的貢獻,在 gcc 2.x 內部設計可瞥見不少。
$ gcc -g -finstrument-functions wrong.c -o wrong $ ./wrong 程式記憶體區段錯誤發生什麼事情呢?請出 gdb 協助:
$ gdb ./wrong GNU gdb 6.6-debian Copyright (C) 2006 Free Software Foundation, Inc. GDB is free software, covered by the GNU General Public License, and you are welcome to change it and/or distribute copies of it under certain conditions. Type "show copying" to see the conditions. There is absolutely no warranty for GDB. Type "show warranty" for details. This GDB was configured as "i486-linux-gnu"... Using host libthread_db library "/lib/tls/i686/cmov/libthread_db.so.1". (gdb) run Starting program: /home/jserv/HelloWorld/helloworld/instrument/wrong Program received signal SIGSEGV, Segmentation fault. 0x0804841d in __cyg_profile_func_enter (this_func=0x8048414, call_site=0x804842d) at wrong.c:7 7 { (gdb) bt #0 0x0804841d in __cyg_profile_func_enter (this_func=0x8048414, call_site=0x804842d) at wrong.c:7 #1 0x0804842d in __cyg_profile_func_enter (this_func=0x8048414, call_site=0x804842d) at wrong.c:7 #2 0x0804842d in __cyg_profile_func_enter (this_func=0x8048414, call_site=0x804842d) at wrong.c:7 ... #30 0x0804842d in __cyg_profile_func_enter (this_func=0x8048414, call_site=0x804842d) at wrong.c:7 #31 0x0804842d in __cyg_profile_func_enter (this_func=0x8048414, call_site=0x804842d) at wrong.c:7 #32 0x0804842d in __cyg_profile_func_enter (this_func=0x8048414, call_site=0x804842d) at wrong.c:7 ---Type既然 "__cyg_profile_func_enter" 是 function hook,則本身不得也被施以 "function instrument",否則就無止盡遞迴了,不過我們也可以發現一件有趣的事情:to continue, or q to quit---
$ nm wrong | grep 8048414 08048414 T __cyg_profile_func_enter如我們所見,"__cyg_profile_func_enter" 的位址被不斷代入 __cyg_profile_func_enter function arg 中。GNU binutils 裡面有個小工具 addr2line,我們可以該工具取得虛擬位址對應的行號或符號:
$ addr2line -e wrong 0x8048414 /home/jserv/HelloWorld/helloworld/instrument/wrong.c:7就 Linux 應用程式開發來說,我們可透過這個機制作以下應用:
$ objdump -d hello | grep -B2 puts ... (省略) ... -- 8048488: e8 87 ff ff ff call 8048414 <__cyg_profile_func_enter> 804848d: c7 04 24 df 85 04 08 movl $0x80485df,(%esp) 8048494: e8 c3 fe ff ff call 804835c看來 GCC 自動生成 __cyg_profile_func_enter 的 function call 動作,並置於 puts 呼叫之前,我們繼續觀察:
$ objdump -d hello | grep -A5 puts ... (省略) ... -- 8048494: e8 c3 fe ff ff call 804835cputs 呼叫之後,GCC 也自動生成 __cyg_profile_func_exit 的 function call 動作,這樣的技巧也被用於 KFT (Kernel Function Trace,前身為 KFI [Kernel function Instrumentation]),同樣以 GCC "-finstrument-functions" 來達到對每個 Linux Kernel function enter/exit 的追蹤功能,但得考慮大量的 static inline function。8048499: bb 00 00 00 00 mov $0x0,%ebx 804849e: 8b 45 04 mov 0x4(%ebp),%eax 80484a1: 89 44 24 04 mov %eax,0x4(%esp) 80484a5: c7 04 24 68 84 04 08 movl $0x8048468,(%esp) 80484ac: e8 8d ff ff ff call 804843e <__cyg_profile_func_exit>
每次都覺得jserv大的文都像是變魔術...
很神
用 Graphviz 可视化函数调用使用开源软件来简化复杂调用结构 |
级别: 初级 M. Tim Jones (mtj@mtjones.com), 资深软件工程师, Emulex 2005 年 7 月 11 日 花一些时间遍历一下源代码,可以向您展现所有的函数调用过程;但是如果函数指针非常复杂,或者代码太长且晦涩难懂,那么这个过程就可能更加困难了。本文将向您介绍如何使用开源软件和一些定制的代码来构建一个动态的图形函数调用生成器。 可以将以图形形式查看应用程序的调用过程看作是一个学习经历。这样做可以帮助您理解应用程序的内部行为,并获得有关程序优化方面的信息。例如,通过对那些经常调用的函数进行优化,您就可以用最少的努力来获得最佳的性能。另外,调用跟踪还可以判断用户函数的最大调用深度,这可以用来对调用栈使用的内存进行有效限制(在嵌入式系统中,这是非常重要的一个考虑因素)。 为了捕获并显示调用图,您需要 4 个元素:GNU 编译器工具链、Addr2line 工具、定制的中间代码和一个名为 Graphviz 的代码。Addr2line 工具可以识别函数、给定地址的源代码行数和可执行映像。定制的中间代码是一个非常简单的工具,它可以减少对图形规范的地址跟踪。Graphviz 工具可以生成图形映像。整个过程如图 1 所示。 图 1. 搜集、简化和可视化跟踪路径的过程 要收集一个函数调用的踪迹,您需要确定每个函数在应用程序中调用的时间。在过去,都是通过在函数的入口处和退出处插入一个惟一的符号来手工检测每个函数的。这个过程非常繁琐,而且很容易出错,通常需要对源代码进行大量的修改。 幸运的是,GNU 编译器工具链(也称为 gcc)提供了一种自动检测应用程序中的各个函数的方法。在执行应用程序时,就可以收集相关的分析数据。您只需要提供两个特殊的分析函数即可。其中一个函数在每次执行想要跟踪的函数时都会调用;而另外一个函数则在每次退出想要跟踪的函数时调用(参见清单 1)。这两个函数都是特别指定的,因此,编译器可以识别它们。 清单 1. GNU 的入口和出口配置函数
在调用一个检测函数时,
在这些分析函数中,您可以记录下地址对,以供以后再进行分析使用。要请求 gcc 所有的检测函数,每个文件都必须使用 因此,现在您就可以为 gcc 提供一些分析函数了,这些函数可以透明地插入应用程序中的函数入口点和函数退出点。但在调用分析函数时,又应该怎样处理所提供的地址呢?您有很多选择,但是为了简便起见,可以将这个地址简单地写入一个文件,要注意哪个地址是函数的入口地址,哪个地址是函数的出口地址(参见清单 2)。 注意:在清单 2 中并没有使用调用 Callsite 信息,因为这些信息对于分析程序来说是不必要的。 清单 2. 分析函数
现在您可以搜集分析数据了,但是您应该在什么地方打开或关闭您的跟踪输出文件呢?到现在为止,还不需要为了进行分析而对源程序进行任何修改。因此,您该如何检测整个应用程序(包括
要创建 constructor 和 destructor 函数,则需要声明两个函数,然后对这两个函数应用 清单 3. 分析 constructor 和 destructor 函数
如果编译分析函数(在 instrument.c)并将它们与目标应用程序链接在一起,然后再执行目标应用程序,结果会生成一个应用程序的调用追踪,追踪记录被写入 trace.txt 文件。跟踪文件与调用的应用程序处于相同的目录中。最终结果是,您可能会得到一个其中满是地址的非常大的文件。为了能够让这些数据更有意义,您可以使用一个不太出名的叫做 Addr2line 的 GNU 工具。
Addr2line 工具(它是标准的 GNU Binutils 中的一部分)是一个可以将指令的地址和可执行映像转换成文件名、函数名和源代码行数的工具。这种功能对于将跟踪地址转换成更有意义的内容来说简直是太棒了。
要了解这个过程是怎样工作的,我们可以试验一个简单的交互式的例子。(我直接从 shell 中进行操作,因为这是最简单地展示这个过程的方法,如清单 4 所示。)这个示例 C 文件(test.c)是通过
在调用 Addr2line 工具时,要使用 清单 4. addr2line 的一个交互式例子
现在您有了一个可以搜集函数函数地址的追踪数据的方法,还可以使用 Addr2line 工具将地址转换为函数名。然而,从应用程序中产生大量的跟踪数据之后,如何对这些数据进行精简,从而使其更有意义呢?这就是使用一些定制的中间代码在开源工具之间建立联系的地方。本文提供了这个工具(Pvtrace)的带有注释的完整代码,包括如何编译和使用该工具的一些说明。(有关的更多信息,请参阅 下载 一节。) 回想一下图 1 中的内容,在执行设置了检测函数的应用程序时,会创建一个名为 trace.txt 的文本文件。这个人们可以读取的文件中包含了一系列地址信息 —— 每行一个地址,每行都有一个前缀字符。如果前缀是 E,那么这个地址就是一个函数的入口地址(也就是说,您正在调用这个函数)。如果前缀是一个 X 字符,那么这个地址就是一个出口地址(也就是说,您正在从这个函数中退出)。 因此,如果在跟踪文件中有一个入口地址(A)紧跟着另外一个入口地址(B),那么您就可以推断是 A 调用了 B。如果一个入口地址(A)后面跟着一个出口地址(A),那么就说明这个函数(A)被调用后就直接返回了。当涉及大量的调用链时,就很难分析究竟是谁调用了谁,因此,一种简单的解决方案是维护一个整个地址的堆栈。每次在跟踪文件中碰到一个入口地址时,就将其压入堆栈。栈顶的地址就代表最后一次被调用的函数(也就是当前的活动函数)。如果后面紧接着是另外一个入口地址,这说明堆栈中的地址调用了这个刚从跟踪文件处读出的地址。在碰到退出函数时,当前的活动函数就会返回,并释放栈顶元素。这会将上下文返到回前一个函数,由此,就可以产生正确的调用链过程。 图 2 介绍了这个概念,以及精简数据的方法。在分析跟踪文件中的调用链时,会构建一个连通矩阵,用来表示哪个函数调用了其他哪些函数。这个矩阵的行表示调用函数的地址,列表示被调用的地址。对于每个调用对来说,行与列的交叉点不断进行累加(调用次数)。当处理完整个跟踪文件时,其结果是该应用程序的整个调用历史的一个非常简单的表示,其中包含了调用的次数。 图 2. 对跟踪数据进行处理和精简,并生成矩阵格式
现在我们已经构建了简化的函数连通性矩阵,接下来应该构建图形的表示了。让我们深入研究 Graphviz,了便理解如何从连通矩阵生成一个调用图。
Graphviz 或 Graph Visualization 是由 AT&T 开发的一个开源的图形可视化工具。它提供了多种画图能力,但是我们重点关注的是它使用 Dot 语言直连图的能力。在本文中,我们将简单介绍如何使用 Dot 来创建一个图形,并展示如何将分析数据转换成 Graphviz 可以使用的规范。(请参阅 参考资料 一节,以获得有关下载这个开源软件的信息。) 使用 Dot 语言,您可以指定三种对象:图、节点和边。为了让您理解这些对象的含义,我们将构建一个例子来展示这些元素的用法。
清单 5 给出了一个简单的定向图(directed graph),其中包含 3 个节点。第一行声明这个图为 G,并且声明了该图的类型(digraph)。接下来的三行代码用于创建该图的节点,这些节点分别名为 node1、node2 和 node3。节点是在它们的名字出现在图规范中时创建的。边是在在两个节点使用边操作( 清单 5. 使用 Dot 符号表示的示例图(test.dot)
要将这个 .dot 文件转换成一个图形映像,则需要使用 Dot 工具,这个工具是在 Graphviz 包中提供的。清单 6 介绍了这种转换。 清单 6. 使用 Dot 来创建 JPG 映像
在这段代码中,我告诉 Dot 使用 test.dot 图形规范,并生成一个 JPG 图像,将其保存在文件 test.jpg 中。所生成的图像如图 3 所示。在此处,我使用了 JPG 格式,但是 Dot 工具也可以支持其他格式,其中包括 GIF、PNG 和 postscript。 图 3. Dot 创建的示例图 Dot 语言还可以支持其他一些选项,包括外形、颜色和很多属性。但是就我们想要实现的功能而言,这个选项就足够了。
现在我们已经看到了整个过程的各个阶段了,下面可以采用一个例子来展示如何将这些阶段合并在一起了。现在,您应该已经展开并安装了 Pvtrace 工具,然后还需要将 instrument.c 文件复制到工作源代码目录中。
在这个例子中,我使用了一个源文件 test.c 进行检测。清单 7 给出了整个过程。在第 3 行中,我使用检测源(instrument.c)来构建(编译并连接)应用程序。然后在第 4 行执行 清单 7. 创建调用跟踪图的整个过程
这个过程的示例输出如图 4 所示。这个示例图是从使用 Q 学习的一个简单增强式学习应用程序中得到的。 图 4. 示例应用程序的跟踪结果 您也可以使用这种方法对更大的应用程序进行分析。我要展示的最后一个例子是 Gzip 工具。我简单地将 instrument.c 加入 Gzip 的 Makefile 中,作为其依赖的一个源文件,然后编译 Gzip,并使用它生成一个跟踪文件。这个图形太大了,不太容易进行更详细的分析,但是下图表示了 Gzip 对一个小文件进行压缩时的处理过程。 图 5. Gzip 跟踪结果
使用开源软件和少量的中间代码,只需要花很少的时间就可以开发出非常有用的项目。通过使用对应用程序进行分析的几个 GNU 编译器扩展,可以使用 Addr2line 工具进行地址转换,并对 Graphviz 应用程序进行图形可视化,然后您就可以得到一个程序,该程序可以对应用程序进行分析,并展示一个说明调用链的定向图。通过图形来查看一个应用程序的调用链对于理解应用程序的内部行为来说非常重要。在正确了解调用链及其各自的频率之后,这些知识可能对调试和优化应用程序非常有用。
|