*** ВНИМАНИЕ: Блог переехал на другой адрес - demin.ws ***
Показаны сообщения с ярлыком visual studio. Показать все сообщения
Показаны сообщения с ярлыком visual studio. Показать все сообщения

среда, 7 декабря 2011 г.

Visual Studio 11 Developer Preview

Поставил на рабочий ноут Visual Studio 11 Developer Preview.

Погонял разные самопальные бенчмарки типа решета Эратосфена, vector<int> vs vector<bool>, std::string vs char* и т.д., пытаясь выявить улучшения или ухудшения в оптимизации. Лично я ничего кардинального не выявил по сравнению с версией 10.

Очевидно, что статический анализ кода и его безопасность в целом сейчас как никогда в моде, поэтому производители компиляторов постепенно закручивают гайки, превращая предупреждения в ошибки.

Например с ключом "/sdl" Студия 11 будет считать приведенные ниже предупреждения ошибками.

Warning Command line switch Description
C4146 /we4146 A unary minus operator was applied to an unsigned type, resulting in an unsigned result
C4308 /we4308 A negative integral constant converted to unsigned type, resulting in a possibly meaningless result
C4532 /we4532 Use of “continue”, “break” or “goto” keywords in a __finally/finally block has undefined behavior during abnormal termination
C4533 /we4533 Code initializing a variable will not be executed
C4700 /we4700 Use of an uninitialized local variable
C4789 /we4789 Buffer overrun when specific C run-time (CRT) functions are used
C4995 /we4995 Use of a function marked with pragma deprecated
C4996 /we4996 Use of a function marked as deprecated

Ссылки по теме:

понедельник, 21 июня 2010 г.

Отладчик в Visual Studio 2010

В отладчике Visual Studio 2010 появилась просто фантастическая штука - отображение трассируемой переменной не где-то в особом окне, а прямо на тексте программы. Это позволяет обращать внимание на переменную, только когда доходишь до ее места в программе.

понедельник, 21 сентября 2009 г.

Двойная точка с запятой в разделе объявления переменных

Казалось бы, невинный пример (vs_double_semicolumn.c):
void main() {
int a;;
int b;
}
Компилируем (в режиме языка С, то есть без /TP):
cl vs_double_semicolumn.c
Результат:
vs_double_semicolumn.c
vs_double_semicolumn.c(3) : error C2143: syntax error : missing ';' before 'type'
Результат в Codegear/Borland примерно такой же (хотя описание ошибки более ясное):
CodeGear C++ 5.93 for Win32 Copyright (c) 1993, 2007 CodeGear
vs_double_semicolumn.c:
Error E2140 vs_double_semicolumn.c 3: Declaration is not allowed here in function main
*** 1 errors in Compile ***
Проблемка заключается в случайной опечатке в виде двойного символа ;. Кстати, пример абсолютно реальный, из жизни. Случайная опечатка - и сразу много вопросов.

Получается, что второй символ ; тут трактуется как пустой оператор, а не пустая декларация переменной. Компилятор решает, что объявления переменных закончены, и начался блок операторов, поэтому резонно ругается на попытку объявить переменную b там, где уже должны быть операторы.

Проверил на gcc, на родных компиляторах AIX, Solaris и HP-UX. Эти все съели пример без проблем.

Посты по теме:

среда, 11 марта 2009 г.

"Легкая" интеграция Perforce в Visual Studio

Одной из многочисленных прелестей использования системы контроля версий является возможность четко понимать, какие файлы ты изменил в процессе работы. Конечно, всегда можно сделать глобальный diff по всем файлам проекта на сервере, но это может занимать минуты при большом проекте.

Например, в Perforce, как и во многих других системах контроля версий это решается выставлением атрибута read-only по умолчанию на все локальные файлы, находящиеся под контролем версий. Это позволяет исключить “случайное” изменение. Если надо изменить файл, то он помечается как рабочий (в Perforce "p4 edit имя_файла"). После этого с файла снимается флаг read-only, и Perforce добавляет его список "открытых" файлов. Теперь, когда вы хотите узнать, какие файлы у вас сейчас на редактировании, то команда Perforce "p4 opened" моментально выдаст список без глобального сканирования изменений. Также "p4 diff" столь же мгновенно отобразит сами изменения. По началу, такой подход может напрягать, и я часто слышу жалобы типа проще открыть сразу весь проект по маске через p4 edit ..., поработать спокойно, а уже под занавес сделать полное сканирование изменений для определений реально измененных файлов и только их отправить на сервер командой "p4 submit".

