ESP32 S2 USB Host 读取键盘数据

ESP32-S2 带有一个集成了收发器的全速 USB OTG 外设,符合 USB 1.1 规范。意思是S2即支持 USB Device 又支持Host。于是,这次测试在 Arduino 环境下通过 ESP32 S2 直接支持读取 USB Keyboard 的按键信息。

准备工作有点复杂:

  1. 必须使用 ESP32 2.0.1 环境,如果你使用 2.0.2 会出现编译不过的情况

2.硬件上GPIO19 和 GPIO20 可以分别作为 USB 的 D- 和 D+,这里我直接飞线接到一个 USB 母头上:

3.安装库下面这两个库

准备完成后,即可编译测试 esp32-usb-host-demos-main 中的 usbhidboot 示例代码。

4.编译上传之后如果想看到结果,还需要将 Core Debug Level 设置为 Verbose ,默认的 None 不会有任何输出

比如,我这边看到的结果如下:

Step to UEFI (246)显示 JPEG 图片的 DXE 驱动

之前介绍过如何在UEFI 下显示 BMP【参考1】,PCX【参考2】,PNG【参考3】和 GIF【参考4】。这次介绍的是一个 DXE Driver, 使用之后会在系统中注册 EFI_JPEGDECODER_PROTOCOL,这样 UEFI Application 可以通过这个 Protocol 进行 JPEG 的解码显示。这个代码来自【参考5】,有兴趣的朋友可以到原文进行查看。

代码分为两个,一个是 DXE Driver,另外一个是用于测试的 UEFI Application

首先介绍 DXE Driver 的编译(这次使用的是EDK2 202108 【参考6】):

1. MdeModulePkg\MdeModulePkg.dsc 修改如下:

[Components]
  #LABZ_Debug_Start
  MdeModulePkg/JpegDecoderDxe/JpegDecoderDxe.inf
  MdeModulePkg/Application/JPGDecoderTest/JPGDecoderTest.inf
  #LABZ_Debug_End
  MdeModulePkg/Application/HelloWorld/HelloWorld.inf
  MdeModulePkg/Application/DumpDynPcd/DumpDynPcd.inf
  MdeModulePkg/Application/MemoryProfileInfo/MemoryProfileInfo.inf

2. MdeModulePkg\MdeModulePkg.dec 修改如下:

[Protocols]
  ##LABZ_Debug_Start
  gEfiJpegDecoderProtocolGuid          = { 0xa9396a81, 0x6231, 0x4dd7, {0xbd, 0x9b, 0x2e, 0x6b, 0xf7, 0xec, 0x73, 0xc2} }
  ##LABZ_Debug_End
  ## Load File protocol provides capability to load and unload EFI image into memory and execute it.
  #  Include/Protocol/LoadPe32Image.h
  #  This protocol is deprecated. Native EDKII module should NOT use this protocol to load/unload image.
  #  If developer need implement such functionality, they should use BasePeCoffLib.
  gEfiLoadPeImageProtocolGuid    = { 0x5CB5C776, 0x60D5, 0x45EE, { 0x88, 0x3C, 0x45, 0x27, 0x08, 0xCD, 0x74, 0x3F }}

3. 在\MdeModulePkg\Include\Protocol\下面放置 :JpegDecoder.h

4. 在 \MdeModulePkg\ 下面放置JpegDecoderDxe目录

接下来加入一个测试的 UEFI Application。为了简化代码,我首先使用工具将图片转化为C语言的 .h文件,代码直接引用对应内存:

#include <Uefi.h>
#include <Library/PcdLib.h>
#include <Library/UefiLib.h>
#include <Library/UefiApplicationEntryPoint.h>
#include <Library/MemoryAllocationLib.h>
#include <Protocol/GraphicsOutput.h>
#include <JpegDecoder.h>
#include "demo.h"

extern EFI_BOOT_SERVICES         *gBS;
extern EFI_SYSTEM_TABLE          *gST;

EFI_GUID GraphicsOutputProtocolGuid = EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID;
EFI_GRAPHICS_OUTPUT_PROTOCOL          *GraphicsOutput = NULL;

