Як створювався PVS-Studio під Linux

У цьому році ми почали робити те, до чого у нас довгий час було спірне відношення, а саме — адаптацію продукту PVS-Studio Linux системі. У статті я розповім про те, як через 10 років існування аналізатора PVS-Studio для Windows, ми вирішили зробити продукт для дистрибутивів Linux. Це велика робота, не обмежується, на жаль, як думає ряд програмістів, виключно компіляцією исходников під цільову платформу.

Введення
Взагалі, консольне ядро аналізатора PVS-Studio зібрано під Linux досить давно. Вже близько трьох років. Відразу відповім на питання про відсутність такої версії в громадському доступі — зробити програмний продукт, навіть на основі вже існуючого, це величезна робота і багато людино-годин, купа непередбачених проблем і нюансів. Тоді ми це тільки передбачали, тому офіційна підтримка аналізатора для Linux систем не починалася.

Будучи автором багатьох статей про перевірку проектів, на відміну від моїх колег, я часто черпав натхнення в проектах з Linux. Ця середовище дуже багата великими і цікавими проектами з відкритим вихідним кодом, які або дуже складно зібрати в Windows, або взагалі неможливо. Саме в рамках таких завдань по перевірці якогось відкритого проекту до сьогоднішнього дня і розвивався PVS-Studio для Linux.

Малими силами портування коду ядра PVS-Studio на Linux зайняло кілька місяців. Заміна кількох звернень до системних функцій і налагодження на проект Chromium дали вже добре працююче консольний додаток. Ця версія аналізатора була додана в регулярні нічні збірки, а також перевірялася аналізатором Clang Static Analyzer. Періодичні перевірки відкритих проектів і контроль складання дозволили аналізатору безпроблемно існувати декілька років і іноді навіть здавалося, що цей інструмент вже можна продавати. Але ви ще не знаєте, як мені доводилося перевіряти проекти на той момент…

Про застосування інструментів статичного аналізу

Перш ніж продовжити розповідь про розробку нашого інструменту, я б хотів трохи розповісти про саму методологію статичного аналізу. Тим самим, відразу відповівши на питання в стилі: «Навіщо користуватися сторонніми інструментами, якщо одразу можна писати код без помилок і робити рев'ю з колегами?». На жаль, досить часто звучить.

Статичний аналіз коду дозволяє виявляти помилки та недоліки у вихідному коді програм. Незалежно від використовуваних інструментів, це відмінна методологія контролю якості коду майбутніх додатків. Якщо є можливість, то корисно поєднувати різні інструменти статичного аналізу.

Серед наших читачів, користувачів або слухачів на конференціях зустрічаються люди, які вважають, що огляду коду з колегами більш ніж достатньо для виявлення помилок на ранньому етапі написання коду. І дещо в таких «огляди» напевно вдається знайти. Але все це час ми говоримо про одне й те ж. Адже статичний аналіз можна розглядати як автоматизований процес огляду коду. Уявіть, що статичний аналізатор — це ще один ваш колега. Такий собі віртуальний людина-робот, який невтомно бере участь у всіх оглядах коду і вказує на підозрілі місця. Хіба це не корисно?!

У багатьох галузях виробництва вдаються до автоматизації з метою виключення так званого «людського фактору». І контроль якості коду — не виняток. Ми не закликаємо відмовлятися від ручного code review, якщо ви практикуєте. Просто використання статичного аналізатора допоможе виявляти ще більше помилок на самому ранньому етапі.

Ще важливий момент — програма не втомлюється і не лінується. Код вносяться помилки різного характеру. Помилки? Їх дуже складно виділити очима. Мовні помилки? Сильно залежать від кваліфікації перевіряючого. Ситуацію погіршують сучасні обсяги коду. Багато функції не поміщаються цілком навіть на великих моніторах. При неповноті контексту пильність перевіряючого зменшується. Плюс вже через 15 хвилин уважного читання коду починає втомлюватися. І чим далі, тим сильніше. Звідси і популярність інструментів автоматичного аналізу, яка зростає з кожним роком.

просили користувачі PVS-Studio від версії для Linux?

Наш продукт завжди був цікавий людям, так або інакше пов'язаних з розробкою програм. Це Windows користувачі, які відразу могли спробувати аналізатор. Не програмісти взагалі або програмісти інших платформ і мов, які також з цікавістю стежать за нашою активністю. Така увага очікувано, оскільки багато помилок є загальними для багатьох мов програмування.