Я постоянно борюсь с таким подходом, так как чаще всего это приводит к тому, что человек забывает отделить реально измененные файлы от остальных, а Perforce как солдат — ему сказали поместить открытые для редактирование файлы на сервер, он и помещает (может в этом есть какая-то задумка пользователя). И получается, что в набор изменений попадают неизмененные файлы. С точки зрения целостности системы проблем нет, но вот при анализе проблемных изменений начинается кошмар, как надо вручную отсеивать нетронутые файлы. Кроме того, неосторожная работы по маске часто приводит к появлению в репозитории временных файлов, которых не заметили или забыли при записи измененных файлов на сервер. Конечно, их можно вычистить потом, но проще сразу их туда просто не класть.

Perforce предоставляет специальный плагин для Visual Studio, который призван облегчить процесс работы с файлами, находящимися под контролем версий. Вы просто работаете в среде, и когда нужно изменить какой-то файл (например, вы просто начали что-то набивать в окне редактора), то студия сама предложить вам открыть файл для редактирования. Вы скажете "Да", и файл открывается для работы. В жизни же все обычно отключают этот надоедливый запрос, разрешая по умолчанию открывать файлы без запроса. И мы снова приходим к варианту, когда имеется огромное количество открытых файлов, а изменены только несколько. Поэтому я предпочитаю не ставить этот плагин, а открыть файлы вручную.

Конечно, работая в студии, не всегда удобно постоянно лазить в командную строку для "p4 edit ...". Хочется делать это прямо из меню. Способ есть, и никакой шибко умный плагин не нужен.

Идем в меню Tools, затем в "External tool...". Далее создаем кнопкой "Add" элементы "&P4 Edit" (Arguments: "edit $(ItemPath)"), "&P4 Revert" (Arguments: "revert $(ItemPath)"), "&P4 Diff" (Arguments: "diff $(ItemPath)") по аналогии с картинкой.

Теперь для открытия за запись файла из текущего окна редактирования надо выбрать в меню Tools->P4 Edit, для отката изменений — Tools->P4 Revert, а для просмотра изменений — Tools->P4 Diff.

Вывод этих команд сохраняется в окне Output.

По вкусу можно добавить аналогичным образом любую команду из арсенала командного клиента Perforce p4, но именно эти обычно нужны в 99% случаев. Я обычно назначаю горячие клавиши на эти пункты меню. Для остального можно уже слазить в командную строку или в графический клиент Perforce (P4V или P4Win).

Правда, если вам потребуется изменить файл проекта или солюшена, то это придется делать либо в командной строке или в графическом клиенте P4V.

Лично для меня подобная схема привносит порядок в работу. Я четко знаю, что я изменил, и могу быстренько пробежаться глазами по изменениям перед отправкой их на сервер, отсеив пару-тройку файлов, которые я открыл таки для редактирование, но в этоге не менял.

И под занавес выскажу свое мнение про графический интерфейс для систем контроля версий. Мы используем Perforce. Да, это централизованная система, а не распределенная, как модно сейчас, но для корпоративной разработки так проще. Perforce хорош. Есть шероховатости, но с ними можно жить.

Лично предпочитаю "разговаривать" с Perforce через командную строку, ибо в командной строке можно сделать все, и удобно, когда надо выполнить много рутинных однотипных операций. Но есть один случай, когда именно графический клиент становится настоящим спасением. Это случай слияний изменений и разрешения конфликтов. Проблемы начинаются, когда ты хочешь зафиксировать на сервере свои изменения, а там уже кто-то "потрогал" твои файлы. Это называется конфликт, и его надо разрешать. Делать это в обычном текстовом редакторе, особенно когда конфликтуют сотни пересекающихся строк, практически нереально. Очень медленно, и вероятность ошибки огромна. В Perforce есть удивительная графическая программа для сравнения и слияния изменений. Обычно, если конфликтующие строки не пересекаются, Perforce сам автоматически смешает в правильном порядке. Если же есть пересекающиеся конфликты, то тут уже нужен человек для понимания, что выкинуть, а что оставить.

Утилита слияния в Perforce предоставляет для этого очень удобный сервис. На уровне строк можно выбирать нужные для включения в слияние. В окне одновременно отображаются три исходника: твой текущий, твой базовый, на основе которого ты делал изменения, и текущий из репозитория, с которым и возникает конфликт. А под этим всем внизу отображается результат слияния. Все подсвечивается разными цветами, максимально облегчая выбор правильного варинта. Я даже не знаю, как это может быть еще лучше сделано. Даже изобилующие конфликтами слияния между целыми ветками (например, из рабочей ветки в основную, где уже успели исправить порядочно ошибок) у нас делаются за несколько часов.

Приятно, что графический клиент Perforce P4V существует не только под Windows (в отличие от старого P4Win). Он есть под Linux, Solaris, FreeBSD и Mac. Если надо работать сразу под несколькими системам, можно запустить у себя на машине X-сервер, и видеть клиентов со всех платформ одновременно. P4V использует Qt, посему выглядит почти одинаково на всех системах.

Кстати, выскажу свое субъективное мнение на тему контроля версий. Если бы мне продавали исходники чего-то сложного, то для меня было бы очень важным критерием среди прочих наличие не только вылизанной для продажи "последней супер версии", но и всей истории разработки, в формате какой-нибудь системы контроля версий и баг-трекинга.