EFI_STATUS
EFIAPI
UefiMain (
        IN EFI_HANDLE        ImageHandle,
        IN EFI_SYSTEM_TABLE  *SystemTable
)
{
        EFI_STATUS                      Status;
        EFI_JPEG_DECODER_PROTOCOL        *JpegDecoderProtocol;
        UINT8* RGB32;
        UINTN DecodedDataSize;
        UINTN Height,Width;
        EFI_JPEG_DECODER_STATUS DecoderStatus;

        Status = gBS->LocateProtocol(
                         &GraphicsOutputProtocolGuid,
                         NULL,
                         (VOID **) &GraphicsOutput);
        if (EFI_ERROR(Status))
        {
                GraphicsOutput = NULL;
                Print(L"Loading Graphics_Output_Protocol error!\n");
                return EFI_SUCCESS;
        }

        Status = gBS->LocateProtocol(
                         &gEfiJpegDecoderProtocolGuid,
                         NULL,
                         (VOID **) &JpegDecoderProtocol);
        if (EFI_ERROR(Status))
        {
                Print(L"Loading Efi_JpegDecoder_Protocol failed!\n");
                return EFI_SUCCESS;
        }

        RGB32 = (UINT8*)AllocatePool(636*373*4);
        JpegDecoderProtocol->DecodeImage(
                JpegDecoderProtocol,
                (UINT8 *)&demo_jpg,
                demo_jpg_size,
                &RGB32,
                &DecodedDataSize,
                &Height,
                &Width,
                &DecoderStatus
        );

        GraphicsOutput->Blt(
                GraphicsOutput,
                (EFI_GRAPHICS_OUTPUT_BLT_PIXEL *) RGB32,
                EfiBltBufferToVideo,
                0, 0,
                0, 0,
                Width, Height, 0);

        Print(L"Height=[%d],Width=[%d]\n",Height,Width);
        FreePool(RGB32);

        return EFI_SUCCESS;
}

简单的说就是在系统中查找EFI_JPEG_DECODER_PROTOCOL   ,然后调用之。需要注意的是在调用的时候需要分配一个内存用于存储解压后的图像,但是这个 PROTOCOL 没有提供获得 JPEG 图片长宽的函数,因此要么直接分配足够大的内存,要么使用者知道图片的尺寸(比如,用于显示的 Logo,在Build的时候一定是知道具体尺寸的)。这里我知道图片尺寸,所以使用下面的方法直接开辟一段内存用于存储:

  RGB32 = (UINT8*)AllocatePool(636*373*4);

使用方法是,首先 load JpegDecoderDxe.efi, 之后运行 jpgt.efi 即可显示:

运行结果:

UEFI Shell下测试显示 JPEG

可以看到,这种方式能够方便的显示 JPEG 格式的图片。

完整的代码下载:

参考:

  1. https://www.lab-z.com/bmplib/ BmpSupportLib
  2. https://www.lab-z.com/pcxdecoder/ UEFI 下 PCX 的解码
  3. https://www.lab-z.com/uefipngdecoder/ UEFI 下的 PNG 解码
  4. https://www.lab-z.com/decodergif/ UEFI 下的 GIF 解码
  5. https://github.com/XENNOK/Insyde_BIOS_Source
  6. https://www.lab-z.com/edk202108/

CH567 的编译和下载

首先介绍一下CH567代码的编译,以自带的EXAMPLE为例:

  1. 在 Project Explorer 上点击鼠标右键,在弹出的菜单上选择 Import
选择 Import

如果没有这个窗口,可以在 Window -> Show View ->Project Explorer 中打开之

打开 Project Explorer

2.再选择Existing Projects into Workspace

导入已经存在的工程

3.这里选择 USB0_DevCH372 作为示例

导入 CH372的例子

4.之后可以用下面这个按钮进行编译,可以编译为 Debug 或者 Release版本

选择编译目标

5.很快就能完成编译

编译成功

在 Output 目录下的 GPIO.bin 就是最终的编译结果。

接下来介绍如何下载到开发板上:

1.CH567 上有2个 USB Port,分别是 USB0 和 USB1。对于CH567 来说,只支持从USB1下载。

电路图上的USB1 和USB0

对应PCB 中,上方是 USB1 下方是 USB0

PCB上的 USB1 和 USB0

2.下载方法是:首先按住DOWNLOAD按钮,然后将USB 线插入USB1中,插入之后松开按钮

DWN 按钮位于 USB1 左侧

3.在设备管理器中能看到新增加的设备(如果没有的话,可能的原因是板子没有上电或者板子有硬件问题)

新出现了一个 USB Module 设备

4.打开下载工具。首先在1的位置切换到 CH56X 页面,然后在2的位置选择芯片型号为 CH567,接下来在3选择要下载的设备(如果没有的话,请检查硬件),最后,在4的位置选择刚才提到编译后生成的文件

5.很快就能完成下载:

下载成功

之后将 USB线连接到 USB0 上,在设备管理器中可以看到如下设备:

CH372 设备

最后多提一下关于串口调试信息的输出,示例代码使用的是 UART3在 PA5上,只需要用USB串口线的 RTX 连接到这个引脚,共地之后使用 115200 波特率。

冷知识:CPU 上电后BIOS运行的第一条指令

我最初看到“CPU 上电后BIOS运行的第一条指令是什么?”这个问题后的第一反应是:一个跳转指令啊。但是实际上并非如此,下面是 Intel TigerLake 平台一个BIOS ROM 末尾处的机器码:

Intel TigerLake 平台第一条BIOS代码

可以看到,上电后 CPU 运行的第一条指令是 0x90(NOP)。

查看 OVMF的代码,在 \OvmfPkg\ResetVector\Ia16\ResetVectorVtf0.asm 中:

;
; The VTF signature
;
; VTF-0 means that the VTF (Volume Top File) code does not require
; any fixups.
;
vtfSignature:
    DB      'V', 'T', 'F', 0

ALIGN   16

resetVector:
;
; Reset Vector
;
; This is where the processor will begin execution
;
    nop
    nop
    jmp     EarlyBspInitReal16
ALIGN   16
fourGigabytes:

同样的,第一条指令也是 NOP。

根据公众号“泰山N思维 ”的说法:

当前BIOS的头两条NOP代码没有特殊的意义,只是为了替换 wbinvd指令(机器码:0f 09,用于flush内部的Cache,并把Cache line数据写回内存)。早期CPU 第一条指令使用wbinvd指令的原因,相对比较“靠谱”的说法是:在CPU开始执行时候,虽然Cache处于Disable状态(CR0.CD),但只代表CPU Core不使用Cache(no-fill-mode),不代表Cache中没有有效的数据,通过wbinvd指令可以invalid无效化Cache中的数据;而新的CPU Invalid的操作已经由硬件执行,不再需要额外操作。后来由于wbinvd指令在某些CPU上会导致hang机问题,并且Intel新的CPU并不需要执行wbinvd指令,所以Intel使用两条nop进行了替换。

就是说这两条指令没有特殊意义,只是因为历史原因需要用2条指令来填充这里。

有兴趣的朋友不妨订阅““泰山N思维  “这个公众号。

泰山N思维 公众号

Step to UEFI (245)Debug Port Protocol 测试

通常情况下,我们在通过串口进行 Debug Message 输出的时候通常直接对 0x3F8 Port进行编程,但是这种方式并不符合 UEFI 规范。正规的做法是通过 EFI_DEBUGPORT_PROTOCOL来实现。在 UEFI 规范中有如下定义:

针对这个 Protocol 编写的测试代码如下:

#include  <Uefi.h>
#include  <Library/UefiLib.h>
#include  <Library/ShellCEntryLib.h>
#include <Protocol/DebugPort.h>
#include  <Library/UefiBootServicesTableLib.h> //global gST gBS gImageHandle

EFI_GUID gEfiDebugPortProtocolGuid = EFI_DEBUGPORT_PROTOCOL_GUID;

int
EFIAPI
main (
        IN int Argc,
        IN char **Argv
)
{
        EFI_STATUS              Status;
        EFI_DEBUGPORT_PROTOCOL  *DebugPort = NULL;
        CHAR8                   *TestMessage="www.lab-z.com";

        Status = gBS->LocateProtocol(
                         &gEfiDebugPortProtocolGuid,
                         NULL,
                         (VOID **) &DebugPort);
        if (EFI_ERROR(Status))
        {
                Print(L"Can't load DebugPortProtocol error!\n");
                return EFI_SUCCESS;
        }

        UINTN   Length=AsciiStrLen(TestMessage);
        DebugPort->Write(
                DebugPort,
                1000,
                &Length,
                (VOID *)TestMessage
        );
        
        gBS->CloseProtocol ( 
                        DebugPort,
                        &gEfiDebugPortProtocolGuid,
                        gImageHandle,
                        NULL );
        
        return EFI_SUCCESS;

}

运行之后主机端的串口程序中可以看到有字符串的输出:

串口工具可以看到输出

完整代码:

做一个Micro USB Host

去年最火爆的就是芯片了,我在21年4月购买的 MAX3421芯片还只要16.6元

MAX3421e 21年3月只要 16.6元

而今天再点进去查看报价已经达到了 130 (咨询下来预期50左右):

MAX3421e 22年2月已经按照 130元报价了

于是萌发了制作一个能够多次复用模块的想法,简单的说设计足够小的 PCB 然后将芯片焊接在上面,将必要的引脚引出,使其成为一个“模块”。

