Статичний аналіз коду

John Carmack
Примітка від перекладача. Спочатку ця стаття була опублікована на сайті AltDevBlogADay. Але сайт, на жаль, припинив своє існування. Більше року ця стаття залишалася недоступною читачам. Ми звернулися до Джону Кармаку, і він сказав, що не проти, щоб ми розмістили цю статтю на нашому сайті. Що ми із задоволенням і зробили. З оригіналом статті можна ознайомитися, скориставшись Wayback Machine — Internet Archive: Static Code Analysis.

Оскільки всі статті на нашому сайті представлені російською та англійською мовою, то ми виконали переклад статті Static Code Analysis на російську мову. А заодно вирішили опублікувати її на Хабре. Тут вже публікувався переказ цієї статті. Але впевнений, багатьом буде цікаво прочитати саме переклад.


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

Відразу потрібно зауважити, що не можна все зводити до якості, і зізнатися в цьому зовсім не означає зрадити якісь свої моральні принципи. Цінність має створюваний вами продукт в цілому, а якість коду — лише один з її компонентів нарівні з вартістю, функціональними можливостями та іншими характеристиками. Світові відомо безліч супер-успішних та шанованих ігрових проектів, напханих багами і без кінця падаючих; та й нерозумно було б підходити до написання гри з тією ж серйозністю, з якою створюють для космічних шатлів. І все ж якість — це, безсумнівно, важливий компонент.

Я завжди намагався писати хороший код. По своїй натурі я схожий на ремісника, яким рухає бажання безперервно щось покращувати. Я прочитав купу книг з нудними назвами глав типу «Стратегії, стандарти і плани якості», а робота в Armadillo Aerospace відкрила мені дорогу в зовсім інший, відмінний від попереднього досвіду світ розробки ПЗ з підвищеними вимогами до безпеки.

Більше десяти років тому, коли ми займалися розробкою Quake 3, я купив ліцензію на PC-Lint і намагався застосовувати його в роботі: приваблювала ідея автоматичного виявлення дефектів в коді. Однак необхідність запуску з командного рядка і перегляду довгих списків діагностичних повідомлень відбили у мене охоту користуватися цим інструментом, і незабаром я відмовився від нього.

З тих пір і кількість програмістів, і розмір кодової бази зросли на порядок, а акцент в програмуванні змістився з мови C на C++. Все це підготувало набагато більш благодатний грунт для програмних помилок. Кілька років тому, прочитавши збірку наукових статей про сучасний статичному аналізі коду, я вирішив перевірити, як змінилося положення справ у цій області за останні десять років з тих пір, як я спробував працювати з PC-Lint.

На той момент код у нас компилировался на 4-му рівні попереджень, при цьому вимкненими ми залишали лише кілька вузькоспеціальних діагностик. З таким підходом — свідомо розглядати кожне попередження, як помилку — програмісти були змушені неухильно дотримуватися цієї політики. І хоча в нашому коді можна було відшукати кілька запорошених куточків, в яких з роками скупчився всякий «сміття», в цілому він був досить сучасним. Ми вважали, що у нас цілком непогана кодова база.

Coverity
Почалося все з того, що я зв'язався з Coverity і підписався на пробну діагностику нашого коду їх інструментом. Це серйозна програма, вартість ліцензії залежить від загальної кількості рядків коду, і ми зупинилися на ціні, вираженої пятизначным числом. Показуючи нам результати аналізу, експерти з Coverity відзначили, що наша база виявилася однією з найчистіших у своїй «ваговій категорії» з усіх, що їм доводилось бачити (можливо, вони говорять це всім клієнтам, щоб підбадьорити їх), однак звіт, який вони нам передали, містив близько сотні проблемних місць. Такий підхід сильно відрізнявся від мого попереднього досвіду роботи з PC-Lint. Співвідношення сигнал/шум в даному випадку виявився надзвичайно високий: більшість з виданих Coverity попереджень дійсно вказували на явно некоректні ділянки коду, які могли мати серйозні наслідки.

Цей випадок буквально відкрив мені очі на статичний аналіз, але висока ціна задоволення деякий час утримувала від покупки інструменту. Ми подумали, що залишився до релізу коді у нас буде не так багато помилок.

Microsoft /analyze
Не виключено, що я, зрештою, наважився б купити Coverity, але поки я розмірковував над цим, Microsoft припинили мої сумніви, реалізувавши нову функцію /analyze 360 SDK. /Analyze раніше був доступний в якості компонента топової, шалено дорогий версії Visual Studio, а потім раптом дістався безкоштовно кожному розробнику під xbox 360. Я так розумію, про якість ігор на 360-й платформі Microsoft піклується більше, ніж про якості З під Windows. :-)