четверг, 5 марта 2009 г.

Как обойтить без макроса NOMINMAX

В комментариях к посту про проблему конфликта имен STL'евских std::min и std::max с одноименными макросами из файла windows.h мне подсказали интересное решение.

Если вместо, например, std::max(a, b) написать (std::max)(a, b), то результат работы препроцессора выглядит так:
#line 3 "minmax.cpp"
int main() {
int a = (std::min)(10, 20);
return 0;
}
вместо:
#line 3 "minmax.cpp"
int main() {
int a = std::(((10) < (20)) ? (10) : (20));
return 0;
}
и конфликта не происходит. Все компилируется без проблем.

Не берусь судить, на сколько это красивое решение, и я бы все-таки предпочел действовать напрямую через макрос NOMINMAX, но выход элегантный.


Другие посты по теме:

четверг, 26 февраля 2009 г.

Unit-тест для Coredump

Как мне подсказали в комментариях к посту про создание coredump файлов в Windows, если изменить код возврата из функции обработки исключения, то можно подавить появление стандартного окна об ошибке. Посему родился unit-тест для этого модуля.

Модифицированный текст файла coredump.cpp, в котором с помощью макроса UNIT_TESTING встроена поддержка для тестирования. Если этот макрос определен, то, как я уже сказал, подавляется появление окна с ошибкой, и coredump файл создается с постоянным именем.

Файл coredump.cpp:
#include <windows.h>
#include <dbghelp.h>
#include <stdio.h> // _snprintf

// Наш обработчик непойманного исключения.
static LONG WINAPI ExceptionFilter(EXCEPTION_POINTERS* ExceptionInfo);

// Статический экземпляр переменной, конструктор которой
// вызывается до начала функции main().
static struct CoredumpInitializer {
CoredumpInitializer() {
SetUnhandledExceptionFilter(&ExceptionFilter);
}
} coredumpInitializer;

LONG WINAPI ExceptionFilter(EXCEPTION_POINTERS* ExceptionInfo) {
char fname[_MAX_PATH];

SYSTEMTIME st;
GetLocalTime(&st);

HANDLE proc = GetCurrentProcess();

#ifdef UNIT_TESTING
lstrcpy(fname, "___coredump.dmp");
#else
// Формируем имя для coredump'а.
_snprintf(
fname, _MAX_PATH,
"coredump-%ld-%ld-%04d%02d%02d%02d%02d%02d%03d.dmp",
GetProcessId(proc), GetCurrentThreadId(),
st.wYear, st.wMonth, st.wDay,
st.wHour, st.wMinute, st.wSecond, st.wMilliseconds
);
#endif

// Открываем файл.
HANDLE file = CreateFile(
fname,
GENERIC_READ|GENERIC_WRITE,
FILE_SHARE_READ,
NULL,
CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
NULL
);

MINIDUMP_EXCEPTION_INFORMATION info;
info.ExceptionPointers = ExceptionInfo;
info.ThreadId = GetCurrentThreadId();
info.ClientPointers = NULL;

// Собственно, сбрасываем образ памяти в файл.
MiniDumpWriteDump(
proc,
GetProcessId(proc),
file,
MiniDumpWithFullMemory,
ExceptionInfo ? &info : NULL,
NULL, NULL
);

CloseHandle(file);

#ifdef UNIT_TESTING
return EXCEPTION_EXECUTE_HANDLER;
#else
return EXCEPTION_CONTINUE_SEARCH;
#endif
}
Теперь, собственно, тест (файл coredump_unittest.cpp):
#include "gtest/gtest.h"

#include <fstream>
#include <windows.h>
#include <stdlib.h>