电路图设计如下:

Arduino Micro USB Host 电路图

为了保证体积足够小,使用了贴片式晶振,这个晶振有4个引脚,分别是2个 GND ,1个XO 一个XI;尺寸是 3.2×2.5mm 厚度是 0.7mm,因此这种封装也被称作SMD3225-4P。

SMD3225-4P 的晶振

PCB 设计如下:

Arduino Micro USB Host PCB

为了尽量缩减体积,上面只保留必要的5个电容,同时选用 0603封装的,不得不承认,0603封装能够极大方便布线。

名称用途 用途名称
INT用于MAX3421通知单片机有中断发生SPI的MOSIMOSI
GNDSPI的MOSIMISO
MD-USB Host D-SPI的片选SS
MD+USB Host D+SPI的 CLOCKSCLK
VBCOMP检查USB 设备的 VBUS 是否存在芯片的 RESET Pin, 正常情况下必须为高RESET
GND芯片供电3.3V
引脚说明
PCB 实物

焊接完成后,我们就可以在 FireBeetle 上进行测试了:

首先,FireBeetle  VCC 给USB 母头供电,同时共地,其余引脚连接如下:

名称FireBeetle FireBeetle名称
INTD3IO23MOSI
GNDGNDIO19MISO
MD-USB 母头 D-D7SS
MD+USB 母头 D+IO18SCLK
VBCOMPUSB 母头 5v3.3VRESET
GNDGND3.3V3.3V
对FireBeetle 的连接方式

之后就可以直接使用 USB Host Shield 2.0 的库了,比如运行 \USBHIDBootMouse.ino 这个示例:

#include <hidboot.h>
#include <usbhub.h>

// Satisfy the IDE, which needs to see the include statment in the ino too.
#ifdef dobogusinclude
#include <spi4teensy3.h>
#endif
#include <SPI.h>

class MouseRptParser : public MouseReportParser
{
protected:
	void OnMouseMove	(MOUSEINFO *mi);
	void OnLeftButtonUp	(MOUSEINFO *mi);
	void OnLeftButtonDown	(MOUSEINFO *mi);
	void OnRightButtonUp	(MOUSEINFO *mi);
	void OnRightButtonDown	(MOUSEINFO *mi);
	void OnMiddleButtonUp	(MOUSEINFO *mi);
	void OnMiddleButtonDown	(MOUSEINFO *mi);
};
void MouseRptParser::OnMouseMove(MOUSEINFO *mi)
{
    Serial.print("dx=");
    Serial.print(mi->dX, DEC);
    Serial.print(" dy=");
    Serial.println(mi->dY, DEC);
};
void MouseRptParser::OnLeftButtonUp	(MOUSEINFO *mi)
{
    Serial.println("L Butt Up");
};
void MouseRptParser::OnLeftButtonDown	(MOUSEINFO *mi)
{
    Serial.println("L Butt Dn");
};
void MouseRptParser::OnRightButtonUp	(MOUSEINFO *mi)
{
    Serial.println("R Butt Up");
};
void MouseRptParser::OnRightButtonDown	(MOUSEINFO *mi)
{
    Serial.println("R Butt Dn");
};
void MouseRptParser::OnMiddleButtonUp	(MOUSEINFO *mi)
{
    Serial.println("M Butt Up");
};
void MouseRptParser::OnMiddleButtonDown	(MOUSEINFO *mi)
{
    Serial.println("M Butt Dn");
};

USB     Usb;
USBHub     Hub(&Usb);
HIDBoot<USB_HID_PROTOCOL_MOUSE>    HidMouse(&Usb);

MouseRptParser                               Prs;

void setup()
{
    Serial.begin( 115200 );
#if !defined(__MIPSEL__)
    while (!Serial); // Wait for serial port to connect - used on Leonardo, Teensy and other boards with built-in USB CDC serial connection
#endif
    Serial.println("Start");

    if (Usb.Init() == -1)
        Serial.println("OSC did not start.");

    delay( 200 );

    HidMouse.SetReportParser(0, &Prs);
}

void loop()
{
  Usb.Task();
}
面包板上进行测试

本文提到的电路图和PCB 下载(立创 EDA)

UEFI Tips: 生成当前ACPI Table Hardware Mapfile

最近在研究 ACPICA ,发现iasl.exe 的一个有趣的功能:生成当前ACPI Table Hardware Mapfile。这个功能可以生成当前 ACPI Table 中硬件资源的列表,例如:

Intel ACPI Component Architecture
ASL Optimizing Compiler version 20140828-32 [Sep 19 2014]
Copyright (c) 2000 - 2014 Intel Corporation

Compilation of "dsdt.dsl" - Fri Sep 19 09:43:52 2014


Resource Descriptor Connectivity Map
------------------------------------

GPIO Controller:  INT33FC   \_SB.GPO0                     // Intel Baytrail GPIO Controller

Pin   Type     Direction    Polarity    Dest _HID  Destination

0000  GpioInt  -Interrupt-  ActiveBoth   INTCFD9   \_SB_.
0000  GpioInt  -Interrupt-  ActiveBoth   INTCFD9   \_SB_.TBAD
0001  GpioInt  -Interrupt-  ActiveBoth   INTCFD9   \_SB_.TBAD
0002  GpioIo   OutputOnly                -Field-   \_SB_.GPO0.CCU2
0003  GpioIo   OutputOnly                -Field-   \_SB_.GPO0.CCU3
0026  GpioIo   InputOnly                80860F14   \_SB_.SDHC
0026  GpioInt  -Interrupt-  ActiveBoth  80860F14   \_SB_.SDHC
0028  GpioIo   OutputOnly               80860F14   \_SB_.SDHC
0029  GpioIo   OutputOnly               80860F14   \_SB_.SDHC
0036  GpioIo   OutputOnly               -No HID-   \_SB_.PCI0.OTG1 
0041  GpioIo   OutputOnly               10EC5640   \_SB_.I2C2.RTEK
005F  GpioIo   OutputOnly                -Field-   \_SB_.GPO0.TCON
0060  GpioInt  -Interrupt-  ActiveBoth   INTCFD9   \_SB_.TBAD
0064  GpioIo   OutputOnly                MCD0001   \MDM_ 

I2C  Controller:  80860F41  \_SB.I2C2                     // Intel Baytrail I2C Host Controller

Type  Address   Speed      Dest _HID  Destination
I2C    0010    00061A80     INT33BE   \_SB_.I2C2.CAM1               // Camera Sensor OV5693
I2C    001C    00061A80    10EC5640   \_SB_.I2C2.RTEK               // Realtek I2S Audio Codec
I2C    0048    00061A80     INT33F0   \_SB_.I2C2.CAMB               // Camera Sensor MT9M114

SPI  Controller:  80860F0E  \_SB.SPI1                     // Intel SPI Controller

Type  Address   Speed      Dest _HID  Destination
SPI    0001    007A1200    AUTH2750   \_SB_.SPI1.FPNT               // AuthenTec AES2750

UART Controller:  80860F0A  \_SB.URT1                     // Intel Atom UART Controller

Type  Address   Speed      Dest _HID  Destination
UART   0000    0001C200     UTK0001   \_SB_.URT1.UART             
UART   0000    0001C200    OBDA8723   \_SB_.URT1.BTH1             

比如,从上面可以看到 Table中有一个名为SPI1的 SPI 控制器,ID 是 80860F0E,然后它下面有一个叫做 FPNT 的设备,HID为AUTH2750,速度是8Mhz(0x7a1200)。

使用方法是: iasl.exe -lm dsdt.asl

生成结果在同一个目录下的 dsdt.map 中。

往事:Windows 95 硬件兼容问题

本文来自 The Old New Thing专栏,作者是Raymond Chen,他是资深的微软工程师参与多个版本的Windows开发,在他的专栏中讲述了很多关于 Windows研发的趣事。本文链接在 https://devblogs.microsoft.com/oldnewthing/20030828-00/?p=42753 。原标题是 “Hardware backwards compatibility” 。

所谓兼容性不单单只存在于软件上,硬件上同样会面临同样的问题。更糟糕的是当硬件出现问题时,人们常常会责怪软件…….

HLT 是一条让 CPU 停机的指令,CPU 执行这条指令之后会进入休眠直到被硬件中断唤醒。这条指令对于笔记本电脑很有帮助,它能够帮助降低整体功耗,测试表明能引入这条指令让笔记本电脑降低3摄氏度。

我们(微软)曾经尝试在 Win95中引入这条指令,当系统不太忙时抽空让CPU“打盹”,但是很快发现很多笔记本电脑(一些时主流笔记本厂商的型号)执行HLT 指令的时候会死机。

最终,我们只得在 Windows 95中取消这条指令。

很快,HLT指令为大众所知,很多人写道“愚蠢的微软。他们为什么不在Windows中支持这个指令”。在这种情况下,我只能默默的坐着听闻人们对于微软的各种批评声音,诸如愚蠢、懒惰和自私。

