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

в 18:34, , рубрики: api, Delphi, метки: ,

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

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

А потом, выполнив очередной умный «паттерн» он начинает разбираться — где же тормозит алгоритм. Причем, если программист более настырен, он изучает реализацию 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.

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

image

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

image

После чего произведем те же шаги, но только с функцией 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».

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

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

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

Автор: Rouse

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js