Step to UEFI (295)手工给 EFI 文件插入代码的试验 (下)

我们继续之前的话题:在一个已经编译成功的 SimpleTest.EFI 中,加入另外一个 Hello.EFI 程序,最终实现Shell 下输入 SimpleTest.EFI, 实际上运行了SimpleTest.EFI 和 Hello.EFI。

前面的代码我们已经实现了大部分,还有一些细节需要处理。

第一个需要注意的是,我们Hello.EFI 是 NASM 生成的,其中有很多对于寄存器和堆栈的操作,这个操作会破坏SimpleTest.efi 需要的运行环境,导致无法跳入后执行出错。

仔细观察 SimpleTest.EFI 反编译结果,在开始处从 Shell 下收到的参数需是放在 rbx 和 rsi 寄存器中的,我们必须妥善保存这两个寄存器才能保证后续的正确运行。

代码头部修改如下:

_start:
	push rdx
	push rcx
	push rdi
	
    push rbx
	push rsi
	
    push rax    ;ConOut requires a push here. I don't know why
 
    ; reserve space for 4 arguments
    sub rsp, 4 * 8

代码尾部修改如下:
    add rsp, 4 * 8
    pop rax     

	pop rsi
	pop rbx
	
	pop rdi
	pop rcx
	pop rdx

	times 20 nop

再次生成一个新的 hello.efi(注意,这样修改之后的代码无法像之前的 EFI Application一样运行了), 用HXD 打开后拷贝代码区放置到 SimpleText.EFI 中。

此外,还有两个位置需要修改:

1.在拷贝到到 SimpleTest.EFI 的带末尾放上 mov  [rsp+0x08],rbx/mov [rsp+x010],rsi 两个操作;

2.跳转回文件头部的指令:

经过这样的改造,在模拟器中测试可以看到:

执行 SimpleTest.EFI 得到了2个输出,这个说明确实运行了2个EFI 。

修改后的 Hello.ASM 相关程序:

修改后的 SimpleTestM.EFI 文件

本文特别感谢Windows 专家天杀提供帮助。他对于 WinPE 结构的非常了解,帮助解决了修改EFI后, 使用模拟器测试崩溃的问题(Section Header 中 .TEXT 的大小需要更新)。

枚举系统中全部 PhysicalDrive

#include <windows.h>
#include <iostream>

void EnumeratePhysicalDrives() {
    HANDLE hDrive;
    DWORD bytesReturned;
    char driveName[24];
    STORAGE_DEVICE_NUMBER deviceNumber;

    // 尝试打开每个可能的物理驱动器
    for (int i = 0; i < 16; i++) {
        // 构造物理驱动器的名称
        sprintf_s(driveName, "\\\\.\\PhysicalDrive%d", i);

        // 尝试打开物理驱动器
        hDrive = CreateFileA(driveName, 0, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);
        if (hDrive == INVALID_HANDLE_VALUE) {
            // 如果无法打开驱动器,可能是因为驱动器不存在
            continue;
        }

        // 尝试获取设备编号
        if (!DeviceIoControl(hDrive, IOCTL_STORAGE_GET_DEVICE_NUMBER, NULL, 0, &deviceNumber, sizeof(deviceNumber), &bytesReturned, NULL)) {
            std::cout << "Failed to get device number for " << driveName << std::endl;
        }
        else {
            std::cout << "Found Physical Drive: " << driveName << ", Device Type: " << deviceNumber.DeviceType << ", Device Number: " << deviceNumber.DeviceNumber << std::endl;
        }

        CloseHandle(hDrive);
    }
}

int main() {
    EnumeratePhysicalDrives();
    return 0;
}

Step to UEFI (294)手工给 EFI 文件插入代码的试验(上)

编译好的 EFI 文件本质上是一个 WinPE 文件,因此我们有机会在文件开始处加入一些我们需要的代码。这次介绍的就是一个手工在 EFI 入口处插入另外一个EFI 代码的试验。