如今,我终于可以在这里为 Windows 95 辩解一番(当然,我仍然不能披露前面提到的主流生产商的名字)。

比如,我最喜欢的一个硬件产品,有着一个非常糟糕的问题:如果你将显卡插入距离电源最远的扩展槽,运行之后系统就会崩溃。很明显这是由于硬件制造商压缩成本造成的。众所周知,硬件厂商追求的 Cost Down 总会导致稀奇古怪的问题。更加不幸的是,Windows95 就是运行在这样一批稀奇古怪的硬件上的操作系统。试想如下场景就能理解为什么我们竭尽全力来适配这些硬件呢:

  • 你有一台工作正常的电脑
  • 你去商店买了一份 Windows 95
  • 你把它拿回家安装到电脑上
  • 砰的一下,你的电脑崩溃了

这样的情况下你会认为是谁的责任。毫无疑问:你不会批评硬件厂商。

========================================

作为一个BIOS工程师,在遇到问题时,不应该轻易的下结论,特别是使用:“之前工作正常,但是现在忽然不正常”这样的断言。因为很可能之前能够工作是因为刚好处于临界状态(margin)运气稍微好一些没有碰到而已。作为从业人员应该用更专业更有逻辑的证据来证明。

Step to UEFI (244)一种内嵌汇编的实现方法

前面介绍过,VS2015并不支持直接内嵌汇编语言。C语言要求有堆栈的环境才能工作,前面的限制对于大多数程序毫无影响。这次尝试在编译过程通过替换数值的方式来实现内嵌汇编。

简单介绍一下思路:

  1. 在要内嵌的C语言源代码中使用预先定义的指令进行填充,比如:_outpd() 这样的指令,它会生成固定长度固定数值的机器码;
  2. 修改 build.py, 当处理包含前面C语言代码的 INF 文件时,修改对应的 Makefile,其中加入生成 OBJ 之后搜索替换的动作

这次以 SecMain.c 为例,进行操作。

Step1. 使用 _outpd() 填充,代码如下

  //
  // Find PEI Core entry point. It will report SEC and Pei Core debug information if remote debug
  // is enabled.
  //
  BootFv = (EFI_FIRMWARE_VOLUME_HEADER *)SecCoreData->BootFirmwareVolumeBase;
  FindAndReportEntryPoints (&BootFv, &PeiCoreEntryPoint);
  SecCoreData->BootFirmwareVolumeBase = BootFv;
  SecCoreData->BootFirmwareVolumeSize = (UINTN) BootFv->FvLength;

  //LABZ_Debug_Start
  _outpd(0x80,0xAB);
  _outpd(0x80,0xAB);
  _outpd(0x80,0xAB);
  _outpd(0x80,0xAB);
  _outpd(0x80,0xAB);  
  _outpd(0x80,0xAB);   
  //LABZ_Debug_End
   
  DEBUG ((DEBUG_INFO,
    "Check [BootFirmwareVolumeBase]-0x%X [PeiCoreEntryPoint]=0x%X BootFirmwareVolumeBase=0x%X \n",
	(UINT64)BootFv,
    (UINT64)(PeiCoreEntryPoint),
	(UINT64)(*PeiCoreEntryPoint)
));

编译之后们可以看到对应的机器码如下:

; 1022 :   _outpd(0x80,0xAB);

  00050	66 ba 80 00	 mov	 dx, 128			; 00000080H
  00054	b8 ab 00 00 00	 mov	 eax, 171		; 000000abH
  00059	ef		 out	 dx, eax

; 1023 :   _outpd(0x80,0xAB);

  0005a	66 ba 80 00	 mov	 dx, 128			; 00000080H
  0005e	b8 ab 00 00 00	 mov	 eax, 171		; 000000abH
  00063	ef		 out	 dx, eax

; 1024 :   _outpd(0x80,0xAB);

  00064	66 ba 80 00	 mov	 dx, 128			; 00000080H
  00068	b8 ab 00 00 00	 mov	 eax, 171		; 000000abH
  0006d	ef		 out	 dx, eax

; 1025 :   _outpd(0x80,0xAB);

  0006e	66 ba 80 00	 mov	 dx, 128			; 00000080H
  00072	b8 ab 00 00 00	 mov	 eax, 171		; 000000abH
  00077	ef		 out	 dx, eax

; 1026 :   _outpd(0x80,0xAB);  

  00078	66 ba 80 00	 mov	 dx, 128			; 00000080H
  0007c	b8 ab 00 00 00	 mov	 eax, 171		; 000000abH
  00081	ef		 out	 dx, eax

