Инструмент для отслеживания локальных вызовов функций в 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

  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, вызывал программу под названием rule, которая вызывала литерал, который в свою очередь вызывал другие функции, которые в итоге вызывали dl_pushlstring, scan (parent: program, то есть третье сканирование сверху), который вызывал dl_pushstring и так далее

Предполагая, что вы можете перекомпилировать (без изменения исходного кода) код, который вы хотите отследить с помощью опции 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). Это было очень легко, и я использую этот скрипт почти каждый день.

Gprof может быть тем, что вы хотите

Посмотрите трассировки, инфраструктуру трассировки для приложений Linux C/C++: https://github.com/baruch/traces

Он требует перекомпиляции вашего кода с его инструментарием, но предоставит список всех функций, их параметров и возвращаемых значений. Есть интерактив, позволяющий легко перемещаться по большим выборкам данных.

ПРИМЕЧАНИЕ. Это не ftrace на основе ядра Linux, а инструмент, который я недавно разработал для выполнения локальной трассировки функций и управления потоком. Linux ELF x86_64/x86_32 поддерживаются публично.

https://github.com/leviathansecurity/ftrace

Надеемся, что инструменты callgrind или cachegrind для Valgrind предоставят вам необходимую информацию.

Другие вопросы по тегам