这次进行一个特别的实验。基本的原理是:

  1. 编写一个在屏幕上输出字符串简单的程序,这样我们能得到一段EFI Shell下对屏幕输出字符串的机器码;
  2. 编写一个宿主程序,这个程序编译后的 EFI 文件使用 4K 对齐,这样话,存放代码的.text段会有足够的空间能够存放下步骤1生成的机器码;
  3. 修改生成的EFI文件的 Section Headers 中给出的.text 大小,保证足够放下我们增加的机器码;
  4. 将步骤1生成的代码,插入在步骤3生成的EFI中。

最终,我们得到一个新的 EFI 程序,运行之后它会先执行步骤1 的代码,然后再执行步骤2的代码。

步骤1:这里使用 NASM 汇编语言来完成。

根据之前的文章【参考1】,编写一个程序实现在屏幕上输出字符串的代码。代码有部分修改,主要是将输出的字符串和代码放在了一起:

bits 64
 
; contains the code that will run
section .text
 
; allows the linker to see this symbol
global _start
 
; see http://www.uefi.org/sites/default/files/resources/UEFI Spec 2_7_A Sept 6.pdf#G8.1001729
struc EFI_TABLE_HEADER
    .Signature    RESQ 1
    .Revision     RESD 1
    .HeaderSize   RESD 1
    .CRC32        RESD 1
    .Reserved     RESD 1
endstruc
 
; see http://www.uefi.org/sites/default/files/resources/UEFI Spec 2_7_A Sept 6.pdf#G8.1001773
struc EFI_SYSTEM_TABLE
    .Hdr                  RESB EFI_TABLE_HEADER_size
    .FirmwareVendor       RESQ 1
    .FirmwareRevision     RESD 1
    .ConsoleInHandle      RESQ 1
    .ConIn                RESQ 1
    .ConsoleOutHandle     RESQ 1
    .ConOut               RESQ 1
    .StandardErrorHandle  RESQ 1
    .StdErr               RESQ 1
    .RuntimeServices      RESQ 1
    .BootServices         RESQ 1
    .NumberOfTableEntries RESQ 1
    .ConfigurationTable   RESQ 1
endstruc
 
; see http://www.uefi.org/sites/default/files/resources/UEFI Spec 2_7_A Sept 6.pdf#G16.1016807
struc EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL
    .Reset             RESQ 1
    .OutputString      RESQ 1
    .TestString        RESQ 1
    .QueryMode         RESQ 1
    .SetMode           RESQ 1
    .SetAttribute      RESQ 1
    .ClearScreen       RESQ 1
    .SetCursorPosition RESQ 1
    .EnableCursor      RESQ 1
    .Mode              RESQ 1
endstruc
 
_start:
 
    push rax    ;ConOut requires a push here. I don't know why
 
    ; reserve space for 4 arguments
    sub rsp, 4 * 8
 
    ; rdx points to the EFI_SYSTEM_TABLE structure
    ; which is the 2nd argument passed to us by the UEFI firmware
    ; adding 64 causes rcx to point to EFI_SYSTEM_TABLE.ConOut
    mov rcx, [rdx + 64]
 
    ; load the address of our string into rdx
    lea rdx, [rel strHello]
 
    ; EFI_SYSTEM_TABLE.ConOut points to EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL
    ; call OutputString on the value in rdx
    call [rcx + EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL.OutputString]
     
    add rsp, 4 * 8
    pop rax     
    ret
	
strHello db __utf16__ `Hello World from LAB-Z.COM!\n\r\0`

codesize equ $ - $$
 
; contains nothing - but it is required by UEFI
section .reloc

编译命令如下:

