แอปพลิเคชัน Windows Native และบริการ Acronis Active Restore

วันนี้เราจะมาเล่าต่อว่าเราร่วมมือกับทีมงานจากมหาวิทยาลัย Innopolis ในการพัฒนาเทคโนโลยี Active Restore เพื่อให้ผู้ใช้สามารถเริ่มทำงานกับเครื่องของตนโดยเร็วที่สุดหลังจากเกิดข้อผิดพลาด เราจะพูดถึงแอปพลิเคชัน Windows ดั้งเดิมรวมถึงคุณสมบัติของการสร้างและการเปิดตัว ด้านล่างนี้เป็นข้อมูลเล็กน้อยเกี่ยวกับโปรเจ็กต์ของเรา รวมถึงคำแนะนำเชิงปฏิบัติเกี่ยวกับวิธีเขียนแอปพลิเคชันแบบเนทีฟ

แอปพลิเคชัน Windows Native และบริการ Acronis Active Restore

ในกระทู้ที่แล้วเราได้คุยกันไปแล้วว่ามันคืออะไร การคืนค่าที่ใช้งานอยู่และพัฒนาการของนักเรียนจาก Innopolis บริการ. วันนี้ฉันต้องการมุ่งเน้นไปที่แอปพลิเคชันแบบเนทีฟ จนถึงระดับที่เราต้องการ "ฝัง" บริการกู้คืนที่ใช้งานอยู่ของเรา หากทุกอย่างเป็นไปด้วยดี เราจะสามารถ:

  • เปิดตัวบริการเร็วกว่ามาก
  • ติดต่อคลาวด์ที่ข้อมูลสำรองตั้งอยู่เร็วกว่ามาก
  • เร็วกว่ามากในการทำความเข้าใจว่าระบบอยู่ในโหมดใด - การบูตหรือการกู้คืนตามปกติ
  • ไฟล์ที่ต้องกู้คืนล่วงหน้าน้อยกว่ามาก
  • อนุญาตให้ผู้ใช้เริ่มต้นได้เร็วยิ่งขึ้น

Native App คืออะไร?

เพื่อตอบคำถามนี้ ลองดูลำดับการโทรที่ระบบทำ เช่น หากโปรแกรมเมอร์ในแอปพลิเคชันของเขาพยายามสร้างไฟล์

แอปพลิเคชัน Windows Native และบริการ Acronis Active Restore
Pavel Yosifovich - การเขียนโปรแกรมเคอร์เนล Windows (2019)

โปรแกรมเมอร์ใช้ฟังก์ชันนี้ สร้างไฟล์ซึ่งประกาศไว้ในไฟล์ส่วนหัว fileapi.h และนำไปใช้ใน Kernel32.dll อย่างไรก็ตาม ฟังก์ชันนี้ไม่ได้สร้างไฟล์ แต่จะตรวจสอบอาร์กิวเมนต์อินพุตและเรียกใช้ฟังก์ชันเท่านั้น NtCreateFile (คำนำหน้า Nt เพียงบ่งชี้ว่าฟังก์ชันเป็นแบบเนทีฟ) ฟังก์ชันนี้ได้รับการประกาศในไฟล์ส่วนหัวของ winternl.h และนำไปใช้ใน ntdll.dll มันเตรียมกระโดดเข้าสู่อวกาศนิวเคลียร์ หลังจากนั้นระบบจะเรียกสร้างไฟล์ ในกรณีนี้ปรากฎว่า Kernel32 เป็นเพียง wrapper สำหรับ Ntdll เหตุผลหนึ่งที่ทำเช่นนี้ก็คือ Microsoft จึงมีความสามารถในการเปลี่ยนฟังก์ชันของโลกพื้นเมือง แต่ไม่ได้สัมผัสอินเทอร์เฟซมาตรฐาน Microsoft ไม่แนะนำให้เรียกใช้ฟังก์ชันดั้งเดิมโดยตรง และไม่ได้จัดทำเอกสารส่วนใหญ่ไว้ โดยวิธีการนี้สามารถพบฟังก์ชันที่ไม่มีเอกสารได้ ที่นี่.