; 1027 :   _outpd(0x80,0xAB);   

  00082	66 ba 80 00	 mov	 dx, 128			; 00000080H
  00086	b8 ab 00 00 00	 mov	 eax, 171		; 000000abH
  0008b	ef		 out	 dx, eax

就是说,我们在 Secmain.obj 中找到 “66 ba 80 00 b8 ab 00 00 00 ef 66 ba 80 00 b8 ab 00 00 00 ef 66 ba 80 00 b8 ab 00 00 00 ef66 ba 80 00 b8 ab 00 00 00 ef 66 ba 80 00 b8 ab 00 00 00 ef 66 ba 80 00 b8 ab 00 00 00 ef” 这一串,替换成需要的代码即可。

Step2. 使用 C# 编写一个程序,接收3个参数:原文件,要搜索的十六进制数值和要替换为的十六进制数值, 写好后的程序名称为 SAR.exe (Search and Replace)

Step3. 要替换为的内容如下:

00000000 488D05F9FFFFFF lea		rax,[$]
00000007 66BA0204       mov     dx,0x402
                        
                        ; 7-0 Bits
0000000B EE             out     dx,al       
                        
                           ; 15-8 Bits
0000000C 48C1E808       shr     rax,8
00000010 EE             out     dx,al       
                        
                        ; 23-16 Bits
00000011 48C1E808       shr     rax,8
00000015 EE             out     dx,al       
                        
                        ; 31-24 Bits
00000016 48C1E808       shr     rax,8
0000001A EE             out     dx,al       
                        
                        ; 39-32 Bits
0000001B 48C1E808       shr     rax,8
0000001F EE             out     dx,al       
                        
                        ; 47-40 Bits
00000020 48C1E808       shr     rax,8
00000024 EE             out     dx,al       
                        
                        ; 55-48 Bits
00000025 48C1E808       shr     rax,8
00000029 EE             out     dx,al       
                        
                        ; 63-56 Bits
0000002A 48C1E808       shr     rax,8
0000002E EE             out     dx,al     
                        
0000002F C3             ret

就是说,我要在生成的 SecMain.obj 中搜索如下十六进制值:

66ba8000b8ab000000ef66ba8000b8ab000000ef66ba8000b8ab000000ef66ba8000b8ab000000ef66ba8000b8ab000000ef66ba8000b8ab000000ef

替换为以下十六进制值(实际上后端还要使用 90 NOP 填充为和上面相同长度的数值):

488D05F9FFFFFF66BA0204EE48C1E808EE48C1E808EE48C1E808EE48C1E808EE48C1E808EE48C1E808EE48C1E808EE

Step4. 在 SecMain对应的makefile 中加入如下代码:

    # indicate there's a thread is available for another build task
        BuildTask._RunningQueueLock.acquire()
        BuildTask._RunningQueue.pop(self.BuildItem)
        BuildTask._RunningQueueLock.release()
        BuildTask._Thread.release()

    ## Start build task thread
    #
    def Start(self):
        EdkLogger.quiet("Building ... <%s>" % repr(self.BuildItem))
        EdkLogger.quiet(type(self.BuildItem))
        #ZivDebug_Start
        if str(self.BuildItem).find('SecMain.inf')!=-1:
            EdkLogger.quiet("Here patch Secmain %s" % repr(self.BuildItem))
            os.system('copy /y D:/stable202108/IAT/makefile D:/stable202108/Build/OvmfX64/NOOPT_VS2015x86/X64/OvmfPkg/Sec/SecMain')
        #ZivDebug_End
        Command = self.BuildItem.BuildCommand + [self.BuildItem.Target]
        self.BuildTread = Thread(target=self._CommandThread, args=(Command, self.BuildItem.WorkingDir))
        self.BuildTread.name = "build thread"
        self.BuildTread.daemon = False
        self.BuildTread.start()

## The class contains the information related to EFI image
#
class PeImageInfo():