c:\nasm\nasm -f win64 hello.asm -l hello.lst
link /NODEFAULTLIB /IGNORE:4001 /OPT:REF /OPT:ICF=10 /MAP /ALIGN:32 /SECTION:.xdata,D /SECTION:.pdata,D /Machine:X64  /DLL /ENTRY:_start  /SUBSYSTEM:EFI_APPLICATION /SAFESEH:NO /DRIVER Hello.obj

除了正常生成的 EFI 文件之外,还生成了 lst 文件,在其中能够看到代码生成的机器码用于对照参考:

在模拟器中测试可以正常执行。

根据【参考2】,具体对应如下:

.text:(代码段),可读、可执行
.data:(数据段),存放全局变量、全局常量等
.idata:(数据段),导入函数的代码段,存放外部函数地址。(当然还有 edata ,导出函数代码段,但不常用)
.rdata:(数据段),资源数据段,程序用到什么资源数据都在这里(包括自己打包的,还有开发工具打包的)

使用 CFF Explorer查看,我们需要的机器码就在.text段中:

步骤2:我们根据【参考3】,编写一个简单的代码,功能上只是向屏幕输出字符串,然后使用 EDK2 进行开发。因为默认情况下,.text 空余空调很小,所以在编译完成后我们再打开 下面这个 makefile文件

edk2\Build\AppPkg\DEBUG_VS2019\X64\AppPkg\Applications\SimpleTest\SimpleTest\Makefile

将下面的一行中修改为 /ALIGN:0x1000

DLINK_FLAGS = /NOLOGO /NODEFAULTLIB /IGNORE:4001 /IGNORE:4281 /OPT:REF /OPT:ICF=10 /MAP /ALIGN:32 /SECTION:.xdata,D /SECTION:.pdata,D /Machine:X64 /LTCG /DLL /ENTRY:$(IMAGE_ENTRY_POINT) /SUBSYSTEM:EFI_BOOT_SERVICE_DRIVER /SAFESEH:NO /BASE:0 /DRIVER /DEBUG

然后进入对应目录,输入 nmake 重新编译,这样就得到一个段以4K 对齐的 EFI 文件。

查看 .text段,这里有足够的空间插入我们的代码:

步骤3,修改Section Headers 中的.text Size,这里我们修改为 1600.

步骤4,我们手工插入。

1.SimpleTestM.EFI的入口地址在 0x400处:

开始处对应的是 _ModuleEntryPoint() 函数,我们程序代码是从ShellAppMain() 函数开始的。

打开hello.efi ,拷贝这一段:

插入(Paste Write)到 SimpleTest.efi 中:

然后手工修改SimpleTest.efi 如下:

入口处修改为跳转指令, 这里使用的是一个相对跳转指令【参考4】

运行之后结果如下:

就是说我们通过 SimpleTestM 运行了hello.efi 中的内容。

当然这里和我们的预期还有差别

当然,这样的代码并不是我们期待的最终结果,我们期望两个程序能够同时运行。

这次只做到了在一个A.EFI中插入另外一个B.EFI 文件,然后运行A.EFI 实际执行B.EFI的代码。

参考:

  1. https://www.lab-z.com/stu207/
  2. https://blog.csdn.net/Simon798/article/details/96876910
  3. https://www.lab-z.com/stu260/
  4. https://www.felixcloutier.com/x86/jmp

给 EXE 加入 Resource

最近忽然想起来一个问题:如何给一个做好的 EXE 加入其他的内容?比如,我编写一个 EXE 需要更改内容而又不想重新 Build 代码。

经过研究,可以通过给EXE 添加 Resource 的方法来实现这一目标。在 https://github.com/tc-hib/go-winres 这里有一个从命令行给 EXE 添加Resource 的项目。配合这个项目可以实现前述目标。

首先,编写一个测试代码,使用 VC 编写在 VS2019 下编译通过:

#include <windows.h>
#include <iostream>

