После некоторых поисков серьёзного вызова для анализатора PVS-Studio выбор пал на открытую коллекцию компиляторов GCC. Да, это уже не первая по счёту проверка этого проекта. Однако поддерживаемые этой коллекцией языки программирования не стоят на месте, и их постоянное развитие влечёт за собой соответствующее постоянное усложнение кода GCC. Таким образом цель — обнаружить ошибки в коде GCC с помощью анализатора PVS-Studio.
Коллекция компиляторов GCC ведёт свою историю с 1987 года, т.е. 36 лет, и занимает очень важную роль в жизни проектов с открытым исходным кодом. Сначала она включала в себя только компилятор для языка С, однако позднее была добавлена поддержка таких языков, как C++, Objective-C, Java, Фортран, Ada, Go, GAS и D. На момент написания статьи самой новой мажорной стабильной версией GCC являлась версия 13. Конкретно в данной статье рассматривается версия 13.1. За свою долгую историю развития GCC обзавёлся большим количеством разработчиков и ещё большим количеством пользователей. Соответственно, проект очень хорошо тестируется, и найти ошибку в таком отлаженном и проверенном коде — это очень интересная задача.
Код компилятора написан по определённому соглашению с обильным использованием макросов, что делает его чтение настоящим кошмаром для неподготовленного пользователя. Это также усложняет работу статическому анализатору PVS-Studio, но не останавливает его. Ниже я рассмотрю 10 фрагментов кода, которые меня заинтересовали в процессе беглого просмотра предупреждений. Более подробный анализ отчёта, к сожалению, затруднён в силу уже упомянутых макросов и требует предварительной настройки анализатора, что выходит за рамки простого написания статьи. Если вы желаете ознакомиться с более ранней проверкой проекта GCC, то вы можете перейти по ссылке.
Фрагмент N1. Неинициализированная структура
static ctf_id_t
gen_ctf_function_type (ctf_container_ref ctfc,
dw_die_ref function,
bool from_global_func)
{
....
ctf_funcinfo_t func_info; // <=
....
{
....
if (....)
{
do
{
....
if (....)
....
else if (....)
{
func_info.ctc_flags |= CTF_FUNC_VARARG; // <=
....
}
}
}
....
}
....
}
Предупреждение анализатора PVS-Studio: V614 Uninitialized variable 'func_info.ctc_flags' used. gcc/dwarf2ctf.cc 676
Объект func_info не инициализируется при объявлении. Затем ниже по коду в одной из веток происходит выставление флага CTF_FUNC_VARARG в поле ctc_flags, однако оно не было проинициализировано.
Фрагмент N2. Возможный нулевой указатель
static struct gcov_info *
read_gcda_file (const char *filename)
{
....
curr_gcov_info = obj_info =
(struct gcov_info *) xcalloc (sizeof (struct gcov_info) +
sizeof (struct gcov_ctr_info) * GCOV_COUNTERS, 1);
obj_info->version = version;
obj_info->filename = filename;
....
}
Предупреждение анализатора PVS-Studio: V522 There might be dereferencing of a potential null pointer 'obj_info'. Check lines: 290, 287. libgcov-util.c 290. libgcov-util.c 287
Анализатор PVS-Studio обнаружил отсутствие проверки на валидность указателя obj_info после аллокации. Известно, что функции xmalloc, xcalloc и xrealloc всегда возвращают валидный указатель, а в случае ошибки программа прервёт своё выполнение. Когда я увидел это предупреждение, то отнёс его к ложным и решил подготовить минимально воспроизводимый пример для будущей отладки. Однако открыв препроцессированный файл, я обнаружил в соответствующей строке вызов обычного calloc. Вот во что на самом деле раскрывается макрос xcalloc:
// libgcc/libgcov.h
....
/* work around the poisoned malloc/calloc in system.h. */
#ifndef xmalloc
#define xmalloc malloc
#endif
#ifndef xcalloc
#define xcalloc calloc
#endif
Предвосхищая будущие комментарии, хочу привести ссылку на статью своего коллеги. В ней он приводил доводы к тому, зачем стоит проверять результат этих функций.
Фрагмент N3. Забытый JSON.
static void
generate_results (const char *file_name)
{
....
json::object *root = new json::object ();
root->set ("format_version", new json::string ("1"));
root->set ("gcc_version", new json::string (version_string));
if (....)
root->set ("current_working_directory", new json::string (bbg_cwd));
root->set ("data_file", new json::string (file_name));
json::array *json_files = new json::array ();
root->set ("files", json_files);
....
if (....)
{
if (....)
{
root->dump (stdout);
printf ("\n");
}
else
{
pretty_printer pp;
root->print (&pp);
....
}
}
}
Предупреждение анализатора PVS-Studio: V773 The function was exited without releasing the 'root' pointer. A memory leak is possible. gcov.cc 1525
Создаваемый json-объект root забыли удалить, в итоге получилась утечка памяти. Скорее всего, такой вывод JSON происходит единоразово, и эта ошибка фактически ни на что не повлияет. Решил рассмотреть этот случай, чтобы показать возможности PVS-Studio в выявлении утечек памяти.
Фрагмент N4. Небезопасная функция, которая может привести к переполнению буфера
char m_flag_chars[256];
....
void
flag_chars_t::add_char (char ch)
{
int i = strlen (m_flag_chars);
m_flag_chars[i++] = ch;
m_flag_chars[i] = 0;
}
Предупреждение анализатора PVS-Studio: V557 Array overrun is possible. The value of 'i' index could reach 256. c-format.cc 1994
На данном участке кода может произойти выход за границу массива при записи терминального нуля, если длина строки m_flag_chars до вызова функции add_char была равна 255.
Фрагмент N5. Лишние вычисления
....
/* Return True when the format flag CHR has been used. */
bool get_flag (char chr) const
{
unsigned char c = chr & 0xff;
return (flags[c / (CHAR_BIT * sizeof *flags)]
& (1U << (c % (CHAR_BIT * sizeof *flags))));
}
....
static fmtresult
format_floating (const directive &dir, const HOST_WIDE_INT prec[2])
{
....
unsigned flagmin = (1 /* for the first digit */
+ (dir.get_flag ('+') | dir.get_flag (' ')));
....
}
Предупреждение анализатора PVS-Studio: V792 The 'get_flag' function located to the right of the operator '|' will be called regardless of the value of the left operand. Perhaps, it is better to use '||'. gimple-ssa-sprintf.cc 1227
Функция get_flag возвращает объект типа bool. Результат двух вызовов функции объединяется оператором побитового "ИЛИ". Опасности такой код не содержит, однако побитовый оператор будет вычислять оба операнда. Воспользовавшись логическим "ИЛИ", можно избавиться от лишнего вычисления, если первый вызов вернёт true:
unsigned flagmin = (1 /* for the first digit */
+ (dir.get_flag ('+') || dir.get_flag (' ')));
Фрагмент N6. Утечка памяти
struct gcov_info *
gcov_read_profile_dir (const char* dir_name,
int recompute_summary ATTRIBUTE_UNUSED)
{
char *pwd;
int ret;
read_profile_dir_init ();
if (access (dir_name, R_OK) != 0)
{
fnotice (stderr, "cannot access directory %s\n", dir_name);
return NULL;
}
pwd = getcwd (NULL, 0); // <=
gcc_assert (pwd);
ret = chdir (dir_name);
if (ret != 0)
{
fnotice (stderr, "%s is not a directory\n", dir_name);
return NULL; // <=
}
#ifdef HAVE_FTW_H
ftw (".", ftw_read_file, 50);
#endif
chdir (pwd);
free (pwd); // <=
return gcov_info_head;;
}
Предупреждение анализатора PVS-Studio: V773 The function was exited without releasing the 'pwd' pointer. A memory leak is possible. libgcov-util.c 450, libgcov-util.c 434
Обратите внимание на вызов функции getcwd. Если ей первым аргументом передаётся нулевой указатель, функция динамически выделяет память под буфер необходимой длины и записывает туда текущую рабочую директорию. Освобождение памяти остаётся на совести разработчика. Как видно, в самом конце функции разработчик освободил память, но забыл сделать это в одной из веток кода с ранним возвратом. В итоге в коде содержится потенциальная, хоть и несущественная, утечка памяти. О способности PVS-Studio обнаруживать утечки памяти можно подробнее почитать здесь.
Исправленный код:
....
ret = chdir (dir_name);
if (ret != 0)
{
fnotice (stderr, "%s is not a directory\n", dir_name);
free(pwd);
return NULL;
}
....
Фрагмент N7. Перезапись параметра
static conversion *
build_aggr_conv (tree type,
tree ctor,
int flags, // <=
tsubst_flags_t complain)
{
unsigned HOST_WIDE_INT i = 0;
conversion *c;
tree field = next_aggregate_field (TYPE_FIELDS (type));
tree empty_ctor = NULL_TREE;
hash_set<tree, true> pset;
flags = LOOKUP_IMPLICIT|LOOKUP_NO_NARROWING; // <=
....
}
Предупреждение анализатора PVS-Studio: V763 Parameter 'flags' is always rewritten in function body before being used. call.cc 994, call.cc 981
Принимаемый по копии параметр функции flags всегда перезаписывается. Возможно, разработчик планировал дополнительно установить флаги в эту переменную или должна была использоваться другая переменная.
Фрагмент N8. Неисполняемый цикл
void
gt_pch_p_13ctf_container (ATTRIBUTE_UNUSED void *this_obj,
void *x_p,
ATTRIBUTE_UNUSED gt_pointer_operator op,
ATTRIBUTE_UNUSED void *cookie)
{
struct ctf_container * x ATTRIBUTE_UNUSED = (struct ctf_container *)x_p;
{
....
size_t l0 = (size_t)(0); // <=
....
if ((*x).ctfc_vars_list != NULL)
{
size_t i0;
for (i0 = 0; i0 != (size_t)(l0) // <=
&& ((void *)(*x).ctfc_vars_list == this_obj); i0++)
{
if ((void *)((*x).ctfc_vars_list) == this_obj)
op (&((*x).ctfc_vars_list[i0]), NULL, cookie);
}
....
}
}
....
}
Предупреждение анализатора PVS-Studio: V621 Consider inspecting the 'for' operator. It's possible that the loop will be executed incorrectly or won't be executed at all. gtype-desc.cc 11865
Переменная l0, от которой зависит состояние цикла, так и осталась равной нулю, в связи с чем цикл никогда не начнёт своего выполнения.
Анализатор PVS-Studio также выдал ещё несколько похожих предупреждений. Вот только некоторые из них:
V621 Consider inspecting the 'for' operator. It's possible that the loop will be executed incorrectly or won't be executed at all. gtype-desc.cc 11874
V621 Consider inspecting the 'for' operator. It's possible that the loop will be executed incorrectly or won't be executed at all. gtype-desc.cc 11883
V621 Consider inspecting the 'for' operator. It's possible that the loop will be executed incorrectly or won't be executed at all. gtype-desc.cc 11892
Фрагмент N9. Сомнительный цикл
static bool
combine_reaching_defs (ext_cand *cand,
const_rtx set_pat,
ext_state *state)
{
....
while ( REG_P (SET_SRC (*dest_sub_rtx))
&& (REGNO (SET_SRC (*dest_sub_rtx)) == REGNO (SET_DEST (set))))
{
....
if (....)
break;
if (....)
break;
....
break;
}
}
Предупреждение анализатора PVS-Studio: V612 An unconditional 'break' within a loop. ree.cc 985
Анализатор PVS-Studio обнаружил, что данный цикл безусловно прерывается на первой итерации. При дальнейшем рассмотрении мы также обнаруживаем множество других break под условиями. Возможно, это способ избежать написания оператора goto, что, однако, выглядит странно, учитывая его постоянное открытое использование в целом по проекту.
Фрагмент N10. Странное ветвление
void
const_fn_result_svalue::dump_to_pp(pretty_printer *pp,
bool simple) const
{
if (simple)
{
pp_printf (pp, "CONST_FN_RESULT(%qD, {", m_fndecl);
for (unsigned i = 0; i < m_num_inputs; i++)
{
if (i > 0)
pp_string (pp, ", ");
dump_input (pp, i, m_input_arr[i], simple);
}
pp_string (pp, "})");
}
else
{
pp_printf (pp, "CONST_FN_RESULT(%qD, {", m_fndecl);
for (unsigned i = 0; i < m_num_inputs; i++)
{
if (i > 0)
pp_string (pp, ", ");
dump_input (pp, i, m_input_arr[i], simple);
}
pp_string (pp, "})");
}
}
Предупреждение анализатора PVS-Studio: V523 The 'then' statement is equivalent to the 'else' statement. svalue.cc 2009, svalue.cc 1998
Обнаружено полное дублирование логики в условных ветках. Я решил поискать причину такого дублирования и воспользовался командой git log –follow -p ./gcc/analyzer/svalue.cc в корне git проекта GCC, чтобы посмотреть разницу между коммитами, однако ничего там не обнаружил. Получается, файл был написан с этой ошибкой изначально.
Заключение
После изучения кода, настолько активно использующего макросы, очень быстро накатывает усталость. Но в этом случае усталость компенсируется удовлетворением от факта работы над таким серьезным проектом, как GCC. PVS-Studio подтвердил свою способность находить малозаметные ошибки даже в хорошо отлаженных кодовых базах, и даже завеса из макросов не стала для него препятствием.
Напоследок хочется пожелать всем допускать как можно меньше ошибок и не плодить баги в релиз :) Ну и пользуясь случаем, приглашаю попробовать триальную версию анализатора и подписаться на дайджест наших статей, чтобы не пропустить интересное.
Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Ignat Grishechko. Checking the GCC 13 compiler with the help of PVS-Studio.
Комментарии (11)
Panzerschrek
06.09.2023 19:15+1Жесть конечно, такой проект и до сих пор выглядит, как будто на Си, а не на C++ написан.
interrupt
06.09.2023 19:15+2Он же исорически был на C, только в 2013 его перевели на сборку c++ компилятором.
Genoik
06.09.2023 19:15Хотел просить про ложные предупреждения, которые выдает PVS-Studio.
Имеется вот такой код:RenderPassFlags flags = {}; <= if (has_flag(renderpass->desc.flags, RenderPassDesc::Flags::ALLOW_UAV_WRITES)) <= { flags |= RenderPassFlags::ALLOW_UAV_WRITES; }
Выдаваемое предупреждение:
V1051 Consider checking for misprints. It's possible that the 'flags' should be used inside 'has_flag' function.
PVS-Studio не видит, что переменная flags не имеет никакого отношения к функции has_flag() ?
Пример
struct InstancedBatch { ... uint32_t instanceCount = 0; ... } instancedBatch = {}; ... auto batch_flush = [&]() { if (instancedBatch.instanceCount == 0) <= return; ... } .... for(...) { instancedBatch.instanceCount++; }
Выдаваемое предупреждение
V547 Expression 'instancedBatch.instanceCount == 0' is always true.
PVS-Studio не видит, что значение instancedBatch.instanceCount увеличивается на единицу каждый цикл чуть ниже по коду ?
Пример
GPUBarrier barriers[] = { GPUBarrier::Image(&res.texture_temporal[0], res.texture_temporal[0].desc.layout, ResourceState::UNORDERED_ACCESS), GPUBarrier::Image(&res.texture_temporal[1], res.texture_temporal[1].desc.layout, ResourceState::UNORDERED_ACCESS), }; ... std::swap(barriers[0].image.layout_before, barriers[0].image.layout_after); std::swap(barriers[1].image.layout_before, barriers[1].image.layout_after); <=
Выдаваемое предупреждение:
V557 Array overrun is possible. The '1' index is pointing beyond array bound.
Я не смог понять, где тут выход за пределы массива ?
Если массив имеет два элемента, то вполне логично, что первый элемент имеет индекс равный 0, а второй элемент имеет индекс равный 1.
graphorismo Автор
06.09.2023 19:15+1Ой. С таким моментам лучше писать в поддержку. Выпишем себе на доработку, поможем, объясним и так далее. P.S. По поводу V557 - проблема в brace-инициализации. На текущий момент мы как раз занимаемся исправлением этой проблемы.
Genoik
06.09.2023 19:15Техподдержка просит корпоративную почту, для обоснования этого даже целая статья имеется. То есть выходит, что просто сообщения от пользователей (если это не потенциальные клиенты) о каких-то ошибках, вам не интересны ?
graphorismo Автор
06.09.2023 19:15Мы работаем и с людьми, написавшими с личной почты. Мы заинтересованы в получении фидбэка от всех пользователей нашего продукта. Единственное отличие между бесплатными пользователями и клиентами будет в оперативности исправления выявленных багов.
Genoik
06.09.2023 19:15+3Хорошо, напишу.
Не спора ради, но процитирую ваш же:
Мы настоятельно просим использовать корпоративную почту при общении с нами.
Так вот, независимо от стадии общения (presale или maintenance) мы просим использовать корпоративную почту.
целесообразно использовать именно корпоративную почту, чтобы общение было конструктивным, быстрым и точным.
Пожалуйста, используйте только корпоративную почту при общении с нами.
После стольких упоминаний, использовать корпоративную почту, даже как-то рука не поднялась написать с личной, из уважения к вам.
Возможно вам стоит в одном тексте уменьшить упоминание корпоративной почты или сделать какую-то отдельную сноску, что пользователям бесплатных лицензий можно писать с личных и это не будет проявлением неуважения к вам и игнорирование ваших многократных просьб.
SquareRootOfZero
А у вас есть пост о том, как вы проверяли PVS-Studio при помощи PVS-Studio?
graphorismo Автор
Однажды написали такую статью. Вообще мы ежедневно прогоняем анализ на нашей кодовой базе как на машинах разработчиков, так и на сборочных серверах. К сожалению, примеры ошибок не сохраняем на будущее.
Предложу идею коллегам, может когда-нибудь напишем свежую статью :)