Step to UEFI (296)虚拟机下的假电池

电池作为现在笔记本必不可少的部件,通过 ACPI 和 Windows 进行交互。

对此,ACPI Spec 定义了几个 Table。换一句话说,Windows 只要能够正确读取出 Table,那么就可以根据上面的信息展示给客户一个电池。

第一个是 _BIX (Battery Information Extended) (特别注意ACPI 4.0定义的 _BIF (Battery Information)已经废止 ),其中给出了电池的信息。

偏移名称大小解释
RevisionDWORD目前版本号为 1
Power UnitDWORD电池容量单位: 0 – [mWh], 同时充放电速度将会以[mW]为单位 1 – [mAh], 同时充放电速度将会以[mA]为单位
Design CapacityDWORD设计容量,单位由上面的 Power Unit 给出 取值范围: 0x00000000-0x7FFF FFFF 0xFFFFFFFF 未知容量
Last Full Charge CapacityDWORD充满后的预期容量 取值范围: 0x00000000-0x7FFF FFFF 0xFFFFFFFF 未知容量
Battery TechnologyDWORD电池位置 0x0000 0000 主电池 0x0000 0001 第二块电池
Design VoltageDWORD设计电压, 取值范围 0x000000000 – 0x7FFFFFFF in [mV] 0xFFFFFFFF – 未知电压
Design Capacity of WarningDWORDOEM 设置的告警容量值 取值范围 0x000000000 – 0x7FFFFFFF in [mWh] or [mAh]
Design Capacity of LowDWORDOEM 设置的低容量值 取值范围 0x000000000 – 0x7FFFFFFF in [mWh] or [mAh]
Cycle CountDWORD充电循环次数 取值范围 0x000000000 – 0xFFFFFFFF
Measurement AccuracyDWORD电池容量测量准确度,以1/1000为单位,比如:80000表示80%
Max Sampling TimeDWORD_BST 中两次测量的最大间隔时间,比如,当前电池容量,放电速度或者剩余容量。以为毫秒单位。0xFFFFFFFF表示该位置无效。
Min Sampling TimeDWORD_BST 中两次测量的最小间隔时间。以为毫秒单位。0xFFFFFFFF表示该位置无效。
Max Averaging IntervalDWORD_BST 中两次测量的平均最大间隔时间。
Min Averaging IntervalDWORDBST 中两次测量的平均最小间隔时间。
Battery Capacity Granularity 1DWORD电池在告警容量值和低容量值之间的颗粒度
Battery Capacity Granularity 2DWORD电池在告警容量值和充满容量值之间的颗粒度
Model Number零结尾ASCII字符串OEM 定义的电池型号
Serial Number零结尾ASCII字符串OEM 定义的电池序列号
Battery Type零结尾ASCII字符串OEM 定义的电池类型
OEM Information零结尾ASCII字符串OEM 定义的在UI上展示的电池OEM信息
Battery Swapping CapabilityDWORD0x0 不可更换电池,例如,内部密封电池,用户无法接触到 0x1关机之后可更换电池 0x10 热插拔电池
上述根据ACPI Spec翻译,如果有错误欢迎指出,会进行订正

第二个是 _BST (Battery Status), 这个用于报告当前电池的状态信息。

偏移名称大小解释
Battery StateDWORDBit0 为1表示正在放电 Bit1  为1表示正在充电 Bit2  为1表示电池预警  
Battery Present RateDWORD电池充放电速度 取值范围 0x000000000 – 0x7FFFFFFF以[mW]或者[mA]为单位 0xFFFFFFFF – 未知速度    
Battery Remaining CapacityDWORD电池剩余容量 取值范围 0x000000000 – 0x7FFFFFFF以[mWh]或者[mAh]为单位 0xFFFFFFFF – 未知容量  
Battery Present VoltageDWORD电池电压 取值范围 0x000000000 – 0x7FFFFFFF以[mV]为单位 0xFFFFFFFF – 未知电压  

