Εφαρμογές Windows Native και υπηρεσία Acronis Active Restore

Σήμερα συνεχίζουμε την ιστορία του πώς εμείς, μαζί με τα παιδιά από το Πανεπιστήμιο Innopolis, αναπτύσσουμε την τεχνολογία Active Restore για να επιτρέψουμε στον χρήστη να αρχίσει να εργάζεται στο μηχάνημά του το συντομότερο δυνατό μετά από μια αποτυχία. Θα μιλήσουμε για εγγενείς εφαρμογές των Windows, συμπεριλαμβανομένων των δυνατοτήτων δημιουργίας και εκκίνησης τους. Κάτω από την περικοπή είναι λίγα για το έργο μας, καθώς και ένας πρακτικός οδηγός για το πώς να γράφετε εγγενείς εφαρμογές.

Εφαρμογές Windows Native και υπηρεσία Acronis Active Restore

Σε προηγούμενες αναρτήσεις έχουμε ήδη μιλήσει για το τι είναι Ενεργή επαναφορά, και πώς αναπτύσσονται οι μαθητές από την Innopolis υπηρεσία. Σήμερα θέλω να εστιάσω στις εγγενείς εφαρμογές, στο επίπεδο των οποίων θέλουμε να «θάψουμε» την ενεργή υπηρεσία ανάκτησης. Εάν όλα πάνε καλά, τότε θα είμαστε σε θέση:

  • Εκκινήστε την ίδια την υπηρεσία πολύ νωρίτερα
  • Επικοινωνήστε με το cloud όπου βρίσκεται το αντίγραφο ασφαλείας πολύ νωρίτερα
  • Πολύ νωρίτερα για να καταλάβετε σε ποια λειτουργία βρίσκεται το σύστημα - κανονική εκκίνηση ή ανάκτηση
  • Πολύ λιγότερα αρχεία για ανάκτηση εκ των προτέρων
  • Επιτρέψτε στον χρήστη να ξεκινήσει ακόμα πιο γρήγορα.

Τι είναι ούτως ή άλλως μια εγγενής εφαρμογή;

Για να απαντήσουμε σε αυτήν την ερώτηση, ας δούμε τη σειρά των κλήσεων που πραγματοποιεί το σύστημα, για παράδειγμα, εάν ένας προγραμματιστής στην εφαρμογή του προσπαθήσει να δημιουργήσει ένα αρχείο.

Εφαρμογές Windows Native και υπηρεσία Acronis Active Restore
Pavel Yosifovich - Windows Kernel Programming (2019)

Ο προγραμματιστής χρησιμοποιεί τη συνάρτηση Δημιουργία αρχείου, το οποίο δηλώνεται στο αρχείο κεφαλίδας fileapi.h και υλοποιείται στο Kernel32.dll. Ωστόσο, αυτή η συνάρτηση από μόνη της δεν δημιουργεί το αρχείο, ελέγχει μόνο τα ορίσματα εισόδου και καλεί τη συνάρτηση NtCreateFile (το πρόθεμα Nt απλώς υποδεικνύει ότι η συνάρτηση είναι εγγενής). Αυτή η συνάρτηση δηλώνεται στο αρχείο κεφαλίδας winternl.h και υλοποιείται στο ntdll.dll. Ετοιμάζεται να πηδήξει στον πυρηνικό χώρο και μετά κάνει μια κλήση συστήματος για να δημιουργήσει ένα αρχείο. Σε αυτήν την περίπτωση, αποδεικνύεται ότι το Kernel32 είναι απλώς ένα περιτύλιγμα για το Ntdll. Ένας από τους λόγους για τους οποίους έγινε αυτό είναι ότι η Microsoft έχει έτσι τη δυνατότητα να αλλάξει τις λειτουργίες του εγγενούς κόσμου, αλλά να μην αγγίζει τις τυπικές διεπαφές. Η Microsoft δεν συνιστά την απευθείας κλήση εγγενών συναρτήσεων και δεν τεκμηριώνει τις περισσότερες από αυτές. Παρεμπιπτόντως, μπορούν να βρεθούν μη τεκμηριωμένες λειτουργίες εδώ.

Το κύριο πλεονέκτημα των εγγενών εφαρμογών είναι ότι το ntdll φορτώνεται στο σύστημα πολύ νωρίτερα από το kernel32. Αυτό είναι λογικό, γιατί ο kernel32 απαιτεί ntdll για να λειτουργήσει. Ως αποτέλεσμα, οι εφαρμογές που χρησιμοποιούν εγγενείς συναρτήσεις μπορούν να αρχίσουν να λειτουργούν πολύ νωρίτερα.

