Инструмент для отслеживания локальных вызовов функций в Linux
Я ищу инструмент, такой как ltrace или strace, который может отслеживать локально определенные функции в исполняемом файле. ltrace отслеживает только динамические библиотечные вызовы, а strace only отслеживает системные вызовы. Например, дана следующая 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.
14 ответов
Предполагая, что вы хотите получать уведомления только о конкретных функциях, вы можете сделать это следующим образом:
компилировать с отладочной информацией (поскольку у вас уже есть символьная информация, у вас, вероятно, также имеется достаточно отладок)
дано
#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
), вы можете делать все что угодно, печатая значение некоторого глобала, выполняя какую-либо команду оболочки или отправляя что-либо по почте, если оно попадет в fatal_bomb_exploded
function:) К сожалению, gcc выводит некоторые сообщения "Текущий язык изменен" между ними. Но это легко сгладить. Ничего страшного.
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
Смотрите также: Наблюдайте, обновления 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
В Solaris ферма (эквивалентно стрису) имеет возможность фильтровать отслеживаемую библиотеку. Я был удивлен, когда обнаружил, что у strace нет такой возможности.
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
Теперь вы остаетесь в удивительной программе с графическим интерфейсом, которая содержит много интересных данных о производительности.
Справа внизу выберите вкладку "График вызовов". Это показывает интерактивный график вызовов, который соотносится с показателями производительности в других окнах при нажатии на функции.
Чтобы экспортировать график, щелкните его правой кнопкой мыши и выберите "Экспорт графика". Экспортированный PNG выглядит так:
Из этого мы можем видеть, что:
- корневой узел
_start
, которая является фактической точкой входа ELF и содержит шаблон инициализации glibc f0
,f1
а такжеf2
называются, как и ожидалось, друг от другаpointed
также отображается, хотя мы вызывали его с указателем на функцию. Возможно, он не был вызван, если мы передали аргумент командной строки.not_called
не отображается, потому что он не вызывался во время выполнения, потому что мы не передали дополнительный аргумент командной строки.
Классная вещь о valgrind
является то, что он не требует каких-либо специальных параметров компиляции.
Следовательно, вы можете использовать его, даже если у вас нет исходного кода, только исполняемый файл.
valgrind
удается сделать это, запустив ваш код через легкую "виртуальную машину".
Проверено на Ubuntu 18.04.
$ sudo yum install frysk
$ ftrace -sym:'*' -- ./a.out
Больше: ftrace.1
Поскольку этот вопрос очень часто появляется в результатах поиска, я добавлю еще один подход, который спустя 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
скрывает столбцы продолжительности и идентификатора потока, которые обычно печатаются спереди
Еще одна полезная опция —-N
, например,-N log_*
отфильтровывает все вызовы функций, имя которых начинается сlog_
. Дополнительную информацию см. на странице руководстваuftrace-replay
.
Если в выходных данных отсутствуют возвращаемые значения, повторите попытку после отключения оптимизации времени компоновки. По какой-то причине они были скрыты в моих тестах.
Запуск примера OP в интерактивном режиме с ванильными опциями (обратите внимание: 30 — это вывод программы на стандартный вывод):
# 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 */
Если функции не встроены, вам даже может повезти, используя 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
будет показывать только выполненные вызовы функций.
Если вы перенесете эту функцию во внешнюю библиотеку, вы также сможете увидеть, как она вызывается (с помощью ltrace).
Это работает потому, что ltrace помещает себя между вашим приложением и библиотекой, и когда весь код интернализуется одним файлом, он не может перехватить вызов.
то есть: ltrace xterm
извергает вещи из библиотек X, а X вряд ли системный.
Помимо этого, единственный реальный способ сделать это - перехват во время компиляции через проф-флаги или символы отладки.
Я только что запустил это приложение, которое выглядит интересно:
http://www.gnu.org/software/cflow/
Но я не думаю, что это то, что вы хотите.
Существует скрипт оболочки для автоматизации вызовов функций трассировки с помощью 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). Это было очень легко, и я использую этот скрипт почти каждый день.
Посмотрите трассировки, инфраструктуру трассировки для приложений Linux C/C++: https://github.com/baruch/traces
Он требует перекомпиляции вашего кода с его инструментарием, но предоставит список всех функций, их параметров и возвращаемых значений. Есть интерактив, позволяющий легко перемещаться по большим выборкам данных.
ПРИМЕЧАНИЕ. Это не ftrace на основе ядра Linux, а инструмент, который я недавно разработал для выполнения локальной трассировки функций и управления потоком. Linux ELF x86_64/x86_32 поддерживаются публично.
Надеемся, что инструменты callgrind или cachegrind для Valgrind предоставят вам необходимую информацию.