Встроенные системы существуют уже давно. Крайне важно, чтобы они были стабильными и надежными, а исправление ошибок в них чрезвычайно затратно. Вот почему разработчики встраиваемых систем получают большую пользу от регулярного использования специализированных инструментов контроля качества кода. В этой статье мы расскажем вам о поддержке GNU Arm Embedded Toolchain в анализаторе PVS-Studio и продемонстрируем некоторые проблемы с кодом, обнаруженные в проекте Mbed OS.

Введение

Анализатор PVS-Studio уже поддерживает несколько коммерческих компиляторов для встраиваемых систем, например:

Теперь к ним присоединяется еще один инструмент разработчика — GNU Embedded Toolchain.

GNU Embedded Toolchain — коллекция компиляторов, разработанная компанией Arm на основе GNU Compiler Collection. Впервые официально выпущенный в 2012 году, он развивался вместе с GCC.

Основная цель GNU Embedded Toolchain — генерация кода, ориентированного на голое железо, то есть кода, предназначенного для работы на ЦП напрямую, без операционной системы. В состав пакета входят компиляторы C и C++, ассемблер, GNU Binutils и библиотека Newlib. Все компоненты с открытым исходным кодом; они распространяются под лицензией GNU GPL. Вы можете загрузить готовые версии наборов инструментов для Windows, Linux и macOS с официального сайта.

Мбед ОС

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

Хотя основная цель этой статьи — рассказать вам о поддержке GNU Embedded Toolchain, на эту тему сложно сказать много. Кроме того, наши читатели наверняка ждут интересных багов и ошибок, так что не будем заставлять их ждать. Давайте вместо этого запустим анализатор для проекта Mbed OS. Это операционная система с открытым исходным кодом, в разработке которой принимает участие Arm.

Официальный сайт: https://www.mbed.com/

Исходный код: https://github.com/ARMmbed/mbed-os

Mbed OS была выбрана не случайно, вот как описывают ее разработчики:

Arm Mbed OS — это встроенная операционная система с открытым исходным кодом, разработанная специально для «вещей» в Интернете вещей. Он включает в себя все функции, необходимые для разработки подключенного продукта на базе микроконтроллера Arm Cortex-M, включая безопасность, возможность подключения, ОСРВ и драйверы для датчиков и устройств ввода-вывода.

Похоже, это идеальный проект для GNU Embedded Toolchain, особенно с учетом участия Arm в его разработке. Сразу скажу заранее, что у меня не было цели найти как можно больше проблем в конкретном проекте, поэтому кратко опишу результаты анализа.

Проблемы

В результате запуска PVS-Studio на исходном коде ОС Mbed было выдано 693 предупреждения, 86 из которых имели высокий приоритет. Многие из них далеко не интересны, поэтому я не буду описывать их все. Например, было много предупреждений V547 (Выражение всегда истинно/ложно), исходящих из похожих фрагментов кода. Конечно, есть способ настроить анализатор, чтобы сильно уменьшить количество ложных или просто неинтересных сообщений, но это не имело отношения к моей цели. Если вы хотите увидеть пример такой настройки, обратитесь к статье Характеристики PVS-Studio Analyzer на примере основных библиотек EFL, 10–15% ложных срабатываний.

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

Утечки памяти

Начнем с одного класса ошибок, часто встречающихся в C и C++, — с утечками памяти.

Предупреждение анализатора: V773 CWE-401 Произошел выход из функции без освобождения указателя read_buf. Возможна утечка памяти. cfstore_test.c 565