Έτσι, οι εγγενείς εφαρμογές των Windows είναι προγράμματα που μπορούν να ξεκινήσουν νωρίς κατά την εκκίνηση των Windows. Χρησιμοποιούν ΜΟΝΟ συναρτήσεις από το ntdll. Ένα παράδειγμα τέτοιας εφαρμογής: autochk που εκτελεί βοηθητικό πρόγραμμα chkdisk για να ελέγξετε το δίσκο για σφάλματα πριν ξεκινήσετε τις κύριες υπηρεσίες. Αυτό ακριβώς είναι το επίπεδο που θέλουμε να είναι το Active Restore μας.

Τι χρειαζόμαστε;

  • DDK (Driver Development Kit), τώρα γνωστό και ως WDK 7 (Windows Driver Kit).
  • Εικονική μηχανή (για παράδειγμα, Windows 7 x64)
  • Δεν είναι απαραίτητο, αλλά τα αρχεία κεφαλίδας που μπορούν να ληφθούν μπορεί να βοηθήσουν εδώ

Τι υπάρχει στον κωδικό;

Ας εξασκηθούμε λίγο και, για παράδειγμα, γράψτε μια μικρή εφαρμογή που:

  1. Εμφανίζει ένα μήνυμα στην οθόνη
  2. Εκχωρεί λίγη μνήμη
  3. Αναμονή για είσοδο πληκτρολογίου
  4. Απελευθερώνει τη χρησιμοποιημένη μνήμη

Στις εγγενείς εφαρμογές, το σημείο εισόδου δεν είναι το κύριο ή το winmain, αλλά η λειτουργία NtProcessStartup, αφού ουσιαστικά εκκινούμε απευθείας νέες διεργασίες στο σύστημα.

Ας ξεκινήσουμε εμφανίζοντας ένα μήνυμα στην οθόνη. Για αυτό έχουμε μια εγγενή συνάρτηση NtDisplayString, το οποίο παίρνει ως όρισμα έναν δείκτη σε ένα αντικείμενο δομής UNICODE_STRING. Το RtlInitUnicodeString θα μας βοηθήσει να το αρχικοποιήσουμε. Ως αποτέλεσμα, για να εμφανίσουμε κείμενο στην οθόνη μπορούμε να γράψουμε αυτή τη μικρή συνάρτηση:

//usage: WriteLn(L"Here is my textn");
void WriteLn(LPWSTR Message)
{
    UNICODE_STRING string;
    RtlInitUnicodeString(&string, Message);
    NtDisplayString(&string);
}

Δεδομένου ότι μόνο οι λειτουργίες από το ntdll είναι διαθέσιμες σε εμάς και απλώς δεν υπάρχουν ακόμα άλλες βιβλιοθήκες στη μνήμη, σίγουρα θα έχουμε προβλήματα με τον τρόπο εκχώρησης της μνήμης. Ο νέος χειριστής δεν υπάρχει ακόμα (επειδή προέρχεται από τον κόσμο πολύ υψηλού επιπέδου της C++) και δεν υπάρχει λειτουργία malloc (απαιτεί βιβλιοθήκες C χρόνου εκτέλεσης). Φυσικά, μπορείτε να χρησιμοποιήσετε μόνο μια στοίβα. Αλλά αν χρειαστεί να εκχωρήσουμε δυναμικά τη μνήμη, θα πρέπει να το κάνουμε στο σωρό (δηλαδή στο σωρό). Ας δημιουργήσουμε λοιπόν ένα σωρό για τον εαυτό μας και ας πάρουμε ανάμνηση από αυτό όποτε το χρειαστούμε.

Η λειτουργία είναι κατάλληλη για αυτήν την εργασία RtlCreateHeap. Στη συνέχεια, χρησιμοποιώντας τα RtlAllocateHeap και RtlFreeHeap, θα καταλάβουμε και θα ελευθερώσουμε μνήμη όταν τη χρειαστούμε.

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);

Ας προχωρήσουμε στην αναμονή για εισαγωγή από το πληκτρολόγιο.

// 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;

//...

HANDLE hKeyBoard, hEvent;
UNICODE_STRING skull, keyboard;
OBJECT_ATTRIBUTES ObjectAttributes;
IO_STATUS_BLOCK Iosb;
LARGE_INTEGER ByteOffset;
KEYBOARD_INPUT_DATA kbData;

// 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);

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;
	}
}

