Статьи

При синтаксическом анализе C, объявлений типов и поддельных заголовков

За последние пару лет pycparser стал довольно популярным (особенно после его использования в cffi ). Это означает, что я получаю больше вопросов по электронной почте, что приводит к тому, что я устаю отвечать на те же вопросы 🙂

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

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

Во-первых, отказ от ответственности. Этот пост предполагает некоторый уровень знакомства с языком программирования C и его компиляцией. Вы должны знать о препроцессоре C (то, что обрабатывает директивы, такие как #include и #define), и иметь общее представление о том, как несколько исходных файлов (чаще всего файл .c и любое количество файлов .h) объединяются в единая единица перевода для компиляции. Если вы не очень разбираетесь в этих понятиях, я бы не стал использовать pycparser, пока вы не узнаете больше о них.

Так в чем проблема?

Проблема возникает, когда код, который вы хотите проанализировать с помощью pycparser #include заголовочный файл:

#include <someheader.h>

int foo() {
    // my code
}

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

Как работать с заголовками с помощью pycparser

В общем, pycparser не занимается заголовками или директивами препроцессора C вообще. Объект CParser ожидает предварительно обработанный код в методе разбора period. Итак, у вас есть два варианта:

  1. Предоставить предварительно обработанный код в pycparser. Это означает, что вы сначала предварительно обрабатываете код, вызывая, скажем, gcc -E (или clang -E , или cpp, или любым другим способом, которым вы должны предварительно обработать код [1] ).
  2. Используйте удобную функцию parse_file в pycparser; он вызовет препроцессор для вас. Вот пример .

Отлично, теперь вы можете обрабатывать заголовки. Тем не менее, это вряд ли решит все ваши проблемы, потому что pycparser будет иметь проблемы с анализом некоторых библиотечных заголовков; в первую очередь, вероятно, возникнут проблемы с анализом заголовков стандартной библиотеки.

Зачем? Поскольку pycparser полностью поддерживает C99, многие заголовки библиотек полны расширений компилятора и других хитрых приемов для совместимости между различными платформами. Хотя их можно полностью проанализировать с помощью pycparser [2] , это требует работы. Работа, которую вы можете не иметь навыков или времени, чтобы сделать. Работа, которая, к счастью, почти наверняка не нужна.

Почему это не нужно? Потому что, по всей вероятности, вам не нужен pycparser для анализа этих заголовков вообще.

Что Pycparser на самом деле нужно для анализа заголовков для

Чтобы понять это смелое утверждение, вы должны сначала понять, почему pycparser должен анализировать заголовки. Давайте начнем с более простого вопроса — зачем компилятору C анализировать заголовки вашего файла?

По ряду причин; некоторые из них синтаксические, но большинство из них семантические. Синтаксические проблемы могут препятствовать синтаксическому анализу кода компилятором . #defines — это одно, а типы — другое.

Например, код C:

{
    T * x;
}

Не может быть должным образом проанализирован, если мы не знаем:

  1. Либо T, либо x являются макросами #, определенными для чего-либо.
  2. T — это тип, который был ранее создан с помощью typedef.

Для подробного объяснения этой проблемы, посмотрите эту статью и другие связанные публикации на моем сайте.

Семантические причины — это те, которые не помешают компилятору анализировать код, но будут препятствовать его правильному пониманию и проверке. Например, объявления используемых функций. Полные объявления структур и так далее. Они занимают подавляющее большинство реальных заголовочных файлов. Но, как выясняется, поскольку pycparser заботится только о разборе кода в AST и не выполняет никакого семантического анализа или дальнейшей обработки, он не заботится об этих проблемах. Другими словами, учитывая код:

{
    foo(a.b);
}

pycparser может создать правильный AST (учитывая, что ни один из foo, a или b не является именами типов). Неважно, что на самом деле объявляет foo, является ли переменная действительно структурным типом или имеет поле с именем b [3] .

Так что pycparser требует очень мало от заголовочных файлов. Так родилась идея «поддельных заголовков».

Поддельные заголовки

Давайте вернемся к этому простому примеру кода:

#include <someheader.h>

int foo() {
    // my code
}

Итак, мы разработали две ключевые идеи:

  1. Pycparser должен знать, что содержит someheader.h, чтобы он мог правильно проанализировать код.
  2. Для выполнения своей задачи pycparser требуется только очень небольшое подмножество someheader.h.

Идея поддельных заголовков проста. Вместо того, чтобы фактически анализировать someheader.h и все остальные заголовки, которые он включает в себя транзитивно (возможно, он включает в себя также много системных и стандартных библиотечных заголовков), почему бы не создать «поддельный» someheader.h, который содержит только те части оригинала, которые необходимы для разбора — #defines и typedefs.

Крутая часть о typedefs заключается в том, что pycparser на самом деле не волнует, какой тип определен. T может быть указателем на функцию, принимающую массив структурных типов, но все, что нужно увидеть pycparser:

typedef int T;

Так что он знает, что T является типом. Неважно, что это за тип .

Итак, что вы должны сделать, чтобы разобрать вашу программу?

Итак, теперь вы, надеюсь, лучше понимаете, что означают заголовки для pycparser, и как обойти необходимость разбирать тонны системных заголовков. Что это на самом деле означает для вашей программы? Придется ли вам теперь просматривать все ваши заголовки, «подделывая их»? Вряд ли. Если ваш код соответствует стандартам C, то, скорее всего, pycparser не будет иметь проблем с анализом всех ваших заголовков. Но вы, вероятно, не хотите, чтобы он анализировал системные хедадеры. В дополнение к нестандартности, эти заголовки обычно большие, что означает более длительное время анализа и большие AST.

Поэтому мое предложение будет таким: пусть pycparser анализирует ваши заголовки, но подделывает системные заголовки и, возможно, любые другие заголовки больших библиотек, используемые вашим кодом. Что касается стандартных заголовков, pycparser уже предоставляет вам хорошие подделки в своей папке утилит. Все, что вам нужно сделать, это передать этот флаг препроцессору [4] :

-I<PATH-TO-PYCPARSER>/utils/fake_libc_include

И он сможет найти заголовочные файлы, такие как stdio.h и sys / types.h с определенными правильными типами.

Я повторюсь: показанный выше флаг почти наверняка достаточен для анализа программы C99, которая зависит только от времени выполнения C (т.е. не имеет других библиотечных зависимостей).

Пример из реального мира

ОК, достаточно теории. Теперь я хочу проработать пример, чтобы помочь обосновать эти предложения в реальности. Я возьму какой-нибудь известный проект C с открытым исходным кодом и использую pycparser для разбора одного из его файлов, полностью показывая все шаги, предпринятые до успешного анализа. Я выберу Redis .

Давайте начнем с клонирования репозитория Redis git:

/tmp$ git clone git@github.com:antirez/redis.git

Я буду использовать последний выпущенный pycparser (версия 2.13 на момент написания). Я также клонирую его репозиторий в / tmp, чтобы я мог легко получить доступ к поддельным заголовкам:

/tmp$ git clone git@github.com:eliben/pycparser.git

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

Давайте возьмем основной файл Redis (redis / src / redis.c) и попытаемся предварительно обработать его. Первый вызов препроцессора просто добавляет пути включения для собственных заголовков Redis (они живут в redis / src) и поддельные заголовки libc pycparser:

/tmp$ gcc -E -Iredis/src -Ipycparser/utils/fake_libs_include redis/src/redis.c > redis_pp.c
# 48 "redis/src/redis.h" 2
In file included from redis/src/redis.c:30:0:
redis/src/redis.h:48:17: fatal error: lua.h: No such file or directory
 #include <lua.h>
             ^
compilation terminated.

Ой, ничего хорошего. Redis ищет заголовки Lua. Давайте посмотрим, несет ли она эту зависимость:

/tmp$ find redis -name lua
redis/deps/lua

В самом деле! Мы также должны иметь возможность добавлять заголовки Lua к пути препроцессора:

/tmp$ gcc -E -Iredis/src -Ipycparser/utils/fake_libs_include \
             -Iredis/deps/lua/src redis/src/redis.c > redis_pp.c

Отлично, ошибок больше нет. Теперь давайте попробуем разобрать его с помощью pycparser. Я загружу pycparser в интерактивный терминал, но любой другой метод (например, запуск одного из примеров скриптов будет работать):

: import pycparser
: pycparser.parse_file('/tmp/redis_pp.c')
... backtrace
---> 55         raise ParseError("%s: %s" % (coord, msg))

ParseError: /usr/include/x86_64-linux-gnu/sys/types.h:194:20: before: __attribute__

Эта ошибка странная. Обратите внимание, где это происходит: в системном заголовке, включенном в предварительно обработанный файл. Но у нас не должно быть системных заголовков — мы указали путь к поддельным заголовкам. Что дает?

Причина, по которой это происходит, заключается в том, что gcc знает о некоторых предварительно заданных каталогах системных заголовков и добавляет их в свой путь поиска. Мы можем заблокировать это, убедившись, что он смотрит только в каталогах, которые мы явно указываем с -I , предоставив ему флаг -nostdinc . Давайте снова запустим препроцессор:

/tmp$ gcc -nostdinc -E -Iredis/src -Ipycparser/utils/fake_libc_include \
                       -Iredis/deps/lua/src redis/src/redis.c > redis_pp.c

Теперь я попытаюсь снова проанализировать предварительно обработанный код:

: pycparser.parse_file('/tmp/redis_pp.c')
... backtrace
---> 55         raise ParseError("%s: %s" % (coord, msg))

ParseError: redis/src/sds.h:74:5: before: __attribute__

ОК, прогресс! Если мы посмотрим в коде, где происходит эта ошибка, мы заметим, что специфичный для GNU __attribute__ pycparser не поддерживает. Нет проблем, давайте просто определим это:

$ gcc -nostdinc -E -D'__attribute__(x)=' -Iredis/src \
                   -Ipycparser/utils/fake_libc_include \
                   -Iredis/deps/lua/src redis/src/redis.c > redis_pp.c

Если я попытаюсь разобрать снова, это работает:

: pycparser.parse_file('/tmp/redis_pp.c')
<pycparser.c_ast.FileAST at 0x7f15fc321cf8>

Теперь я также могу запустить один из примеров сценариев, чтобы увидеть, что мы можем сделать что-то более интересное с AST:

/tmp$ python pycparser/examples/func_defs.py redis_pp.c
sdslen at redis/src/sds.h:47
sdsavail at redis/src/sds.h:52
rioWrite at redis/src/rio.h:93
rioRead at redis/src/rio.h:106
rioTell at redis/src/rio.h:119
rioFlush at redis/src/rio.h:123
redisLogRaw at redis/src/redis.c:299
redisLog at redis/src/redis.c:343
redisLogFromHandler at redis/src/redis.c:362
ustime at redis/src/redis.c:385
mstime at redis/src/redis.c:396
exitFromChild at redis/src/redis.c:404
dictVanillaFree at redis/src/redis.c:418
... many more lines
main at redis/src/redis.c:3733

Это позволяет нам увидеть все функции, определенные в redis.c, и заголовки, включенные в него, используя pycparser.

Это было довольно просто — все, что мне нужно было сделать, это установить правильные флаги препроцессора. В некоторых случаях это может быть немного сложнее. Наиболее очевидная проблема, с которой вы можете столкнуться, — это новый заголовок, который вам нужно будет подделать. К счастью, это очень просто — просто взгляните на существующие (скажем, на stdio.h). Эти заголовки могут быть скопированы в другие имена / каталоги, чтобы убедиться, что препроцессор найдет их правильно. Если вы думаете, что есть стандартный заголовок, который я забыл включить в поддельные заголовки, откройте проблему, и я добавлю ее.

Обратите внимание, что нам не нужно было подделывать заголовки Redis (или Lua в этом отношении). pycparser справился с ними просто отлично. То же самое имеет большой шанс быть верным и для вашего C-проекта.


[1] В Linux в командной строке должен быть хотя бы gcc. В OS X вам нужно установить «инструменты разработчика командной строки», чтобы получить лязг командной строки. Если вы находитесь в Microsoft-land, я рекомендую скачать готовые бинарные файлы clang для Windows .
[2] И это было сделано многими людьми. pycparser был сделан для анализа стандартной библиотеки C, windows.h, частей заголовков ядра Linux и так далее.
[3] Обратите внимание, что это описывает наиболее распространенное использование pycparser, которое заключается в том, чтобы выполнять простой анализ источника или каким-либо образом переписывать части существующего источника. Более сложные применения могут фактически потребовать полного анализа определений типов, структур и объявлений функций. Фактически, вы, безусловно, можете создать настоящий компилятор C, используя pycparser в качестве внешнего интерфейса. Такое использование потребует полного разбора заголовков, поэтому поддельные заголовки не подойдут. Как я упоминал выше, можно заставить pycparser анализировать фактические заголовки библиотек и т. Д .; это просто требует больше работы.
[4] В зависимости от того, какой именно препроцессор вы используете, вам может потребоваться предоставить ему другой флаг, указывающий ему игнорировать системные заголовки, пути которых жестко заданы в нем. Читайте на примере для более подробной информации.