Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.
Разработка статического анализаторакода для обнаружения ошибокпереноса программ на 64-битныесистемыАвтор: Евгений РыжковДа...
прежде чем говорить о новом инструменте, необходимо все-таки более подробно описать теошибки, обнаружением которых должен ...
1.1 Использование "магических" константНаличие "магических" констант (то есть непонятно каким образом рассчитанных значени...
ptr = ptr + (A + B); //Invalid pointer value on 64-bit platformprintf("%in", *ptr); //Access violation on 64-bit platformД...
class CSampleApp : public CWinApp {  ...  virtual void WinHelp(DWORD dwData, UINT nCmd);};Неприятности проявят себя при ко...
Для анализатора кода ни генерация кода, ни его оптимизация не требуются. То есть необходиморазработать часть компилятора, ...
Рисунок 2 - Схема компилятора переднего планаДругая же часть традиционного компилятора (back-end compiler) отвечает за опт...
Рисунок 3 - Конечный автомат, описывающий часть лексического анализатора (рисунок из [3])Как уже говорилось, на данном эта...
ли код программы выводимым из грамматики заданного языка? В результате проверкивыводимости получается дерево разбора кода,...
Рисунок 5 - Пример дерева кодаВажно отметить, что для каких-то простых языков программирования в результате построениядере...
void call_func(double x);int main(){     int a = 2;     float b = 3.0;     call_func(a+b);}Рисунок 6 - Пример кода (вычисл...
Рисунок 7 - Пример дерева кода, дополненного информацией о типахПосле завершения работы модуля семантического анализа вся ...
все реальные типы данных, важные с точки зрения переноса кода на 64-битные системы, в кодепрограмм (например, ptrdiff_t, s...
Библиографический список  1. Карпов А. 20 ловушек переноса Си++ - кода на 64-битную платформу // RSDN Magazine #1-     200...
Upcoming SlideShare
Loading in …5
×

Разработка статического анализатора кода для обнаружения ошибок переноса программ на 64-битные системы

917 views

Published on

В статье рассмотрена задача разработки программного инструмента под названием статический анализатор. Разрабатываемый инструмент используется для диагностики потенциально опасных синтаксических конструкций языка Си++ с точки зрения переноса программного кода на 64-битные системы. Акцент сделан не на самих проблемах переноса, возникающих в программах, а на особенностях создания специализированного анализатора кода. Анализатор предназначен для работы с кодом программ на языках Си и Си++.

Published in: Technology
  • Be the first to comment

  • Be the first to like this

Разработка статического анализатора кода для обнаружения ошибок переноса программ на 64-битные системы

  1. 1. Разработка статического анализаторакода для обнаружения ошибокпереноса программ на 64-битныесистемыАвтор: Евгений РыжковДата: 26.03.2009АннотацияВ статье рассмотрена задача разработки программного инструмента под названием статическийанализатор. Разрабатываемый инструмент используется для диагностики потенциально опасныхсинтаксических конструкций языка Си++ с точки зрения переноса программного кода на 64-битные системы. Акцент сделан не на самих проблемах переноса, возникающих в программах, ана особенностях создания специализированного анализатора кода. Анализатор предназначен дляработы с кодом программ на языках Си и Си++.ВведениеОдной из современных тенденций развития информационных технологий является переноспрограммного обеспечения на 64-разрядные процессоры. Старые 32-битные процессоры (исоответственно программы) имеют ряд ограничений, которые мешают производителямпрограммных средств и сдерживают прогресс. Прежде всего, таким ограничением являетсяразмер максимально доступной оперативной памяти для программы (2 гигабайта). Хотясуществуют некоторые приемы, которые позволяют в ряде случаях обойти это ограничение, вцелом можно с уверенностью утверждать, что переход на 64-битные программные решениянеизбежен.Перенос программного обеспечения на новую архитектуру для большинства программ означаеткак минимум необходимость их перекомпиляции. Естественно, возможны варианты. Но в рамкахданной статьи речь идет о языках Си и Си++, поэтому перекомпиляция неизбежна. К сожалению,эта перекомпиляция часто приводит к неожиданным и неприятным последствиям.Изменение разрядности архитектуры (например, с 32 бит на 64) означает, прежде всего,изменение размеров базовых типов данных, а также соотношений между ними. В результатеповедение программы после перекомпиляции для новой архитектуры может измениться.Практика показывает, что поведение не только может, но и реально меняется. Причемкомпилятор часто не выдает диагностических сообщений на те конструкции, которые являютсяпотенциально опасными с точки зрения новой 64-битной архитектуры. Конечно же, наименеекорректные участки кода будут обнаружены компилятором. Тем не менее, далеко не всепотенциально опасные синтаксические конструкции можно найти с помощью традиционныхпрограммных инструментов. И именно здесь появляется место для нового анализатора кода. Но
  2. 2. прежде чем говорить о новом инструменте, необходимо все-таки более подробно описать теошибки, обнаружением которых должен будет заниматься наш анализатор.1 Некоторые ошибки переноса программ на 64-битные системыПодробный разбор и анализ всех потенциально опасных синтаксических конструкций языковпрограммирования Си и Си++ выходит за рамки данной статьи. Читателей, интересующихся этойпроблематикой, отсылаем к энциклопедической статье [1], где приведено достаточно полноеисследование вопроса. Для целей проектирования анализатора кода необходимо все-такипривести здесь основные типы ошибок.Прежде чем говорить о конкретных ошибках, напомним некоторые типы данных, используемые вязыках Си и Си++. Они приведены в таблице 1.Название типа Размер- Размер- Описание ность типа в ность типа в битах (32- битах (64- битная битная система) система)ptrdiff_t 32 64 Знаковый целочисленный тип, образующийся при вычитании двух указателей. В основном используется для хранения размеров и индексов массивов. Иногда используется в качестве результата функции, возвращающей размер или -1 при возникновении ошибки.size_t 32 64 Беззнаковый целочисленный тип. Результат оператора sizeof(). Часто служит для хранения размера или количества объектов.intptr_t, uintptr_t, 32 64 Целочисленные типы, способные хранить вSIZE_T, SSIZE_T, себе значение указателя.INT_PTR,DWORD_PTR и такдалееТаблица N1. Описание некоторых целочисленных типов.Эти типы данных замечательны тем, что их размер изменяется в зависимости от архитектуры. На64-битных системах размер равен 64 битам, а на 32-битных - 32 битам.Введем понятие "memsize-тип":ОПРЕДЕЛЕНИЕ: Под memsize-типом мы будем понимать любой простой целочисленный тип,способный хранить в себе указатель и меняющий свою размерность при изменении разрядностиплатформы с 32-бит на 64-бита. Все типы, перечисленные в таблице 1, являются как раз memsize-типами.Подавляющее большинство проблем, возникающих в коде программ (в контексте поддержки 64бит), связано с неиспользованием или некорректным использованием memsize-типов.Итак, приступим к описанию потенциальных ошибок.
  3. 3. 1.1 Использование "магических" константНаличие "магических" констант (то есть непонятно каким образом рассчитанных значений) впрограммах само по себе является нежелательным. Однако в контексте переноса программ на 64-битные системы у "магических" чисел появляется еще один очень важный недостаток. Они могутпривести к некорректной работе программ. Речь идет о тех "магических" числах, которыеориентированы на какую-то конкретную особенность архитектуры. Например, на то, что размеруказателя составляет 32 бита (4 байта).Рассмотрим простой пример.size_t values[ARRAY_SIZE];memset(values, ARRAY_SIZE * 4, 0);На 32-битной системе данный код был вполне корректен, однако размер типа size_t на 64-битнойсистеме увеличился до 8 байт. К сожалению, в коде использовался фиксированный размер (4байта). В результате чего массив будет заполнен нулями не полностью.Есть и другие варианты некорректного применения подобных констант.1.2 Адресная арифметикаРассмотрим типовой пример ошибки в адресной арифметикеunsigned short a16, b16, c16;char *pointer;...pointer += a16 * b16 * c16;Данный пример корректно работает с указателями, если значение выражения "a16 * b16 * c16" непревышает UINT_MAX (4Gb). Такой код мог всегда корректно работать на 32-битной платформе,так как программа никогда не выделяла массивов больших размеров. На 64-битной архитектуреразмер массива превысил UINT_MAX элементов. Допустим, мы хотим сдвинуть значениеуказателя на 6.000.000.000 байт, и поэтому переменные a16, b16 и c16 имеют значения 3000, 2000и 1000 соответственно. При вычислении выражения "a16 * b16 * c16" все переменные, согласноправилам языка Си++, будут приведены к типу int, а уже затем будет произведено их умножение.В ходе выполнения умножения произойдет переполнение. Некорректный результат выражениябудет расширен до типа ptrdiff_t и произойдет некорректное вычисление указателя.Но подобные ошибки возникают не только на больших данных, но и на обычных массивах.Рассмотрим интересный код для работы с массивом, содержащим всего 5 элементов. Примерработоспособен в 32-битном варианте и не работоспособен в 64-битном:int A = -2;unsigned B = 1;int array[5] = { 1, 2, 3, 4, 5 };int *ptr = array + 3;
  4. 4. ptr = ptr + (A + B); //Invalid pointer value on 64-bit platformprintf("%in", *ptr); //Access violation on 64-bit platformДавайте проследим, как происходит вычисление выражения "ptr + (A + B)": • Согласно правилам языка Си++ переменная A типа int приводится к типу unsigned. • Происходит сложение A и B. В результате мы получаем значение 0xFFFFFFFF типа unsigned.Затем происходит вычисление выражения "ptr + 0xFFFFFFFFu", но результат будет зависеть отразмера указателя на данной архитектуре. Если сложение будет происходить в 32-битнойпрограмме, то данное выражение будет эквивалентно "ptr - 1" и мы успешно распечатаем число 3.В 64-битной программе к указателю честным образом прибавится значение 0xFFFFFFFFu, врезультате чего указатель окажется далеко за пределами массива. И при доступе к элементу поданному указателю нас ждут неприятности.1.3 Совместное использование целочисленных типов и типовпеременной размерностиСмешанное использование memsize- и не memsize-типов в выражениях может приводить кнекорректным результатам на 64-битных системах и быть связано с изменением диапазонавходных значений. Рассмотрим ряд примеров:size_t Count = BigValue;for (unsigned Index = 0; Index != Count; ++Index){ ... }Это пример вечного цикла, если Count > UINT_MAX. Предположим, что на 32-битных системах этоткод работал с диапазоном менее UINT_MAX итераций. Но 64-битный вариант программы можетобрабатывать больше данных и ему может потребоваться большее количество итераций.Поскольку значения переменной Index лежат в диапазоне [0..UINT_MAX], то условие "Index !=Count" никогда не выполнится, что и приводит к бесконечному циклу.1.4 Виртуальные и перегруженные функцииЕсли у Вас в программе имеются большие иерархии наследования классов с виртуальнымифункциями, то существует вероятность использования по невнимательности аргументовразличных типов, которые фактически совпадают на 32-битной системе. Например, в базовомклассе Вы используете в качестве аргумента виртуальной функции тип size_t, а в наследнике - типunsigned. Соответственно, на 64-битной системе этот код будет некорректен.Такая ошибка не обязательно кроется в сложных иерархиях наследования, и вот один изпримеров:class CWinApp { ... virtual void WinHelp(DWORD_PTR dwData, UINT nCmd);};
  5. 5. class CSampleApp : public CWinApp { ... virtual void WinHelp(DWORD dwData, UINT nCmd);};Неприятности проявят себя при компиляции данного кода под 64-битную платформу. Получатсядве функции с одинаковыми именами, но с различными параметрами, в результате чегоперестанет вызываться пользовательский код.Похожие проблемы возможны и при использовании перегруженных функций.Как уже говорилось, это далеко не полный список потенциальных проблем (см. [1]), тем не менее,он позволяет сформулировать требования к анализатору кода.2 Требования к анализатору кодаНа основе списка потенциально-опасных конструкций, диагностирование которых необходимо,можно сформулировать следующие требования: 1. Анализатор должен позволять осуществлять лексический разбор кода программы. Это необходимо для анализа использования потенциально опасных числовых констант. 2. Анализатор должен позволять осуществлять синтаксический разбор кода программы. Только на уровне лексического анализа невозможно выполнить все необходимые проверки. Стоит отметить сложность синтаксиса языков Си и, особенно, Си++. Из этого следует необходимость именно полноценного синтаксического анализа, а не, например, поиска на основе регулярных выражений. 3. Важной составляющей частью анализатора является анализ типов. Сложность типов в целевых языках такова, что подсистема вычисления типов является достаточно трудоемкой. Тем не менее, обойтись без нее нельзя.Необходимо отметить, что конкретная архитектура реализации перечисленного функционалароли не играет, однако эта реализация должна быть полноценной.В литературе по разработке компиляторов [2] сказано, что традиционный компилятор имеетследующие фазы своей работы: Рисунок 1 - Фазы работы традиционного компилятораОбратим внимание, что это "логические" фазы работы. В реальном компиляторе какие-то этапыобъединены, какие-то выполняются параллельно с другими. Так, например, достаточно частофазы синтаксического и семантического анализа объединены.
  6. 6. Для анализатора кода ни генерация кода, ни его оптимизация не требуются. То есть необходиморазработать часть компилятора, которая отвечает за лексический, синтаксический исемантический анализ.3 Архитектура анализатора кодаИсходя из рассмотренных требований к разрабатываемой системе, можно предложитьследующую структуру анализатора кода: 1. Модуль лексического анализа. Математическим аппаратом данного модуля являются конечные автоматы. В качестве результата лексического анализа получается набор лексем. 2. Модуль синтаксического анализа. Математический аппарат - грамматики; в результате работы получается дерево разбора кода. 3. Модуль семантического (контекстного) анализа. Математическим аппаратом также являются грамматики, но особого вида: либо специальным образом "расширенные" грамматики, либо так называемые атрибутные грамматики [3]. Результатом является дерево разбора кода с проставленной дополнительной информацией о типах (либо атрибутированное дерево разбора кода). 4. Система диагностики ошибок. Это та часть анализатора кода, которая непосредственно отвечает за обнаружение потенциально опасных конструкций с точки зрения переноса кода на 64-битные системы.Перечисленные модули являются стандартными [4] для традиционных компиляторов (рисунок 2),точнее для той части компилятора, которая называется компилятор переднего плана (front-endcompiler).
  7. 7. Рисунок 2 - Схема компилятора переднего планаДругая же часть традиционного компилятора (back-end compiler) отвечает за оптимизацию икодогенерацию и в данной работе не представляет интереса.Таким образом, разрабатываемый анализатор кода должен иметь в своем составе компиляторпереднего плана для того, чтобы обеспечить необходимый уровень анализа кода.3.1 Модуль лексического анализаЛексический анализатор представляет собой конечный автомат, описывающий правилалексического разбора конкретного языка программирования.Описание лексического анализатора может быть не только в виде конечного автомата, но и в видерегулярного выражения. И тот, и другой варианты описания равнозначны, так как легкопереводятся друг в друга. На рисунке 3 приведена часть конечного автомата, описывающегоанализатор языка Си.
  8. 8. Рисунок 3 - Конечный автомат, описывающий часть лексического анализатора (рисунок из [3])Как уже говорилось, на данном этапе возможен лишь анализ одного типа потенциально опасныхконструкций - использование "магических" констант. Все другие виды анализа будут выполнятьсяна следующих этапах.3.2. Модуль синтаксического анализаМодуль синтаксического анализа работает с аппаратом грамматик для того, чтобы по наборулексем, полученных на предыдущем этапе, построить дерево разбора кода (английский термин -abstract syntax tree). Точнее можно сформулировать задачу синтаксического анализа так. Является
  9. 9. ли код программы выводимым из грамматики заданного языка? В результате проверкивыводимости получается дерево разбора кода, но суть именно в определении принадлежностикода конкретному языку программирования.В результате разбора кода строится дерево кода. Пример такого дерева для фрагмента кода нарисунке 4 приведен на рисунке 5.int main(){ int a = 2; int b = a + 3; printf("%d", b);}Рисунок 4 - Пример кода (для дерева разбора кода)
  10. 10. Рисунок 5 - Пример дерева кодаВажно отметить, что для каких-то простых языков программирования в результате построениядерева кода структура программы становится полностью известной. Однако для сложного языкавроде Си++ необходим дополнительный этап, когда построенное дерево будет дополняться,например, информацией о типах данных.3.3. Модуль семантического анализВ модуле семантического анализа наибольший интерес представляет подсистема вычислениятипов. Дело в том, что типы данных в Си++ представляют собой довольно сложный и очень сильнорасширяемый набор сущностей. Помимо базовых типов, характерных для любых языковпрограммирования (целое, символ и т.п.), в Си++ есть понятие указателей на функции, шаблонов,классов и так далее.Столь сложная подсистема типов не позволяет выполнить полный анализ программы на стадиисинтаксического анализа. Поэтому на вход модуля семантического анализа подается дереворазбора кода, которое затем дополняется информацией уже обо всех типах данных.Здесь же происходит и операция вычисления типов. Язык Си++ позволяет кодировать достаточносложные выражения, при этом определить их тип зачастую не просто. На рисунке 6 показанпример кода, для которого необходимо вычисление типов при передаче аргументов в функцию.
  11. 11. void call_func(double x);int main(){ int a = 2; float b = 3.0; call_func(a+b);}Рисунок 6 - Пример кода (вычисление типа).В данном случае необходимо вычислить тип результата выражения (a+b), и добавитьинформацию о типе в дерево (рисунок 7).
  12. 12. Рисунок 7 - Пример дерева кода, дополненного информацией о типахПосле завершения работы модуля семантического анализа вся возможная информация опрограмме становится доступной для дальнейшей обработки.3.4 Система диагностики ошибокГоворя об обработке ошибок, разработчики компиляторов имеют в виду особенности поведениякомпилятора при обнаружении некорректных кодов программ. В этом смысле ошибки можноразделить на несколько типов [2]: • лексические - неверно записанные идентификаторы, ключевые слова или операторы; • синтаксические - например, арифметические выражения с несбалансированными скобками; • семантические - такие как операторы, применяемые с несовместимыми с ними операндами.Все эти типы ошибок означают, что вместо корректной с точки зрения языка программированияпрограммы на вход компилятору подали некорректную программу. И задача компилятора состоитв том, чтобы, во-первых, диагностировать ошибку, а, во-вторых, по возможности продолжитьработу по трансляции или остановиться.Совсем другой подход к ошибкам возникает, если мы говорим о статическом анализе исходныхкодов программ с целью выявления потенциально опасных синтаксических конструкций.Основное отличие заключается в том, что на вход синтаксического анализатора кода подаетсялексически, синтаксически и семантически абсолютно корректный программный код. Поэтомуреализовывать систему диагностики некорректных конструкций в статическом анализаторе также, как и систему диагностики ошибок в традиционном компиляторе, к сожалению, нельзя.4 Реализация анализатора кодаРеализация анализатора кода состоит из реализации двух частей: • компилятора переднего плана (front end compiler); • подсистемы диагностики потенциально опасных конструкций.Для реализации компилятора переднего плана будем использовать существующую открытуюбиблиотеку анализа Си++ кода OpenC++ [6], точнее ее модификацию VivaCore [7]. Это рукописныйсинтаксический анализатор кода, в котором осуществляется анализ методом рекурсивного спуска(рекурсивный нисходящий анализ) с возвратом. Выбор рукописного анализатора обусловленсложностью языка Си++ и отсутствием готовых описанных грамматик этого языка дляиспользования средств автоматического создания анализаторов кода типа YACC и Bison.Для реализации подсистемы поиска потенциально опасных конструкций, как уже было сказано вразделе 3.4, использовать традиционную для компиляторов систему диагностики ошибок нельзя.Используем для этого несколько приемов по модификации базовой грамматики языка Си++.Прежде всего, необходимо поправить описание базовых типов языка Си++. В разделе 1 быловведено понятие memsize-типов, то есть типов переменной размерности (таблица 1). Все данныетипы в программах будем обрабатывать как один специальный тип (memsize). Другими словами,
  13. 13. все реальные типы данных, важные с точки зрения переноса кода на 64-битные системы, в кодепрограмм (например, ptrdiff_t, size_t, void* и др.) будут обрабатываться как один тип.Далее необходимо внести расширение в понятие грамматики, добавив в ее правила выводасимволы-действия [5]. Тогда процедура рекурсивного спуска, которая выполняет синтаксическийанализ, также будет выполнять некоторые дополнительные действия по проверке семантики.Именно эти дополнительные действия и составляют суть статического анализатора кода.Например, фрагмент грамматики для проверки корректности использования виртуальныхфункций (из раздела 1.4) может выглядеть так:<ЗАГОЛОВОК_ВИРТУАЛЬНОЙ_ФУНКЦИИ> > <virtual> <ЗАГОЛОВОК_ФУНКЦИИ> CheckVirtual()Здесь CheckVirtual() - это тот самый символ-действие. Действие CheckVirtual() будет вызвано, кактолько процедура рекурсивного спуска обнаружит объявление виртуальной функции ванализируемом коде. А уже внутри процедуры CheckVirtual() будет осуществляться проверкакорректности аргументов в объявлении виртуальной функции.Проверки всех потенциально опасных конструкций в языках Си и Си++, о которых говорится в [1],оформлены в аналогичные символы-действия. Сами эти символы-действия добавлены вграмматику языка, точнее в синтаксический анализатор, который вызывает символы-действия приразборе кода программы.5 РезультатыРассмотренная в работе архитектура и структура анализатора кода легли в основу коммерческогопрограммного продукта Viva64 [8]. Viva64 - это статический анализатор кода программ,написанных на языках Си и Си++. Он предназначен для обнаружения в исходном коде программпотенциально опасных синтаксических конструкций с точки зрения переноса кода на 64-битныесистем.6 ЗаключениеСтатический анализатор - это программа, состоящая из двух частей: • компилятора переднего плана (front end compiler); • подсистемы диагностики потенциально опасных синтаксических конструкций.Компилятор переднего плана является традиционным компонентом обычного компилятора,поэтому принципы его построения и разработки достаточно хорошо изучены.Подсистема диагностики потенциально опасных синтаксических конструкций является темэлементом статического анализатора кода, который и делает анализаторы уникальными,отличающимися по кругу решаемых задач. Так, в рамках данной работы рассматривалась задачапереноса кода программ на 64-битные системы. Именно свод знаний о 64-битном программномобеспечении лег в основу подсистемы диагностики.Объединение компилятора переднего плана из проекта VivaCore [7] и свода знаний о 64-битномпрограммном обеспечении [1] позволило разработать программный продукт Viva64 [8].
  14. 14. Библиографический список 1. Карпов А. 20 ловушек переноса Си++ - кода на 64-битную платформу // RSDN Magazine #1- 2007. 2. Ахо А., Сети Р., Ульман Д.. Компиляторы: принципы, технологии и инструменты. : Пер. с англ. - М.: Издательский дом "Вильямс", 2003. - 768 с.: ил. - Парал. тит. англ. 3. Серебряков В.А., Галочкин М.П.. Основы конструирования компиляторов. М.: Едиториал УРСС, 2001. - 224 с. 4. Зуев Е.А. Принципы и методы создания компилятора переднего плана Стандарта Си++. Диссертация на соискание ученой степени кандидата физико-математических наук. Москва, 1999. 5. Формальные грамматики и языки. Элементы теории трансляции / Волкова И. А., Руденко Т. В. ; Моск. гос. ун-т им. М. В. Ломоносова, Фак. вычисл. математики и кибернетики, 62 с. 21 см, 2-е изд., перераб. и доп. М. Диалог-МГУ 1999. 6. OpenC++ (C++ frontend library). http://opencxx.sourceforge.net/. 7. VivaCore Library. http://www.viva64.com/ru/vivacore-library/. 8. Viva64 Tool. http://www.viva64.com/ru/viva64-tool/.

×