В статье сформулированы правила диагностики потенциально опасных синтаксических конструкций в исходном коде программ на языке Си++. Описаны принципы построения статического анализатора исходного кода, реализующего проверку указанных правил.
Правила статического анализа кода для диагностики потенциально опасных конструкций с точки зрения 64-битных программ
1. Правила статического анализа кода для
диагностики потенциально опасных конструкций с
точки зрения 64-битных программ
Евгений Рыжков, октябрь 2008
2. Аннотация
В статье сформулированы правила диагностики потенциально опасных синтаксических конструкций в
исходном коде программ на языке Си++. Описаны принципы построения статического анализатора исходного
кода, реализующего проверку указанных правил.
Введение
Задача статического анализа исходного кода известна давно [1] и существуют традиционные методы ее
решения как с теоретической так и с практической точек зрения.
Вместе с тем развитие индустрии промышленной разработки программного обеспечения ставит перед
разработчиками статических анализаторов кода новые задачи. Речь идет о переносе кода (портированиии,
миграции) приложений на 64-битные платформы, поддержке параллельного программирования и так далее.
В этих задачах, стоящих уже перед многими программистами, возникает множество нюансов и проблем [2, 3].
В их диагностике могут помочь различные инструменты и методики [4].
В настоящей статье рассматривается один из подходов к диагностике проблем в коде 64-битных приложений.
А именно - разработка специализированного статического анализатора кода.
3. Статический анализатор кода состоит из двух частей:
компилятора переднего плана (front end compiler) - модуля, выполняющего разбор исходного кода,
лексический и синтаксический анализ, а также построение дерева разбора для дальнейшего анализа;
набора правил диагностики потенциально опасных конструкций.
Под потенциально опасными конструкциями будем понимать такие конструкции в коде программ, которые
при переносе приложения на 64-битную платформу могут привести к некорректной работе программ. Следует
не путать их с дефектами [5] в коде программ, которые являются ошибками и требуют исправления в любом
случае. В отличие от дефектов потенциально опасные конструкции, которые диагностирует статический
анализатор кода, должны быть просмотрены программистом. И уже программист принимает решение,
является ли данный код некорректным в конкретной ситуации. Если код признается программистом
некорректным, то он подлежит исправлению.
Таким образом, задача статического анализатора заключается в диагностике с помощью набора правил
потенциально опасных конструкций.
4. Разработка модуля анализа
Принципы построения статического анализатора кода хорошо изучены и приведены в литературе [6]. Поэтому
для реализации анализатора, предназначенного для разработки 64-битных приложений, следует выбрать
традиционный подход к построению модуля анализа.
Так как разрабатываемый анализатор кода предназначен для языков Си и Си++, то исходя из знаний о типе
этих языков программирования и нужно конструировать модуль анализа.
Язык Си++ задается контекстно-свободной (КС) грамматикой (классификация по Хомскому). Для разбора
программ на языке Си++ используется синтаксический анализатор, распознающий КС-грамматику.
Лексический же разбор реализован на основе регулярной грамматики. Необходимость и лексического, и
синтаксического разборов объясняется особенностью проверяемых правил.
Распознавание языка Си++ реализуется, методом рекурсивного спуска (рекурсивный нисходящий анализ) с
возвратом. Такое распознавание реализовано в библиотеке анализа кода VivaCore [7].
В результате разбора кода получается синтаксическое дерево разбора (derivation tree). Дерево разбора по
сравнению с абстрактным синтаксическим деревом (abstract syntax tree) содержит больше информации,
которая необходима в ряде случаев для дальнейшего анализа. После чего специальный алгоритм
осуществляет проход по дереву и выполняет проверку определенных правил.
5. Типы данных
Прежде чем говорить о каких-либо правилах диагностики потенциально опасных конструкций, необходимо
определится с архитектурой, для которой будут разрабатываться правила. Для нас наиболее важна такая
составляющая архитектуры, как модель данных. Модель данных [2] - это соотношение размеров основных
типов данных на конкретной архитектуре. Так, модель данных 64-битной архитектуры Windows называется
LLP64. В то время как для 64-битной архитектуры Linux применяется модель LP64. В дальнейшем все правила
будут приводиться для архитектуры LLP64, однако они абсолютно также применимы и к архитектуре LP64
после замены определений основных базовых типов.
Введем множество T - множество всех целочисленных базовых и производных от них типов языка C++, в том
числе указателей. Примеры int, bool, short int, size_t, void*, указатели на классы.
Введем множество S - множество размеров этих типов (в байтах), такое что Tt Ss . Примеры: 1, 2, 4, 8,
16, 32, 64.
Количество элементов в множествах T и S различно, элементов в T больше чем в S .
Введем операцию соответствия 32 , при которой тип языка C++ отображается в рамках 32-битной архитектуры
в размер этого типа: SsSt 32 , а также операцию 64 , при которой тип языка отображается в рамках 64-
битой архитектуры в размер этого типа: SsSt 64 . Формально операции выглядят так: ST :32 и
ST :64 .
6. Введем множество T- множество всех memsize-типов (типов переменной размерности) языка C++, TT .
Примеры size_t, ptrdiff_t, int*, void*.
Элементы множества T обладают тем свойством, что
Tt :
SsSt
SsSt
*
64
32
, *
ss .
Другими словами, memsize-типы - это StStTtTT 6432:, .
Введем множество TT 32 - это все типы данных, являющиеся 32-битными в рамках как 32-битной, так и 64-
битной архитектуры, т.е. StStTtTT 64323232323232 :, . Пример: int.
По аналогии введем множество TT 64 - это все типы данных, являющиеся 64-битными в рамках как 32-битной,
так и 64-битной архитектуры. Пример: long long.
7. Размеры всех memsize-типов на 32-битной архитектуре равны одному числу q =4 (4 байта):
SpTt , верно что qSt 64 . Размеры всех memsize-типов на 64-битной архитектуре равны числу *
q =8 (8
байт).
Введем множество P - типы данных "указатели" в языке C++, TP .
Введем операцию разыменования типа *
следующим образом:
TP :*
.
Данная операция предназначена для получения типа данных, на который указывает указатель: tp *
.
Пример: intint* *
.
Введем множество D , состоящее из всех типов, производных от типа double. Пример: double, long double.
8. Правила анализа корректности кода
Все правила анализа корректности кода представлены в виде функций, которые принимают некоторые
аргументы (разные для разных правил), а возвращают true в случае некорректного кода и false, в случае
корректного. Все правила составлены по результатам изучения и обработки ошибок переноса кода на 64-
битные платформы [2].
Приведение 32-битных целых типов к memsize-типам
Следует считать опасными конструкции явного и неявного приведения целых типов размерностью 32 бита к
memsize-типам.
Примеры:
unsigned a, c;
size_t b = a;
array[c] = 1;
.,
,,
),( 2321
211
иначеfalse
TtTtеслиtrue
ttF
9. Приведение memsize-типов к целым 32-битным типам
Следует считать опасными конструкции явного и неявного приведения memsize-типов к целым типам
размерностью 32 бита.
Пример:
size_t a;
unsigned b = a;
.,
,,
),( 3221
212
иначеfalse
TtTtеслиtrue
ttF
10. Memsize-типы в виртуальных функциях
Опасными следует считать виртуальную функцию, удовлетворяющую ряду условий.
а). Функция объявлена в базовом классе и в классе-потомке.
б). Типы аргументов функций не совпадают, но эквивалентны на 32-битной системе (например: unsigned,
size_t) и не эквивалентны на 64-битной.
Пример:
class Base {
virtual void foo(size_t);
};
class Derive : public Base {
virtual void foo(unsigned);
};
11. Рассмотрим кортежи 1M и 2M , представляющие собой наборы элементов из множества T . Ошибочной
следует считать ситуацию, при которой в 32-битном режиме кортежи 1M и 2M совпадают, а в 64-битном -
различны.
.,
,..1,)()()()(
)()()()(,
),(
642641322321
642641322321
213
иначеfalse
niSmSmSmSm
SmSmSmSmеслиtrue
MMF
iiii
iiii
12. Memsize-типы в перегруженных функциях
Опасными следует считать вызов перегруженных функций с аргументом типа memsize. При этом функции
должны быть перегружены для целых 32-х и 64-битных типов данных.
Пример:
void WriteValue(__int32);
void WriteValue(__int64);
...
ptrdiff_t value;
WriteValue(value);
13. Рассмотрим вызов функции c n фактическими аргументами. Если существует 2 или более перегруженных
функции с таким же количеством аргументов, то необходимо выполнить следующую проверку.
A - кортеж типов фактических параметров функции;
1A - кортеж типов формальных параметров первой перегруженной функции;
2A - кортеж типов формальных параметров второй перегруженной функции;
.,
,..1,)()()()()(,
),,( 641322642321
214
иначеfalse
niTaTaTaTaTaеслиtrue
AAAF iiiii
14. Приведение типов указателей на memsize-типы
Опасным следует считать явное приведение одного типа указателя к другому, если один из них ссылается на
32-/64-битный тип, а другой на memsize-тип.
Пример:
int *array;
size_t *sizetPtr = (size_t *)(array);
.,
,
,
),(
64322
*
11
*
2
64322
*
21
*
1
215
иначеfalse
TTtpTtp
TTtpTtpеслиtrue
ppF
Приведение memsize-типов к double
Опасным следует считать явные и неявные приведения memsize-типа к double и наоборот.
Пример:
size_t a;
double b = a;
.,
,,
),( 1221
216
иначеfalse
TtDtTtDtеслиtrue
ttF
15. Memsize-типы в функции с переменным количеством аргументов
Опасным следует считать передачу memsize-типа (кроме указаталей) в функцию с переменным количеством
аргументов.
Пример:
size_t a;
printf("%u", a);
Пусть K - кортеж всех фактических типов, которые являются параметрами функции с переменным
количеством аргументов. Пусть функция вызывается с m аргументами.
.,
,..1,/,
)(7
иначеfalse
miPTесли ktrue
KF i
16. Опасные константы
Опасным следует считать использование констант определенного вида. Введем множество N целых чисел,
которые можно записать средствами языка C++. Введем множество "опасных" констант NC . Примеры
"опасных" констант: 4, 32, 0xffffffff и т.д.
.,
,,
)(8
иначеfalse
Ccеслиtrue
cF
17. Memsize-типы в объединениях
Опасным следует считать наличие в объединениях (union) членов memsize-типов.
Пример:
union PtrNumUnion {
char *m_p;
unsigned m_n;
} u;
Все типы данных, входящие в union будем называть кортежем U .
.,
,,
)(9
иначеfalse
TUеслиtrue
UF
18. Исключения и memsize-типы
Опасным следует считать бросание и обработку исключений с использованием memsize-типов.
Пример:
char *p1, *p2;
try {
throw (p1 - p2);
}
catch (int) {
...
}
.,
,,
)(10
иначеfalse
Tесли ttrue
tF
19. Заключение
Рассмотренные в статье правила диагностики потенциально опасных конструкций с точки зрения 64-битных
приложений могут быть реализованы в любом статическом анализаторе кода.
Однако в настоящий момент они реализованы в полном виде лишь в анализаторе кода Viva64
(www.viva64.com). Программный продукт Viva64 обеспечивает диагностику ошибок, специфичных для 64-
битных Windows приложений. Viva64 представляет собой lint-подобный статический анализатор Си/Си++ кода.
Инструмент Viva64 интегрируется в среду разработки Visual Studio 2005/2008 и предоставляет удобный
пользовательский интерфейс для проверки программных проектов.
20. Литература
1. Scott Meyers, Martin Klaus "A First Look at C++ Program Analyzers.", 1997,
http://www.aristeia.com/ddjpaper1_frames.html.
2. Андрей Карпов, Евгений Рыжков. 20 ловушек переноса C++-кода на 64-битную платформу. RSDN
Magazine #1-2007. стр. 65 - 75.
3. Алексей Колосов, Евгений Рыжков, Андрей Карпов. 32 подводных камня OpenMP при
программировании на C++. RSDN Magazine #2-2008. стр. 3 - 17.
4. E. А. Рыжков, А.Н. Карпов. Подходы к верификации и тестированию 64-битных приложений.
"ИНФОРМАЦИОННЫЕ ТЕХНОЛОГИИ" N7, 2008, Стр. 41 - 45.
5. Макконнелл С. Совершенный код. Мастер-класс / Пер. с англ.- М.: Издательство "Русская редакция",
СПб.: Питер, 2007.- 896 с.: ил.
6. Системное программное обеспечение / А.В. Гордеев, А.Ю. Молчанов. - СПб.: Питер, 2002. - 736 с:.ил.
7. Евгений Рыжков, Андрей Карпов. Сущность библиотеки анализа кода VivaCore. RSDN Magazine #1-2008.
стр. 56 - 63.