пятница, 7 декабря 2012 г.

Нужны ли недокументированные API?


В чем прелесть высокоуровневых языков программирования?
Программист перестает задумываться о том "как оно на самом деле фунциклирует" и выдает на гора тонну не сопровождаемого кода, который иногда даже работает, но периодически подтормаживает. Более того, частенько, он даже не собирается об этом задумываться, так как у него банально отсутствуют необходимые знания (увы, так сейчас учат).

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

А потом, выполнив очередной умный "паттерн" он начинает разбираться - где же тормозит алгоритм. Причем, если программист более настырен, он изучает реализацию VCL и иногда даже докапывается до сути, где получается так, что тормоза упираются в вызовы известных ему по документации API, пройдя к которым он со спокойной душой останавливается и закрывает тикет в багтрекере фразой: "функция ХХХ тормозит, вариантов обхода нет".

Не встречались с ситуацией?
Значит повезло...





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

Допустим мы хотим написать приложение, которому требуется актуальный список текущих куч указанного процесса (не важно для чего, допустим мы антивирус :) ).

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

  procedure QueryProcessHeap1;
  var
    hSnapShot: THandle;
    HeapList: THeapList32;
    HeapEntry: THeapEntry32;
    Start: DWORD;
  begin
    Start := GetTickCount;
    Writeln('Heap info:');
    Writeln;
    hSnapShot := CreateToolhelp32Snapshot(TH32CS_SNAPHEAPLIST, 0);
    try
      HeapList.dwSize := SizeOf(THeapList32);
      if Heap32ListFirst(hSnapShot, HeapList) then
      repeat
        HeapEntry.dwSize := SizeOf(THeapEntry32);
        if Heap32First(HeapEntry, GetCurrentProcessId, HeapList.th32HeapID) then
        repeat
          Writeln(Format('Heap addr: 0x%p, size: %d',
            [Pointer(HeapEntry.dwAddress), HeapEntry.dwBlockSize]));
        until not Heap32Next(HeapEntry);
      until not Heap32ListNext(hSnapShot, HeapList);
    finally
      CloseHandle(hSnapShot);
    end;
    Writeln;
    Writeln('DONE. Time elapsed: ', GetTickCount - Start);
  end;

Код простой до невозможности - используются стандартные функции TlHelp32 без изысков.
Но нюанс, попробуйте его выполнить и посмотреть на время его работы.

Около 150 миллисекунд. Шутки по поводу того, что возрастающие мощности компьютеров программисты стараются нивелировать неоптимизированным кодом, конечно смешные, но...
Это же мы попробовали взять данные только у своего процесса, а сколько там их на машине пользователя крутиться-то будет? Да хотя-бы полтинник, и к чему мы придем?

К почти пятисекундной задержке при запросе только куч процесса (я уже не говорю о просто перечислении процессов нитей и т.п.).

Ну а действительно, а что мы можем сделать?

Мы не виноваты - так работает функции Heap32ххх и на самом деле данные тормоза обусловлены задержкой на их вызове (это действительно так). И вот мы сидим своим сервисом на сервере, жрем процессорные ресурсы и разводим руками: "Фсе пропало - шэф".

Таки нет, ребят - мы виноваты в том, что останавливаемся на созданных нами же барьерах.

У программиста под руками всегда есть отладчик, благодаря которому можно проанализировать, что же именно происходит, как минимум развернув стек вызовов при помощи CPU-View.

С учетом того, что я пишу статьи для людей интересующихся защитой ПО, я не буду рассказывать как им пользоваться (вы и сами должны это знать), поэтому давайте поставим бряк на вызове функции Heap32First и просто протрассируем программу через F7.

Через несколько десятков трассировочных шагов мы увидим:


Еще немного потрассируем и выйдем на:


После чего произведем те же шаги, но только с функцией Heap32Next.

Как вы думаете, что там мы обнаружим?
Увы - те же RtlCreateQueryDebugBuffer + RtlQueryProcessDebugInformation и в конце RtlDestroyQueryDebugBuffer.

А вот это уже заковыка, что-ж такое происходит? Получается что на каждый наш чих при вызове API функций из библиотеки tlhelp32.dll в действительности вызываются все эти три апишки, первая из которых собирает информацию о процессе, вторая делает выборку всего одной записи из нее и потом все закрывается. Не слишком ли накладно?

А теперь давайте вспомним - сколько там времени нам понадобилось для перечисления куч процесса? 150 миллисекунд? Ну так есессно, почему бы и нет, когда на каждый вызов Heap32Next заново собиралась вся информация о процессе.