З технічної точки зору аналізатор Microsoft всього лише проводить локальний аналіз, тобто він поступається глобального аналізу Coverity, однак коли ми його включили, він вивалив гори повідомлень — набагато більше, ніж видав Coverity. Так, там було багато помилкових спрацьовувань, але і без них знайшлося чимало всяких страшних, по-справжньому страшних бек.

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

Зрештою, я домігся, щоб весь використаний код скомпилировался в запускається файл під 360-ю платформу без жодного попередження при включеному /analyze, і встановив такий режим компіляції в якості стандартного для 360-збірок. Після цього код у кожного програміста, який працює на тій же платформі, всякий раз при компіляції перевірявся на наявність помилок, так що він міг відразу правити баги по мірі їх внесення в програму замість того, щоб потім ними займався я. Звичайно, з-за цього процес компіляції дещо сповільнилося, але /analyze — однозначно найшвидший інструмент з усіх, з якими мені доводилося мати справу, і, повірте мені, воно того варто.

Одного разу ми в якомусь проекті випадково вимкнули статичний аналіз. Минуло кілька місяців, і коли я помітив це і знову включив його, інструмент видав купу нових попереджень про помилки, внесених код за цей час. Подібним же чином програмісти, які працюють тільки під PC або PS3, вносять в репозиторій код з помилками і перебувають у невіданні, поки не отримають лист із звітом про «невдалу 360-складання». Ці приклади наочно демонструють, що в процесі своєї повсякденної діяльності розробники раз за разом роблять помилки певних видів, та /analyze надійно уберегал нас від більшої їх частини.

Брюс Доусон не раз згадував у своєму блозі про роботу з /analysis.

PVS-Studio
Оскільки ми могли використовувати /analyze тільки на 360-коді, великий обсяг нашої кодової бази як і раніше залишався не покритим статичним аналізом — це стосувалося коду під платформи PC і PS3, а також всіх програм, що працюють тільки на PC.

Наступним інструментом, з яким я познайомився, був PVS-Studio. Він легко інтегрується в Visual Studio і пропонує зручний демо-режим (спробуйте самі!). У порівнянні з /analyze PVS-Studio жахливо повільний, але він зумів виловити деяку кількість нових критичних багів, причому навіть у тому коді, який був вже повністю очищено з точки зору /analyze. Крім очевидних помилок PVS-Studio відловлює безліч інших дефектів, які являють собою помилкові программистские кліше, нехай і здаються на перший погляд нормальним кодом. З-за цього практично неминучий певний відсоток помилкових спрацьовувань, але, чорт візьми, в нашому коді такі шаблони знайшлися, і ми їх поправили.

На сайті PVS-Studio можна знайти велику кількість чудових статей про інструменті, і багато з них містять приклади з реальних open-source проектів, що ілюструють конкретно ті види помилок, про які йдеться в статті. Я думав, не вставити сюди кілька показових діагностичних повідомлень, видаваних PVS-Studio, але на сайті вже з'явилися набагато більш цікаві приклади. Так що відвідайте сторінку і подивіться самі. І так — коли будете читати ці приклади, не треба посміхатися і говорити, що ви б так ніколи не написали.

PC-Lint
Зрештою, я повернувся до варіанту з використанням PC-Lint у зв'язці з Visual Lint для інтеграції в середовище розробки. Згідно з легендарною традицією світу Unix інструмент можна налаштувати на виконання практично будь-якого завдання, однак його інтерфейс не дуже дружній і його не можна просто так взяти і запустити». Я придбав набір з п'яти ліцензій, але його освоєння виявилося настільки трудомістким, що, наскільки я знаю, всі інші розробники від нього в підсумку відмовилися. Гнучкість дійсно має свої переваги — так, наприклад, мені вдалося налаштувати його для перевірки всього нашого коду під платформу PS3, хоча це і відняло в мене чимало часу і зусиль.

І знову в тому коді, який вже був чистим з точки зору /analyze і PVS-Studio, знайшлися нові важливі помилки. Я чесно намагався вичистити його так, щоб і lint не лаявся, але не вдалося. Я поправив весь системний код, але здався, коли побачив, скільки попереджень він видав на ігровий код. Я розсортував помилки за класами і зайнявся найбільш критичними з них, ігноруючи масу інших, належать більше до стилістичних недоробок або потенційним проблемам.

Я вважаю, що спроба виправити величезний обсяг коду по максимуму з точки зору PC-Lint свідомо приречена на провал. Я написав деяку кількість коду з нуля в тих місцях, де слухняно намагався позбавитися від кожного настирливого «линтовского» коментаря, але для більшості досвідчених C/C++-програмістів такий підхід до роботи над помилками — вже занадто. Мені до цих пір доводиться возитися з налаштуваннями PC-Lint, щоб підібрати найбільш підходящий набір попереджень і вичавити з інструменту максимум користі.