// 回调函数用于枚举资源
BOOL CALLBACK EnumResNameProc(HMODULE hModule, LPCWSTR lpszType, LPWSTR lpszName, LONG_PTR lParam) {
    // 每找到一个资源,就增加计数
    (*(int*)lParam)++;
    return TRUE; // 继续枚举
}

int main()
{
    HMODULE hModule = GetModuleHandle(NULL); // 获取当前模块句柄
    int resourceCount = 0; // 用于计数的变量

    // 枚举所有RT_RCDATA类型的资源
    EnumResourceNames(hModule, RT_RCDATA, EnumResNameProc, (LONG_PTR)&resourceCount);

    std::cout << "Number of RT_RCDATA resources: " << resourceCount << std::endl;

    return 0;
}

代码非常简单,单纯的输出当前EXE RT_RCDATA类型的 Resource 数量。

接下来将go-winres.exe放在同一个目录下,然后运行下面的命令

go-winres.exe init

对应的会生成 winres 目录,其中有下面三个文件

前面2个是可以作为EXE 的图标的,winres.json是配置文件。例如,我们对这个目录放置一个 png 文件,然后修改如下,增加 RT_RCDATA的部分:

  "RT_GROUP_ICON": {
    "APP": {
      "0000": [
        "icon.png",
        "icon16.png"
      ]
    }
  },
  "RT_RCDATA": {
    "OTHER": {
      "0000": "2.png"
    }
  },  
  "RT_MANIFEST": {
    "#1": {
      "0409": {
        "identity": {
          "name": "",
          "version": ""

运行如下命令:

go-winres.exe patch ResourceTest.exe

工具会自动给 ResourceTest.exe 添加内容,之后再次运行:

如果使用 CFF 工具还可以看到多了一个 Resource。这样,你可以在代码中先判断Resource数量,然后再进行动作。

官方提供的版本(和 Github上的相同,0.3.3版本)

VC 编写的WAVE测试文件生成器

最近为了调试,写了一个 WAVE 的生成器,能够生成指定采样率,指定格式的 WAVE文件,这样可以在输出端直接查看输出是否是指定的数据。特别注意的是,WAVE 中,使用 int16 (有符号十六进制)来表示当前的信号,更具体来说,负数使用补码来表示。代码如下:

// WaveGenerator.cpp : This file contains the 'main' function. Program execution begins and ends there.
//

#include <Windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <math.h>
#pragma pack(push, 1)
struct Chunk1 {          // 'R', 'I', 'F', 'F'
	char chunk_id[4];
	uint32_t chunk_size;
	char format[4];      // 'W', 'A', 'V', 'E'
};

struct Chunk2 {
	char chunk_id[4];
	uint32_t chunk_size;
	int16_t wFormatTag;
	int16_t nChannels;
	int32_t nSamplesPerSec;
	int32_t nAvgBytesPerSec;
	int16_t nBlockAlign;  // => num_channels * bits_per_sample / 8
	int16_t wBitsPerSample;
};

struct Chunk3 {
	char chunk_id[4];
	uint32_t chunk_size;
};
#pragma pack(pop) // 恢复到之前的对齐设置

#define AUDIODURATION  5  // 音频时长,以秒为单位
#define SAMPLEBITS  16  // 采样位
#define SAMPLERATE  96000  // 采样率
#define CHANNELNUM  2  // Channel 数量
int main()
{
	Chunk1 chunk1;
	chunk1.chunk_id[0] = 'R'; chunk1.chunk_id[1] = 'I'; chunk1.chunk_id[2] = 'F'; chunk1.chunk_id[3] = 'F';
	// 数据长度= Chunk123 总长 + RAW 数据长度(双声道,SAMPLEBITS Bits)
	chunk1.chunk_size = sizeof(Chunk1)-8 + sizeof(Chunk2) + sizeof(Chunk3)+ AUDIODURATION* CHANNELNUM *(SAMPLEBITS/8)* SAMPLERATE;
	chunk1.format[0] = 'W'; chunk1.format[1] = 'A'; chunk1.format[2] = 'V'; chunk1.format[3] = 'E';

	Chunk2 chunk2;

	chunk2.chunk_id[0] = 'f'; chunk2.chunk_id[1] = 'm'; chunk2.chunk_id[2] = 't'; chunk2.chunk_id[3] = ' ';
	chunk2.chunk_size = sizeof(Chunk2)-8;
	chunk2.wFormatTag = 0x0001; //PCM 格式
	chunk2.nChannels = CHANNELNUM;
	chunk2.nSamplesPerSec = SAMPLERATE;
	chunk2.nAvgBytesPerSec = CHANNELNUM * SAMPLERATE * (SAMPLEBITS / 8);
	chunk2.nBlockAlign = CHANNELNUM * (SAMPLEBITS / 8);
	chunk2.wBitsPerSample = SAMPLEBITS;

	Chunk3 chunk3;
	chunk3.chunk_id[0] = 'd'; chunk3.chunk_id[1] = 'a'; chunk3.chunk_id[2] = 't'; chunk3.chunk_id[3] = 'a';
	chunk3.chunk_size = AUDIODURATION * CHANNELNUM * SAMPLERATE * (SAMPLEBITS / 8);

	// 打开文件用于写入,以二进制模式
	FILE* file = fopen("C:\\WaveGenerator\\Debug\\example.wav", "wb");
	fwrite(&chunk1, sizeof(chunk1), 1, file);
	fwrite(&chunk2, sizeof(chunk2), 1, file);
	fwrite(&chunk3, sizeof(chunk3), 1, file);
	short int s=0,e=0;
	for (int i = 0; i < AUDIODURATION* SAMPLERATE; i++) {
		//s = sin(i * 2 * 3.1415 / (SAMPLERATE/1000)) * (65535 / 2);
		// 左声道
		s++;
		fwrite(&s, sizeof(s), 1, file);
		// 右声道
		e = 0; 
		fwrite(&e, sizeof(e), 1, file);
	}

	fclose(file);
	//getchar();
}

小米2代体脂秤维修

最近给一个朋友维修了一下小米2代体脂秤。他遇到的问题是:插入电池后电池发热。拿到手后,看到了现象:装入电池后,电池组和线都会发烫。 

使用万用表进行测量,直接测量电池盒两端电阻为 170欧姆,装入电池后测量发现输出电流达到3A左右,这就是发热原因。 

根据部分资料【参考1】,上图中红色框芯片为CST34M96,用于蓝牙通讯;绿色框中芯片为CS1256,这是用于获得体脂数据并且处理的单片机;紫色方框中是稳压(降压)芯片,型号未知丝印D3E1,照片是拆除这个芯片后拍摄。 

不上电测量正常,上电后有问题猜测是电源芯片出了问题。于是拆除了稳压芯片。这部分电路如下: 

VDD 是电池盒输入;经过芯片降压输出为 AVCC,最终提供给后端使用。万用表测试表明AGND 和 GND 是连通的。 

拆除芯片后,从 AVCC 送入 3.3V,数码管会有动作,证明后面的芯片都是能够正常工作的,所以选择一个稳压芯片替换。 

最终,替换稳压/降压芯片后,外加更换C4电容(测量发现C4短路了,可能被击穿了?)后,可以正常工作。

参考: 

1. https://www.mydigit.cn/forum.php?mod=viewthread&tid=286533&page=1 

SlimBootLoader 在 LattePanda Mu 上启动 Ubuntu

这次带来是全网唯一的开源 X86 BIOS 方案,通过它能够在 Latte Panda Mu上引导启动 Ubuntu。

为了在Latte Panda Mu使用SlimBootLoader,还需要额外准备如下硬件:

  1. 烧写IFWI的硬件工具,比如:SF100烧录器;
  2. 查看串口Log 的工具,比如:CH343 串口转 USB工具,用于配置安装Ubuntu;SBL 的Shell 没有提供字符或者图形界面,所有的数据都是通过串口进行交互的,所以需要准备串口工具;
  3. 准备2个U盘,一个用于制作 Ubuntu安装盘,我这边使用64GB 的金邦U盘,使用DiskGuinus格式化为 FAT32(注意不要使用 exFat)。将 Ubuntu 解压后全部放入 U盘根目录即可;另外U盘一个用于制作引导脚本。

第一步,生成能够启动的 Latte Panda Mu 的 IFWI。根据【参考1】,配置编译环境,首先确保能够编译通过 QEMU。

a.编译 BIOS 代码:

Python BuildLoader.py build adln

b.生成IFWI,这里没有使用 mFIT 而是直接使用代码替换原代码中的 BIOS Region。

python Platform/AlderlakeBoardPkg/Script/StitchLoader.py -i LattePanda.bin -s Outputs/adln/SlimBootloader.bin -o LattePanda_ifwi.bin -p 0xAA000007

上述的命令中,LattePanda.bin 是DFRobot 提供的原始的IFWI,其中的BIOS是AMI CodeBase;SlimBootloader.bin 是前面从 SBL Build出来的BIOS;最后的LattePanda_ifwi.bin就是生成的新的 IFWI。

d.关机状态下,将USB 转串口工具连接到LattePanda Mu 的PCH UART0 上

c.使用 SF100或者其他烧录器将LattePanda_ifwi.bin烧录到 LattePanda Mu上

d.启动LattePanda Mu后,会很快完成启动显示SBL Logo。 这时候 SBL 并不会从屏幕和键盘进行交互,而是通过串口。在另外一台电脑上打开串口工具即可看到一个文本的Shell界面,输入 boot 命令,然后调整从USB 设备启动例如:

至此,Firmware 方面的准备已经完成。

第二步,安装 Ubuntu 以及设定【参考2】。

  1. 其中需要一些时间,请耐心等待,之后进入下面这个界面。左边是直接在U盘提供的Live Linux 中工作,右边是进行安装,这里需要选择左侧按钮“Try Ubuntu”

从左上角的  Activities 打开一个 Terminal

e.使用 lsblk列出当前的设备,LattePanda Mu 内置了 eMMC,这里是mmcblk0设备

c.使用 sudo disk /dev/mmcblk0擦除硬盘,命令如下:

sudo gdisk /dev/mmcblk0

x

z

y

y

d. 创建GPT分区

sudo gdisk /dev/mmcblk0

o

y

w

y

e.接下来即可进行Ubuntu安装了

f.分区需要特别处理,划分出一个500MB 的FAT32分区用于引导

g.剩余容量减去4GB 左右,设定为  Ext4 分区

h.最后创建一个 4GB 的分区作为  SWAP 分区

i.接下来都选择 Continue

j.安装结束后,选择 Continue Testing

k.接下来使用 lsblk

l.输入如下命令:

Mkdir root
Sudo mount /dev/mmcblk0  root/

m.插入另外的U盘其中放入三个脚本以及内容如下的 cmdline.txt

echo "root=/dev/ mmcblk0  ro quiet splash" > cmdline.txt

n.运行如下批处理(其中的PEM文件来自编译SBL的代码中 )。

python3 GenContainer.py create -cl CMDL:cmdline.txt KRNL:./root/boot/vmlinuz-<kernel-version> INRD:./root/boot/initrd.img-<kernel-version> -k ./SblKeys/OS1_TestKey_Priv_RSA3072.pem -a RSA3072_PKCS1_SHA2_384 -t CLASSIC -o sbl_os
sudo cp sbl_os ./root/boot
sudo umount ./root

这里提供一个下载方便直接测试:

o.最终,拔掉U盘,即可启动

上述操作的完整视频:

这里只是简单的第一步,自行编译BIOS并且启动了X86的 Ubuntu,很多功能还没有调试,可能在使用中遇到问题(比如, X4 PCIE 支持, Audio 的支持)。如果你对性能和体积有一些要求,或者是对于BIOS定制有一些特别要求,不妨考虑 DFRobot推出的 LattePanda MU,它由一块核心板以及底板构成,厂家提供了电路图以及对应接口,可以很方便的实现定制。

本文提到的代码可以在 https://gitee.com/willok/slimbootloader 下载到,由天杀提供。

参考:

  1. https://www.lab-z.com/sblb/
  2. https://slimbootloader.github.io/how-tos/boot-ubuntu.html#setup-spn-os-container-boot

Timed GPIO

Timed GPIO 或者称作 Timed I/O 是基于系统时钟产生或者接收信号的功能。从 EHL/TGL(11代)平台开始,Intel 引入了这个功能。

这个功能的背景是:一些时候我们需要产生精确的 GPIO 时序,但是因为系统CPU分配的原因,这一点难以实现。比如,我们在代码中每隔0.001s需要反转一次GPIO,如果使用代码来驱动的话,无法保证CPU每隔0.001s执行一次,这样会导致实际生成的信号会有“抖动”(Jitter)。因此,引入了 Time-Aware GPIO(TGPIO)。直接从硬件上保证能够在指定的时间产生信号。除了能够输出信号(GPO),还能够作为 GPI,这样可以精确的测量两个信号的间隔。

有兴趣的朋友可以更深入的进行研究。

参考:

1.https://edc.intel.com/content/www/us/en/design/ipla/software-development-platforms/servers/platforms/intel-pentium-silver-and-intel-celeron-processors-datasheet-volume-1-of-2/005/timed-gpio-time-sync/

2.https://edc.intel.com/content/www/us/en/design/products/platforms/processor-and-core-i3-n-series-datasheet-volume-1-of-2/001/timed-gpio/

3.https://edc.intel.com/content/www/us/en/design/ipla/software-development-platforms/servers/platforms/intel-pentium-silver-and-intel-celeron-processors-datasheet-volume-1-of-2/005/timed-gpio-time-sync/

WIM安装解决方案

很多年前,Ghost 是通用的安装方案,但是随着时代的进步,Ghost 方案逐渐被淘汰。取而代之的是WIM 的方案,最新的 OS 和 Driver 都使用 WIM 的方式进行分发。需要注意的是:市面上很多能够实现 WIM 安装的 Windows PE 盘会在安装过程中对安装后的Windows 插入应用程序或者驱动程序。这样的结果可能导致影响你的测试结果。因此,这次制作了Windows PE 盘以及一个WIM 安装工具。这一套具有如下优点:

  1. 安装过程中不会添加任何额外软件;
  2. 方便使用,启动WindowPE 之后只需要键盘即可选择 WIM 文件进行安装;
  3. 体积较小,包括 WindowsPE 镜像,整体不超过350MB。

接下来介绍如何部署和使用这一套软件。

首先,使用 Ventoy 制作一个 USB 启动盘。需要注意的是:这个U盘上的内容会被完全清除掉。

安装完成 Ventoy 之后,需要手工将Winpe062907.iso和AutoWim目录拷贝到U盘Ventoy 的 exFAT分区上。同时,将需要安装的 WIM 同样放置于根目录下。

之后,使用这个U盘进行启动。

启动Ventoy后从菜单上选择从 Winpe062907.iso 启动。

启动之后,这个 WinPE 会自动执行 AutoWim 目录中的InWim.bat批处理。自动列出U盘根目录下的所有 WIM 请用户选择,选择之后会对硬盘进行自动分区,然后部署 WIM 内容。最后会执行 bcdboot 修复ESP 分区。

相信这个工具会成为你测试的得力助手,有兴趣的朋友不妨试试看。

ISO 镜像下载

AutoWim 目录下载

工作的视频可以在下面看到: