В Windows есть специальный механизм, который позволяет без больших усилий получить огромное количество информации о производительности системы. Как ты наверное догадался, все это добро находится в реестре. И для доступа к нему используется специальный ключ - HKEY_PERFORMANCE_DATA.
демонстрационный проект (12,5кб)
исходник и релиз my_perfproc.dll (22 кб)
PerfLook - обозреватель ключа HKEY_PERFORMANCE_DATA (216 кб)
Угадай, где у винды самая большая помойка. :) Правильно! В реестре. Но господин Regedit показывает нам далеко не всю доступную информацию. Например, там есть огромное количество инфы о производительности системы, о процессах и системных объектах, о работе устройств, об активности сетевых соединений и еще много чего. Ты можешь получить список запущенных процессов и контролировать работу каждого процесса : загрузку процесса, используемую память, pid, приоритет, системные объекты и т.п. (Мало того, эту же инфу может удаленно читать и писать в логи твой сисадмин! В конце этой статьи я покажу тебе, как оставить его с носом).
Если хочешь убедиться – набирай в командной строке: mmc %systemroot%\system32\perfmon.msc или просто - perfmon. Откроется консоль управления с системным монитором. Кликай мышкой на окно с графиками и жми CTRL-I. Откроется окошко для добавления счетчика производительности.
Смотри сколько инфы можно получить о системе! В выпадающем списке можно выбрать тип объекта наблюдения (хотя правильнее было бы назвать его классом). В правом листбоксе перечислены экземпляры этого объекта, а в левом счетчики производительности. Можно выбрать счетчики для каждого экземпляра объекта. Выбери объект Process и в правом листбоксе ты увидишь список всех запущенных процессов. Вот его-то мы для примера и выковырнем из реестра. (Еще ты можешь попробовать мою программку PerfLook, она пока недоработана, но для наших целей сойдет.)
Вся нужная нам инфа хранится в разделе HKEY_PERFORMANCE_DATA. Но, как ты наверное заметил, Regedit этот раздел не показывает и многие о нем просто не знают. У г-на Regedit’а на это есть свои причины. Дело в том что этот раздел чисто виртуальный, никакая инфа из него на диск не сохраняется и существует только тогда, когда кто-то ее запрашивает.
Просто так к этому разделу не подступиться. Например, перечислить ключи с помощью RegEnumKeyEx не получится. Доступ к информации в HKEY_PERFORMANCE_DATA надо получать с помощью функции RegQueryValueEx. Эту функцию ты наверняка уже знаешь, но при обращении к HKEY_PERFORMANCE_DATA ее надо использовать по-другому.
|
Во втором параметре ей надо задать набор счетчиков в виде нультерминальной строки:
Дальше в функции RegQueryValueEx происходит примерно такая последовательность действий. Функция открывает ключик HKLM\SYSTEM\CurrentControlSet\Services\ и просматривает все подключи. В каждом подключе она ищет подключ Performance.
Если находит, то читает параметры:
· Library – имя dll-ки, возвращающей данные.
· Object List – список индексов возвращаемых счетчиков.
· Open – Имя функции инициализации dll-ки.
· Collect – Имя функции возращающей значения счетчиков.
· Close – Имя функции, которая вызывается, когда dll-ка больше не нужна.
Если в Object List есть индекс нужного счетчика, то функция загружает dll-ку и вызывает функцию Open. Для получения данных счетчиков вызывается Collect, а для освобождения ресурсов Close. Если интересны прототипы этих функций, читай MSDN или посмотри пример переходника для perfproc.dll. В функцию Collect передается буфер и dll-ка заполняет его блоком данных. В начале блока идет структура PERF_OBJECT_TYPE, а затем данные экземпляров объекта и счетчиков. Если все прошло нормально, то Collect возвращает ERROR_SUCCESS, а если буфер слушком маленький, то - ERROR_MORE_DATA. Все блоки данных от всех dll-ек собираются вместе, спереди к ним добавляется структура PERF_DATA_BLOCK и возвращаются в пятом параметре lpcbData функции RegQueryValueEx.
В итоге RegQueryValueEx возвращает ERROR_SUCCESS и здоровенную структуру данных (десятки килобайт) в пятом параметре lpcbData, или ERROR_MORE_DATA и тогда lpcbData оказывается неопределен. При получении ERROR_MORE_DATA надо выделять буфер побольше и снова вызывать функцию RegQueryValueEx, пока она не возвратит ERROR_SUCCESS.
Итак мы решили-таки получить данные счетчика из реестра. Сразу возникает пара вопросов.
Первый вопрос – где взять индекс счетчика? У объекта Process он равен «230». Tебе интересно, откуда он взялся? Тогда открывай Regedit и смотри ключ HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Perflib\.
Там есть один или несколько подключей с цифровыми трехзначными именами. Это языковой идентификатор. Для английского языка: 009 (он есть всегда), для русского – 019.
Идентификатор установленного в системе по умолчанию языка можно получить с помощью функции GetSystemDefaultUILanguage:
var
language: LANGID;
begin
language := WORD(GetSystemDefaultUILanguage) and $00FF;
end;
или из реестра: HKLM\System\CurrentControlSet\Control\Nls\Language, параметр InstallLanguage (по той же формуле).
В этом подключе смотри параметр Counter. Кликай по нему дважды и все увидишь. Там перечислены все индексы и имена счетчиков в виде нультерминальных строчек. В конце последней строчки 2 терминальных нуля – последний закрывает весь блок. Ищи в именах «Process» и перед ним увидишь его индекс – «230».
Второй вопрос – как все-таки извлечь список процессов? Вот это уже не так просто. Структура данных довольно сложная, поэтому рассмотрим только нужные нам моменты.
В самом начале полученного буфера лежит структура PERF_DATA_BLOCK. В ней содержится общее описание полученных данных. Нужные поля:
· Signature - UNICODE-сигнатура «PERF»
· TotalByteLength – общий размер полученных данных
· HeaderLength – размер структуры TPerfDataBlock
· NumObjectTypes – количество просматриваемых объектов. У нас будет только 1 – Process.
Сразу за ней идут информационные блоки для каждого объекта. (Вот они-то и возвращаются описанными выше dll-ками.) Их количество указано в TPerfDataBlock. NumObjectTypes.
В начале информационного блока идет заголовок в виде структуры PERF_OBJECT_TYPE. Затем идут описания всех счетчиков, доступных для данного типа объекта, в виде структур PERF_COUNTER_DEFINITION. Их количество определено в PERF_OBJECT_TYPE..NumCounters.
Дальше возможно два варианта:
1. Если у типа объекта есть экземпляры, то дальше идут блоки с описаниями этих экземпляров. Количество этих блоков есть в PERF_OBJECT_TYPE. NumInstances. В начале каждого блока стоит структура PERF_INSTANCE_DEFINITION. За ней структура PERF_COUNTER_BLOCK с одним единственным полем ByteLength: размер блока данных счетчиков, включая размер структуры PERF_COUNTER_BLOCK. Затем собственно данные счетчиков для экземпляра объекта. Смещения данных каждого счетчика от начала PERF_COUNTER_BLOCK записано в PERF_COUNTER_DEFINITION.CounterOffset
2. Если экземпляров у объекта нет, то все счетчики относятся в самому типу объекта. Так тоже бывает. В этом случае сразу за PERF_COUNTER_DEFINITION идет только один блок данных счетчиков, начинающийся с PERF_COUNTER_BLOCK.
Вот собственно и все. :) Чтобы тебе стало немного понятнее я нарисовал схемку.
Хотя нет. Вот с этого момента как раз и начинается самое интересное. Чтобы получить значение счетчика недостаточно знать его смещение. Надо знать размер данных и их тип. Все это можно определить по полю PERF_COUNTER_DEFINITION.CounterType. Оно состоит из битовых полей, определяющих различные свойства счетчика.
Так счетчик может быть простым числом (например PID процесса), текстовой строкой, просто нулем и постоянно меняющимся счетчиком. В последнем случае реальное значение рассчитывается по специальным формулам с использованием данных других счетчиков и полей вышеописанных структур. Это довольно муторный материал и его мы рассматривать не будем. Вся нужная инфа есть в MSDN.
Для примера. Загрузка процессора процессом выдается в счетчике «% Processor Time», который имеет тип - PERF_100NSEC_TIMER. Чтобы получить реальное значение загрузки в процентах надо снять два последовательных значения счетчика и использовать формулу: 100*(X1-X0)/(Y1-Y0), где X0 и X1 – значения счетчика, а Y0 и Y1 – значения поля PERF_DATA_BLOCK.PerfTime100nSec во время получения данных счетчика. |
Вот теперь можно приступить собственно к кодингу. Напоминаю, что мы пытаемся получить список процессов из реестра. Наша функция FillProcessesList будет заполнять переданный ей TStringList списком имен процессов и их PID’ами. Определения структур я тут не привожу для экономии места. Смотри их в коде примера. Да!, структуры я обозвал по Дельфиньему.
|
Я не стал заморачиваться в поиском индексов объектов по реестру, а сразу записал их в константы. Но если кому надо, то могу помочь с кодом функции поиска.
В коде прокомментирована практически каждая строчка. Функция работает просто. Сначала резервирует буфер и пытается получить результат из RegQueryValueEx. Если не получается, то резервирует буфер побольше и пытается снова. Потом пробегается по буферу в поисках объекта с индексом 230 (Process). Когда находит то получает имена всех экземпляров этого объекта, тут же у каждого экземпляра перебирает счетчики с поисках счетчика с индексом 784 (ID Process или PID). Потом собирает эти данные в одну строчку, которую добавляет в TStringList.
К статье прилагается небольшой проект, который получает с помощью этой функции список процессов с pid’ами и выводит его в TMemo.
Рис 1. Отладка полным ходом :)
На самом деле все не так страшноТы наверное ужаснулся уже от объема работы, которую надо проделать, чтобы хоть что-то получить. Мелкомягкие программеры тоже так подумали, сжалились над нами и, начиная с Windows 2000, выпустили специальную библиотеку Performance Data Helper (pdh.dll) Кроме того есть механизм WMI (Windows Management Instrumentation), который больше подходит для .NET-платформы. Теперь получение данных о производительности получается уже несколько понятнее. Например окошко со списком счетчиком на самом первом рисунке создается с помощью функции PdhBrowseCounters, а индекс счетчика по его имени находится с помощью PdhLookupPerfIndexByName Вот так будет выгдядеть получение списка процессов с помощью PDH:
unit Sample3;
interface
uses Windows, Classes, SysUtils;
const // Уровни детализации PERF_DETAIL_NOVICE = 100; // Новичок PERF_DETAIL_ADVANCED = 200; // Продвинутый PERF_DETAIL_EXPERT = 300; // Эксперт PERF_DETAIL_WIZARD = 400; // Максимальный
PDH_MORE_DATA = -2147481646; //Все данные не помещаются в предоставленный буфер
function FillProcessesList(var slProcesses: TStringList): Boolean;
implementation
function PdhEnumObjectItems(szDataSource: PChar; szMachineName: PChar; szObjectName: PChar; mszCounterList: PChar; pcchCounterListLength: PDWORD; mszInstanceList: PChar; pcchInstanceListLength: PDWORD; dwDetailLevel: DWORD; dwFlags: DWORD ): LongInt; stdcall; external 'PDH.DLL' name 'PdhEnumObjectItemsA';
function FillProcessesList(var slProcesses: TStringList): Boolean; var pdhStatus: LongInt; // Возвращаемое значение PDH-функций sObjectName: array [0..7] of Char; // Буфер для имени объекта "Process" pCounterList: PAnsiChar; // Буфер для списка счетчиков CounterListLength: DWORD; // Длина буфера для списка счетчиков pInstanceList: PAnsiChar; // Буфер для списка экземпляров (процессов), после // заполнения здесь будет цепочка нультерминальных // строк и в конце общий #0. InstanceListLength: DWORD;// Длина буфера для списка экземпляров pThisInstance: PAnsiChar; // Указатель на имя текущего экземпляра begin Result := False; sObjectName := 'Process'; // Собираемся получить список процессов CounterListLength := 0; // Обнуляем длины буферов InstanceListLength := 0; // В первый вызов функции вместо буферов ставим nil и получаем длины буферов pdhStatus := PdhEnumObjectItems(nil, // информация реального времени nil, // локальная машина @sObjectName, // Имя объекта nil, @CounterListLength, // передаем 0 nil, @InstanceListLength,// передаем 0 PERF_DETAIL_WIZARD, //Максимальная детализация 0); // PdhEnumObjectItems должна вернуть PDH_MORE_DATA, если нет - ошибка if pdhStatus = PDH_MORE_DATA then begin // Резервируем память под буферы GetMem(pCounterList, CounterListLength); GetMem(pInstanceList, InstanceListLength); try // Вторым вызовом функции получаем информацию pdhStatus := PdhEnumObjectItems(nil, // информация реального времени nil, // локальная машина @sObjectName, // Имя объекта pCounterList, // Буфер счетчиков @CounterListLength, pInstanceList, // Буфер имен @InstanceListLength, PERF_DETAIL_WIZARD, 0); // Проверяем успешность выполнения функции PdhEnumObjectItems if pdhStatus = ERROR_SUCCESS then begin // Ставим указатель на начало буфера экземпляров pThisInstance := pInstanceList; // Крутим цикл пока не кончатся имена (в конце буфера стоит #0) while pThisInstance^ <> #0 do begin // Пропускаем экземпляр с общими данными if pThisInstance <> '_Total' then // Добавляем имя процесса к списку slProcesses.Add(pThisInstance); // Перемещаем указатель на следующее имя inc(pThisInstance, Length(pThisInstance)+1); end; // Фунция сработала успешно, если досюда дошло Result := True; end; finally // В любом случае освобождаем память буферов FreeMem(pCounterList); FreeMem(pInstanceList); end; end; end;
end. |
Ну а теперь обещанная шутка с сисадмином. Дело в том что к реестру можно подключаться удаленно (используется RPC – Remote Procedure Call) с помощью функции RegConnectRegistry. Если твой комп в локалке настроен соответствующе, то админ может получать данные из твоего реестра и контролировать, например, запущенные у тебя процессы. Сейчас мы его от этого отучим.
Самый простой и мирный способ. В ключе HKLM\SYSTEM\CurrentControlSet\Services\PerfProc\Performance создать параметр «Disable Performance Counters» типа REG_DWORD равным 1. Все. Теперь perfproc.dll (она и выдает данные о процессах) вообще не будет загружаться. Объекта «Process» в выходных данных RegQueryValueEx просто не будет.
Но можно поступить гораздо интереснее. Вместо perfproc.dll (в %WINDIR%\System32) можно записать свою dll-ку. Загружаться она будет в адресное пространство загружающего процесса, то есть выполнится на компе админа. Чтобы все прошло гладко, надо сделать нашу dll-ку похожей на perfproc.dll. Здесь я привожу код переходника, который работает точно также как и perfproc.dll, но при загрузке библиотеки выдаем сообщение с именем запускающего процесса. Код для разнообразия написан на VC6.
// my_perfproc.cpp // Переходник для perfproc.dll
#include "stdafx.h" #include <windows.h>
typedef DWORD (CALLBACK* pRealOpen)(LPWSTR); typedef DWORD (WINAPI* pRealClose)(); typedef DWORD (WINAPI* pRealCollect)(LPWSTR,LPVOID,LPDWORD,LPDWORD);
pRealClose pCloseSysProcessObject; pRealOpen pOpenSysProcessObject; pRealCollect pCollectSysProcessObjectData; HMODULE hRealDll = NULL;
BOOL APIENTRY DllMain( HANDLE hModule, DWORD dwReason, LPVOID lpReserved) { char sExeName[MAX_PATH]; switch (dwReason) { case DLL_PROCESS_ATTACH: GetModuleFileName(NULL, sExeName, MAX_PATH); MessageBox(0,sExeName,"EXE Module",0); break; case DLL_THREAD_ATTACH: break; case DLL_THREAD_DETACH: break; case DLL_PROCESS_DETACH: break; }
return TRUE; }
DWORD WINAPI CloseSysProcessObject() { DWORD ret = pCloseSysProcessObject(); FreeLibrary(hRealDll); return 0; }
DWORD CALLBACK OpenSysProcessObject(LPWSTR lpDeviceNames) { if (!hRealDll) hRealDll = LoadLibrary("perfproc.dll"); if (!hRealDll) return GetLastError();
pOpenSysProcessObject = (pRealOpen) GetProcAddress(hRealDll, "OpenSysProcessObject"); pCloseSysProcessObject = (pRealClose) GetProcAddress(hRealDll, "CloseSysProcessObject"); pCollectSysProcessObjectData = (pRealCollect) GetProcAddress(hRealDll, "CollectSysProcessObjectData"); if (! pOpenSysProcessObject || !pCloseSysProcessObject || !pCollectSysProcessObjectData) { FreeLibrary(hRealDll); hRealDll = NULL; return ERROR_INVALID_DATA; } DWORD ret = pOpenSysProcessObject(lpDeviceNames); return ret; }
DWORD WINAPI CollectSysProcessObjectData(LPWSTR lpwszValue, LPVOID *lppData, LPDWORD lpcbBytes, LPDWORD lpcObjectTypes)
{ DWORD dwDataSize = *lpcbBytes; DWORD dwNumObjects = 0; void* p = *lppData;
DWORD ret = pCollectSysProcessObjectData(lpwszValue,&p,&dwDataSize,&dwNumObjects); *lpcbBytes = dwDataSize; *lpcObjectTypes = dwNumObjects; *lppData = (char*)(*lppData) + dwDataSize; return ret; } |
Эта библиотека экспортирует те же функции, что и perfproc.dll. При вызове функции OpenSysProcessObject она загружает perfproc.dll и получает адреса настоящих функций из perfproc.dll. При обращении к функциям нашей dll-ки вызываются настоящие функции perfproc.dll. Естественно имя этой библиотеки надо прописать в реестре вместо perfproc.dll (HKLM\SYSTEM\CurrentControlSet\Services\PerfProc\Performance параметр Library)
Дальше твои действия ограничены только полетом твоей фантазии. Можно разбирать данные, возвращенные CollectSysProcessObjectData, и вырезать из них сведения о каком-то конкретном процессе. А можно в любой из функций выполнять любой код (на какой у тебя прав хватит): от перезагрузки компа, до установки трояна и кражи паролей. Можно просто убить вызывающий процесс (TerminateProcess(GetCurrentProcess,0)). Когда будешь отлаживать dll-ку, то при ошибках будет появляться параметр «Disable Performance Counters». Убирай его, чтобы либа снова стала загружаться.
Это все. Конечно, я тут осветил не всю информацию по теме, но общее представление ты, надеюсь, получил. Удачи в освоении недр системы.
Орехов Роман aka tripsin.