Как получить граф вызывающего из заданного символа в двоичном
Этот вопрос связан с вопросом, который я задал ранее в этот день: мне интересно, возможно ли сгенерировать график звонящего из заданной функции (или имени символа, например, взятого из nm
), даже если интересующая функция не является частью "моего" исходного кода (например, находится в библиотеке, например malloc()
)
Например знать где malloc
используется в моей программе с именем foo
Я бы сначала посмотрел имя символа:
nm foo | grep malloc
U malloc@@GLIBC_2.2.5
А затем запустите инструмент (для которого может потребоваться специально скомпилированная / связанная версия моей программы или некоторые артефакты компилятора):
find_usages foo-with-debug-symbols "malloc@@GLIBC_2.2.5"
Который будет генерировать (текстовый) график звонящего, который я затем смогу обработать дальше.
Читая этот вопрос, я нашел radare2, который, кажется, выполняет почти все, что вы можете себе представить, но каким-то образом мне пока не удалось сгенерировать график вызывающего абонента из данного символа...
Прогресс
С помощью radare2
Мне удалось создать dot
Граф вызывающего из исполняемого файла, но что-то по-прежнему не хватает. Я компилирую следующую программу на C++, которую, я уверен, должен использовать malloc()
или же new
:
#include <string>
int main() {
auto s = std::string("hello");
s += " welt";
return 0;
}
Я компилирую его со статическими библиотеками, чтобы быть уверенным, что все вызовы, которые я хочу проанализировать, могут быть найдены в двоичном файле:
g++ foo.cpp -static
Запустив nm a.out | grep -E "_Znwm|_Znam|_Znwj|_Znaj|_ZdlPv|_ZdaPv|malloc|free"
Вы можете увидеть много символов, которые используются для выделения памяти.
Теперь я бегу radare2
на исполняемом файле:
r2 -qAc 'agCd' a.out > callgraph.dot
С небольшим сценарием (вдохновленным этим ответом) я ищу путь вызова из любого символа, содержащего "sym.operatornew", но, похоже, его нет!
Есть ли способ убедиться, что вся информация, необходимая для генерации графа вызовов из / в любую функцию, которая вызывается внутри этого двоичного файла?
Есть ли лучший способ запустить radare2? Похоже, что разные типы визуализации графа вызовов предоставляют разную информацию - например, генератор изображений ascii действительно предоставляет имена для символов, не предоставляемых генератором точек, в то время как генератор точек предоставляет гораздо больше деталей относительно вызовов.
1 ответ
В общем, вы не можете извлечь точный граф потока управления из двоичного файла из-за косвенных переходов и вызовов там. Косвенный вызов машинного кода запрыгивает в содержимое какого-либо регистра, и вы не можете надежно оценить все значения, которые может принять регистр (это может оказаться доказанным эквивалентным проблеме остановки).
Есть ли способ убедиться, что вся информация, необходимая для генерации графа вызовов из / в любую функцию, которая вызывается внутри этого двоичного файла?
Нет, и эта проблема эквивалентна проблеме остановки, поэтому никогда не будет надежного способа получить этот граф вызовов (в полной и надежной форме).
Компилятор C++ (обычно) генерирует косвенные переходы для вызовов виртуальных функций (они переходят через виртуальную таблицу) и, вероятно, при использовании разделяемой библиотеки (подробнее читайте в статье Дреппера " Как писать общие библиотеки").
Посмотрите на инструмент BINSEC (разработанный коллегами из CEA, LIST и INRIA), по крайней мере, чтобы найти ссылки.
Если вы действительно хотите найти большинство (но не все) динамических выделений памяти в исходном коде C++, вы можете использовать статический анализ исходного кода (например, Frama-C или Frama-Clang) и другие инструменты, но они не являются серебряной пулей.
Помните, что распределение функций, таких как malloc
или же operator new
может быть помещен в расположение указателя функции (и ваш код C++ может иметь где-то глубоко скрытый распределитель, тогда вы, вероятно, будете иметь косвенные вызовы malloc
)
Возможно, вы могли бы потратить месяцы усилий на написание своего собственного плагина GCC для поиска звонков malloc
после оптимизации внутри компилятора GCC (но обратите внимание, что плагины GCC привязаны к одной конкретной версии GCC). Я не уверен, что оно того стоит. Мой старый (устаревший, не обслуживаемый) проект GCC MELT смог найти звонки malloc
с размером выше некоторой заданной константы. Возможно, по крайней мере через год - конец 2019 года или позже - мой проект-преемник ( bismon, финансируемый проектом CHARIOT H2020) может стать достаточно зрелым, чтобы помочь вам.
Помните также, что GCC способен к довольно причудливой оптимизации, связанной сmalloc
, Попробуйте скомпилировать следующий код C
//file mallfree.c
#include <stdlib.h>
int weirdsum(int x, int y) {
int*ar2 = malloc(2*sizeof(int));
ar2[0] = x; ar2[1] = y;
int r = ar2[0] + ar2[1];
free (ar2);
return r;
}
сgcc -S -fverbose-asm -O3 mallfree.c
, Вы увидите, что генерируетсяmallfree.s
ассемблерный файл не содержит вызова malloc
или free
, Такая оптимизация разрешена правилом" как если" и практически полезна для оптимизации большинства случаев использования стандартных контейнеров C++.
Поэтому то, что вы хотите, непросто даже для явно "простого" кода C++ (и в общем случае невозможно).
Если вы хотите закодировать плагин GCC и потратить на этот вопрос более года (или могли бы заплатить за это не менее 500 тыс. Евро), свяжитесь со мной. Смотрите также https://xkcd.com/1425/ (ваш вопрос практически невозможен).
Кстати, что вас действительно волнует, так это динамическое выделение памяти в оптимизированном коде (вы действительно хотите встраивание и устранение мертвого кода, и GCC делает это довольно хорошо с -O3
или же -O2
). Когда GCC не оптимизируется вообще (например, с -O0
которая является неявной оптимизацией), она будет выполнять много "бесполезного" динамического выделения памяти, особенно с кодом C++ (используя стандартную библиотеку C++). См. Также CppCon 2017: Мэтт Годболт "Что мой компилятор сделал для меня в последнее время? Откручиваем крышку компилятора ".