Windows Native Applications kaj Acronis Active Restore servo
Hodiaŭ ni daŭrigas la rakonton pri kiel ni, kune kun la infanoj de Innopolis University, disvolvas Active Restore-teknologion por permesi al la uzanto komenci labori sur sia maŝino kiel eble plej baldaŭ post fiasko. Ni parolos pri denaskaj Vindozaj aplikoj, inkluzive de la funkcioj de ilia kreado kaj lanĉo. Sub la tranĉo estas iom pri nia projekto, kaj ankaŭ praktika gvidilo pri kiel verki indiĝenajn aplikojn.
En antaŭaj afiŝoj ni jam parolis pri kio ĝi estas Aktiva Restarigo, kaj kiel studentoj de Innopolis evoluas servo. Hodiaŭ mi volas koncentriĝi pri indiĝenaj aplikoj, al kies nivelo ni volas "entombigi" nian aktivan reakivan servon. Se ĉio funkcias, tiam ni povos:
Lanĉu la servon mem multe pli frue
Kontaktu la nubon, kie troviĝas la sekurkopio, multe pli frue
Multe pli frue por kompreni en kia reĝimo estas la sistemo - normala lanĉo aŭ reakiro
Multe malpli da dosieroj por retrovi anticipe
Permesu al la uzanto komenci eĉ pli rapide.
Kio estas denaska aplikaĵo ĉiukaze?
Por respondi ĉi tiun demandon, ni rigardu la sinsekvon de alvokoj, kiujn la sistemo faras, ekzemple, se programisto en sia aplikaĵo provas krei dosieron.
Pavel Yosifovich - Vindoza Kerna Programado (2019)
La programisto uzas la funkcion Krei dosieron, kiu estas deklarita en la kapdosiero fileapi.h kaj efektivigita en Kernel32.dll. Tamen, ĉi tiu funkcio mem ne kreas la dosieron, ĝi nur kontrolas la enigajn argumentojn kaj vokas la funkcion NtCreateFile (la prefikso Nt nur indikas, ke la funkcio estas denaska). Ĉi tiu funkcio estas deklarita en la kapa dosiero winternl.h kaj efektivigita en ntdll.dll. Ĝi prepariĝas por salti en nuklean spacon, post kio ĝi faras sistemvokon por krei dosieron. En ĉi tiu kazo, rezultas, ke Kernel32 estas nur envolvaĵo por Ntdll. Unu el la kialoj kial tio estis farita estas ke Microsoft tiel havas la kapablon ŝanĝi la funkciojn de la denaska mondo, sed ne tuŝi la normajn interfacojn. Mikrosofto ne rekomendas voki denaskajn funkciojn rekte kaj ne dokumentas la plej multajn el ili. Cetere, nedokumentitaj funkcioj troveblas tie.
La ĉefa avantaĝo de indiĝenaj aplikoj estas, ke ntdll estas ŝarĝita en la sistemon multe pli frue ol kernel32. Ĉi tio estas logika, ĉar kernel32 postulas ntdll por funkcii. Kiel rezulto, aplikoj kiuj uzas denaskajn funkciojn povas komenci labori multe pli frue.
Tiel, Windows Native Applications estas programoj kiuj povas komenci frue en Vindoza ekkuro. Ili NUR uzas funkciojn de ntdll. Ekzemplo de tia aplikaĵo: autochk kiu plenumas chkdisk ilo por kontroli la diskon por eraroj antaŭ ol komenci la ĉefajn servojn. Ĉi tio estas ĝuste la nivelo, kiun ni volas, ke nia Aktiva Restarigo estu.
Kion ni bezonas?
DDK (Driver Development Kit), nun ankaŭ konata kiel WDK 7 (Windows Driver Kit).
Virtuala maŝino (ekzemple Windows 7 x64)
Ne necesas, sed kapdosieroj elŝuteblaj povas helpi tie
Kio estas en la kodo?
Ni ekzercu iomete kaj, ekzemple, skribu malgrandan aplikaĵon, kiu:
Montras mesaĝon sur la ekrano
Asignas iom da memoro
Atendas klavaran enigon
Liberigas uzitan memoron
En indiĝenaj aplikoj, la enirpunkto ne estas ĉefa aŭ winmain, sed la funkcio NtProcessStartup, ĉar ni fakte rekte lanĉas novajn procezojn en la sistemo.
Ni komencu montrante mesaĝon sur la ekrano. Por tio ni havas denaskan funkcion NtDisplayString, kiu prenas kiel argumenton montrilon al UNICODE_STRING-strukturobjekto. RtlInitUnicodeString helpos nin pravalorigi ĝin. Kiel rezulto, por montri tekston sur la ekrano ni povas skribi ĉi tiun malgrandan funkcion:
//usage: WriteLn(L"Here is my textn");
void WriteLn(LPWSTR Message)
{
UNICODE_STRING string;
RtlInitUnicodeString(&string, Message);
NtDisplayString(&string);
}
Ĉar nur funkcioj de ntdll disponeblas al ni, kaj simple ankoraŭ ne estas aliaj bibliotekoj en memoro, ni certe havos problemojn pri kiel asigni memoron. La nova funkciigisto ankoraŭ ne ekzistas (ĉar ĝi venas de la tro altnivela mondo de C++), kaj ne ekzistas malloc-funkcio (ĝi postulas rultempajn C-bibliotekojn). Kompreneble, vi povas nur uzi stakon. Sed se ni bezonas dinamike asigni memoron, ni devos fari ĝin sur la amaso (t.e. amaso). Do ni kreu amason por ni mem kaj prenu memoron de ĝi kiam ajn ni bezonas ĝin.
La funkcio taŭgas por ĉi tiu tasko RtlCreateHeap. Poste, uzante RtlAllocateHeap kaj RtlFreeHeap, ni okupos kaj liberigos memoron kiam ni bezonos ĝin.
PVOID memory = NULL;
PVOID buffer = NULL;
ULONG bufferSize = 42;
// create heap in order to allocate memory later
memory = RtlCreateHeap(
HEAP_GROWABLE,
NULL,
1000,
0, NULL, NULL
);
// allocate buffer of size bufferSize
buffer = RtlAllocateHeap(
memory,
HEAP_ZERO_MEMORY,
bufferSize
);
// free buffer (actually not needed because we destroy heap in next step)
RtlFreeHeap(memory, 0, buffer);
RtlDestroyHeap(memory);
Ĉio, kion ni bezonas, estas uzi NtReadFile sur malfermita aparato, kaj atendu ĝis la klavaro resendas iun ajn premon al ni. Se la ESC-klavo estas premata, ni daŭre laboros. Por malfermi la aparaton, ni devos voki la funkcion NtCreateFile (ni bezonos malfermi DeviceKeyboardClass0). Ni ankaŭ vokos NtCreateEventpravalorigi la atendan objekton. Ni mem deklaros la strukturon KEYBOARD_INPUT_DATA, kiu reprezentas la klavardatumojn. Ĉi tio faciligos nian laboron.
La indiĝena aplikaĵo finiĝas per funkciovoko NtTerminateProcessĉar ni simple mortigas nian propran procezon.
La tuta kodo por nia malgranda aplikaĵo:
#include "ntifs.h" // WinDDK7600.16385.1incddk
#include "ntdef.h"
//------------------------------------
// Following function definitions can be found in native development kit
// but I am too lazy to include `em so I declare it here
//------------------------------------
NTSYSAPI
NTSTATUS
NTAPI
NtTerminateProcess(
IN HANDLE ProcessHandle OPTIONAL,
IN NTSTATUS ExitStatus
);
NTSYSAPI
NTSTATUS
NTAPI
NtDisplayString(
IN PUNICODE_STRING String
);
NTSTATUS
NtWaitForSingleObject(
IN HANDLE Handle,
IN BOOLEAN Alertable,
IN PLARGE_INTEGER Timeout
);
NTSYSAPI
NTSTATUS
NTAPI
NtCreateEvent(
OUT PHANDLE EventHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN EVENT_TYPE EventType,
IN BOOLEAN InitialState
);
// https://docs.microsoft.com/en-us/windows/win32/api/ntddkbd/ns-ntddkbd-keyboard_input_data
typedef struct _KEYBOARD_INPUT_DATA {
USHORT UnitId;
USHORT MakeCode;
USHORT Flags;
USHORT Reserved;
ULONG ExtraInformation;
} KEYBOARD_INPUT_DATA, *PKEYBOARD_INPUT_DATA;
//----------------------------------------------------------
// Our code goes here
//----------------------------------------------------------
// usage: WriteLn(L"Hello Native World!n");
void WriteLn(LPWSTR Message)
{
UNICODE_STRING string;
RtlInitUnicodeString(&string, Message);
NtDisplayString(&string);
}
void NtProcessStartup(void* StartupArgument)
{
// it is important to declare all variables at the beginning
HANDLE hKeyBoard, hEvent;
UNICODE_STRING skull, keyboard;
OBJECT_ATTRIBUTES ObjectAttributes;
IO_STATUS_BLOCK Iosb;
LARGE_INTEGER ByteOffset;
KEYBOARD_INPUT_DATA kbData;
PVOID memory = NULL;
PVOID buffer = NULL;
ULONG bufferSize = 42;
//use it if debugger connected to break
//DbgBreakPoint();
WriteLn(L"Hello Native World!n");
// inialize variables
RtlInitUnicodeString(&keyboard, L"DeviceKeyboardClass0");
InitializeObjectAttributes(&ObjectAttributes, &keyboard, OBJ_CASE_INSENSITIVE, NULL, NULL);
// open keyboard device
NtCreateFile(&hKeyBoard,
SYNCHRONIZE | GENERIC_READ | FILE_READ_ATTRIBUTES,
&ObjectAttributes,
&Iosb,
NULL,
FILE_ATTRIBUTE_NORMAL,
0,
FILE_OPEN,FILE_DIRECTORY_FILE,
NULL, 0);
// create event to wait on
InitializeObjectAttributes(&ObjectAttributes, NULL, 0, NULL, NULL);
NtCreateEvent(&hEvent, EVENT_ALL_ACCESS, &ObjectAttributes, 1, 0);
WriteLn(L"Keyboard readyn");
// create heap in order to allocate memory later
memory = RtlCreateHeap(
HEAP_GROWABLE,
NULL,
1000,
0, NULL, NULL
);
WriteLn(L"Heap readyn");
// allocate buffer of size bufferSize
buffer = RtlAllocateHeap(
memory,
HEAP_ZERO_MEMORY,
bufferSize
);
WriteLn(L"Buffer allocatedn");
// free buffer (actually not needed because we destroy heap in next step)
RtlFreeHeap(memory, 0, buffer);
RtlDestroyHeap(memory);
WriteLn(L"Heap destroyedn");
WriteLn(L"Press ESC to continue...n");
while (TRUE)
{
NtReadFile(hKeyBoard, hEvent, NULL, NULL, &Iosb, &kbData, sizeof(KEYBOARD_INPUT_DATA), &ByteOffset, NULL);
NtWaitForSingleObject(hEvent, TRUE, NULL);
if (kbData.MakeCode == 0x01) // if ESC pressed
{
break;
}
}
NtTerminateProcess(NtCurrentProcess(), 0);
}
PS: Ni povas facile uzi la funkcion DbgBreakPoint() en nia kodo por haltigi ĝin en la erarserĉilo. Vere, vi devos konekti WinDbg al virtuala maŝino por kerna senararigado. Instrukcioj pri kiel fari tion troveblas tie aŭ simple uzi VirtualKD.
Kompilo kaj muntado
La plej facila maniero por konstrui indiĝenan aplikaĵon estas uzi DDK (Driver Development Kit). Ni bezonas la antikvan sepan version, ĉar pli postaj versioj havas iomete malsaman aliron kaj laboras proksime kun Visual Studio. Se ni uzas la DDK, tiam nia projekto bezonas nur Makefile kaj fontojn.
Faru dosieron
!INCLUDE $(NTMAKEENV)makefile.def
fontoj:
TARGETNAME = MyNative
TARGETTYPE = PROGRAM
UMTYPE = nt
BUFFER_OVERFLOW_CHECKS = 0
MINWIN_SDK_LIB_PATH = $(SDK_LIB_PATH)
SOURCES = source.c
INCLUDES = $(DDK_INC_PATH);
C:WinDDK7600.16385.1ndk;
TARGETLIBS = $(DDK_LIB_PATH)ntdll.lib
$(DDK_LIB_PATH)nt.lib
USE_NTDLL = 1
Via Makefile estos ĝuste la sama, sed ni rigardu fontojn iom pli detale. Ĉi tiu dosiero specifas la fontojn de via programo (.c-dosieroj), konstruopciojn kaj aliajn parametrojn.
TARGETNAME - la nomo de la plenumebla dosiero kiu devus esti produktita finfine.
TARGETTYPE - tipo de rulebla dosiero, ĝi povas esti pelilo (.sys), tiam la kampovaloro devus esti DRIVER, se biblioteko (.lib), tiam la valoro estas LIBRARY. En nia kazo, ni bezonas ruleblan dosieron (.exe), do ni fiksas la valoron al PROGRAMO.
UMTYPE - eblaj valoroj por ĉi tiu kampo: konzolo por konzola aplikaĵo, fenestroj por labori en fenestra reĝimo. Sed ni devas specifi nt por ricevi denaskan aplikaĵon.
BUFFER_OVERFLOW_CHECKS - kontrolante la stakon por bufro superfluo, bedaŭrinde ne nia kazo, ni malŝaltas ĝin.
MINWIN_SDK_LIB_PATH - ĉi tiu valoro rilatas al la variablo SDK_LIB_PATH, ne zorgu, ke vi ne havas tian sisteman variablon deklarita, kiam ni rulas kontrolitan konstruon de la DDK, ĉi tiu variablo estos deklarita kaj indikos al la necesaj bibliotekoj.
FONTOJ - listo de fontoj por via programo.
INKLUAS - kapdosierojn kiuj estas bezonataj por kunigo. Ĉi tie ili kutime indikas la vojon al la dosieroj kiuj venas kun la DDK, sed vi povas aldone specifi iujn ajn aliajn.
TARGETLIBS - listo de bibliotekoj kiuj devas esti ligitaj.
USE_NTDLL estas postulata kampo kiu devas esti agordita al 1 pro evidentaj kialoj.
USER_C_FLAGS - ajnaj flagoj, kiujn vi povas uzi en antaŭprocesoraj direktivoj kiam vi preparas aplikan kodon.
Do por konstrui, ni devas ruli x86 (aŭ x64) Checked Build, ŝanĝi la labordosierujon al la projekta dosierujo kaj ruli la Build-komandon. La rezulto en la ekrankopio montras, ke ni havas unu plenumeblan dosieron.
Ĉi tiu dosiero ne povas esti lanĉita tiel facile, la sistemo malbenas kaj sendas nin pensi pri ĝia konduto kun la jena eraro:
Kiel lanĉi denaskan aplikaĵon?
Kiam aŭtochk komenciĝas, la startsekvenco de programoj estas determinita de la valoro de la registra ŝlosilo:
La seanca administranto efektivigas programojn el ĉi tiu listo unu post la alia. La seanca administranto serĉas la ruleblajn dosierojn mem en la dosierujo system32. La registr-ŝlosila valorformato estas jena:
autocheck autochk *MyNative
La valoro devas esti en deksesuma formato, ne la kutima ASCII, do la ŝlosilo montrita supre estos en la formato:
Post instalado kaj rekomenco, eĉ antaŭ ol la ekrano de elekto de uzantoj aperos, ni ricevos la jenan bildon:
La rezulto
Uzante la ekzemplon de tia malgranda aplikaĵo, ni estis konvinkitaj, ke estas tute eble ruli la aplikaĵon je la Windows Native-nivelo. Poste, la infanoj de Innopolis University kaj mi daŭre konstruos servon, kiu komencos la procezon de interago kun la ŝoforo multe pli frue ol en la antaŭa versio de nia projekto. Kaj kun la apero de la win32-ŝelo, estus logike transdoni kontrolon al plenrajta servo, kiu jam estis disvolvita (pli pri ĉi tio tie).
En la sekva artikolo ni tuŝos alian komponanton de la Aktiva Restariga servo, nome la UEFI-ŝoforo. Abonu nian blogon por ke vi ne maltrafu la sekvan afiŝon.