以本人的电脑(HP 840 G6)为例,设备管理器中可以看到电池:

使用 HE 直接读取 ACPI Table:

根据上面的整理出两个对应的Table, 放在 BAT0 设备中

Device (BAT0)
        {
            Name (_HID, EisaId ("PNP0C0A") /* Control Method Battery */)  // _HID: Hardware ID
            Name (_UID, One)  // _UID: Unique ID
            Method (_DSM, 4, Serialized)  // _DSM: Device-Specific Method
            {
                If (LEqual (Arg0, ToUUID ("4c2067e3-887d-475c-9720-4af1d3ed602e") /* Battery Thermal Limit */))
                {
                    Switch (ToInteger (Arg2))
                    {
                        Case (0x03)
                        {
                            Return (Package (0x01)
                            {
                                0x1E
                            })
                        }

                    }
                }
                Else
                {
                    Return (Package (0x01)
                    {
                        Zero
                    })
                }
            }

            Method (_STA, 0, NotSerialized)  // _STA: Status
            {
                Return (0x1F)
            }

            Method (_BIX, 0, NotSerialized)  // _BIX: Battery Information Extended
            {
                Return ( Package (0x15)
						{
							1, 
							1, 
							20000, 
							20000, 
							0, 
							4300, 
							2000, 
							1000, 
							10, 
							80000, 
							1000, 
							500, 
							750, 
							500, 
							0x64, 
							0x64, 
							"LABZBAT0", 
							"202410", 
							"MODOL1", 
							"LABZBAT0", 
							One
						})
            }

            Method (_BST, 0, NotSerialized)  // _BST: Battery Status
            {
                Return ( Package (0x04)	{
               			 1, 
               			 100, 
                		 10000, 
                		 4200
           			   })
            }

        }

     }

接下来选择使用 VirtualBox 虚拟机,根据【参考1】,替换内部的 ACPI Table, 最终效果如下:

就是说,我们成功的在这个虚拟机中安装了一块电量为 50% 的电池。

本文提到的修改后的 ACPI 源代码可以在这里下载:

参考:

1.https://www.lab-z.com/arcpi/

CH554 USB Host配合 ESP32-C3实现USB键盘转蓝牙

之前使用 Ch9350制作过一个 USB Host Shield 【参考1】,能够读取USB键盘鼠标的输入。最近在研究 Ch554 ,使用Ch554e制作了一个同样功能的Shield,配合ESP32-C3 能够实现USB 键盘转蓝牙的功能。

使用 Ch554 的有优点如下:

  1. 价格较低,相对于Ch9350 10元的价格,最便宜的 Ch554e 只要不到1.5元;
  2. 焊接友好,对于 TSSOP-20/SOP-16或者MSOP-10普通人都能够很好的进行焊接;
  3. 如果你的设计对于体积敏感,可以选择MSOP-10 封装的 Ch554e;
  4. 外围电路简单,只需要2个电容和1个电阻

缺点:

  1. 需要自己使用 keil 编写程序;
  2. 兼容性比不上 Ch9350,可能出现无法驱动的USB设备;

这次带来就是基于 Ch554e的设计。硬件部分设计如下:

下方就是CH554e的最小系统,外部配合2个0.1uf电容,以及1个10K电阻即可工作。下载方法是:上电之前短接 DL 位置,然后再上电使用WCHISPStudio即可。不过在研发阶段建议专门准备一个开发板便于操作。同时,官方的例子都是用第一个UART作为调试输出,而Ch554只有第2个 Uart可供使用。

根据上述电路设计的PCB如下:

这是一个底板,上面直接连接 DFRobot ESP32-C3即可。焊接后的板卡如下:

直接安装在 ESP32-C3上即可使用:

接下来开始代码的设计,首先设计的是 Ch554的代码,这里直接使用官方的代码进行简单修改。

