أداة لتتبع مكالمات الوظائف المحلية في Linux
سؤال
أنا أبحث عن أداة مثل ltrace أو strace التي يمكن أن تتبع وظائف محددة محليا في تنفيذ. يتتبع 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.
المحلول
على افتراض أنك تريد فقط أن يتم إخطارها لوظائف محددة ، يمكنك القيام بذلك على هذا النحو:
تجميع معلومات التصحيح (بما أن لديك بالفعل معلومات رمزية ، وربما يكون لديك أيضًا ما يكفي من الأخطاء)
معطى
#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 بإخراج بعض الرسائل "اللغة الحالية". ولكن هذا هو بسهولة خبث. ليس مهما.
نصائح أخرى
صنبور النظام يمكن استخدامها على صندوق 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
أنظر أيضا: مراقبة التحديثات ، SystemTap و Oprofile
استخدام uprobes (منذ Linux 3.5)
على افتراض أنك تريد تتبع جميع الوظائف في ~/Desktop/datalog-2.2/datalog
عند تسميتها بالمعلمات -l ~/Desktop/datalog-2.2/add.lua ~/Desktop/datalog-2.2/test.dl
cd /usr/src/linux-`uname -r`/tools/perf
for i in `./perf probe -F -x ~/Desktop/datalog-2.2/datalog`; do sudo ./perf probe -x ~/Desktop/datalog-2.2/datalog $i; done
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
sudo ./perf report -G
على افتراض أنه يمكنك إعادة ترجمة (لا يوجد تغيير في المصدر) الرمز الذي تريد تتبعه مع خيار 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
على سولاريس ، يتمتع الجمالون (مكافئ Strace) بالقدرة على تصفية المكتبة ليتم تتبعها. لقد فوجئت عندما اكتشفت أن Strace ليس لديه مثل هذه القدرة.
$ sudo yum install frysk
$ ftrace -sym:'*' -- ./a.out
أكثر: ftrace.1
إذا قمت بإجراء هذه الوظيفة الخارجية في مكتبة خارجية ، فيجب أن تكون قادرًا أيضًا على رؤيتها يتم استدعاؤها ، (مع LTRACE).
السبب في أن هذا يعمل لأن Ltrace يضع نفسه بين تطبيقك والمكتبة ، وعندما يتم استيعاب جميع التعليمات البرمجية مع الملف الوحيد ، لا يمكن اعتراض المكالمة.
أي: ltrace xterm
ينشر الأشياء من مكتبات X ، و X بالكاد نظام.
خارج هذا ، فإن الطريقة الحقيقية الوحيدة للقيام بذلك هي اعتراض وقت الترجمة عبر أعلام البروفيسور أو رموز التصحيح.
لقد ركضت للتو على هذا التطبيق ، والذي يبدو مثيرًا للاهتمام:
http://www.gnu.org/software/cflow/
لكني لا أعتقد أن هذا ما تريد.
إذا لم تكن الوظائف محصورة ، فقد يكون لديك محظوظ باستخدام 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
سوف تظهر فقط مكالمات الوظيفة المنفذة.
يوجد برنامج نصي 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-project-debugger/
نسخة من الأداة - callgraph.tar.gz
http://web.archive.org/web/20090317091725/http://superadditive.com/software/callgraph.tar.gz
يتفريغ جميع الوظائف من البرنامج وإنشاء ملف أمر GDB مع نقاط التوقف على كل وظيفة. في كل نقطة انقطاع ، يتم تنفيذ "Backtrace 2" و "Contens".
هذا البرنامج النصي بطيء إلى حد ما على الشفرة الكبيرة (~ آلاف الوظائف) ، لذلك أقوم بإضافة مرشح في قائمة الوظائف (عبر EGREP). كان الأمر سهلاً للغاية ، وأنا أستخدم هذا البرنامج النصي تقريبًا.
GPROF قد يكون ما تريد
انظر Traces ، إطار تتبع لتطبيقات Linux C/C ++:https://github.com/baruch/traces#readme
إنه يتطلب إعادة تجميع الكود الخاص بك مع أداةها ، ولكنها ستوفر قائمة بجميع الوظائف ومعلماتها وقيم الإرجاع. هناك تفاعلي للسماح بسهولة التنقل في عينات البيانات الكبيرة.
KcacheGrind
https://kcachegrind.github.io/html/Home.html
Test program:
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;
}
Usage:
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
You are now left inside an awesome GUI program that contains a lot of interesting performance data.
On the bottom right, select the "Call graph" tab. This shows an interactive call graph that correlates to performance metrics in other windows as you click the functions.
To export the graph, right click it and select "Export Graph". The exported PNG looks like this:
From that we can see that:
- the root node is
_start
, which is the actual ELF entry point, and contains glibc initialization boilerplate f0
,f1
andf2
are called as expected from one anotherpointed
is also shown, even though we called it with a function pointer. It might not have been called if we had passed a command line argument.not_called
is not shown because it didn't get called in the run, because we didn't pass an extra command line argument.
The cool thing about valgrind
is that it does not require any special compilation options.
Therefore, you could use it even if you don't have the source code, only the executable.
valgrind
manages to do that by running your code through a lightweight "virtual machine".
Tested on Ubuntu 18.04.
Hopefully the callgrind or cachegrind tools for Valgrind will give you the information you seek.
NOTE: This is not the linux kernel based ftrace, but rather a tool I recently designed to accomplish local function tracing and control flow. Linux ELF x86_64/x86_32 are supported publicly.