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

Edit

Несколько разъяснений:

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

[[email protected] cpp]$ g++ -g3 test.cpp
[[email protected] 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:) К сожалению, gcc выводит некоторые сообщения "Current Language changed" между ними. Но это легко вышло. Ничего страшного.

Ответ 2

System Tap можно использовать в современном ящике Linux (Fedora 10, RHEL 5 и т.д.).

Сначала загрузите para-callgraph.stp script.

Затем запустите:

$ 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

Смотрите также: Наблюдать, обновлять системные файлы и oprofile

Ответ 3

Использование 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

list of functions in datalog binarycall tree when selecting dl_pushlstring, showing how main called loadfile called dl_load called program called rule which called literal which in turn called other functions that ended up calling dl_pushlstring, scan (parent: program, that is, the third scan from the top) which called dl_pushstring and so on

Ответ 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 ферма (эквивалент strace) имеет возможность фильтровать отслеживаемую библиотеку. Я был удивлен, когда обнаружил, что у strace нет такой возможности.

Ответ 5

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

Подробнее: ftrace.1

Ответ 6

Если вы внесете эту функцию во внешнюю библиотеку, вы также сможете увидеть ее вызов (с помощью ltrace).

Причина этого в том, что ltrace ставит себя между вашим приложением и библиотекой, и когда весь код интернализируется с одним файлом, он не может перехватить вызов.

т.е.: ltrace xterm

извергает материал из X-библиотек, а X вряд ли является системой.

Вне этого, единственный реальный способ сделать это - перехват времени компиляции через флагов prof или символы отладки.

Я просто просмотрел это приложение, которое выглядит интересно:

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

Но я не думаю, что ты хочешь.

Ответ 7

Если функции не встроены, вам может даже повезти с помощью 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 <[email protected]>
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 <[email protected]>
8053328:    83 e8 01                sub    $0x1,%eax

Требуется немного усилий, чтобы пробраться через весь ассемблер, но вы можете видеть все возможные вызовы от данной функции. Это не так просто использовать как gprof или некоторые другие упомянутые утилиты, но имеет несколько отличительных преимуществ:

  • Обычно вам не нужно перекомпилировать приложение для его использования.
  • Он показывает все возможные вызовы функций, тогда как что-то вроде gprof будет показывать только выполненные вызовы функций.

Ответ 8

Существует оболочка script для автоматизации вызовов функций трассировки с помощью 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".

Этот script довольно медленный на большом порте (~ тысячи функций), поэтому я добавляю фильтр в список функций (через egrep). Это было очень просто, и я использую этот script почти день.

Ответ 9

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

Ответ 10

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

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

Ответ 11

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 выглядит так:

s7dYK.png

Из этого мы видим, что:

  • корневым узлом является _start, который является фактической точкой входа ELF и содержит шаблон инициализации glibc
  • f0, f1 и f2 вызываются, как и ожидалось, друг от друга
  • pointed также показано, хотя мы вызывали его с указателем на функцию. Возможно, он не был вызван, если мы передали аргумент командной строки.
  • not_called не отображается, потому что он не вызывался во время выполнения, потому что мы не not_called дополнительный аргумент командной строки.

Крутая вещь в valgrind состоит в том, что он не требует никаких специальных параметров компиляции.

Следовательно, вы можете использовать его, даже если у вас нет исходного кода, только исполняемый файл.

valgrind удается сделать это, запустив ваш код через облегченную "виртуальную машину".

Проверено на Ubuntu 18.04.

Ответ 13

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

https://github.com/leviathansecurity/ftrace