为了便于使用我们使用和Ch9350相同的输出格式:

0-157 AB数据头,固定数值
0288表示有效帧值
03NN后续数据长度,从04开始到最后的校验和
0410  固定值 [7:6]:00 – 保留 [5:4]:01 – 鼠标 [3]  :0  – 保留 [2:1]:00 – 未知 [0]  :0  – 端口1
05-AA BB CC…..MM键盘数据,例如:08 00 00 00 00 00 00 00
XXNum帧序列号
XXCheckSum校验和,从05开始的数据和

例如:实际发送的一个数据:

57 AB 88 0B 10 08 00 00 00 00 00 00 00 00 08

代码是基于WCH 官方修改而来的,基本原理是:比较每一次收到的数据(RxBuffer)是否和上一次(LastBuffer)相同,如果不同,那么进行上报。使用上面介绍的数据报文格式:

                        IsSame=TRUE;
                        for ( i = 0; i < len; i ++ ){
                            if (LastBuffer[i]!=RxBuffer[i]) {
                                IsSame=FALSE;
                                LastBuffer[i]=RxBuffer[i];
                            }
                        }
                        //只有与前一次不同才进行输出
                        if (IsSame==FALSE) {
                            checksum=0x00;
                            CH554UART1SendByte(0x57);CH554UART1SendByte(0xAB);CH554UART1SendByte(0x88);CH554UART1SendByte(len+3);CH554UART1SendByte(0x10);
                            for ( i = 0; i < len; i ++ ){
                                CH554UART1SendByte(RxBuffer[i]);
                                checksum=checksum+RxBuffer[i];
                            }
                            checksum=checksum+counter;
                            CH554UART1SendByte(counter); CH554UART1SendByte(checksum);
                            counter++;
                        }

代码使用 Keil4 编译通过。

ESP32-C3代码如下:

#include <Arduino.h>
#include <BleKeyboard.h>

BleKeyboard bleKeyboard;

#define DEBUGMODE 0

void setup() {
  Serial.begin(115200);
  Serial1.begin(115200, SERIAL_8N1, RX, TX);

  bleKeyboard.begin();

}

void loop() {
  while (Serial.available()) {
    char c = Serial.read();
    if (c == '1') {
      Serial.println("get1");
    }
    if (c == '3') {
      ESP.restart();
    }
  }

  //根据 CH9350 Spec 每次最多输出 72Bytes
  byte Data[72];
  unsigned int CounterLast = Serial1.available();
  unsigned int CounterCurrent = 0;


  // 如果当前串口有数据
  if (CounterLast != 0) {
    // 进行简单测试,如果当前还在传输数据那么持续接收
    while (CounterCurrent != CounterLast) {
      CounterLast = Serial1.available();
      delayMicroseconds(500);
      CounterCurrent = Serial1.available();
    }
  }

  if (CounterCurrent > 0) {
    // 一次性将数据收取下来
    Serial1.readBytes(Data, CounterCurrent);
    unsigned int i = 0;
    unsigned int Length;
    while (i < CounterCurrent) {
      // 识别帧头
      if ((Data[i] == 0x57) && (Data[i + 1] == 0xAB)) {
        // 有效键值帧
        if (Data[i + 2] == 0x88) {
          // 获得数据长度
          Length = Data[i + 3];
          if (DEBUGMODE) {
            //Serial.print("Ln:");Serial.print(Length);
            for (int j = 1; j < Length + 1; j++) {
              if (Data[i + 3  + j] < 16) {
                Serial.print("0");
              }
              Serial.print(Data[i + 3  + j], HEX);
              Serial.print(" ");
            }
            Serial.println(" ");
          }

          //如果是键盘
          if (Data[i + 4] == 0x10) {
            if (DEBUGMODE) {
              Serial.print("Key");
              for (int j = 1; j < Length + 1; j++) {
                Serial.print(Data[i + 3  + j], HEX);
                Serial.print(" ");
              }
              Serial.println(" ");
            }
            
            //判断为Dostyle键盘
            if (Data[i + 3  + 1] == 0x10) { 
              if (bleKeyboard.isConnected() == true) {
                bleKeyboard.sendReport((KeyReport*)(&Data[i + 3  + 2]));
              }
            }
          }
          i = i + 3 + Length;
        } else if (Data[i + 2] == 0x82) {
          i = i + 3; // 跳过
        }
      }
      i++;
    } // while (i < Counter)
  }

}

