Что такое Clang?


Я провёл последние несколько месяцев, работая с Clang, фронтендом LLVM. Clang умеет парсить и анализировать любой исходный код на языках семейства С (C, C++, ObjectiveC, и т.п....) и имеет удивительную модульную структуру, которая делает его простым в использовании.


Если вы ищете статический анализатор кода, я настоятельно рекомендую Clang, он существенно превосходит другие статические анализаторы (такие, как CIL...) и хорошо документирован. Также список рассылки Clang очень активен и полезен, если вы застряли на чём-то.

Лично я использую Clang для статического анализа драйверов ввода-вывода ядра Linux, включая драйвера камеры и драйвера DRM графической карты. Код ядра, особенно код драйвера, может быть очень сложным и трудным для анализа, но Clang позволяет нам легко поддерживать его. Давайте посмотрим, что можно сделать с его помощью.

Как работает Clang?


В большинстве случаев, Clang запустит препроцессор (который разворачивает все макросы) и парсит исходник, превращая его в абстрактное синтаксическое дерево (AST). C AST работать гораздо проще, чем с исходным кодом, но вы всегда можете получить ссылки на исходник. Фактически, каждая структура в Clang-е, используемая для представления кода (AST, CFG и т.п.), всегда имеет ссылку на оригинальный исходник, полезный для целей анализа, рефакторинга и т.п.

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

Clang AST


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

В общем, Clang AST сделано из двух очень гибких классов: Decl и Stmt. У обоих есть множество подклассов, вот несколько примеров:

FunctionDecl — прототип или объявление функции
BinaryOperator — бинарный оператор, например (a + b)
CallExpr — вызов функции, например, foo(x);

Большинство классов имеют «говорящие» имена, например, ForStmt, IfStmt, и ReturnStmt. Вы поймёте суть AST, поиграв с ним несколько минут. Вы можете найти документацию по классам AST, поискав что-либо вроде “Clang FunctionDecl.”

Как использовать Clang?


Clang может использоваться как прямая замена gcc и предлагает несколько крутых инструментов статического анализа. Как программист (а не как нормальный пользователь!), вы можете получить доступ к всей мощи clang, используя его как библиотеку одним из трёх способов, в зависимости от того, как вы решите.

Для начала, ознакомьтесь с описанием интерфейсов clang. В дополнение к тому, что написано в этом описании, я выделю другие существенные различия между различными интерфейсами clang.

Clang Plugin


Ваш код является плагином, и запускается каждый раз заново для каждого файла исходника, что означает, что вы не можете сохранять глобальную информацию или другую контекстную информацию между двумя разными исходными файлами (но вы можете запустить плагин для множества файлов последовательно). Плагин запускается путём передачи соответствующих опций системе компиляции (Clang, Make и т.п.) через аргументы командной строки. Это похоже на то, как вы включаете оптимизацию в GCC (т.е. "-O1"). Вы не можете запустить какую-либо свою задачу до или после того, как исходный файл будет проанализирован.

LibTooling (Clang Tool)


Ваш код — обычная программа на С++, с нормальной функцией main(). LibTooling используется для запуска некоторого анализа на исходном коде (с множеством файлов, при желании) без запуска обычного процесса компиляции. Новый экземпляр кода для анализа (и новый AST) будет создан для каждого нового файла исходника (как и в случае Clang Plugin), но вы можете сохранять контекстную информацию между файлами исходников в своих глобальных переменных. Так как у вас есть функция main(), вы можете запускать какие-либо задачи перед или после того, как clang завершит анализ ваших исходных файлов.

LibClang


LibClang хорош тем, что это стабильный API. Clang периодически меняется, и если вы используете Plugin или Libtooling, вам нужно будет править ваш код, чтобы отслеживать эти изменения (но это не так сложно!). Если вам нужен доступ к Clang API из языков, отличных от C++ (например, из Python), вы должны использовать LibClang.

Примечание: LibClang не даёт полный доступ к AST (только высокоуровневый доступ), но другие два варианта дают. Как правило, нам нужен полный доступ к AST.

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

Начинаем работать с Clang


Сейчас, когда вы знаете основы, давайте начнём! Эта инструкция будет работать на любой версии Linux (и, возможно, OS X), но тестировалось на Ubuntu. Вы можете получить LLVM и Clang, проделав следующие шаги (взято из официальной инструкции к Clang):