Будем бороться - пишем код (сразу предупреждаю, код наколеночный, немного с выводом адресов путает, но для демки пойдет).

  procedure QueryProcessHeap2;
  var
    I, A: Integer;
    pDbgBuffer: PRtlDebugInformation;
    pHeapInformation: PRtlHeapInformation;
    pHeapEntry: PRtrHeapEntry;
    dwHeapStartAddr, dwAddr, dwLastSize: DWORD;
    hit_seg_count: Integer;
    Start: DWORD;
  begin
    Start := GetTickCount;
    Writeln('Heap info:');
    Writeln;
    pDbgBuffer := RtlCreateQueryDebugBuffer(0, False);
    if pDbgBuffer <> nil then
    try
      // Запрашиваем информацию по списку куч процесса
      if RtlQueryProcessDebugInformation(GetCurrentProcessId,
        RTL_QUERY_PROCESS_HEAP_SUMMARY or RTL_QUERY_PROCESS_HEAP_ENTRIES,
        pDbgBuffer) = 0 then
      begin
        // Получаем указатель на кучу по умолчанию
        pHeapInformation := @pDbgBuffer^.Heaps^.Heaps[0];
        // перечисляем все ее блоки...
        for I := 0 to pDbgBuffer^.Heaps^.NumberOfHeaps - 1 do
        begin
          // начиная с самого первого
          pHeapEntry := pHeapInformation^.Entries;
          dwAddr := DWORD(pHeapEntry^.u.s2.FirstBlock) +
            pHeapInformation^.EntryOverhead;
          dwLastSize := 0;
          for A := 0 to pHeapInformation^.NumberOfEntries - 1 do
          begin
            dwHeapStartAddr := dwAddr;
            hit_seg_count := 0;
            while (pHeapEntry^.Flags and RTL_HEAP_SEGMENT) = RTL_HEAP_SEGMENT do
            begin
              // Если блок отмечен флагом RTL_HEAP_SEGMENT,
              // то рассчитываем новый адрес
              dwAddr := DWORD(pHeapEntry^.u.s2.FirstBlock) +
                pHeapInformation^.EntryOverhead;
              Inc(pHeapEntry);
              Inc(hit_seg_count);
              // проверка выхода за границы блоков
              if A + hit_seg_count >=
                Integer(pHeapInformation^.NumberOfEntries - 1) then
                Continue;
            end;
            // Если блок не самый первый в сегменте, то текущий адрес блока равен,
            // адресу предыдущего блока + размер предыдущего блока
            if hit_seg_count = 0 then
              Inc(dwAddr, dwLastSize);
            Writeln(Format('Heap addr: 0x%p, size: %d',
              [Pointer(dwHeapStartAddr), dwAddr - dwHeapStartAddr]));
           // Запоминаем адрес последнего блока
           dwLastSize := pHeapEntry^.Size;
           // Переходим к следующему блоку
           Inc(pHeapEntry);
          end;
          // Переходим к следующей куче
          Inc(pHeapInformation);
        end;
      end;
    finally
      RtlDestroyQueryDebugBuffer(pDbgBuffer);
    end;
    Writeln;
    Writeln('DONE. Time elapsed: ', GetTickCount - Start);
  end;

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

Недокументированно?
Та и шут с ним, не стесняйтесь вникать в тонкости :)
Только не забывайте про расстояние между "когда можно" и "когда нужно" :)

Какие минусы могут быть при использовании недокументированных вызовов?
Ну во первых никто не может дать гарантии что параметры и структуры вызова данных функций не изменятся при очередном патче, да и вообще никто не гарантирует что она сама останется в списках экспорта библиотеки. Правда если честно я с таким поведением никогда не встречался, за исключением одной единственной функции - AllocateAndGetTcpExTableFromStack. Но с ней вообще все получилось как-то не понятно, изначально она появилась в Windows XP. До тех пор пока не вышла Vista она считалась недокументированной, однако с выходом Vista ее задокументировали, но сразу исключили из состава библиотеки Iphlpapi.dll, написав "This function is no longer available for use as of Windows Vista".

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

Исходник можно посмотреть тут: http://rouse.drkb.ru/blog/heap.zip

---

Александр (Rouse_) Багель
Декабрь, 2012

8 комментариев:

  1. Ни за что обидел фреймворки и API.

    ОтветитьУдалить
  2. Доброго)
    Александр, отличный пример. Однако, как педагог не могу пройти мимо -- Я бы предпоследнюю строку растянул на абзац минимум, указал бы что действительно "нужно" бывает крайне редко. Во избежание, так сказать.

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

    Спасибо за пример ещё раз. Познавательно)

    ОтветитьУдалить
    Ответы
    1. Незачто, последний абзац растянул, как и просили :)

      Удалить
  3. Сделал для себя выводы, спасибо за статью! :) Gmail...

    ОтветитьУдалить
  4. Это конечно хорошо, а где брать описание недокументированных функций и их параметров, а-ля

    function RtlDestroyQueryDebugBuffer(
    DebugBuffer: PRtlDebugInformation): NTSTATUS; stdcall; external ntdll;

    или

    _RTL_HEAP_INFORMATION = record
    BaseAddress: Pointer;
    Flags: ULONG;
    EntryOverhead: USHORT;
    CreatorBackTraceIndex: USHORT;
    BytesAllocated: ULONG;
    BytesCommitted: ULONG;
    NumberOfTags: ULONG;
    NumberOfEntries: ULONG;
    NumberOfPseudoTags: ULONG;
    PseudoTagGranularity: ULONG;
    Reserved: array [0..4] of ULONG;
    Tags: Pointer;
    Entries: PRTL_HEAP_ENTRY;
    end;
    ?

    ОтветитьУдалить
    Ответы
    1. Часть описано у Шрайбера, часть в исходниках ReactOS, если не нахожу в этих источниках ищу в интернете, если не нахожу и там, разбираю в дизассемблере (но до этого очень редко доходит).

      Удалить