Το μόνο που χρειαζόμαστε είναι να χρησιμοποιήσουμε NtReadFile σε μια ανοιχτή συσκευή και περιμένετε έως ότου το πληκτρολόγιο επιστρέψει οποιοδήποτε πάτημα σε εμάς. Εάν πατηθεί το πλήκτρο ESC, θα συνεχίσουμε να εργαζόμαστε. Για να ανοίξουμε τη συσκευή, θα χρειαστεί να καλέσουμε τη συνάρτηση NtCreateFile (θα χρειαστεί να ανοίξουμε το DeviceKeyboardClass0). Θα καλέσουμε και εμείς NtCreateEventγια να αρχικοποιήσετε το αντικείμενο αναμονής. Θα δηλώσουμε μόνοι μας τη δομή KEYBOARD_INPUT_DATA, η οποία αντιπροσωπεύει τα δεδομένα του πληκτρολογίου. Αυτό θα διευκολύνει τη δουλειά μας.

Η εγγενής εφαρμογή τελειώνει με μια κλήση συνάρτησης NtTerminateProcessγιατί απλώς σκοτώνουμε τη δική μας διαδικασία.

Όλος ο κώδικας για τη μικρή μας εφαρμογή:

#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: Μπορούμε εύκολα να χρησιμοποιήσουμε τη συνάρτηση DbgBreakPoint() στον κώδικά μας για να τη σταματήσουμε στο πρόγραμμα εντοπισμού σφαλμάτων. Είναι αλήθεια ότι θα χρειαστεί να συνδέσετε το WinDbg σε μια εικονική μηχανή για εντοπισμό σφαλμάτων πυρήνα. Μπορείτε να βρείτε οδηγίες για το πώς να το κάνετε αυτό εδώ ή απλά χρησιμοποιήστε VirtualKD.

Σύνταξη και συναρμολόγηση

Ο ευκολότερος τρόπος για να δημιουργήσετε μια εγγενή εφαρμογή είναι να χρησιμοποιήσετε DDK (Driver Development Kit). Χρειαζόμαστε την αρχαία έβδομη έκδοση, καθώς οι νεότερες εκδόσεις έχουν μια ελαφρώς διαφορετική προσέγγιση και συνεργάζονται στενά με το Visual Studio. Εάν χρησιμοποιούμε το DDK, τότε το έργο μας χρειάζεται μόνο Makefile και πηγές.

Makefile

!INCLUDE $(NTMAKEENV)makefile.def

πηγές:

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

Το Makefile σας θα είναι ακριβώς το ίδιο, αλλά ας δούμε τις πηγές με λίγο περισσότερες λεπτομέρειες. Αυτό το αρχείο καθορίζει τις πηγές του προγράμματός σας (αρχεία .c), τις επιλογές κατασκευής και άλλες παραμέτρους.

  • TARGETNAME – το όνομα του εκτελέσιμου αρχείου που πρέπει να παραχθεί στο τέλος.
  • TARGETTYPE – τύπος εκτελέσιμου αρχείου, μπορεί να είναι πρόγραμμα οδήγησης (.sys), τότε η τιμή του πεδίου πρέπει να είναι DRIVER, εάν βιβλιοθήκη (.lib), τότε η τιμή είναι ΒΙΒΛΙΟΘΗΚΗ. Στην περίπτωσή μας, χρειαζόμαστε ένα εκτελέσιμο αρχείο (.exe), οπότε ορίζουμε την τιμή σε PROGRAM.
  • UMTYPE – πιθανές τιμές για αυτό το πεδίο: κονσόλα για μια εφαρμογή κονσόλας, παράθυρα για εργασία σε λειτουργία παραθύρου. Αλλά πρέπει να καθορίσουμε nt για να λάβουμε μια εγγενή εφαρμογή.
  • BUFFER_OVERFLOW_CHECKS – ελέγχοντας τη στοίβα για υπερχείλιση buffer, δυστυχώς όχι στην περίπτωσή μας, την απενεργοποιούμε.
  • MINWIN_SDK_LIB_PATH – αυτή η τιμή αναφέρεται στη μεταβλητή SDK_LIB_PATH, μην ανησυχείτε ότι δεν έχετε δηλωθεί μια τέτοια μεταβλητή συστήματος.
  • ΠΗΓΕΣ – μια λίστα πηγών για το πρόγραμμά σας.
  • ΠΕΡΙΛΑΜΒΑΝΕΙ – αρχεία κεφαλίδας που απαιτούνται για τη συναρμολόγηση. Εδώ συνήθως υποδεικνύουν τη διαδρομή προς τα αρχεία που συνοδεύουν το DDK, αλλά μπορείτε επιπλέον να καθορίσετε οποιαδήποτε άλλα.
  • TARGETLIBS – λίστα βιβλιοθηκών που πρέπει να συνδεθούν.
  • Το USE_NTDLL είναι ένα υποχρεωτικό πεδίο που πρέπει να οριστεί σε 1 για προφανείς λόγους.
  • USER_C_FLAGS – τυχόν σημαίες που μπορείτε να χρησιμοποιήσετε σε οδηγίες προεπεξεργαστή κατά την προετοιμασία του κώδικα εφαρμογής.