Висновки
Я чимало дізнався, пройшовши через все це. Боюся, що дехто з моїх висновків з працею сприйняті людьми, яким не доводилося особисто розбирати сотні повідомлень про помилки в стислі терміни і щоразу відчувати нудоту, приступаючи до їх виправленню, і стандартною реакцією на мої слова «ну у нас-то все в порядку» або «все не так погано».

Перший крок на цьому шляху — чесно зізнатися собі, що ваш код кишить багами. Для більшості програмістів це гірка пігулка, але, не проковтнувши її, ви мимоволі будете сприймати будь-яку пропозицію по зміні і поліпшенню коду з роздратуванням, а то й неприхованою ворожістю. Ви повинні захотіти піддати свій код критиці.

Автоматизація необхідна. Коли бачиш повідомлення про жахливі збої в автоматичних системах, неможливо не відчути таке зловтіха, проте на кожну помилку в автоматизації доводиться легіон людських помилок. Заклики до того, щоб «писати більш якісний код», благі наміри про проведення більшого числа сеансів інспекції коду, парного програмування і так далі просто-напросто не діють, особливо коли в проект залучені десятки людей і працювати доводиться в дикій поспіху. Величезна цінність статичного аналізу полягає в можливості при кожному запуску знаходити хоча б невеликі порції помилок, доступних цією методикою.

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

Інструменти статичного аналізу змушені працювати «з однією рукою, пов'язаної за спиною»: їм доводиться робити висновки на основі аналізу мов, які зовсім не обов'язково надають інформацію для таких висновків, і в цілому робити дуже обережні припущення. Тому ви повинні допомагати своєму аналізатору, наскільки можливо — віддавати перевагу індексації перед арифметикою з покажчиками, тримати граф викликів в єдиному вихідному файлі, використовувати явні анотації і т. п. Все, що може здатися статичного аналізатора неочевидним, майже напевно зіб'є з пантелику і ваших колег-програмістів. Характерне «хакерское» відраза до мов зі строгою статичної типізацією («bondage and discipline languages») на ділі виявляється недалекоглядним: потреби великих, довготривалих проектів, у розробку яких залучені великі команди програмістів, кардинально відрізняються від дрібних і швидких завдань, виконуваних для себе.

Нульові покажчики — це найнагальніша проблема в мові C/C++, принаймні у нас. Можливість подвійного використання єдиного значення як прапора, так і адреси приводить до неймовірного числа критичних помилок. Тому завжди, коли для цього є можливість, C++ слід віддавати перевагу посиланнями, а не вказівниками. Хоча посилання «насправді» є ні що інше як той же покажчик, вона пов'язана неявним зобов'язанням про неможливість рівності нулю. Виконуйте перевірки покажчиків на нуль, коли вони перетворюються у посилання — це дозволить вам згодом забути про цю проблему. У сфері ігробудування існує безліч глибоко укорінених програмістських шаблонів, які несуть потенційну небезпеку, але я не знаю способу, як повністю і безболісно перейти від перевірок на нуль до посилань.

Другою за важливістю проблемою в нашій кодової базі були помилки з printf-функціями. Вона додатково ускладнювалася тим, що передача idStr замість idStr::c_str() практично кожен раз закінчувалася падінням програми. Однак, коли ми стали використовувати анотації /analyze для функцій із змінною кількістю аргументів, щоб повірки типів виконувалися коректно, проблема була вирішена раз і назавжди. В корисних попередження аналізатора ми зустрічали десятки таких дефектів, які могли призвести до падіння, якби якогось хибного умові запустити відповідну гілку коду — це, між іншим, говорить ще і про те, як малий був відсоток покриття нашого коду тестами.

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

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

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

Неможливо провести достовірні контрольні випробування при розробці ПО, але наш успіх від використання аналізу коду був настільки виразним, що я можу дозволити собі просто заявити: не використовувати аналіз коду — безвідповідально! Автоматичні консольні логи про падіння містять об'єктивні дані, які ясно показують, що Rage, навіть будучи за багатьма показниками першопрохідцем, виявився набагато стабільніше і здоровіше, ніж більшість сучасних ігор. Запуск Rage на PC, на жаль, провалився — готовий посперечатися, що AMD не використовують статичний аналіз при розробці своїх графічних драйверів.

Ось вам готовий рецепт: якщо у вашій версії Visual Studio є вбудований /analyze, увімкніть його і спробуйте попрацювати так. Якщо б мене попросили вибрати з безлічі інструментів один, я б зупинився саме на цьому рішенні від Microsoft. Всім іншим, хто працює в Visual Studio, я раджу хоча б спробувати PVS-Studio в демо-режимі. Якщо ви розробляєте комерційне, придбання інструментів статичного аналізу буде одним з кращих способів вкладення коштів.

І наостанок коментар з твіттера:

Дейв Ревелл @dave_revell Чим більше я застосовую статичний аналіз на своєму коді, тим більше дивуюся, як комп'ютери взагалі запускаються.

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

0 коментарів

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