Користувачі Linux довго і наполегливо запитували про роботу аналізатора на цій платформі. Питання і аргументи можна узагальнити наступним чином:

  • Утиліта командного рядка — Інтеграція з IDE не потрібна!»
  • Інсталятор не потрібен — «Самі все встановимо!»
  • Документація не потрібна — «Самі запустимо!»
Подальша розповідь буде багаторазово підтверджувати невідповідність заяв користувачів їх очікуванням.

Міф про розуміння складальних скриптів

Після спілкування з людьми з великих комерційних проектів з'ясувалося, що досить багато розробників не знайомі зі складанням проектів і насправді такі знання не завжди потрібні в повному обсязі. Як зібрати/налагодити свій проект/модуль кожен розробник знає. Але зазвичай все це знання полягає в наборі декількох магічних команд, які виконують програмісти. Образно кажучи, у них є велика кнопка, на яку досить натиснути і на виході отримують зібрані модулі. Але про те, як все це працює всередині, вони мають лише загальне уявлення. А за складальними скриптами часто стежить спеціальна людина.

У таких випадках необхідний інструмент для перевірки проекту без інтеграції, як мінімум для ознайомлення з аналізатором.

Linux версія аналізатора з'явилися саме після того, в Windows ми зробили систему моніторингу компіляторів, яка дозволила перевіряти будь-які проекти на цій платформі. Як потім з'ясувалося, там досить багато серйозних проектів, які збираються за допомогою компілятора від Microsoft, але при цьому не мають проекту Visual Studio. Так ми написали статті про перевірку Qt, Firefox, CryEngine5 і навіть сотрудничали з Epic Games, виправляючи помилки у їх коді. Наше дослідження показало, що якщо знати інформацію про компіляторі: директорію запуску, параметри командного рядка і змінні оточення, то цієї інформації достатньо, щоб покликати препроцесор і виконати аналіз.