Step5. 研究 Build.py, 最终找到如下位置,判断当前处理的是否为 Secmain.inf, 如果是,那么替换对应的 Makefile, 然后将补丁程序,搜索文件和替换文件放置到对应的位置,最后执行 makefile

    ## Start build task thread
    #
    def Start(self):
        EdkLogger.quiet("Building ... <%s>" % repr(self.BuildItem))
        EdkLogger.quiet(type(self.BuildItem))
        #ZivDebug_Start
        if str(self.BuildItem).find('SecMain.inf')!=-1:
            EdkLogger.quiet("Here patch Secmain %s" % repr(self.BuildItem))
            os.system('copy /y D:/stable202108/IAT/makefile D:/stable202108/Build/OvmfX64/NOOPT_VS2015x86/X64/OvmfPkg/Sec/SecMain')
        #ZivDebug_End
        Command = self.BuildItem.BuildCommand + [self.BuildItem.Target]
        self.BuildTread = Thread(target=self._CommandThread, args=(Command, self.BuildItem.WorkingDir))
        self.BuildTread.name = "build thread"
        self.BuildTread.daemon = False
        self.BuildTread.start()

这样,编译过程中可以看到如下信息,表明进行了替换动作:

在最终生成的 OVMF.fd 中可以找到我们替换进去的代码(这一段是没有压缩的,所以可以直接搜索到):

在QEMU上跑起来之后,查看生成的 Debug Log 可以看到有输出 RIP:

最后文章中提到的替换工具和实验用到的Binary 可以在下面下载:

调试小故事(3)2周时间找到的 Bug, 只用一行代码解决

来自:Quora网站,作者 Udayan Banerji “作为程序员,你解决的最有趣的问题是什么”的回答。

当年我在 Intel 做编译器工程师的时候,曾经遇到过一个离奇的 Bug。简单的说,一个用于测试设备性能的安卓应用程序会随机崩溃。程序非常简单,界面上有个按钮,按下后会开始测试运行一段时间。

1.我没有这个应用程序的源代码,只能看到它编译后的字节码(Bytecode)。首先我在调试器中测试,第一次没有问题,随后也没有问题。前后至少跑了30次都没有看到崩溃的现象;

2.在调试器之外运行程序才能看到随机崩溃的问题。经过观察运行20次左右程序会崩溃一次;

3.我在字节码中搜索20这个数值,所有包括20次循环,20次递归的字样。没有发现任何潜在的问题,程序仍然会崩溃;

4.我发现一个更糟糕的现象:如果在这个安卓手机上使用 USB 键盘,问题会消失;

5.经过一个周末的休息之后,我重整旗鼓,再次投入这个问题中。这次从崩溃后的 Java 环境中入手。从日志看起来,出现问题时有一个断言(assert)错误:一个大浮点数不等于 NaN (“Not a Numnber”)

6.于是我返回继续在字节码中查找浮点数除法的相关内容。我在字节码中一个又一个的检查了所有的浮点运算相关代码。然后将代码转化为 x86 汇编语句,放在一个循环中执行测试。最终,我发现了一段运行20次就会崩溃的代码。这一刻我的心情犹如“鸟渴催宵漏,鸡鸣引曙光”描述的一样;

7.接下来我逐步分析这段汇编代码,发现了8个处除以0的操作。真相只有一个!一个数除以0结果是无穷大,这在电脑中被视作一种异常,同时无穷大在浮点运算中会被记作NaN。但是,还有未解的疑问!

8.手工测试除以 0 的汇编代码并不会出现崩溃。写一个除以0的运算,并且循环20次仍然不会崩溃;但是当我在这20次循环之后继续做一些运算,结果是错误的。

9.拼图只差最后一块;

10.我祭出了大招,打开 dbg 开始观察CPU 寄存器;

11.观察发现 x87的堆栈在缓慢增长,直到它最大容量(8 个浮点数),这里提到的x87 意思是 8087,它是专门用来进行浮点运算的辅助处理器,作为 8086的数字协处理器,很早之前就出现在现代电脑的 CPU 中;

12.x87对于除以0产生的异常硬件无法自动处理,在编译生成代码的过程中编译器会将所有浮点运算相关内容转化为 x87 的指令,同时还会添加处理除以0这种异常的相关代码,但是这次编译器的除以0代码没有清空 x87 的堆栈。

13.当 x87 的堆栈溢出后,没有抛出错误,而是对于所有浮点运算都返回 NaN。前面我们已经知道NaN也是除以0得到的结果(堆栈溢出是一种堆栈错误。当发生这个错误时,必须在编译器中清空堆栈,否则它会持续发生)

14.所以,当发生8次除以0之后, x87 堆栈满了,所有的后续在 x87 上的操作都会被视作除以0错误返回 NaN.

15.最终的解决代码只有一行:在处理除以0 的路径上增加清除 x87 堆栈的一行操作。

本文来自:

https://www.forbes.com/sites/quora/2017/11/14/the-most-interesting-bug-ive-fixed-in-my-programming-career-to-date/?sh=261e5509b826