int32_t cfstore_test_init_1(void)
{
   ....
  read_buf = (char*) malloc(max_len);
  if(read_buf == NULL) {
    CFSTORE_ERRLOG(....);
    return ret;
  }
  ....
  while(node->key_name != NULL)
  {
    ....
    ret = drv->Create(....);
    if(ret < ARM_DRIVER_OK){
      CFSTORE_ERRLOG(....);
      return ret;              // <=
    }
  ....
  free(read_buf);
  return ret;
}

Это классическая ошибка, связанная с манипуляциями с динамической памятью. Буфер, выделенный с помощью malloc, используется только внутри функции и освобождается перед выходом из функции. Проблема в том, что этого не происходит, если функция возвращается преждевременно. Также обратите внимание на похожий код в двух блоках if. Похоже, что программист скопировал верхний фрагмент кода и просто забыл добавить вызов free.

Вот еще один пример, похожий на предыдущий.

Предупреждение анализатора: V773 CWE-401 Произошел выход из функции без отпускания указателя интерфейс. Возможна утечка памяти. nanostackemacinterface.cpp 204

nsapi_error_t Nanostack::add_ethernet_interface(
    EMAC &emac,
    bool default_if,
    Nanostack::EthernetInterface **interface_out,
    const uint8_t *mac_addr)
{
  ....
  Nanostack::EthernetInterface *interface;
  interface = new (nothrow) Nanostack::EthernetInterface(*single_phy);
  if (!interface) {
    return NSAPI_ERROR_NO_MEMORY;
  }
  nsapi_error_t err = interface->initialize();
  if (err) {
    return err;              // <=
  }
  *interface_out = interface;
  return NSAPI_ERROR_OK;
}

Указатель на выделенную память возвращается через out-параметр, но этого не происходит, если вызов метода initialize завершается ошибкой — в этом случае происходит утечка памяти, так как interface локальная переменная выходит из области видимости, и указатель просто теряется. Здесь должен был быть вызов delete, или, по крайней мере, адрес, хранящийся в переменной interface, должен был быть возвращен в любом случае, чтобы вызывающая сторона могла освободить память .

Мемсет

Использование функции memset часто приводит к ошибкам. Примеры таковых вы можете увидеть в статье Самая опасная функция в мире C/C++.

Давайте проверим это предупреждение:

V575 CWE-628 Функция memset обрабатывает элементы 0. Проверьте третий аргумент. mbed_error.c 282

mbed_error_status_t mbed_clear_all_errors(void)
{
    ....
    //Clear the error and context capturing buffer
    memset(&last_error_ctx, sizeof(mbed_error_ctx), 0);
    //reset error count to 0
    error_count = 0;
    ....
}

Здесь предполагалось обнулить память, занимаемую структурой last_error_ctx , но программист разместил второй и третий аргументы в неправильном порядке. В результате ровно 0 байт заполняется значением sizeof(mbed_error_ctx).

Вот похожее предупреждение, которое появляется сотней строк выше:

V575 CWE-628 Функция memset обрабатывает элементы 0. Проверьте третий аргумент. mbed_error.c 123

Безусловный оператор «возврата» в цикле

Предупреждение анализатора: V612 CWE-670 Безусловный возврат внутри цикла. thread_network_data_storage.c 2348

bool thread_nd_service_anycast_address_mapping_from_network_data (
          thread_network_data_cache_entry_t *networkDataList,
          uint16_t *rlocAddress,
          uint8_t S_id)
{
  ns_list_foreach(thread_network_data_service_cache_entry_t,
                  curService, &networkDataList->service_list) {
    // Go through all services
    if (curService->S_id != S_id) {
      continue;
    }
    ns_list_foreach(thread_network_data_service_server_entry_t,
                    curServiceServer, &curService->server_list) {
      *rlocAddress = curServiceServer->router_id;
      return true;                     // <=
    }
  }
  return false;
}

В этом фрагменте кода ns_list_foreach — это макрос, который заменяется оператором for. Внутренний цикл выполняет не более одной итерации из-за вызова return сразу после строки, которая инициализирует выходной параметр функции. Этот код может работать по плану, однако внутренний цикл в этом контексте выглядит довольно странно. Скорее всего, инициализация rlocAddress и последующий возврат должны происходить при каком-то условии. Также возможно, что внутренний контур является избыточным.

Ошибки в условиях

Как я сказал во вступлении, было много неинтересных V547, поэтому я их бегло проверил. Только пару случаев стоило посмотреть.

V547 CWE-570 Выражение pcb-›state == LISTEN всегда ложно. lwip_tcp.c 689

enum tcp_state
{
  CLOSED      = 0,
  LISTEN      = 1,
  ....
};
struct tcp_pcb *
tcp_listen_with_backlog_and_err(struct tcp_pcb *pcb,
                                 u8_t backlog,
                                 err_t *err)
{
  ....
  LWIP_ERROR("tcp_listen: pcb already connected",
             pcb->state == CLOSED,
             res = ERR_CLSD; goto done);
  /* already listening? */
  if (pcb->state == LISTEN)              // <=
  {
    lpcb = (struct tcp_pcb_listen*)pcb;
    res = ERR_ALREADY;
    goto done;
  }
  ....
}

Анализатор считает, что условие pcb-›state == LISTEN всегда ложно. Давайте посмотрим, почему это так.

Перед оператором if находится вызов LWIP_ERROR, который представляет собой макрос, работающий аналогично assert. Он определяется следующим образом:

#define LWIP_ERROR(message, expression, handler) do { if (!(expression)) { \
  LWIP_PLATFORM_ERROR(message); handler;}} while(0)