Плануючи перевіряти Linux проекти, я одразу розумів, що не розберуся з інтеграцією аналізатора в кожен конкретний проект, тому зробив аналогічну систему моніторингу для ProcFS (/proc/id's). Брав код з Windows плагіна і запускав його в mono для аналізу файлів. Кілька років такий спосіб використовувався для перевірки проектів, найбільші з яких — ядро Linux і FreeBSD. Незважаючи на тривале використання такого способу, він ні в якому разі не годиться для масового використання. Продукт ще не готовий.

Вибір технології моніторингу

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

  • (-) Clang scan-build — подивившись скрипти Clang'a, ми зробили прототип, який аналогічним чином прописував виклик аналізатора в змінні CC/CXX. Ми раніше стикалися з тим, що таким способом не вдається перевірити деякі відкриті проекти з допомогою Clang Static Analyzer. Розібравшись трохи краще в цьому способі, ми зрозуміли, що часто в проектах в ці змінні дописують і прапори компіляції. І при перевизначенні змінних їх не вдається зберегти. Тому ми відмовилися від цього способу.

  • (+) strace — утиліта видає досить докладний лог трасування, в якому більша частина залогированных процесів не відноситься до компіляції. Також у форматі виведення цієї утиліти відсутня так необхідна нам робоча директорія процесу. Але директорію вдалося знайти, зв'язавши дочірні та батьківські процеси, а програма на C++ дуже швидко парсити такий файл, запускаючи аналіз знайдених файлів паралельно. Так можна перевірити проекти з будь складальної системою і познайомитися з аналізатором. Наприклад, нещодавно ми знову перевірили Linux Kernel, тепер легко і просто.

  • (+) JSON Compilation Database — такий формат можна отримати для CMake проекту, вказавши один додатковий прапор. В ньому є вся потрібна інформація для аналізу без зайвих процесів. Підтримали.

  • (±) LD_PRELOAD — інтеграція аналізатора через заміщення функцій. Цей спосіб не буде працювати, якщо складання проекту вже здійснюється таким чином. Також є утиліти, що дозволяють отримати JSON Compilation Database для CMake-проектів за допомогою LD_PRELOAD (Наприклад, Bear). У них є невелика відмінність від CMake, але ми їх теж підтримали. І якщо в проекті немає залежності від якихось встановлених змінних оточення, то ми теж зможемо перевірити проект. Тому ±.
Появу регулярних тестів
Існують різні способи тестування програмного забезпечення. Для тестування аналізатора і діагностик найефективнішими є прогони на великий кодової базі відкритих проектів. Для початку ми вибрали близько 30 великих проектів з відкритим вихідним кодом. Раніше я згадував, що зібраний аналізатор в Linux проіснував не один рік і проекти для статей регулярно перевірялися. Здавалося, що все добре працює. Але тільки почавши повноцінне тестування, ми побачили повну картину недоробки аналізатора. Перед початком аналізу необхідно виконати розбір коду, щоб знайти потрібні конструкції. Незважаючи на незначний вплив нерозбірного коду на якість аналізу, все ж ситуація неприємна. Нестандартні розширення є у всіх компіляторах, але MS Visual C/C++ ми їх давно підтримуємо, а c GCC нам довелося почати цю боротьбу майже з самого початку. Чому майже? У Windows ми давно підтримали роботу з GCC (MinGW), але він там не так сильно поширений, тому ні у нас, ні у користувачів проблем не виникало.

Розширення компіляторів


В даному розділі мова піде про код, який ви, як я сподіваюся, більше ніде не побачите: коду, який використовує розширення GCC. Здавалося б, навіщо вони можуть нам знадобитися? У більшості кросплатформених проектів їх навряд чи стануть використовувати. По-перше, як показує практика, їх використовують. Розробляючи систему тестування проектів під Linux, ми зустрічали такий код. Але основна проблема виникає при розборі коду стандартної бібліотеки: вже там розширення використовуються на всю силу. У препроцессированных файлах з свого проекту ніколи не можна бути впевненим: заради оптимізації звична вам функція memset може виявитися макрос, що складається з statement expression. Але про все по порядку. Які нові конструкції ми зустріли, перевіряючи проекти під Linux?

Одним з перших зустрінутих розширень стали designated initializers. За допомогою них можна ініціалізувати масив в довільному порядку. Особливо це зручно, якщо індексується він enum: ми в явному вигляді вказуємо індекс, підвищуючи тим самим читабельність і зменшуючи ймовірність помилитися при його модифікації надалі. Виглядає дуже красиво і просто:

enum Enum {
A,
B,
C
};

int array[] = {
[A] = 10,
[B] = 20,
[C] = 30,
}

Заради спортивного інтересу трохи ускладнимо приклад:

enum Enum {
A,
B,
C
};

struct Struct {
int d[3];
};

struct Struct array2[50][50] = {
[A][42].d[2] = 4
};

Тобто, як ініціалізатор в даній конструкції може йти будь-яка послідовність індексацій та звернень до членів структури. В якості індексу може бути також вказано діапазон:

int array[] = {
[0 ... 99] = 0,
[100 ... 199] = 10,
}

Невелике, але дуже корисний з точки зору безпеки розширення GCC пов'язано з нульовим покажчиком. Про проблему використання NULL було сказано вже багато слів, не буду повторюватися. Для GCC ситуація трохи краще, тому що NULL в C++ оголошено __null. Таким чином, GCC вберігає нас від подібних пострілів в ногу:

int foo(int *a);
int foo(int a);

void test() {
int a = foo(NULL);
}

При компіляції отримуємо помилку:

test.c: In function 'void test()':
test.c:20:21: error: call of overloaded 'foo(NULL)' is ambiguous
int a = foo(NULL);
^
test.c:10:5: note: candidate: int foo(int*)
int foo(int *a) {
^
test.c:14:5: note: candidate: int foo(int)
int foo(int a) {

У GCC є можливість задавати атрибути __attribute__(()). Є цілий список атрибутів для функцій, змінних і типів, за допомогою яких можна керувати линковкой, вирівнюванням, оптимизациями і багатьма іншими речами. Одним з цікавих атрибутів є transparent_union. Якщо зробити такий union параметром функції, то в якості аргументу можна передавати не тільки сам union, але і покажчики з цього перерахування. Ось такий код буде коректний:

typedef union {
long *d;
char *ch;
int *i;
} Union __attribute((transparent_union));

void foo(Union arg);

void test() {
long d;
char ch;
int i;

foo(&d); //ok
foo(&ch); //ok
foo(&i); //ok
}

Прикладом, який використовує transparent_union, може послужити функція wait: вона може приймати як int* union wait*. Зроблено це на догоду сумісності з POSIX та 4.1 BSD.

Про вкладені функції в GCC ви напевно чули. У них можна використовувати змінні, оголошені до функції. Ще вкладену функцію можна передавати за вказівником (хоча і не варто, зі зрозумілих причин, викликати її за цим вказівником після завершення роботи основної функції).

int foo(int k, int b, int x1, int x2) {
int bar(int x) {
return k * x + b;
}
return bar(x2) - bar(x1);
}

void test() {
printf("%d\n", foo(3, 4, 1, 10)); //205
}

Але чи знали ви, що з такої функції можна зробити goto в «функцію-батька»? Особливо ефектно це виглядає у поєднанні з передачею такої функції в іншу.

int sum(int (*f)(int), int from, int to) {
int s = 0;
for (int i = from; i <= to; ++i) {
s += f(i);
}
return s;
}

int foo(int k, int b, int x1, int x2) {
__label__ fail;
int bar(int x) {
if (x >= 10)
goto fail;
return k * x + b;
}
return sum(bar, x1, x2);
fail:
printf("Exceptions in my C?!\n");
return 42;
}

void test() {
printf("%d\n", foo(3, 4, 1, 10)); //42
}

На практиці правда такий код може призвести до вкрай сумних наслідків: exception safety — досить складна тема навіть для C++ з RAII, не кажучи вже про C. Тому краще так не робити.

До речі про goto. У GCC мітки можна зберігати в покажчики і переходити по ним. А якщо записати їх в масив, вийде таблиця переходів:

int foo();
int test() {
__label__ fail1, fail2, fail3;
static void *errors[] = {&&fail1, &&fail2, &&fail3};
int rc = foo();
assert(rc >= 0 && rc < 3);
if (rc != 0)
goto *errors[rc];
return 0;

fail1:
printf("Fail 1");
return 1;
fail2:
printf("Fail 2");
return 2;
fail3:
printf("Fail 3");
return 3;
}

А це невелике розширення Clang. З цим компілятором PVS-Studio працювати вміє вже давно, тим не менш, навіть зараз ми не перестаємо дивуватися новим конструкціям, що з'являються в мові і компіляторах. Ось одна з них:

void foo(int arr[static 10]);

void test()
{
int a[9];
foo(a); //warning

int b[10];
foo(b); //ok
}

При такому записі компілятор перевіряє, що переданий масив має 10 або більше елементів і видає попередження, якщо це не так:

test.c:16:5: warning: array argument is too small; contains 9
elements, callee requires at least 10 [-Warray-bounds]
foo(a);
^ ~
test.c:8:14: note: callee declares array parameter as static here
void foo(int arr[static 10])
^ ~~~~~~~~~~~

Закрите тестування Beta версії. Хвиля 1
Підготувавши стабільну версію аналізатора, документацію і кілька способів перевірки проектів без інтеграції, ми почали закрите тестування.

Коли ми почали видавати аналізатор першим тестерам, з'ясувалося, що надавати аналізатор виконуваним файлом недостатньо. Ми отримували відгуки від «У вас відмінний продукт, ми знайшли купу помилок» до «Я не довіряю вашому додатку і не буду його встановлювати в /usr/bin!». На жаль, останніх було більше. Таким чином, аргументи форумчан про можливості самостійної роботи з виконуваним файлом були перебільшені. У такому вигляді працювати з аналізатором можуть або хочуть не всі. Необхідно скористатися якимись загальноприйнятими способами поширення в Linux.

Закрите тестування Beta версії. Хвиля 2
Отримавши перші відгуки, ми зупинили тестування і поринули в напружену роботу майже на 2 тижні. Тестування на чужому коді виявило ще більше проблем з компіляторами. Т. к. на базі GCC створюються компілятори і крос-компілятори під різні платформи, то наш аналізатор почали використовувати для перевірки чого завгодно, навіть програмного забезпечення різних залізяк. В принципі аналізатор справлявся зі своїми обов'язками, і ми отримували листи-відгуки, але деякі фрагменти коду аналізатор пропускав через розширень, які нам доводилося підтримувати.

Помилкові спрацьовування властиві будь-якому статичного аналізатора, але в Linux їх кількість у нас трохи зросла. Тому ми занялисьдоработкой діагностик під нову платформу і компілятори.

Великий доопрацюванням було створення Deb/Rpm-пакетів. Після їх появи, невдоволення по установці PVS-Studio відпали. Був, мабуть, лише один чоловік, якого обурила установка пакета з використанням sudo. Хоча таким чином ставиться майже весь софт.

Закрите тестування Beta версії. Хвиля 3
Ми також зробили невелику перерву на доопрацювання і внесли наступні зміни:

  1. Відмова від конфігураційних файлів для швидкої перевірки — після введення Deb/Rpm пакетів перше місце зайняла проблема з заповненням конфігураційного файлу для аналізатора. Довелося допрацювати режим швидкої перевірки проектів без конфігураційного файлу всього з двома обов'язковими параметрами: шлях до ліцензійного файлу і шлях до звіту аналізатора. Можливість розширеної настройки цього режиму залишилася.

  2. Поліпшена робота з логом утиліти strace — спочатку лог утиліти strace оброблявся скриптом на мові Perl, на якому був зроблений прототип. Скрипт повільно працював і погано распараллеливал аналіз. Після переписування цього функціоналу на С++ обробка файлу прискорилося, також стало легше підтримувати весь код на одній мові програмування.

  3. Доопрацювання Deb/Rpm-пакетів — т. к. для роботи режиму швидкої перевірки потрібна утиліта strace і в перші пакети входили Perl/Python скрипти, то ми не одразу правильно прописали всі залежності, а пізніше зовсім відмовилися від скриптів. Пізніше кілька людей написали про попередження при установці аналізатора через графічні менеджери, і ми їх швидко усунули. Тут хочеться відзначити користь від способу тестування, який ми для себе налаштували: у Docker розгортаються кілька десятків дистрибутивів Linux і в них встановлюються зібрані пакети. Можливість запуску програм теж перевірялася. Таке тестування дозволило нам оперативно вносити нові зміни в пакети і тестувати їх.

  4. Інші доробки з аналізатору та документації. Всі наші напрацювання ми відображали в документації. Ну а робота по доопрацюванню аналізатора не припиняється ніколи: це нові діагностики і поліпшення існуючих.
Закрите тестування Beta версії. Хвиля 4 (Release Candidate)
В останній хвилі розсилки аналізатора у користувачів вже відсутні проблеми з встановленням, запуском та налаштуванням аналізатора. Ми отримуємо листи-відгуки, приклади знайдених реальних помилок і приклади помилкових спрацьовувань.

Також користувачі стали більше цікавитися розширеними налаштуваннями аналізатора. Тому ми почали допрацьовувати документацію на предмет, як інтегрувати аналізатор в Makefile/CMake/QMake/QtCreator/Cline. Як це виглядає, я покажу далі.

Опрацьовані способи інтеграції
Інтеграція в Makefile/Makefile.am
Незважаючи на зручність перевірки проекту без інтеграції, безпосередньо у прямої інтеграції в складальну систему є ряд переваг:

  • Тонка настройка аналізатора;
  • Інкрементальний аналіз;
  • Розпаралелювання аналізу на рівні складальної системи;
  • Інші переваги від складальної системи.
Коли аналізатор викликається там же, де і компілятор, то в аналізатора правильно налаштоване оточення, робоча директорія та всі параметри. У цьому випадку виконуються всі умови для правильного і якісного аналізу.

Приблизно так виглядає інтеграція в Makefile:

.cpp.o:
$(CXX) $(CFLAGS) $(DFLAGS) $(INCLUDES) $< o $@
pvs-studio --cfg $(CFG_PATH) --source-file $< --language C++
--cl-params $(CFLAGS) $(DFLAGS) $(INCLUDES) $<

Інтеграція в CMake/CLion
Вивчивши інтеграцію аналізатора в CMake, стало можливим використання PVS-Studio в CLion. Можна отримувати як файл зі звітом аналізатора, так і виводити попередження в IDE для перегляду проблемних місць.



Інтеграція в CMake/QtCreator
Для роботи з CMake проектами в QtCreator точно також можна зберігати звіт або відразу переглядати попередження в IDE. На відміну від CLine, QtCreator вміє відкривати для перегляду звіти, збережені у форматі TaskList.



Інтеграція в QMake/QtCreator
Для QMake проектів ми теж передбачили спосіб простої інтеграції:

pvs_studio.target = pvs
pvs_studio.output = true
pvs_studio.license = /path/to/PVS-Studio.lic
pvs_studio.cxxflags = -std=c++14
pvs_studio.sources = $${ДЖЕРЕЛА}
include(PVS-Studio.pri)

Висновок
До чого ми дійшли за час розробки:

  1. Аналізатор легко встановити з пакету або сховища;
  2. З аналізатором легко познайомитися, виконавши перевірку без інтеграції аналізатора в складальну систему;
  3. Для регулярного використання аналізатора можна налаштувати інкрементальний аналіз на машині кожного розробника;
  4. Налаштування повної перевірки на build-сервері;
  5. Інтеграція з популярними IDE.
Такий інструмент вже можна показувати людям, що ми і зробили.

Завантажити та спробувати аналізатор можна посилання. Слідкуйте за нашими новинами і надсилайте проекти для перевірки, тепер і в Linux!


Якщо хочете поділитися цією статтею з англомовної аудиторією, то прошу використовувати посилання на переклад: Svyatoslav Razmyslov: The History of Development PVS-Studio for Linux .

Прочитали статтю і є питання?Часто до наших статей задають одні і ті ж питання. Відповіді на них ми зібрали тут: Відповіді на питання читачів статей про PVS-Studio, версія 2015. Будь ласка, ознайомтеся зі списком.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.