Скачать и установить (например, с помощью apt-get) все необходимые пакеты.
(Типичный дистрибутив Linux идёт со всем необходимым, кроме subversion).
Смените директорию на ту, в которую вы хотите установить LLVM (например, ~/static_analysis/). Будем называть её директорией верхнего уровня. Выполните следующие команды в терминале:

$ svn co http://llvm.org/svn/llvm-project/llvm/trunk llvm
$ cd llvm/tools
$ svn co http://llvm.org/svn/llvm-project/cfe/trunk clang
$ cd clang/tools
$ svn co http://llvm.org/svn/llvm-project/clang-tools-extra/trunk extra
$ cd ../../../..    #go back to top directory
$ cd llvm/projects
$ svn co http://llvm.org/svn/llvm-project/compiler-rt/trunk compiler-rt
$ cd ../..          #go back to top directory
$ cd llvm
$ ./configure
$ make              #this takes a few hours
$ sudo make install

Компиляция LLVM и Clang займёт некоторое время.

Для проверки запустите:

$ clang --version

Можно протестировать Clang, запустив классический пример Hello World:

$ clang hello.c -o hello
$ ./hello

В этом руководстве я использую Clang 3.4 на Ubuntu 13.04, но вы можете использовать другие варианты и того, и другого.

Сейчас перейдём к программированию на Clang.

Продолжение следует.