TEST(Coredump, CoredumpCreation) {
const char* coredump = "___coredump.dmp";

// На всякий случай заведомо стираем старые файлы.
EXPECT_EQ(0, std::system("cmd.exe /c del ___coredump_main.* 1>nul 2>&1"));

// Создаем файл с тестовой программой.
std::string program = "int main() { *(char *)0 = 0; return 0; }";
std::ofstream os("___coredump_main.cpp");
os << program << std::endl;
os.close();

// Компилируем тестовую программу с опцией UNIT_TESTING.
// С этой опцией coredump файл будет создаваться с постоянным
// именем "___coredump.dmp", и будет подавляется окно с сообщением
// об ошибке.
EXPECT_EQ(
0, std::system(
"cl /Zi /DUNIT_TESTING /Fe___coredump_main.exe"
" ___coredump_main.cpp coredump.cpp dbghelp.lib"
" 1>nul 2>&1"
)
);

// На всякий случая удаляем старый coredump файл.
std::remove(coredump);

// Убеждаемся, что файл действительно удалился.
std::ifstream isdel(coredump);
EXPECT_FALSE(isdel.good());
isdel.close();

// Запускаем тестовую программу.
ASSERT_EQ(0xC0000005, std::system("___coredump_main.exe"));

// Проверяем, создался ли файл coredump.dmp.
std::ifstream is(coredump);
EXPECT_TRUE(is.good());
is.close();

// Удаляем за собой временные файлы.
EXPECT_EQ(0, std::system("cmd.exe /c del ___coredump_main.* 1>nul 2>&1"));
std::remove(coredump);
}
Данный тест имеет ряд существенных недостатков. Во-первых, он использует файловую систему, и во-вторых, он вызывает компилятор, что занимает небольшое, но все же время. Недостатки неприятные, но в целом приемлемые.
Кстати, Google Test Framework умеет делать так называемые "смертельные" (death) тесты. То есть можно протестировать именно аварийное "падение" фрагмента кода, например, из-за нарушения защиты памяти, и для проведения такого теста не надо вручную компилировать что-либо, как мы делали тут. К сожалению, эта возможность основана на использования юниксового системного вызова fork() и поэтому доступна только на UNIX платформах.
Дежурный файл для запуска тестов runner.cpp:
#include "gtest/gtest.h"
int main(int argc, char* argv[]) {
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
Традиционно, для компиляции тестов нам нужна Google Test Framework. Как я уже писал, вы можете скачать мою модификацию этой библиотеки, которая сокращена без потери какой-либо функциональности до двух необходимых файлов gtest/gtest.h и gtest-all.cc.
Компилируем, например в Visual Studio 2008:
cl /EHsc /I. /Fecoredump_unittest_vs2008.exe /DWIN32 runner.cpp coredump_unittest.cpp gtest\gtest-all.cc
Запускаем:
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from Coredump
[ RUN ] Coredump.CoredumpCreation
[ OK ] Coredump.CoredumpCreation
[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran.
[ PASSED ] 1 test.
Работает.

Сразу скажу, я проверял все это только под Windows XP SP2 и Server 2003. Пожалуйста, сообщайте, если есть какие-то проблемы или тонкости под другими виндами.

Как это часто бывает в unit-тестировании, тест получился больше, чем сам тестируемый код. Но повторюсь — это того стоит. Буквально скоро расскажу о моих приключениях с модулем таймера, и как меня выручили тесты.


Другие посты по теме:

среда, 25 февраля 2009 г.

Coredump для Windows

Тривиальная задачка: что будет, если запустить вот такую программу?

Файл main.cpp:

int main() {
*(char *)0 = 0;
return 0;
}
В UNIX'е самым вероятным исходом будет сообщение:
Segmentation fault
Это означает безусловное падение из-за нарушения защиты памяти. Если пользователь разрешил создание coredump файлов (например, для командного интерпретатора bash это делается командой):
ulimit -c unlimited
то после запуска будет создан файл coredump. Этот файл фактически содержит образ памяти вашей программы в момент возникновения ошибки, и его можно открыть в отладчике, например в gdb:
gdb -c coredump
После чего командой bt (back trace) можно точно установить место в программе, в котором произошла ошибка. Естественно, для удобства, программа должна быть скомпилирована с включенной отладочной информацией.
Когда ошибка происходит в процессе отладки у вас на глазах, то проще запустить программу сразу под отладчиком. Но когда, например, ошибка происходит у заказчика, и вас нет рядом, то тогда можно попросить прислать вам coredump файл для анализа. Во многих случаях этого хватает для локализации проблемы.
А что делать под Windows? Запуск приведенной программы под виндами обычно приведет вот к такому сообщению:


Неискушенный пользователь обычно жмет "Don't send", и затем программа благополучно падает, и не останется никакой информации произошедшей ошибке. Конечно, программа может вести подробное журналирование, но часто ошибки подобного рода редко удается локализовать по журналам.

Я слышал, что может так случиться, что после нажатия "Send Error Report" Майкрософт с вами свяжется и поможет решить проблему. Со мной такого ни разу не случалось, увы.
В Windows тоже есть схожий механизм создания “на лету” образов памяти работающего процесса, но для его использования надо немного потрудиться.

Windows предоставляет механизм исключений, чем-то схожий с исключениями в С++ и системными сигналами в UNIX. На С++ эти исключения похожи try-catch синтаксисом, а на UNIX — тем, что можно перехватывать исключительные ситуации (например, ошибки) в программе типа нашей (для UNIX можно было бы перехватить сигнал SIGSEGV и получить управление при возникновении подобной ошибки).

Естественно, сигналы в UNIX используются не только для этого.
Итак, ниже я приведу исходный текст небольшого модуля, который будет создавать аналогичный по смыслу юниксовому coredump'у файл, по которому можно будет установить причину аварийного завершения программы. Все, что нужно — это прилинковать этот модуль в ваш проект. Ничего вызывать специально не надо. Модуль инициализируется автоматически.

Файл coredump.cpp:

#include <windows.h>
#include <dbghelp.h>
#include <stdio.h> // _snprintf

// Наш обработчик непойманного исключения.
static LONG WINAPI ExceptionFilter(EXCEPTION_POINTERS* ExceptionInfo);

// Статический экземпляр переменной, конструктор которой
// вызывается до начала функции main().
static struct CoredumpInitializer {
CoredumpInitializer() {
SetUnhandledExceptionFilter(&ExceptionFilter);
}
} coredumpInitializer;

LONG WINAPI ExceptionFilter(EXCEPTION_POINTERS* ExceptionInfo) {
char fname[_MAX_PATH];

SYSTEMTIME st;
GetLocalTime(&st);

HANDLE proc = GetCurrentProcess();

// Формируем имя для coredump'а.
_snprintf(
fname, _MAX_PATH,
"coredump-%ld-%ld-%04d%02d%02d%02d%02d%02d%03d.dmp",
GetProcessId(proc), GetCurrentThreadId(),
st.wYear, st.wMonth, st.wDay,
st.wHour, st.wMinute, st.wSecond, st.wMilliseconds
);

// Открываем файл.
HANDLE file = CreateFile(
fname,
GENERIC_READ|GENERIC_WRITE,
FILE_SHARE_READ,
NULL,
CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
NULL
);

MINIDUMP_EXCEPTION_INFORMATION info;
info.ExceptionPointers = ExceptionInfo;
info.ThreadId = GetCurrentThreadId();
info.ClientPointers = NULL;

// Собственно, сбрасываем образ памяти в файл.
MiniDumpWriteDump(
proc,
GetProcessId(proc),
file,
MiniDumpWithFullMemory,
ExceptionInfo ? &info : NULL,
NULL, NULL
);

CloseHandle(file);

return EXCEPTION_CONTINUE_SEARCH;
}
Данный модуль создает статический объект coredumpInitializer, конструктор которого вызывается до функции main(). Конструктор устанавливает специальный обработчик для системных исключений, в котором в файл и записывается образ памяти текущего процесса при возникновении системных ошибок. Имя файла содержит идентификатор процесса, идентификатор потока и текущее время. Этот файл можно открыть в Visual Studio просто запустив его, либо в самой студии в меню File->Open->Project/Solution выбрать этот файл, указав его тип как "Dump files". Далее надо запустить программу через отладчик, нажав F5, и отладчик остановится на месте возникновения ошибки. Естественно, для работы на уровне исходного текста необходимо наличие файла с расширением .PDB, содержащего символьную информацию, в том каталоге, где расположен ваш coredump файл. Файл с раширением .PDB обычно создается при компиляции с включенной отладочной информацией, например, при использования ключа "/Zi".
Надо отметить, что если ошибка в вашей программе произойдет до функции main(), например, при инициализации каких-то статических объектов, то данный модуль может и не сработать, так как вызов конструктора объекта coredumpInitializer может быть запланирован уже после проблемного места. Вообще, статические переменные и объекты — это источник многих проблем, в основном из-за неопределенного порядка их инициализации, и их использования стоит избегать по возможности.
Итак, компилируем:
cl /Zi main.cpp coredump.cpp dbghelp.lib
Для компиляции необходима библиотека DbgHelp.lib. Она обычно входит в состав Windows SDK в составе студии. Также получившийся исполняемый файл main.exe будет использовать динамическую библиотеку DbgHelp.dll. DbgHelp.dll входит в состав так называемых redistributable файлов, то есть которые вы имеете право распространять вместе с программой на случай, если у клиента нет этой dll'ки. Эта dll'ка с большой вероятностью находится у вас в каталоге C:\WINDOWS\system32.
Теперь при запуске программы main.exe и после закрытия пользователем окна с сообщением об ошибке (см. картинку выше), будет создан .DMP файл с именем, начинающийся со слова coredump, например coredump-4584-3240-20090226022327093.dmp. Этот файл уже можно открыть в Visual Studio.

Теперь, когда у заказчика программа падает, можно попросить его прислать такой файл для анализа. Единственное, надо хранить .PDB файлы, соответствующие отданным заказчику исполняемым модулям.

Пока я не придумал, как сделать для модуля coredump.cpp автоматизированный unit-тест. Проблема в том, что тут надо как-то подавить вывод окна с ошибкой (см. картинку выше). Если это сделать, то тест может быть вполне себе автоматизированным.
Более подробная информация вопросу создания образов памяти процесса есть в хорошей статье “Effective minidumps”.


Другие посты по теме:

понедельник, 23 февраля 2009 г.

Статический анализ кода

Все программисты ошибаются, даже самые продвинутые. Хорошим подспорьем в отлове ошибок, которые просмотрел программист, являются анализаторы кода.
Конечно, последней инстанцией будет непосредственно клиент, который непременно сообщит, что программа падает, но всегда хочется минимизировать такие случаи. Хорошо, когда процедура внедрения у заказчика вообще существует. Это позволяет без особых проблем сделать “критическое обновление” уже после релиза. Но в некоторых областях цена допущенных до клиента ошибок крайне высока. Например, для области встраиваемых систем любая минимальная утечка памяти будет фатальной, или, например, для разработчиков игр под игровые консоли будет трудновато “донести” до клиентов критические обновления, необходимость в которых выяснилась сразу после релиза и начала продаж (как это обычно бывает).
Перейдем от слов к делу и рассмотрим конкретный пример работы статического анализатора.

Вот пример "не очень хорошей программы":

01 class A {
02 public:
04 A() {
05 char* __p = new char[10];
06 __p = new char[10];
07
08 char* a = (char *)0;
09 *a = 0;
10
11 char c[10];
12 c[10] = 0;
13 }
14 ~A() {
15 delete __p;
16
17 char* a = new char[100];
18 return;
19
20 delete[] a;
21 }
22 private:
23 char* __p;
24 };
25
26 int main() {
27 A a;
28 return 0;
29 }
Тут без микроскопа видно, что проблем полно:

  • Утечка памяти в строке 05. Указатель __p явно имеет неправильное объявление в виде лишнего char*, которое перекрывает декларацию этого указателя в классе в строке 23. Оператор delete в строке 15 скорее всего закончится аварийно, так как значение __p для него будут неопределенно.
  • Строка 06 присваивает указателю __p адрес вновь распределенной памяти, тем самым затирая старое значение, присвоенное в строке 05, которое будет потеряно.
  • Строки 08 и 09 — это обращение по нулевому указателю, приводящее к нарушению защиты памяти.
  • Строки 12 и 12 — это типичное переполнение буфера (buffer overrun)
  • Память под указателем в строке 17 никогда не будет освобождена. Это утечка памяти.
Достаточно для начала. Не спорю, пример очень вычурный, но ошибки то весьма типичные, а когда они перемешаны с “правильным” кодом, их обнаружение становится серьезной проблемой.

Теперь возьмем "микроскоп".

Посмотрим, что сможет сделать для нас Visual Studio. Начиная с версии 2005 у компилятора cl.exe появился ключ /analyze, который включает дополнительный анализ и вывод предупреждений о потенциальных проблемах. К сожалению, этот ключ есть только в версии студии Team (в Professional его нет).

Компилируем в Visual Studio 2008 Team:

cl /W3 /O2 /analyze /EHsc bad.cpp
Вот, что дает анализ:

c:\sandbox\analyze\bad.cpp(12) : warning C6201: Index '10' is out of valid index range '0' to '9' for possibly stack allocated buffer 'c'

c:\sandbox\analyze\bad.cpp(5) : warning C6211: Leaking memory '__p' due to an exception. Consider using a local catch block to clean up memory: Lines: 5, 6
c:\sandbox\analyze\bad.cpp(9) : warning C6011: Dereferencing NULL pointer 'a': Lines: 5, 6, 8,9

c:\sandbox\analyze\bad.cpp(12) : warning C6386: Buffer overrun: accessing 'c', the writable size is '10' bytes, but '11' bytes might be written: Lines: 5, 6, 8, 9, 11, 12

Не так много, как хотелось бы, но хоть что-то. Переполнение буфера в строке 12 обнаружено. Запись по нулевому указателю в строке 09 тоже найдена. Давайте разберемся с сообщением об утечке памяти. Нам сообщается, что возможна утечка, если в строке 06 произойдет исключение (std::bad_alloc, например), тогда память, распределенная в строке 05 будет потеряна. Это, конечно, проблема, но все-таки суть ошибки передана неверно. Как мне показалось, анализатор в cl.exe работает последовательно, то есть он следует ходу компиляции, отсюда и “последовательный” характер смысла выведенных предупреждений.

Мы в компании для статического анализа используем Coverity Prevent for C/C++. Есть еще похожий продукт — Klocwork. Эти два продукта делают примерно одну и ту же работу примерно с одинаковым результатом. Мы выбрали первый из-за более подходящей нам ценовой политики и более простого встраивания в систему сборки.

Суть анализа, проводимого данными продуктами, это подобие псевдо-компиляции, когда строится синтаксическое дерево разбора и на основе его проводится анализ всех возможных ветвлений программы. Проходя все ветки, анализатор и проводит свои многочисленные проверки. Прикол в том, что анализ может найти проблему в таком закоулке кода, который может выполняется то раз в год (и раз в год программа падает), и может вы сами никогда не видели, как этот кусок программы работает. Анализ на уровне синтаксиса языка позволяет находить парные проблемы, которые могут быть расположены в разных частях исходного текста (например, поиск несоответствий в конструкторе и деструкторе). Также понимание синтаксиса дает возможность анализировать вложенные вызовы, когда, например, неверный указатель "проявляет" себя только двух или тремя уровнями выше.

Программы-анализаторы типа lint (или тот же ключ “/analyze”), которые просто ищут шаблоны "плохого" кода на уровне лексем, обычно выдаются миллиарды предупреждений, из которых только единицы ценны. При таком подходе разработчику быстро надоедает заниматься выуживанием “жемчужин” из общего потока мусора, и он перестает это делать. Анализаторы же в Coverity и Klocwork выдаются крайне точные сообщения, и процент ложных срабатываний крайне мал (по крайне мере на моем опыте). Также, в каждом из этих продуктов можно самостоятельно настраивать анализатор, фокусируя его на специфичных конкретно для вас потенциальных проблемах, отключая ненужные проверки для уменьшения “шума”.

Идея, лежащая в этих продуктах, это дать не просто нечто, генерирующее тонны текстовых файлов, в которых надо копаться вручную. Тут дается целая среда для автоматизации анализа: групповая работа, система интеграции с контролем версий, позволяющая отдельно проверять каждый внесенный кусок кода и моментально локализовывать время, место и автора “проблем”, система рецензирования когда по исправлению ошибок, общая база данных по ошибкам, которая исключает повторный анализ уже исправленных ошибок, так как положение ошибки характеризуется не просто именем файла и номером строки, на контекстом, и поэтому даже когда ошибка “переехала” в другое место, то он не будет заявлена как новая. Обычно время псевдо-компиляции равно времени вашей обычной сборки, а время самого анализа может занимать в среднем в 3-4 раза дольше. Анализатор прекрасно может использовать многоядерные системы для радикального ускорения процесса. Например, мы с интегрировали статический анализ с системой автоматических “ночных” сборок.

Естественно, никакой анализатор — это не панацея, и все 100% ошибок он не найдет, но изрядную долю выловит, позволив вам потратить освободившееся время на внесение новых ошибок.

Кстати, обе эти конторы всегда организуют бесплатный тест-драйв. Можно попробовать, чего такого интересного сможет найти их анализатор в конкретно вашем коде. Честно могу сказать, это производит впечатление даже на самых заядлых зануд и скептиков среди разработчиков и менеджеров. Когда на ваших глазах открывается такое в коде, что волосы дыбом встают, то к этому невозможно остаться равнодушным. Например, мы сопровождаем большое количество так называемого legacy кода, и тут, конечно, статический анализ проявляет себя во всей красе, хотя и новом, объектно-ориентированном и unit-оттестированном коде тоже бывают ошибки. Это человеческий фактор и от него никуда не деться.

Так вот, анализатор Coverity нашел все проблемы в данной маленькой, но очень плохой программе, включая несоответствие распределения памяти в конструкторе, и ее “неправильном” освобождении в деструкторе.
У нас в отделе есть даже специальная копилка, если в твоем коде статический анализатор находит серьезную проблему, типа утечки или какой-нибудь “неприятности” с указателями или памятью, то принято внести в кассу посильную сумму, чтобы ее можно было потратить коллективно при очередном походе в паб. А пабе как-то особенно продуктивно обсуждаются темы типа кто, куда и какую ошибку внес.
Сейчас мы рассмотрели статический анализ кода. Также существует также динамический анализ, когда уже в процессе работы программы специальными средствами производится автоматизированный поиск ошибок. Лично я постоянно использую совершенно волшебный динамический анализатор Valgrind. Valgrind не так удобен, как мне кажется, для полностью автоматизированной проверки и больше подходит, когда надо поймать какой-то конкретный глюк, например, явную утечку памяти, обнаруженную функциональными тестами, но не выявленную статическим анализом.

Отдельной строкой хочу отметить Borland/CodeGear Codeguard, входящий в состав одноименной студии. Данная библиотека может опционально встраиваться борландовым компилятором в код, шпигуя его сотнями проверок на различные утечки, неправильную работу с указателями и прочими неприятностями. Код при этом замедляется в разы и порой делает невозможным отладку вычислительно тяжелых алгоритмов, но вот находимые с помощью Codeguard’а ошибки порой дорогого стоят.

Анализаторы кода (статические или динамические) являются крайне необходимым инструментом. А конкретно, статические, позволяют автоматизировано находится “плохие” места кода, которые проглядели программисты.

Голубая (Борланд) палитра для Visual Studio

В процессе перехода Visual Studio 2008 с Professional на Team System в очередной раз слетели настройки палитры. Так сложилось, годы работы на борланде приучили меня к голубому фону и желтым буквам, и ничего уже поделать нельзя. Как-то давно, покопав в интернете на тему разных палитр для Visual Studio, я пришел вот к такой трехшаговой комбинации, которая за минуту превращает стандартную микрософтовскую белую палитру в 90%-e подобие борландовой.

Menu -> Tools -> Options -> Environment -> Fonts and Colors:
Font -> Fixedsys
TextEditor:
Plain text -> Yellow/Navy
KeyWord -> Lime/(фон оставить по умолчанию)

Конечно, тут есть еще что доработать по мелочам, но лично мне и этого хватает.

А вот для чего я перешел с Professional на Team я расскажу буквально скоро.

воскресенье, 15 февраля 2009 г.

std::min() и std::max() в Visual Studio

Простейший кусок кода (файл minmax.cpp):
#include <algorithm>
int main() {
int a = std::min(10, 20);
return 0;
}
Все тривиально и отлично компилируется и в Visual Studio, и в CodeGear/Borland Studio, и Cygwin. Но допустим потребовались какие-то функции из Windows API, и вы подключили файл windows.h:
#include <algorithm>
#include <windows.h>
int main() {
int a = std::min(10, 20);
return 0;
}
Теперь компиляция в Visual Studio (я проверял в 2005 и 2008) будет падать со следующей ошибкой:
minmax.cpp
minmax.cpp(4) : error C2589: '(' : illegal token on right side of '::'
minmax.cpp(4) : error C2059: syntax error : '::'
Постановка #include <windows.h> до #include <algorithm> проблемы не решает.
Очевидно, проблема в том, что кто-то переопределил значение слова min. Запустим препроцессор и проверим догадку:
cl /P minmax.cpp
И что мы видим? А видим мы следующее (фрагмент файла minmap.i):
#line 7 "minmax.cpp"
int main() {
int a = std::(((10) < (20)) ? (10) : (20));
return 0;
}
Естественно, это каша с точки зрения синтаксиса, и компилятор ругается совершенно законно.

Покопавшись в заголовочных файлах Windows SDK, в файле WinDef.h, который косвенно подключается через windows.h, я нашел корень зла:

#ifndef NOMINMAX

#ifndef max
#define max(a,b) (((a) > (b)) ? (a) : (b))
#endif

#ifndef min
#define min(a,b) (((a) < (b)) ? (a) : (b))
#endif

#endif /* NOMINMAX */
Вот теперь ясно, что делать — надо определить макрос NOMINMAX, тем самым заблокировать определение min и max:
#define NOMINMAX
#include <algorithm>
#include <windows.h>
int main() {
int a = std::min(10, 20);
return 0;
}
Вот теперь в Visual Studio все нормально компилируется.

Забавно, что в Cygwin и CodeGear/Borland исходный пример компилируется без проблем. В борландовой версии windows.h я нашел вот такой фрагмент:

#if defined(__BORLANDC__)
...
# if defined(__cplusplus)
# define NOMINMAX /* for WINDEF.H */
...
# endif
...
#endif /* __BORLANDC__ */
Эдак они заранее оградились от проблемы, принудительно запретив проблемные макросы.

Вывод: Порой промежуточные результаты работы препроцессора являются крайне полезной информацией.

На всякий случай напомню, как его запускать для перечисленных мной компиляторов:

Visual Studio:

cl /P имя_исходника.cpp
Borland/CodeGear Studio:
cpp32 имя_исходника.cpp
Cygwin:
cpp имя_исходника.cpp
Прочие флаги командной строки должны повторять флаги при обычной компиляции. Для препроцессора важны определения макросов (обычно это флаги -D и -U) и пути для поиска включаемых файлов (обычно это флаг -I).


Другие посты по теме:

четверг, 29 января 2009 г.

Скрипты для Visual Studio

По роду работы у меня на компьютере стоят сразу несколько версий Visual Stdio: 2003, 2005 и 2008. В целом они легко уживаются на одной машине, и при работе в графической оболочке обычно не возникает каких-либо неудобств или конфликтов. Но вот при работе через командную строку обычно надо как-то понимать, какой именно компилятор хочется вызвать (ведь имя то у него одно - cl.exe, a версий три). В итоге я убрал из путей PATH все ссылки на каталоги разных версии студии, и сделал вот такие скрипты, помещенные в любой каталог, находящийся в списке путей PATH.

Visual Studio 2003, файл: cl2003.cmd:
@"%VS71COMNTOOLS%\vsvars32.bat"
Visual Studio 2005, файл: cl2005.cmd:
@"%VS80COMNTOOLS%\vsvars32.bat"
Visual Studio 2008, файл: cl2008.cmd:
@"%VS90COMNTOOLS%\vsvars32.bat"
Если вы ставили студии по умолчанию стандартным образом, то в системе должны быть переменные окружения VS71COMNTOOLS, VS80COMNTOOLS и VS90COMNTOOLS, задающие расположение конкретной версии. Скрипт же vsvars32.bat поставляется вместе со студией и автоматически настраивает все необходимое для компилятора окружение.

Теперь компиляция в версии 2005 делается, например, вот таким cmd-файлом:
call cl2005.cmd
cl /Fetest.exe test.cpp
Очевидно, что для перехода на версию 2003 или 2008 надо заменить всего одну цифру. Очень удобно.