>
>
>
PPSSPP или всё же psp? Смотрим баги в к…

Алексей Смольскас
Статей: 7

PPSSPP или всё же psp? Смотрим баги в коде из прошлого

В мире видеоигр эмуляторы стали настоящей находкой для поклонников классических проектов. PPSSPP — один из самых популярных эмуляторов для PlayStation Portable (PSP), который позволяет нам вновь окунуться в атмосферу любимых игр, но уже на современных устройствах. Однако чтобы играть без сбоев и лагов, нужно убедиться, что код эмулятора хорошо написан.

Предисловие

Эта статья является сиквелом вот этой статьи. Хочется похвалить разработчиков PPSSPP, так как практически ВСЕ ошибки из предыдущей части, о которых я написал, были исправлены в течение месяца и в релиз 1.18.1 не вошли. К сожалению, все ошибки, найденные на момент написания предыдущей статьи, расписать мне не удалось из-за нехватки сил и времени. Каюсь. Возможно, поэтому ошибки, о которых пишу сегодня, всё так же находятся в кодовой базе проекта :)

Ошибок в проекте, как и этой статьи, могло бы не быть, если бы разработчики воспользовались PVS-Studio. К слову, кажется, это хороший момент напомнить о том, что в PVS-Studio есть возможность получить бесплатную лицензию для open source проектов. Подробнее об этом можно прочитать на специальной странице.

Результаты проверки

Как я уже сказал выше, это продолжение другой статьи, поэтому ошибки найдены при проверке кода PPSSPP версии 1.17.1 при помощи анализатора PVS-Studio на Linux. Версия анализатора — 7.32. Но эти ошибки попали в релиз 1.18.1, поэтому все ссылки на срабатывания в коде ниже ведут на версию 1.18.1. Также отмечу, что на момент написания статьи актуальная версия анализатора PVS-Studio — 7.34. Об изменениях в этой версии можно почитать тут.

Undefined behavior

Фрагмент N1

А начнём мы с такого фрагмента:

static int sceNetAdhocctlGetAddrByName(const char *nickName,
                                       u32 sizeAddr, u32 bufAddr)
{
  ....
  // Copied to null-terminated var to prevent unexpected behaviour on Logs
  memcpy(nckName, nickName, ADHOCCTL_NICKNAME_LEN); 

  ....
  if (netAdhocctlInited)
  {
    // Valid Arguments
    if (nickName != NULL && buflen != NULL)
    {
      ....
    }
    ....
  }
}

Предупреждение PVS-Studio: V595 The 'nickName' pointer was utilized before it was verified against nullptr. Check lines: 6143, 6152. sceNetAdhoc.cpp 6143

Как мы видим, в memcpy передаётся указатель nickname в качестве исходного буфера. Чуть ниже по коду проверяется, не нулевой ли этот указатель. Значит, разработчик предполагает, что формальный параметр может быть нулевым. За точками, в которых я скрыл промежуточный код, он тоже не изменяется (можете проверить, ткнув на номер строки предупреждения).

А что по этому поводу думает memcpy? Цитирую cpperefence:

If either dest or src is an invalid or null pointer, the behavior is undefined, even if count is zero.

Дополнительное срабатывание:

  • V595 The 'buf2' pointer was utilized before it was verified against nullptr. Check lines: 364, 365. __sceAudio.cpp 364

Фрагмент N2

Теперь рассмотрим следующий фрагмент:

