#include <windows.h>
#include <iostream>
// 功能:获取指定物理驱动器的ProductId
std::string GetStorageDeviceProductId(const std::string& drivePath) {
// 打开物理驱动器
HANDLE hDevice = CreateFileA(drivePath.c_str(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);
if (hDevice == INVALID_HANDLE_VALUE) {
std::cerr << "Failed to open device: " << drivePath << std::endl;
return "";
}
STORAGE_PROPERTY_QUERY query = {};
query.PropertyId = StorageDeviceProperty;
query.QueryType = PropertyStandardQuery;
// 分配足够大的缓冲区来存储STORAGE_DEVICE_DESCRIPTOR及其附加数据
BYTE buffer[1024] = {};
DWORD bytesReturned = 0;
// 查询存储设备属性
BOOL result = DeviceIoControl(hDevice, IOCTL_STORAGE_QUERY_PROPERTY, &query, sizeof(query), &buffer, sizeof(buffer), &bytesReturned, NULL);
if (!result) {
std::cerr << "Failed to query storage device properties." << std::endl;
CloseHandle(hDevice);
return "";
}
// 获取STORAGE_DEVICE_DESCRIPTOR结构
STORAGE_DEVICE_DESCRIPTOR* deviceDescriptor = reinterpret_cast<STORAGE_DEVICE_DESCRIPTOR*>(buffer);
// ProductId是一个以NULL结尾的字符串,位于STORAGE_DEVICE_DESCRIPTOR之后
// 确保ProductIdOffset不为0
std::string productId = "";
if (deviceDescriptor->ProductIdOffset != 0) {
productId = reinterpret_cast<const char*>(buffer + deviceDescriptor->ProductIdOffset);
}
CloseHandle(hDevice);
return productId;
}
int main() {
// 示例:获取PhysicalDrive0的ProductId
std::string productId = GetStorageDeviceProductId("\\\\.\\PhysicalDrive0");
std::cout << "ProductId: " << productId << std::endl;
return 0;
}
作者: ziv2013
检查 PhysicalDrive 类型
具体的类型定义在【参考1】:
typedef enum _STORAGE_BUS_TYPE {
BusTypeUnknown = 0x00,
BusTypeScsi,
BusTypeAtapi,
BusTypeAta,
BusType1394,
BusTypeSsa,
BusTypeFibre,
BusTypeUsb,
BusTypeRAID,
BusTypeiScsi,
BusTypeSas,
BusTypeSata,
BusTypeSd,
BusTypeMmc,
BusTypeVirtual,
BusTypeFileBackedVirtual,
BusTypeSpaces,
BusTypeNvme,
BusTypeSCM,
BusTypeUfs,
BusTypeNvmeof,
BusTypeMax,
BusTypeMaxReserved = 0x7F
} STORAGE_BUS_TYPE, *PSTORAGE_BUS_TYPE;
示例代码:
#include <windows.h>
#include <iostream>
#include <winioctl.h>
void QueryDriveInterfaceType(int driveNumber) {
HANDLE hDrive;
char drivePath[256];
sprintf_s(drivePath, "\\\\.\\PhysicalDrive%d", driveNumber);
// 打开物理驱动器
hDrive = CreateFileA(drivePath, 0, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);
if (hDrive == INVALID_HANDLE_VALUE) {
std::cerr << "Unable to open " << drivePath << std::endl;
return;
}
// 准备查询
STORAGE_PROPERTY_QUERY query;
memset(&query, 0, sizeof(query));
query.PropertyId = StorageDeviceProperty;
query.QueryType = PropertyStandardQuery;
// 接收数据的缓冲区
BYTE buffer[1024];
DWORD bytesRead;
// 查询设备属性
BOOL result = DeviceIoControl(hDrive, IOCTL_STORAGE_QUERY_PROPERTY, &query, sizeof(query),
&buffer, sizeof(buffer), &bytesRead, NULL);
if (result) {
STORAGE_DEVICE_DESCRIPTOR* deviceDescriptor = (STORAGE_DEVICE_DESCRIPTOR*)buffer;
switch (deviceDescriptor->BusType) {
case BusTypeScsi:
std::cout << drivePath << " is SCSI" << std::endl;
break;
case BusTypeAtapi:
std::cout << drivePath << " is ATAPI" << std::endl;
break;
case BusTypeAta:
std::cout << drivePath << " is ATA" << std::endl;
break;
case BusTypeSata:
std::cout << drivePath << " is SATA" << std::endl;
break;
// 添加其他需要的接口类型
default:
std::cout << drivePath << " has an unknown interface type: " << (int)deviceDescriptor->BusType << std::endl;
break;
}
} else {
std::cerr << "Failed to query storage properties for " << drivePath << std::endl;
}
CloseHandle(hDrive);
}
int main() {
// 示例:查询PhysicalDrive0的接口类型
QueryDriveInterfaceType(0);
return 0;
}
参考:
1.https://learn.microsoft.com/en-us/windows/win32/api/winioctl/ne-winioctl-storage_bus_type
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 代码的试验。
这次进行一个特别的实验。基本的原理是:
- 编写一个在屏幕上输出字符串简单的程序,这样我们能得到一段EFI Shell下对屏幕输出字符串的机器码;
- 编写一个宿主程序,这个程序编译后的 EFI 文件使用 4K 对齐,这样话,存放代码的.text段会有足够的空间能够存放下步骤1生成的机器码;
- 修改生成的EFI文件的 Section Headers 中给出的.text 大小,保证足够放下我们增加的机器码;
- 将步骤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的代码。
参考:
- https://www.lab-z.com/stu207/
- https://blog.csdn.net/Simon798/article/details/96876910
- https://www.lab-z.com/stu260/
- 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();
}
对于 FPF/FIT/BootGuard 的介绍
小米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
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/