Создать граф потока управления из файла сборки

Я хотел бы создать граф потока управления (CFG) из файла сборки, используя язык C. Я думал об этом, и вот мои идеи: 1. создавать блоки - обрабатывать файл сборки построчно - находить важные инструкции, такие как имя функции, имя блока, инструкции перехода, инструкции вызова или выхода/возврата и, возможно, некоторые другие - найти их может с регулярными выражениями? Но я еще не нашел реализации регулярных выражений для C в Windows. - после сопоставления инструкций выше сохранить инструкции перед сопоставлением с какой-либо структурой, и это мой блок 2. создать CFG - каким-то образом из блоков создать CFG, но пока я понятия не имею

Может ли кто-нибудь дать мне несколько советов, как это сделать? Кроме того, если есть лучший язык для этого, я был бы признателен, если бы вы сказали мне.
Спасибо за ваше время и помощь.


person Paul    schedule 20.02.2016    source источник
comment
Скорее, это зависит от того, насколько хорошо вам нужно выполнить работу. Специальные регулярные выражения на любом языке, который вам наиболее удобен, являются разумным первым приближением. У вас будут проблемы с макросами, внешними связями, вычисляемыми ветвями и т.п., но тогда идеальное решение невозможно для начала, поэтому все зависит от того, насколько хорошая эвристика вам нужна.   -  person doynax    schedule 21.02.2016
comment
Если вы выполняете специальный анализ, вы получите в лучшем случае специальные графы потока управления. Почему вам нужен плохой CFG, мне не понятно; что бы вы сделали с ним полезного?   -  person Ira Baxter    schedule 21.02.2016
comment
Для чего вам это нужно? Может быть, есть другое направление для решения реальной проблемы, стоящей за этим.   -  person Ira Baxter    schedule 21.02.2016
comment
Я хочу реализовать метод под названием «Расширенная версия проверки потока управления» с использованием утверждений для исходного кода сборки.   -  person Paul    schedule 21.02.2016
comment
C не имеет особых преимуществ для этой задачи. Существуют языки высокого уровня, упрощающие синтаксический анализ текста. Я бы использовал Perl, потому что знаю его, но есть много хороших вариантов. Питон популярен.   -  person Peter Cordes    schedule 23.02.2016
comment
Я думал об использовании Java, но пока не знаю, как это сделать.   -  person Paul    schedule 23.02.2016


Ответы (3)


Что нужно ОП, так это дисциплинированный подход к выполнению этой задачи.

Ему нужен хороший синтаксический анализатор исходного кода на ассемблере, чтобы он знал, что у него есть точное его представление. Помимо чистой части синтаксического анализа, ему придется полностью эмулировать ассемблер, включая все сложности, такие как макросы, условные блоки, счетчики нескольких местоположений, абсолютные/относительные/внешние символы и т. д. (Создайте хороший синтаксический анализатор, полагаясь исключительно на на регулярных выражениях не будет работать.)

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

(Вот трюк, который я использую в большом x86-приложении. Я хочу добавить проверку работоспособности в свой код во многих местах. Тесты работоспособности выглядят так:

  <test for some sane condition>
  jf  location+3       ; this branchs to a breakpoint in the middle of the next instruction
  cmp  al, 0xCC        ; the immediate value is a BREAKPOINT opcode

Они компактны, и точка останова возникает, когда происходит что-то плохое. Но при анализе этой программы на предмет потока управления "jmp false" иногда переходит в то, что кажется серединой инструкции. Как ОП это смоделирует?)

Следующей сложностью являются указатели на код. Код на ассемблере часто генерирует много указателей на другие инструкции, а затем прячет эти указатели в разных местах (инструкция вызова помещает их в стек данных для x86), извлекает их, а затем выполняет «непрямой jmp». Если вы хотите знать, куда может пойти этот jmp, вам нужно отслеживать возможные значения, которые могут содержаться в ячейке памяти, что означает, что вам нужно выполнить анализ потока данных (как значения попадают туда и откуда) и объединить с графом вызовов построение (не можете добраться до этой функции? Хорошо, тогда то, куда она идет, не повлияет на этот код), чтобы вычислить разумный ответ.

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

C не предназначен специально для поддержки этой задачи. Он может сделать это с достаточным количеством дополнительного пота, но это верно для любого языка программирования.

(Проверьте мою биографию на наличие такого фреймворка. OP может использовать любой фреймворк, который ему подходит).

person Ira Baxter    schedule 20.02.2016
comment
Кажется, что работы много. Я хочу сделать это для небольших программ, которые используют простой алгоритм, такой как быстрая сортировка или ханойская башня, или, может быть, какая-то программа, в которой можно открыть файл, прочитать его и записать в него или что-то в этом роде. Я получаю исходный код сборки из C, используя GCC, и я буду использовать синтаксис AT&T. Будет ли это менее сложно, чем мне кажется после прочтения вашего поста (кстати, очень интересно - я не знал обо всех аспектах задачи, которую собираюсь выполнить) - person Paul; 21.02.2016
comment
В коде на ассемблере нет простых алгоритмов. Быстрая сортировка использует рекурсию (кодовые указатели в стеке) и разделение значений для сортировки (часто реализуется с помощью указателей, а не индексов массива в ассемблере). - person Ira Baxter; 21.02.2016
comment
Если вам нужна дополнительная информация, прочитайте мое эссе о жизни после синтаксического анализа; поищите в сети или посмотрите мою биографию. - person Ira Baxter; 21.02.2016
comment
Я как-то не понимаю. Когда у меня есть сборочный файл (инструкция за инструкцией), где может возникнуть проблема, инструкция за инструкцией и поиск инструкции, которая говорит мне, что есть конец блока. Я сохраняю всю инструкцию перед этой инструкцией в какую-то структуру. После того, как я проанализирую файл, я могу каким-то образом создать блок за блоком CFG и найти преемника (ов) и предшественника. - person Paul; 21.02.2016
comment
Если бы ассемблерный код состоял из прямых переходов от одного куска кода к другому, вы конструировали бы CFG относительно простым способом. Это не так; Что делает ассемблер мощным, так это неограниченное использование указателей на данные и на код с сумасшедшими косвенными обращениями, поддерживающими эффективность. В частности, косвенный регистр jmp технически может находиться в любой ячейке памяти в адресном пространстве вашей программы. Вы не можете смоделировать это разумно с помощью блок-схемы. Таким образом, вы должны выяснить, что может быть в реестре, чтобы ограничить количество целей (анализ точек), и это сложно. - person Ira Baxter; 21.02.2016
comment
... это легкий случай. Затем идут таблицы переходов с плохо определенными границами, инструкции в пространстве данных и наоборот. а затем действительно сумасшедшие вещи, когда указатель изменяется каким-то очень необычным образом. (Я пишу сложную систему времени выполнения на ассемблере для языка параллельного программирования; полезно маскировать указатели кода, чтобы удалить младшие N битов, где N имеет много общего с выравниванием кэша. Это приведет к сбою общего анализатора точек; он должен знать, как манипулируют указателями и почему, так что сложно, плюс специальные знания). - person Ira Baxter; 21.02.2016

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

Используя разборку, выполните два прохода. Первый этап заключается в создании структур данных для представления каждой инструкции и для сбора всех целей перехода.

Второй этап заключается в создании структур, представляющих базовые блоки (блок кода с одной точкой входа). и выход). Свяжите каждый базовый блок с его преемником(ами). Базовый блок может иметь ноль, одного или двух последователей (или N последователей в случае таблицы переходов). Базовый блок, оканчивающийся на RET, не имеет последователей. Базовый блок, заканчивающийся безусловным переходом, имеет одного преемника. А у базового блока с условным переходом есть два преемника — либо провал, либо цель перехода. Базовые блоки без предшественников являются либо точками входа в подпрограмму, либо мертвым (или недействительным) кодом.

Цели перехода — это начало базового блока, как и инструкция, следующая за безусловным переходом (который должен быть целью перехода или точкой входа подпрограммы).

За исключением запуска программы (через реальное оборудование или эмулятор), точное знание целей непрямых переходов невозможно. Я предлагаю поддержать пару простых случаев: 1) таблица переходов, где таблица находится внутри программы 2) переход через ячейку глобальной памяти, которая использовалась для ссылки на другой исполняемый файл (и дизассемблер перечисляет, что является целью). В первом случае базовый блок может иметь произвольное количество последователей. Во втором случае базовый блок имеет одного преемника.

Обратите внимание, что я намеренно опускаю CALL как часть CFG. Когда я реализовал CFG grapher, я сделал именно это. Мой граф показывал только одну функцию за раз. Если дважды щелкнуть CALL, отобразится CFG подпрограммы.

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

Я опускаю INT и IRET, потому что предполагаю, что вы имеете дело с приложениями пользовательского режима. Если нет, то относитесь к INT как к вызовам, а к IRET — как к RET. Аппаратная процедура обслуживания прерываний (ISR) может быть вызвана из любого места, где разрешены прерывания, поэтому (обычно) не будет каких-либо прямых вызовов ISR - она ​​просто будет сидеть в стороне. В более общих чертах, если вы имеете дело с программным обеспечением режима ядра, у вас будет множество других соображений.

person Χpẘ    schedule 21.02.2016
comment
Дизассемблирование не решит проблему jmps-to-middle инструкции. Он может даже не распознать, когда область памяти на самом деле является кодом, или может неправильно интерпретировать область данных как содержащую код. Этот подход добавляет шума, и вы не избегаете сложной проблемы анализа точек. Что вы сделали, так это избежали самой легкой части: анализа исходного кода сборки. - person Ira Baxter; 21.02.2016
comment
Дизассемблер objconv Agner Fog (agner.org/optimize) уже выдает выходные данные с отмеченными целевыми ответвлениями. IDK, что он делает с переходами в середину инструкции (что вы можете найти в запутанном коде машинного языка). Это открытый исходный код (я забыл лицензию), так что вы могли бы даже использовать его в качестве отправной точки. - person Peter Cordes; 23.02.2016
comment
@IraBaxter Я подозреваю, что OP в основном касается неассемблерного кода (в частности, кода C). Ясно, что на ассемблере можно закодировать что-то, что сделает невозможным создание точной CFG. Однако это не означает, что CFG нельзя сгенерировать с пользой. IDA — хороший пример продукта, который с пользой генерирует CFG. - person Χpẘ; 25.02.2016
comment
Вы имеете в виду, что он заинтересован в дизассемблировании ванильного скомпилированного кода C? Я согласен, что в целом поведение объектного кода, вероятно, стало бы намного чище при нормальных обстоятельствах. Компилятор C, предназначенный для создания сложного для декодирования объектного кода, сделал бы его намного хуже. (Я реализовал компилятор x86 для параллельного машинного кода с очень малым размером зерна; его код было бы трудно дизассемблировать, поскольку он проделывает несколько странных трюков с размещением данных в потоках инструкций). YMMV или, что более вероятно, HisMMV. - person Ira Baxter; 25.02.2016

Я столкнулся с той же проблемой, что и вы, и не нашел готовых решений, поэтому я написал простое самостоятельно с помощью Python: https://github.com/Kazhuu/asm2cfg.

Обратите внимание, что я тестировал его только с дампами дизассемблирования функций из GDB. Думаю, это можно расширить для использования с objdump.

person Kazooie    schedule 11.02.2021