ข้อได้เปรียบหลักของแอปพลิเคชันเนทิฟคือ ntdll ถูกโหลดเข้าสู่ระบบเร็วกว่าเคอร์เนล 32 มาก นี่เป็นตรรกะ เนื่องจาก kernel32 ต้องใช้ ntdll ในการทำงาน เป็นผลให้แอปพลิเคชันที่ใช้ฟังก์ชันดั้งเดิมสามารถเริ่มทำงานได้เร็วกว่ามาก

ดังนั้น Windows Native Applications จึงเป็นโปรแกรมที่สามารถเริ่มทำงานได้ตั้งแต่เนิ่นๆ ในการบูต Windows พวกเขาใช้ฟังก์ชันจาก ntdll เท่านั้น ตัวอย่างของแอปพลิเคชันดังกล่าว: อัตโนมัติ ใครเป็นผู้ดำเนินการ ยูทิลิตี้ chkdisk เพื่อตรวจสอบข้อผิดพลาดของดิสก์ก่อนเริ่มบริการหลัก นี่คือระดับที่เราต้องการให้ Active Restore เป็น

สิ่งที่เราต้องการ?

  • ดีดีเค (ชุดพัฒนาไดรเวอร์) ปัจจุบันรู้จักกันในชื่อ WDK 7 (ชุดไดรเวอร์ Windows)
  • เครื่องเสมือน (เช่น 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 กับเครื่องเสมือนเพื่อการดีบักเคอร์เนล คำแนะนำเกี่ยวกับวิธีการทำเช่นนี้สามารถพบได้ ที่นี่ หรือเพียงแค่ใช้ เสมือนKD.

การรวบรวมและการประกอบ

วิธีที่ง่ายที่สุดในการสร้างแอปพลิเคชันแบบเนทีฟคือการใช้งาน ดีดีเค (ชุดพัฒนาไดร์เวอร์) เราต้องการเวอร์ชันเก่าที่ XNUMX เนื่องจากเวอร์ชันหลังๆ มีแนวทางที่แตกต่างออกไปเล็กน้อยและทำงานอย่างใกล้ชิดกับ 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) ค่าจะเป็น LIBRARY ในกรณีของเรา เราต้องการไฟล์ปฏิบัติการ (.exe) ดังนั้นเราจึงตั้งค่าเป็น PROGRAM
  • UMTYPE – ค่าที่เป็นไปได้สำหรับฟิลด์นี้: คอนโซลสำหรับแอปพลิเคชันคอนโซล, windows สำหรับการทำงานในโหมดหน้าต่าง แต่เราต้องระบุ nt เพื่อรับแอปพลิเคชันเนทิฟ
  • BUFFER_OVERFLOW_CHECKS – ตรวจสอบสแต็กเพื่อหาบัฟเฟอร์ล้น น่าเสียดายไม่ใช่กรณีของเรา เราปิดมันแล้ว
  • MINWIN_SDK_LIB_PATH – ค่านี้อ้างอิงถึงตัวแปร SDK_LIB_PATH ไม่ต้องกังวลว่าคุณจะไม่มีการประกาศตัวแปรระบบดังกล่าว เมื่อเรารันบิลด์ที่ตรวจสอบแล้วจาก DDK ตัวแปรนี้จะถูกประกาศและจะชี้ไปยังไลบรารีที่จำเป็น
  • แหล่งที่มา – รายการแหล่งที่มาสำหรับโปรแกรมของคุณ
  • รวม – ไฟล์ส่วนหัวที่จำเป็นสำหรับการประกอบ โดยปกติจะระบุเส้นทางไปยังไฟล์ที่มาพร้อมกับ 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

เพิ่ม.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 สมัครสมาชิกบล็อกของเราเพื่อให้คุณไม่พลาดโพสต์ถัดไป

ที่มา: will.com

เพิ่มความคิดเห็น