Если условие ложно, макрос сообщает об ошибке и выполняет все, что ему передается через аргумент handler. В текущем фрагменте кода у нас есть безусловный goto.

В этом примере проверяется условие ‘pcb-›state == CLOSED’, то есть переход к метке done происходит только тогда, когда pcb-›state имеет любое другое значение. Оператор if после вызова LWIP_ERROR проверяет, равно ли pcb-›state LISTEN — условие, которое никогда не выполняется потому что состояние в этой строке может быть равно только ЗАКРЫТО.

Еще одно предупреждение, связанное с условиями: V517 CWE-570 Обнаружено использование паттерна if (A) {…} else if (A) {…}. Существует вероятность наличия логической ошибки. Проверьте строки: 62, 65. libdhcpv6_server.c 62

static void libdhcpv6_address_generate(....)
{
  ....
  if (entry->linkType == DHCPV6_DUID_HARDWARE_EUI64_TYPE) // <=
  {
    memcpy(ptr, entry->linkId, 8);
   *ptr ^= 2;
  }
  else if (entry->linkType == DHCPV6_DUID_HARDWARE_EUI64_TYPE)// <=
  {
    *ptr++  = entry->linkId[0] ^ 2;
    *ptr++  = entry->linkId[1];
  ....
  }
}

Здесь if и else if точно проверяют одно и то же условие, что делает код внутри блока else if недостижимым. Подобные ошибки часто связаны с методом программирования копировать-вставить.

Бесхозное выражение

Давайте посмотрим на забавный фрагмент кода.

Предупреждение анализатора: V607 Бесхозяйное выражение ‘& Discover_response_tlv’. thread_discovery.c 562

static int thread_discovery_response_send(
                thread_discovery_class_t *class,
                thread_discovery_response_msg_t *msg_buffers)
{
  ....
  thread_extension_discover_response_tlv_write(
             &discover_response_tlv, class->version,
             linkConfiguration->securityPolicy);
  ....
}

Теперь давайте проверим определение макроса thread_extension_discover_response_tlv_write:

#define thread_extension_discover_response_tlv_write \
( data, version, extension_bit)\
(data)

Макрос расширяется до своего аргумента data, вызов которого внутри функции thread_discovery_response_send превращается в выражение (&discover_response_tlv) после предварительной обработки.

У меня нет дальнейших комментариев. Возможно, здесь нет никакой ошибки, но такой код всегда делает меня похожим на картинку выше :).

Вывод

Расширен список компиляторов, поддерживаемых в PVS-Studio. Если у вас есть проект, предназначенный для сборки с помощью GNU Arm Embedded Toolchain, я предлагаю вам попробовать проверить его с помощью нашего анализатора. Демо-версия доступна здесь. Также обратите внимание, что у нас есть бесплатная лицензия, которая вполне подойдет для некоторых небольших проектов разработки.