Επομένως, για να δημιουργήσουμε, πρέπει να εκτελέσουμε το x86 (ή x64) Checked Build, να αλλάξουμε τον κατάλογο εργασίας στον φάκελο του έργου και να εκτελέσουμε την εντολή Build. Το αποτέλεσμα στο στιγμιότυπο οθόνης δείχνει ότι έχουμε ένα εκτελέσιμο αρχείο.

Εφαρμογές Windows Native και υπηρεσία Acronis Active Restore

Αυτό το αρχείο δεν μπορεί να ξεκινήσει τόσο εύκολα, το σύστημα βρίζει και μας στέλνει να σκεφτούμε τη συμπεριφορά του με το ακόλουθο σφάλμα:

Εφαρμογές Windows Native και υπηρεσία Acronis Active Restore

Πώς να ξεκινήσετε μια εγγενή εφαρμογή;

Όταν ξεκινά το autochk, η ακολουθία εκκίνησης των προγραμμάτων καθορίζεται από την τιμή του κλειδιού μητρώου:

HKLMSystemCurrentControlSetControlSession ManagerBootExecute

Ο διαχειριστής συνεδρίας εκτελεί προγράμματα από αυτήν τη λίστα ένα προς ένα. Ο διαχειριστής συνεδρίας αναζητά τα ίδια τα εκτελέσιμα αρχεία στον κατάλογο system32. Η μορφή τιμής του κλειδιού μητρώου είναι η εξής:

autocheck autochk *MyNative

Η τιμή πρέπει να είναι σε δεκαεξαδική μορφή, όχι στο συνηθισμένο ASCII, επομένως το κλειδί που εμφανίζεται παραπάνω θα έχει τη μορφή:

61,75,74,6f,63,68,65,63,6b,20,61,75,74,6f,63,68,6b,20,2a,00,4d,79,4e,61,74,69,76,65,00,00

Για να μετατρέψετε τον τίτλο, μπορείτε να χρησιμοποιήσετε μια ηλεκτρονική υπηρεσία, για παράδειγμα, αυτό.

Εφαρμογές Windows Native και υπηρεσία Acronis Active Restore
Αποδεικνύεται ότι για να ξεκινήσουμε μια εγγενή εφαρμογή, χρειαζόμαστε:

  1. Αντιγράψτε το εκτελέσιμο αρχείο στο φάκελο system32
  2. Προσθέστε ένα κλειδί στο μητρώο
  3. Επανεκκινήστε το μηχάνημα

Για ευκολία, εδώ είναι ένα έτοιμο σενάριο για την εγκατάσταση μιας εγγενούς εφαρμογής:

install.bat

@echo off
copy MyNative.exe %systemroot%system32.
regedit /s add.reg
echo Native Example Installed
pause

add.reg

REGEDIT4

[HKEY_LOCAL_MACHINESYSTEMCurrentControlSetControlSession Manager]
"BootExecute"=hex(7):61,75,74,6f,63,68,65,63,6b,20,61,75,74,6f,63,68,6b,20,2a,00,4d,79,4e,61,74,69,76,65,00,00

Μετά την εγκατάσταση και την επανεκκίνηση, ακόμη και πριν εμφανιστεί η οθόνη επιλογής χρήστη, θα λάβουμε την ακόλουθη εικόνα:

Εφαρμογές Windows Native και υπηρεσία Acronis Active Restore

Σύνολο

Χρησιμοποιώντας το παράδειγμα μιας τόσο μικρής εφαρμογής, πειστήκαμε ότι είναι πολύ πιθανό να εκτελεστεί η εφαρμογή σε επίπεδο Windows Native. Στη συνέχεια, τα παιδιά από το Πανεπιστήμιο Innopolis και εγώ θα συνεχίσουμε να χτίζουμε μια υπηρεσία που θα ξεκινήσει τη διαδικασία αλληλεπίδρασης με τον οδηγό πολύ νωρίτερα από την προηγούμενη έκδοση του έργου μας. Και με την έλευση του κελύφους win32, θα ήταν λογικό να μεταφερθεί ο έλεγχος σε μια ολοκληρωμένη υπηρεσία που έχει ήδη αναπτυχθεί (περισσότερα για αυτό εδώ).

Στο επόμενο άρθρο θα αγγίξουμε ένα άλλο στοιχείο της υπηρεσίας Active Restore, δηλαδή το πρόγραμμα οδήγησης UEFI. Εγγραφείτε στο blog μας για να μην χάσετε την επόμενη ανάρτηση.

Πηγή: www.habr.com

Προσθέστε ένα σχόλιο