Комментарии (19)


  1. 32bit_me Автор
    12.07.2018 08:10

    .


  1. AxeFizik
    12.07.2018 08:11

    Оборвали на самом интересном месте. :)


    1. 32bit_me Автор
      12.07.2018 08:12

      Придётся немного подождать. Или пройти по ссылке на оригинальный текст.


  1. ob1
    12.07.2018 09:57

    А не устарел ли материал? Описываются Ubuntu 13.04 и Clang 3.5, когда текущие Ubuntu 18.04 и Clang 6.0.


    1. 32bit_me Автор
      12.07.2018 10:16
      +1

      Пока это только введение. Там, где будет код, я приведу его к API clang 6. Версия операционной системы здесь ни на что не влияет.


      1. ob1
        12.07.2018 10:48

        Спасибо, понятно.


    1. FlexFerrum
      12.07.2018 10:50

      Лучше 5.0. Когда шестая (dev-сборка) версия будет доступна на конвейерах типа travis или appveyor — не ясно.


  1. Biga
    12.07.2018 10:12
    +2

    К слову, у LibreOffice есть отличная пачка плагинов к clang для статического анализа кода проекта.

    Их можно посмотреть в качестве примеров, а также просто брать и использовать.

    Особенно must have плагин — это проверка на неявный каст bool в другие типы (implicitboolconversion.cxx). Он находил нам такие баги, что мы не знали плакать или смеяться.

    Сразу предупрежу, что с последней версией clang эти плагины могут не скомпиляться — надо проявить немного упорства в исправлении расхождений в api, но у меня это не заняло много времени.


    1. 32bit_me Автор
      12.07.2018 10:14

      Спасибо, это интересно.


  1. kapac_er
    12.07.2018 10:13

    И инструкция по сборке уже не рабочая, а в остальном все норм:

    kar@pc:~/static_analysis/llvm$ ./configure
    ################################################################################
    ################################################################################
    The LLVM project no longer supports building with configure & make.

    Please migrate to the CMake-based build system.
    For more information see: llvm.org/docs/CMake.html
    ################################################################################
    ################################################################################


  1. Biga
    12.07.2018 10:13

    К слову, у LibreOffice есть отличная пачка плагинов к clang для статического анализа кода проекта.

    Их можно посмотреть в качестве примеров, а также просто брать и использовать.

    Особенно must have плагин — это проверка на неявный каст bool в другие типы (implicitboolconversion.cxx). Он находил нам такие баги, что мы не знали: плакать или смеяться.

    Сразу предупрежу, что с последней версией clang эти плагины могут не скомпиляться — надо проявить немного упорства в исправлении расхождений в api, но у меня это не заняло много времени.


  1. FlexFerrum
    12.07.2018 11:54

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

    Строго говоря, это не так. LibTooling используется для упрощения настройки clang frontend — предоставляет механизмы удобного разбора командной строки, созданием окружения для компилятора и т. п. А запустить потом можно любой frontend action. Хоть на препроцессинг, хоть на статический анализ, хоть на полноценную компиляцию. Ну и да: интеграция с ast matchers — отдельная вишенка на торте. Но что-то мне подсказывает, что в переводе об этом ничего не будет.

    Скачать и установить (например, с помощью apt-get) все необходимые пакеты.
    (Типичный дистрибутив Linux идёт со всем необходимым, кроме subversion).
    Смените директорию на ту, в которую вы хотите установить LLVM (например, ~/static_analysis/). Будем называть её директорией верхнего уровня. Выполните следующие команды в терминале:

    Сейчас уже проще скачать нужные dev-пакеты из репозиториев и не заниматься убийством собственного жёсткого диска.

    Ну и довольно неплохая преза по Clang AST лежит здесь: llvm.org/devmtg/2013-04/klimek-slides.pdf


    1. 32bit_me Автор
      12.07.2018 15:36

      Спасибо за развёрнутый комментарий. Про AST matchers я напишу потом отдельно.


  1. Centimo
    12.07.2018 23:08

    > и хорошо документирован
    > вы получите удовольствие, изучая различные классы элементов Clang AST
    Простите, но автор наркоман. Второй месяц ковыряюсь в CLang, и впечатления почти сугубо отрицательные. Вместо однородной структуры тех же AST, есть жуткое месиво из тысячи разных классов, что невероятно трудно уложить в голове. Многие вещи делаются через пень-колоду (хотите получить тип переменной из объявления — вот вам цепочка из пяти вызовов), постоянно приходится что-то куда-то кастовать, сплошные голые пойнтеры и прочие мерзости. И ладно бы была годная документация, но она фактические отсутствует. Есть пара туториалов на официальном сайте и доксиген, ничего не объясняющий и почти никак не помогающий. По итогу приходится заниматься цифровой археологией, находя ответы на интересующие вопросы по маленьким кусочкам. На некоторые вопросы я так и не получил ответов (https://stackoverflow.com/questions/49028456/how-to-receive-the-current-depth-in-recursiveastvisitor-clang — судя по отсутствию какой-либо реакции, это невозможно).

    Полезная ссылка для интересующихся: white-knight-is-alive.blogspot.com/2016/01/clang-api_20.html — несколько вводных туториалов на русском.


    1. FlexFerrum
      13.07.2018 00:24

      Честно говоря, из вопроса на SO не очень понятно — какая именно задача решается и зачем нужно знать, на какой глубине вы сейчас находитесь. AST в кланге анализировать не то, чтобы просто — это правда. Сам в презентации об этом красочно рассказывал. Но, с другой стороны, это и не rocket science. И разделение классов узлов AST на группы (statements/expressions, types, declarations) — в общем, обосновано.


      1. Centimo
        13.07.2018 01:03

        > Честно говоря, из вопроса на SO не очень понятно — какая именно задача решается и зачем нужно знать, на какой глубине вы сейчас находитесь.
        Ну, насколько я понял, «visitor» — единственный (можно, наверное, ручками, но там наверняка свихнуться можно) способ обхода AST дерева. И как понять, что ты находишься в условном узле или листе дерева? Как отследить отношение «родитель — ребёнок» в этом дереве?

        > Но, с другой стороны, это и не rocket science.
        Понятно, что ничего нерешаемого нет. Просто складывается ощущение, что CLang специально делали так, чтобы усложнить жизнь пользователю.


        1. FlexFerrum
          13.07.2018 01:14

          Ну, насколько я понял, «visitor» — единственный (можно, наверное, ручками, но там наверняка свихнуться можно) способ обхода AST дерева. И как понять, что ты находишься в условном узле или листе дерева? Как отследить отношение «родитель — ребёнок» в этом дереве?

          Да. Можно ещё ручками. Это так. То есть способа 3: DOM-like («ручками»), SAX-like (с помощью визиторов) и XPath-like (с помощью матчеров).
          Отношение «родитель-потомок» отследить несложно. Можно идти вверх по дереву (если это возможно, например, в декларациях), либо в визиторе хранить стейт. Вот, например, визитор, который превращает «абстрактный» QualType в нечто вразумительное и пригодное для простого анализа:
          TypeUnwrapper
          На выходе этот код даёт структуру, хранящую упрощённую информацию о типе.
          Понятно, что ничего нерешаемого нет. Просто складывается ощущение, что CLang специально делали так, чтобы усложнить жизнь пользователю.

          Думаю, цели были немножко другими. :) Ну а уж как получилось — так получилось.


  1. 0xd34df00d
    13.07.2018 02:09

    Я в свободное время попиливаю байндинги к libclang для целей анализа и беганья по AST из более дружелюбных к этому языков. Есть где-то описание того, что я теряю, используя стабильный и FFI-дружественный C API вместо нативного C++ API?


    1. FlexFerrum
      13.07.2018 11:40

      Вот тут, от самого вендора.