int internal_profiler_find_cat(const char *category_name, bool create_missing)
{
  int i;
  for (i = 0; i < MAX_CATEGORIES; i++)
  {
    const char *catname = categories[i].name;
    if (!catname)
      break;
#ifdef UNIFIED_CONST_STR
    if (catname == category_name)
    {
#else
    if (!strcmp(catname, category_name))                          // <=
    {
#endif
      return i;
    }
  }

  if (i < MAX_CATEGORIES && category_name && create_missing)      // <=
  {
    ....
  }     
}

Предупреждение PVS-Studio: V595 The 'category_name' pointer was utilized before it was verified against nullptr. Check lines: 103, 109. Profiler.cpp 103

У нас есть проверка указателя category_name, но сходу можно не заметить, где он используется.

В случае, если не определена директива UNIFIED_CONST_STR, нулевой указатель category_name может прилететь в strcmp. А это приводит к UB.

Фрагмент N3

static void __GameModeNotify(u64 userdata, int cyclesLate)
{
  ....
  if (gameModeSocket < 0)
  {
    // ReSchedule
    CoreTiming::ScheduleEvent(usToCycles(GAMEMODE_UPDATE_INTERVAL) - cyclesLate,
                              gameModeNotifyEvent, userdata);
    return;
  }

  auto sock = adhocSockets[gameModeSocket - 1];
  ....
}

Предупреждение PVS-Studio: V557 Array underrun is possible. The value of 'gameModeSocket - 1' index could reach -1. sceNetAdhoc.cpp 191

Обратим внимание на инициализацию переменной sock. Мы видим, что её пытаются проинициализировать элементом массива adHocSockets с индексом gameModeSocket - 1.

Теперь обратим внимание на проверку выше. Она отсеивает все отрицательные значения для переменной gameModeSocket.

Проблема в том, что забыли учесть случай, когда gameModeSocket будет равен нулю. Тогда индекс gameModeSocket - 1 получится отрицательным, и это приведёт к UB.

Исправленный код:

if (gameModeSocket <= 0)

Фрагмент N4

SoftGPU::SoftGPU(GraphicsContext *gfxCtx, Draw::DrawContext *draw)
  : GPUCommon(gfxCtx, draw)
{
  ....
  drawEngine_ = new SoftwareDrawEngine();
  if (!drawEngine_)
    return;
  ....
}

Предупреждение PVS-Studio: V668 There is no sense in testing the 'drawEngine_' pointer against null, as the memory was allocated using the 'new' operator. The exception will be generated in the case of memory allocation error. SoftGpu.cpp 436

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

Вероятно, такой фрагмент возник в результате рефакторинга, а раньше здесь использовался malloc.

Фрагмент N5

static std::vector<MicWaitInfo> waitingThreads;
....
static void __MicBlockingResume(u64 userdata, int cyclesLate)
{
  ....
  int count = 0;
  for (auto waitingThread : waitingThreads)
  {
    if (waitingThread.threadID == threadID)
    {
      ....
      if (Microphone::isHaveDevice())
      {
        if (Microphone::getReadMicDataLength() >= waitingThread.needSize)
        {
          ....
          waitingThreads.erase(waitingThreads.begin() + count);    // <=
        }
        else
        {
          ....
        }
      } 
      else
      {
        ....
        waitingThreads.erase(waitingThreads.begin() + count);      // <=
        readMicDataLength += waitingThread.needSize;
      }
    }

    ++count;
  }
}

Предупреждения PVS-Studio:

  • V789 Iterators for the 'waitingThreads' container, used in the range-based for loop, become invalid upon the call of the 'erase' function. sceUsbMic.cpp 73
  • V789 Iterators for the 'waitingThreads' container, used in the range-based for loop, become invalid upon the call of the 'erase' function. sceUsbMic.cpp 87

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

Обратим внимание на код waitingThreads.erase(waitingThreads.begin() + count);. Он появляется в двух местах в зависимости от того, по какой ветке условий мы пойдём.

В подобном коде может возникнуть ряд проблем. После такого удаления инвалидируются все итераторы и ссылки в точке удаления или после неё, включая итератор end(). То есть при следующем проходе цикла будет обрабатываться уже невалидный итератор.

В качестве решения можно воспользоваться так называемой erase-remove идиомой, а начиная с C++20, появились std::erase/std::erase_if. Но для этого придётся хорошенько отрефакторить код.

Фрагмент N6

void Int_VecDo3(MIPSOpcode op)
{
  ....
  u32 lastsat = (currentMIPS->vfpuCtrl[VFPU_CTRL_DPREFIX] & 3) << (n + n - 2);
  ....
}

Предупреждениe PVS-Studio: V610 Undefined behavior. Check the shift operator '<<'. The right operand is negative ('(n + n - 2)' = [-2..6]). MIPSIntVFPU.cpp 2155

Чтобы понять в чём тут ошибка, необходимо посмотреть на объявление переменной n.

Вот оно:

u32 n = GetNumVectorElements(sz);

Так почему же ругается анализатор?

Всё дело в том, что отработал межпроцедурный анализ. Просмотрев определение функции GetNumVectorElements, анализатор узнал, что она может возвращать значения в диапазоне [0 .. 4].

Дальше, увидев, что n никак не изменяется по коду, он может сделать вывод, что в выражении (n + n - 2) возможный диапазон значений результата [-2..6].

Теперь посмотрим, что такое vpfuCtrl. Это массив 32-битных беззнаковых чисел. Таким образом, оператор здесь производит обычный побитовый сдвиг влево на -2 элемента. Как это? А как получится.

Аналогичные срабатывания:

  • V610 Undefined behavior. Check the shift operator '<<'. The right operand is negative ('(n - 1)' = [-1..3]). MIPSIntVFPU.cpp 2261
  • V610 Undefined behavior. Check the shift operator '<<'. The right operand is negative ('(n + n - 2)' = [-2..6]). MIPSIntVFPU.cpp 2262

Фрагмент N7

inline float Float16ToFloat(float16 ix)
{
    float x;
    memcpy(&x, &ix, sizeof(float));
    return x;
}

Предупреждение PVS-Studio: V512 A call of the 'memcpy' function will lead to the '& ix' buffer becoming out of range. math_util.h 23

Мы пытаемся скопировать значение из типа float16 в тип float. Тип float16 это unsigned short:

typedef unsigned short float16;

Размеры unsigned short и float определяются реализацией, однако на многих платформах они равны 16 и 32 битам соответственно.

Дальше обратим внимание на третий аргумент в вызове memcpy. Он говорит, какое количество байтов надо скопировать из переменной ix. В случае, когда размер float равен 32 битам (4 байта), а размер unsigned short 16 битам (2 байта), произойдёт выход за границы буфера ix.

Избыточные проверки

Фрагмент N8

void Jit::Comp_SVQ(MIPSOpcode op)
{
  CONDITIONAL_DISABLE(LSU_VFPU);

  int imm = (signed short)(op&0xFFFC);
  int vt = (((op >> 16) & 0x1f)) | ((op&1) << 5);
  MIPSGPReg rs = _RS;

  CheckMemoryBreakpoint(0, rs, imm);

  switch (op >> 26)
  {
    case 53: //lvl.q/lvr.q
    {
      if (!g_Config.bFastMemory)
      {
        DISABLE;
      }

      DISABLE;
  ....
}

Предупреждение PVS-Studio: V523 [CWE-691] The 'then' statement is equivalent to the subsequent code fragment. CompVFPU.cpp 313

Обратим внимание, что в then-ветке конструкции if написан такой же код, как и после самой конструкции. А вот что из себя представляет сам макрос DISABLE:

#define DISABLE { fpr.ReleaseSpillLocks(); Comp_Generic(op); return; }

Понятно, что ничего серьёзного здесь не произойдёт, так как мы всё равно выйдем из функции. Вопрос в том, является ли здесь лишним условие, или в теле должен был быть другой код, который выполнится перед выходом из функции? Или это банально оставленная TODO?

Однозначно по такому коду сказать ничего нельзя, так как комментариев тоже нет. Именно поэтому этот код и плох.

Похожие срабатывания:

  • V523 The 'then' statement is equivalent to the subsequent code fragment. IRCompVFPU.cpp 2375
  • V523 The 'then' statement is equivalent to the subsequent code fragment. IRCompVFPU.cpp 1167

Фрагмент N9

void BlockAllocator::Block::DoState(PointerWrap &p)
{
  ....
  size_t tagLen = strlen(tag);
  if (tagLen != sizeof(tag))
    memset(tag + tagLen, 0, sizeof(tag) - tagLen);
  DoArray(p, tag, sizeof(tag));
}

Предупреждение PVS-Studio: V547 Expression 'tagLen != sizeof (tag)' is always true. BlockAllocator.cpp 508

Интересная проверка. Взглянем на декларацию массива tag:

char tag[32];

Выражение sizeof(tag) будет равно 32. Выражение tagLen != sizeof(tag) окажется false, только когда результат strlen(tag) будет равен 32. Однако максимальное значение, которое может вернуть strlen для tag, это 31, так как последний символ должен быть нуль-терминалом. То есть единственный вариант, когда условие не выполнится, это если tag будет не нуль-терминированной строкой. Но в этом случае всплыл бы один нюанс — поведение strlen оказалось бы неопределённым. Поэтому выражение всегда вычисляется как true и memset всегда зануляет sizeof(tag) - tagLen байт.

Ещё срабатывания V547:

  • V547 Expression 'leftvol >= 0' is always true. sceAudio.cpp 121
  • V547 Expression 'rightvol >= 0' is always true. sceAudio.cpp 124

Фрагмент N10

void netAdhocValidateLoopMemory()
{
  // Allocate Memory if it wasn't valid/allocated
  // after loaded from old SaveState
  if (   !dummyThreadHackAddr
      || (   dummyThreadHackAddr
          && strcmp("dummythreadhack",
                    kernelMemory.GetBlockTag(dummyThreadHackAddr)) != 0))
  {
  ....
}

Предупреждение PVS-Studio: V728 An excessive check can be simplified. The '||' operator is surrounded by opposite expressions '!dummyThreadHackAddr' and 'dummyThreadHackAddr'. sceNetAdhoc.cpp 1121

Думаю, вполне очевидное срабатывание и место, которое можно упростить.

Представим, что мы — компилятор и разбираем это условие. Так как мы обязаны выполнять его слева направо, мы посмотрим на левую часть от оператора ||. Условие !dummyThreadHackAddr может быть true или false. Если true, то мы сразу заходим внутрь. Если false, то это значит, что подвыражение dummyThreadHackAddr — это всегда true.

Поэтому проверку можно упростить:

if (   !dummyThreadHackAddr 
    || strcmp("dummythreadhack",
              kernelMemory.GetBlockTag(dummyThreadHackAddr)) != 0))

Аналогичные срабатывания:

  • V728 An excessive check can be simplified. The '||' operator is surrounded by opposite expressions '!matchingThreadHackAddr' and 'matchingThreadHackAddr'. sceNetAdhoc.cpp 1126
  • V728 An excessive check can be simplified. The '||' operator is surrounded by opposite expressions '!regs[i].away' and 'regs[i].away'. RegCache.cpp 352

Производительность

Фрагмент N11

void QueueCallback(void (*func)(VulkanContext *vulkan, void *userdata),
                   void *userdata)
{ 
  callbacks_.push_back(Callback(func, userdata));
}
void VulkanRenderManager::EndCurRenderStep()
{
  for (VKRGraphicsPipeline *pipeline : pipelinesToCheck_)
  {
    if (!pipeline)
    {
      // Not good, but let's try not to crash.
      continue;
    }

    if (!pipeline->pipeline[(size_t)rpType])
    {
      pipeline->pipeline[(size_t)rpType] = Promise<VkPipeline>::CreateEmpty();
      _assert_(renderPass);
      compileQueue_.push_back(CompileQueueEntry(pipeline,
            renderPass->Get(vulkan_, rpType, sampleCount),rpType, sampleCount));
      needsCompile = true;
    }
  }
}

Предупреждение PVS-Studio: V823 Decreased performance. Object may be created in-place in the 'compileQueue_' container. Consider replacing methods: 'push_back' -> 'emplace_back'. VulkanRenderManager.cpp 883

Для начала посмотрим, что из себя представляет контейнер compileQueue_:

std::vector<CompileQueueEntry> compileQueue_;

Теперь посмотрим, что такое CompileQueueEntry:

struct CompileQueueEntry
{
  CompileQueueEntry(VKRGraphicsPipeline *p,
                    VkRenderPass _compatibleRenderPass,
                    RenderPassType _renderPassType,
                    VkSampleCountFlagBits _sampleCount)
  : type(Type::GRAPHICS), graphics(p),
         compatibleRenderPass(_compatibleRenderPass),
         renderPassType(_renderPassType), sampleCount(_sampleCount){}

  enum class Type
  {
    GRAPHICS,
  };

  Type type;
  VkRenderPass compatibleRenderPass;
  RenderPassType renderPassType;
  VKRGraphicsPipeline* graphics = nullptr;
  VkSampleCountFlagBits sampleCount;

};

Переменная compileQueue_ — это обычный std::vector, а CompileQueueEntry — это структура, которая имеет пользовательский конструктор. Соответственно, код можно сделать немного эффективнее, если заменить push_back на emplace_back, о чём я писал в предыдущей статье.

В нашем случае можно убрать дополнительное копирование пяти полей структуры CompileQueueEntry при перемещении временного объекта в вектор. Да, такое копирование не тяжеловесное. Но таких мест в программе может быть много. Да и нет гарантии, что потом эта структура не станет тяжеловесной, а вставку не забудут поменять на emplace_back.

Кстати, начиная с C++20, emplace_back будет работать также и с агрегатными типами.

Дополнительные срабатывания:

  • V823 Decreased performance. Object may be created in-place in the 'callbacks_' container. Consider replacing methods: 'push_back' -> 'emplace_back'. VulkanContext.h 128
  • V823 Decreased performance. Object may be created in-place in the 'threads_' container. Consider replacing methods: 'push_back' -> 'emplace_back'. HTTPServer.cpp 48

Разное

Фрагмент N12

bool Key(const KeyInput &key) override
{
  std::vector<int> pspKeys;               // <=
  bool showInfo = false;
  if (HasFocus() && UI::IsInfoKey(key))
  {
    // If the button mapped to triangle, then show the info.
    if (key.flags & KEY_UP)
    {
      showInfo = true;
    }
  }
  else if (hovering_ && key.deviceId == DEVICE_ID_MOUSE 
                     && key.keyCode == NKCODE_EXT_MOUSEBUTTON_2)
  {
    // If it's the right mouse button, and it's not otherwise mapped,
    // show the info also.
    if (key.flags & KEY_DOWN)
    {
      showInfoPressed_ = true;
    }
    if ((key.flags & KEY_UP) && showInfoPressed_)
    {
      showInfo = true;
      showInfoPressed_ = false;
    }
  }

  if (showInfo) 
  {
     TriggerOnHoldClick();
     return true;
  }

  return Clickable::Key(key);
}

Предупреждение PVS-Studio: V808 'pspKeys' object of 'vector' type was created but was not utilized. MainScreen.cpp 159

Понятно, что пустой вектор не занимает много места в памяти. Ошибки такого типа опасны тем, что предполагалось использование одной переменной, а вместо неё случайно воспользовались другой. Такое может случиться, например, при опечатке.

В этом фрагменте, как видно, всё в порядке. Скорее всего, вектор перестали использовать после рефакторинга кода, и анализатор может подсвечивать такие места.

Фрагмент N13

VkResult VulkanContext::InitDebugUtilsCallback()
{
  // We're intentionally skipping
  // VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT
  // and VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT,
  // just too spammy.
  int bits = VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT
           | VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT
           | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT;
  ....
}

Предупреждение PVS-Studio: V501 There are identical sub-expressions 'VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT' to the left and to the right of the '|' operator. VulkanContext.cpp 867

Ошибки такого паттерна очень похожи на предыдущий фрагмент. Возможно, предполагалось проверить другой флаг, но в результате, например, copy-paste, его могли забыть поменять. Такие ошибки, как правило, тяжело ищутся глазами, но статический анализ с ними справляется на раз-два.

Заключение

Представьте, что вы садитесь в космический корабль, который перенесёт вас в мир ностальгии, где каждое нажатие кнопки пробуждает в вас воспоминания о захватывающих приключениях на PSP. Но для того, чтобы этот космический корабль мог взлететь, важно, чтобы у него была надёжная система управления. Под надёжной системой управления я подразумеваю хороший код. А под хорошим кодом я подразумеваю не только то, что он должен быть качественно написан, но и то, что он должен быть протестирован и отлажен.

Поэтому важно не забывать тестировать код различными методиками, в том числе и статическими анализаторами. Получить пробную версию анализатора PVS-Studio можно здесь.