使用时,只需要给ESP32-C3供电,连接好USB键盘后就可以搜索蓝牙键盘进行连接使用了。工作的测试视频在:

https://www.bilibili.com/video/BV1zi421a7NM/?vd_source=cf6121716e06cb669a27c10276f9c920

 Ch554 的代码:

ESP32 C3 代码:

参考:

1. https://mc.dfrobot.com.cn/thread-316678-1-1.html

又一次研究JY901心得

又一次尝试使用 Jy901 模块,没有成功应用,但是通过实验有一些心得记录如下。

1.模块默认使用输出 9600Hz 波特率通讯,10Hz回报;

2.恢复模块默认配置的方法有两种,一种是短接,另外一种是串口命令

3.一些串口配置的方法:

a.	ff aa 03 03 00  设置回传速率为1Hz
b.	ff aa 02 08 00  设置只输出0x53包(手册上提到JY901无法输出四元数)
c.	ff aa 00 00 00 保存当前设置(比如,进行了上述设定之后,需要保存之后下一次上电才能继续使用)

4.使用的轴如下图所示(不要看模块PCB上的标注,是错的)

5.输出范围:X轴±180;Y轴±90;Z轴±180

6.DataSheet上描述的角度输出如下:

其中的计算方法有问题,按照它的方法不会有正负的区别:

例如: 55 53 A0 A1 B0 B1 C0 C1 T0 T1 SUM

其中的 0xA1A0 输出范围是 0000-7FFF ,对应着 -180~+180;0xB1B0 输出范围是 0000-7FFF ,对应着 -90~+90;0xC1C0 输出范围是 0000-7FFF ,对应着 -180~+180.

因此实际可以选择如下处理方法:

  fX = JY901.stcAngle.Angle[0] - 0x4000;
  fX = fX * 180.0 / 0x4000;

  fY = JY901.stcAngle.Angle[1] - 0x4000;
  fY = fY * 90.0 / 0x4000;

  fZ = JY901.stcAngle.Angle[2] - 0x4000;
  fZ = fZ * 180.0 / 0x4000;

符号和方向满足右手原则:拇指指向轴方向,然后四个手指方向是正,相反是负。

参考:

1. https://wenku.baidu.com/view/13665ba8b307e87100f69630.html?ind=1&fr=wenchuang&_wkts_=1719042668522&bdQuery=jy901+%E6%95%B0%E6%8D%AE%E6%A0%BC%E5%BC%8F

WebSerial 3DModelViewer 四元数处理格式

在下面这个网页可以以3D 模式直接展示当前姿态:

https://adafruit.github.io/Adafruit_WebSerial_3DModelViewer

其中以欧拉角模式来演示的例子可以在【参考1】看到。如果想看到以四元数为参数的姿态演示,可以使用下面的格式输出:

  Serial.print("Quaternion: ");
  dtostrf(W, 1, 3, str);
  Serial.print(W,4);
  Serial.print(", ");
  dtostrf(X, 1, 2, str);
  Serial.print(X,4);
  Serial.print(", ");
  dtostrf(Y, 1, 2, str);
  Serial.print(Y,4);
  Serial.print(", ");
  dtostrf(Z, 1, 2, str);
  Serial.println(Z,4);

参考:

1.https://www.lab-z.com/bmx60rab/

获得物理硬盘 PID 的方法

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

检查 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 代码的试验。

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

  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版本)