Step to UEFI (207)汇编语言编写UEFI Application

使用汇编语言来编写UEFI Application 完全没有问题,理论上编写 DXE Driver 也没有问题。最近抽空研究了一下这个问题。

从网上的资料来说,可以使用 FASM来完成这个工作,在http://x86asm.net/articles/uefi-programming-first-steps/ 可以看到一篇名为 “UEFI Programming – First Steps”的文章。但是经过我的实验使用 FASM 最出来的EFI 在文件头部上(PE Header)就存在很大的问题,比如: Image Size 给出来的不正确,还有 BaseImage 给出的不正确。有可能是我没有使用正确的参数导致的,但是看起来研究会很麻烦,并且最终能否成功严重存疑于是放弃了。

接下来研究如何使用 Nasm 来实现编写一个 UEFI Application。在 https://hackerpulp.com/os/os-development-windows-1-building-uefi-applications-nasm/https://github.com/BrianOtto/nasm-uefi 找到了一个例子,经过修改可以正常工作。

1.原文是编写一个 BootLoader,所以最后完成显示之后Hang 住即可,这里我们需要正常返回,最终ASM 代码如下:

; Copyright 2018-2019 Brian Otto @ https://hackerpulp.com
; 
; Permission to use, copy, modify, and/or distribute this software for any 
; purpose with or without fee is hereby granted, provided that the above 
; copyright notice and this permission notice appear in all copies.
; 
; THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 
; REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 
; AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 
; INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 
; LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE 
; OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 
; PERFORMANCE OF THIS SOFTWARE.

; generate 64-bit code
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

codesize equ $ - $$

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

; contains the data that will be displayed
section .data
    ; this must be a Unicode string
    strHello db __utf16__ `Hello World !\n\r\0`

datasize equ $ - $$

上面代码很简单,就是根据传入的参数调用 EFI_SYSTEM_TABLE.ConOut 来完成字符串显示。特别的,根据之前的经验要在调用ConOut的时候多向堆栈压入一个8字节内容,否则模拟器会崩溃。

2.使用 Nasm(推荐使用 NASM 2.14 win64 或者更高版本)编译生成 OBJ 文件:

nasm -f win64 nasmuefi.asm

3.使用 Link 来生成 EFI 文件。原文使用的参数不全,导致编译出来的EFI 文件无法运行。经过研究可以使用下面的参数:

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 /BASE:0 /DRIVER NasmUEFI.obj

(可以加入/DEBUG 生成带有 PDB文件名信息的EFI 文件)

        这样我们得到了 NasmUEFI.EFI 文件,大小是 768 bytes.

可以在NT32 模拟器上运行,同样的我在实体机上验证过也可以正常运行。

可以看到,汇编语言可以用来编写UEFI Application,相比C语言来说复杂的多。

完整的代码下载:

Step to UEFI (206)EFI 文件研究(3):显示自定义字符串

目标:对于一个已经存在,但是没有 Source Code 的 EFI Application ,通过一种方法来插入代码实现在运行时显示自定义字符串。

前面关于 EFI 文件的研究提到对于一个 EFI Application 来说,运行后会把全部内容加载到内存中。如果我们能在EFI中找到足够大的 “缝隙” ,可以将代码插其中然后通过修改EntryPoint 处加入跳转到缝隙处我们的代码执行之后再跳转会继续执行。

经过观察,我发现“MZ”标志后有一段可以使用的“缝隙”,这次试验就在这里加入代码完成。

首先遇到的问题是:我们需要插入什么样的代码来完成显示。众所周知,UEFI 下需要使用ConOut的OutputString来实现显示。当然无法手工直接编写汇编语句,于是在 \MdePkg\Library\UefiApplicationEntryPoint\ApplicationEntryPoint.c文件中,直接插入要显示的代码:

EFI_STATUS
EFIAPI
_ModuleEntryPoint (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
{
   SystemTable->ConOut->OutputString(SystemTable->ConOut,L"www.lab-z.com\n\r");
   
  EFI_STATUS                 Status;

  
  if (_gUefiDriverRevision != 0) {
    //
    // Make sure that the EFI/UEFI spec revision of the platform is >= EFI/UEFI spec revision of the application.
    //
    if (SystemTable->Hdr.Revision < _gUefiDriverRevision) {
      return EFI_INCOMPATIBLE_VERSION;
    }
  }
…….省略……

我们通过在编译过程中插入 /FAsc /Od 指令的方法强制生成汇编代码。查看上面代码生成的内容如下:

$LN25:
  00000	48 89 5c 24 08	 mov	 QWORD PTR [rsp+8], rbx
  00005	57		 push	 rdi
  00006	48 83 ec 20	 sub	 rsp, 32			; 00000020H
; 46   :   
; 47   :     SystemTable->ConOut->OutputString(SystemTable->ConOut,L"www.lab-z.com\n\r");

  0000a	48 8b 42 40	 mov	 rax, QWORD PTR [rdx+64]
  0000e	48 8b da	 mov	 rbx, rdx
  00011	48 8b f9	 mov	 rdi, rcx
  00014	48 8d 15 00 00
	00 00		 lea	 rdx, OFFSET FLAT:??_C@_1CA@KGEHCEOJ@?$AAw?$AAw?$AAw?$AA?4?$AAl?$AAa?$AAb?$AA?9?$AAz?$AA?4?$AAc?$AAo?$AAm?$AA?6?$AA?$AN?$AA?$AA@
  0001b	48 8b c8	 mov	 rcx, rax
  0001e	ff 50 08	 call	 QWORD PTR [rax+8]
; File c:\buildbs\201903\mdepkg\library\uefibootservicestablelib\uefibootservicestablelib.c

最开始处标记的代码编译器生成的,不对应任何C语句。其中的 lea rdx,Offset 部分没有在编译阶段生成,所以为 00。从上面可以看出来,UEFI 调用Application 之后,ImageHandle 作为第一个参数放在RCX中,RDX中存放的第二个参数是指向SystemTable的指针。

接下来,我们修改Application 的 EntryPoint 为跳转,即从 0x2C0跳转到0x002。为了简单起见,使用NASM设计代码如下:

[BITS 64]
org 2C0h
jmp 0x2

Nasm 编译后再反编译,获得机器码如下:

C:\NASM>ndisasm -b 64 inst
00000000  E93DFDFFFF        jmp qword 0xfffffffffffffd42

接下来我们需要恢复现场,去掉前面ApplicationEntryPoint.c 插入的代码后再重新编译检查EFI中需要替换的代码。同样查看生成的ApplicationEntryPoint.cod文件:

_ModuleEntryPoint PROC					; COMDAT
; File c:\buildbs\201903\mdepkg\library\uefibootservicestablelib\uefibootservicestablelib.c

; 62   :   gBS = SystemTable->BootServices;

  00000	48 8b 42 60	 mov	 rax, QWORD PTR [rdx+96]
  00004	48 89 05 00 00
	00 00		 mov	 QWORD PTR gBS, rax
; File c:\buildbs\201903\mdepkg\library\uefiruntimeservicestablelib\uefiruntimeservicestablelib.c

; 46   :   gRT = SystemTable->RuntimeServices;

  0000b	48 8b 42 58	 mov	 rax, QWORD PTR [rdx+88]
  0000f	48 89 05 00 00
	00 00		 mov	 QWORD PTR gRT, rax
; File c:\buildbs\201903\mdepkg\library\uefibootservicestablelib\uefibootservicestablelib.c

; 50   :   gImageHandle = ImageHandle;

  00016	48 89 0d 00 00
	00 00		 mov	 QWORD PTR gImageHandle, rcx

就是说,我们需要将原来在 0x2C0处的值替换成跳转的代码 E9 7D FD FF FF 。原来处于0x2C0处的机器可以从COD文件中看到(特别注意,我们打开 EFI看到0x2C0处的机器是 48 8B 42 60 48 89 05 45 1B 00 00,这是OBJ文件在Link 之后的结果,和CL.EXE 直接生成的有所差别)。

之后,我们需要跳转到它的下一条语句(mov QWORD PTR gBS, rax这个的下一条)在 0x2C0+ 0xB。

有了上面的经验接下来设计位于 0x02 处的代码:

BITS 64
DEFAULT REL
nop
nop     ;还可以使用ORG 2 设定编译的起始偏移,这里使用NOP 是为了便于反编译观察
;EFI文件之前EnteryPoint处的动作我们需要重新做一次
DB 0x48,0x8b,0x42,0x60        ; mov rax, QWORD PTR [rdx+96]
DB 0x48,0x89,0x05,0x03,0x1E,0x00,0x00   ;mov QWORD PTR gBS, rax

push rdi ;为了保证OutputString()正常工作,需要多Push 8Bytes
push rcx
push rdx
sub rsp,80 ;从实践上来看,下面的函数会损坏堆栈
mov rax,[dword rdx+64] ;SystemTable->ConOut
lea rdx,[MyString]
mov rcx,rax
call [rax+8]   ;Call SystemTable->ConOut->OutputString()
add rsp,80
pop  rdx
pop  rcx
pop  rdi

jmp 0x2CB

MyString:
    db "www.lab-z.com",0

直接使用 Nasm inst.asm 即可编译,编译后再使用 ndisasm -b 64 inst反编译查看:

可以看到反编译后,前面的指令都是能够和汇编代码对应上的。接下来就是直接将上面的代码写入从 0x02开始的EFI文件中。

之后可以在 NT32模拟器上运行,同样也可以在实体机上运行:

接下来介绍解决这个问题的时候遇到的各种坑:

1.0x3C 处的0xB8,这是属于Dos Header 上的 E_lfanew ,给出了 PE Header 的起始位置,这里是不可以覆盖的,否则会产生 Load Error。如果你的代码很大,有可能会不小心覆盖掉,这样会导致EFI Image 加载错误。这也是为什么这次试验字符串放置在0x40的原因;

2.代码起始处的 push rdi , 不一定是RDI,任何8Bytes的寄存器都可以,但是如果没有这语句在调用ConOut的时候这个函数内部会发生错误;

3.实践发现在调用 SystemTable->ConOut 的时候会损坏堆栈,比如下面的调用方式(按道理2个参数的情况下是通过寄存器来进行参数传递的):

push rdx 
 Call ConOut
pop rdx 

执行后会导致 rdx 内容的损坏。因此,这里采用先做PUSH,之后  sub rsp,80 把堆栈指针移开这样的操作来避免堆栈内容的损坏。

4.修改之前的 EFI文件EntryPoint 处的 48 89 05 45 1B 00 00 (mov QWORD PTR gBS, rax),这是一个相对的操作,搬移到其他地址之后需要重新修正。比如,之前这个指令位于 0x2C4:

实际访问到的位置在 0x2C4 + 7 (这个指令的长度) + 0x1B45=0x1E10;当这个指令被移动到0x006 处执行时,需要重新计算其中的偏移: 0x1E10-7-0x006=0x1E03,所以对应机器码如下:

上述使用【参考1】提供的反编译工具。此外,还有 ConOut 中字符串偏移的计算方法与此类似,有兴趣的朋友可以手工试验。

上面的问题都可以使用之前提到的动态调试方法进行查看验证,特别是一些偏移计算上,如果不是很确定可以通过 UltraEdit 进行编辑不断试验和调整。

本文提到的Hello.EFI 原始文件和修改后的文件下载:

参考:

1. https://defuse.ca/online-x86-assembler.htm#disassembly2

ESP32 实现蓝牙键盘

手上有一块 DFRobot 出品的 FireBeelte,它的主控芯片是 ESP32 自带 WIFI 和 蓝牙,因此可以直接模拟成蓝牙键盘。

首先需要安装ESP32 BLE for Arduino这个库,在https://github.com/nkolban/ESP32_BLE_Arduino

然后安装Bluetooth LE Keyboard 这个库,在 https://github.com/T-vK/ESP32-BLE-Keyboard

之后,还需要修改\ESP32-BLE-Keyboard-master\BleKeyboard.cpp 文件,在前面插入  HIDINPUT 和HIDOUTPUT 的定义,否则编译会报错:

#if defined(CONFIG_ARDUHAL_ESP_LOG)
  #include "esp32-hal-log.h"
  #define LOG_TAG ""
#else
  #include "esp_log.h"
  static const char* LOG_TAG = "BLEDevice";
#endif

//LABZ_Debug_Start
#define HIDINPUT(size)             (0x80 | size)
#define HIDOUTPUT(size)            (0x90 | size)
//LABZ_Debug_End

// Report IDs:
#define KEYBOARD_ID 0x01
#define MEDIA_KEYS_ID 0x02

之后打开ESP32-BLE-Keyboard-master\examples\SendKeyStrokes  中的示例文件编译上传即可。

Step to UEFI (205)NT32下动态查看 Application(下)

前面提到了使用 Image Size 作为CPU Debug Break的触发条件,相比使用Image 的Size作为触发条件,使用 Image Name 作为触发条件要方便很多,每次只需要重新编译Application 然后运行之即可,因此这里研究如何实现。

首先要解决的是哪里取得Image Name。通过观察可以得知当我们运行 NT32 模拟器时,每次调用 EFI Application时候会在 Debug 窗口显示加载的Image 名称:

这个显示的功能位于 \MdeModulePkg\Core\Dxe\Image\Image.c如下函数中:

EFI_STATUS
CoreLoadPeImage (
  IN BOOLEAN                     BootPolicy,
  IN VOID                        *Pe32Handle,
  IN LOADED_IMAGE_PRIVATE_DATA   *Image,
  IN EFI_PHYSICAL_ADDRESS        DstBuffer    OPTIONAL,
  OUT EFI_PHYSICAL_ADDRESS       *EntryPoint  OPTIONAL,
  IN  UINT32                     Attribute
  ) 

具体代码如下:

    DEBUG ((DEBUG_INFO | DEBUG_LOAD,
           "Loading driver at 0x%11p EntryPoint=0x%11p ",
           (VOID *)(UINTN) Image->ImageContext.ImageAddress,
           FUNCTION_ENTRY_POINT (Image->ImageContext.EntryPoint)));

    //
    // Print Module Name by Pdb file path.
    // Windows and Unix style file path are all trimmed correctly.
    //
    if (Image->ImageContext.PdbPointer != NULL) {
      StartIndex = 0;
      for (Index = 0; Image->ImageContext.PdbPointer[Index] != 0; Index++) {
        if ((Image->ImageContext.PdbPointer[Index] == '\\') || (Image->ImageContext.PdbPointer[Index] == '/')) {
          StartIndex = Index + 1;
        }
      }
      //
      // Copy the PDB file name to our temporary string, and replace .pdb with .efi
      // The PDB file name is limited in the range of 0~255.
      // If the length is bigger than 255, trim the redudant characters to avoid overflow in array boundary.
      //
      for (Index = 0; Index < sizeof (EfiFileName) - 4; Index++) {
        EfiFileName[Index] = Image->ImageContext.PdbPointer[Index + StartIndex];
        if (EfiFileName[Index] == 0) {
          EfiFileName[Index] = '.';
        }
        if (EfiFileName[Index] == '.') {
          EfiFileName[Index + 1] = 'e';
          EfiFileName[Index + 2] = 'f';
          EfiFileName[Index + 3] = 'i';
          EfiFileName[Index + 4] = 0;
          break;
        }
      }

      if (Index == sizeof (EfiFileName) - 4) {
        EfiFileName[Index] = 0;
      }
      DEBUG ((DEBUG_INFO | DEBUG_LOAD, "%a", EfiFileName)); // &Image->ImageContext.PdbPointer[StartIndex]));
    }
    DEBUG ((DEBUG_INFO | DEBUG_LOAD, "\n"));

  DEBUG_CODE_END ();

简单的说,有些 Application包含了 PDB 信息的EFI 文件可以从Image中获得文件名称。

第一个关键位置在于 Image->ImageContext.PdbPointer,其中的ImageContext 定义如下:

   /// PeCoffLoader ImageContext
  PE_COFF_LOADER_IMAGE_CONTEXT  ImageContext;
  PE_COFF_LOADER_IMAGE_CONTEXT  结构体定义在 \MdePkg\Include\Library\PeCoffLib.h 文件中:
///
/// The context structure used while PE/COFF image is being loaded and relocated.
///
typedef struct {
…..省略…..
  ///
  /// Set by PeCoffLoaderLoadImage() to point to the PDB entry contained in the CodeView area.
  /// The PdbPointer points to the filename of the PDB file used for source-level debug of
  /// the image by a debugger.
  ///
  CHAR8                             *PdbPointer; 
…..省略…..
} PE_COFF_LOADER_IMAGE_CONTEXT;

在\MdePkg\Library\BasePeCoffGetEntryPointLib\PeCoffGetEntryPoint.c 有定义如下函数用来取得这个指针:

/**
  Returns a pointer to the PDB file name for a PE/COFF image that has been
  loaded into system memory with the PE/COFF Loader Library functions.

  Returns the PDB file name for the PE/COFF image specified by Pe32Data.  If
  the PE/COFF image specified by Pe32Data is not a valid, then NULL is
  returned.  If the PE/COFF image specified by Pe32Data does not contain a
  debug directory entry, then NULL is returned.  If the debug directory entry
  in the PE/COFF image specified by Pe32Data does not contain a PDB file name,
  then NULL is returned.
  If Pe32Data is NULL, then ASSERT().

  @param  Pe32Data   The pointer to the PE/COFF image that is loaded in system
                     memory.

  @return The PDB file name for the PE/COFF image specified by Pe32Data or NULL
          if it cannot be retrieved.

**/
VOID *
EFIAPI
PeCoffLoaderGetPdbPointer (
  IN VOID  *Pe32Data
  )

    if (Magic == EFI_IMAGE_NT_OPTIONAL_HDR32_MAGIC) {
      //
      // Use PE32 offset get Debug Directory Entry
      //
      NumberOfRvaAndSizes = Hdr.Pe32->OptionalHeader.NumberOfRvaAndSizes;
      DirectoryEntry = (EFI_IMAGE_DATA_DIRECTORY *)&(Hdr.Pe32->OptionalHeader.DataDirectory[EFI_IMAGE_DIRECTORY_ENTRY_DEBUG]);
      DebugEntry     = (EFI_IMAGE_DEBUG_DIRECTORY_ENTRY *) ((UINTN) Pe32Data + DirectoryEntry->VirtualAddress);

使用 SFF 分析,上面的位置是下面绿色框中 Data Directories[x] 中的EFI_IMAGE_DIRECTORY_ENTRY_DEBUG (该值为6),即右侧红色框中的值。可以看到在文件中的0x1AD0位置,大小为0x54。

继续使用 SFF 可以直接查看 Debug Directory 的内容:

直接查看 0x1B24 位置就可以看到信息:

PeCoffLoaderGetPdbPointer函数对应的代码上有一个扫描的动作,最终确定 PDB File Name:

  //
  // Scan the directory to find the debug entry.
  //
  for (DirCount = 0; DirCount < DirectoryEntry->Size; DirCount += sizeof (EFI_IMAGE_DEBUG_DIRECTORY_ENTRY), DebugEntry++) {
    if (DebugEntry->Type == EFI_IMAGE_DEBUG_TYPE_CODEVIEW) {
      if (DebugEntry->SizeOfData > 0) {
        CodeViewEntryPointer = (VOID *) ((UINTN) DebugEntry->RVA + ((UINTN)Pe32Data) + (UINTN)TEImageAdjust);
        switch (* (UINT32 *) CodeViewEntryPointer) {
        case CODEVIEW_SIGNATURE_NB10:
          return (VOID *) ((CHAR8 *)CodeViewEntryPointer + sizeof (EFI_IMAGE_DEBUG_CODEVIEW_NB10_ENTRY));
        case CODEVIEW_SIGNATURE_RSDS:
          return (VOID *) ((CHAR8 *)CodeViewEntryPointer + sizeof (EFI_IMAGE_DEBUG_CODEVIEW_RSDS_ENTRY));
        case CODEVIEW_SIGNATURE_MTOC:
          return (VOID *) ((CHAR8 *)CodeViewEntryPointer + sizeof (EFI_IMAGE_DEBUG_CODEVIEW_MTOC_ENTRY));
        default:
          break;
        }
      }
    }
  }

根据上面的代码,最终代码如下,就是根据上面的代码取出PDB文件名,然后通过比较确定加载的Image是否触发BreakPoint。

EFI_STATUS
EFIAPI
CoreStartImage (
  IN EFI_HANDLE  ImageHandle,
  OUT UINTN      *ExitDataSize,
  OUT CHAR16     **ExitData  OPTIONAL
  )
……省略……
  SetJumpFlag = SetJump (Image->JumpContext);
  //
  // The initial call to SetJump() must always return 0.
  // Subsequent calls to LongJump() cause a non-zero value to be returned by SetJump().
  //
  if (SetJumpFlag == 0) {
    RegisterMemoryProfileImage (Image, (Image->ImageContext.ImageType == EFI_IMAGE_SUBSYSTEM_EFI_APPLICATION ? EFI_FV_FILETYPE_APPLICATION : EFI_FV_FILETYPE_DRIVER));
    //LABZDEBUG_Start
                UINTN Index;
                UINTN StartIndex;
                CHAR8 EfiFileName[256];
            //
            // Print Module Name by Pdb file path.
            // Windows and Unix style file path are all trimmed correctly.
            //
            if (Image->ImageContext.PdbPointer != NULL) {
              StartIndex = 0;
              for (Index = 0; Image->ImageContext.PdbPointer[Index] != 0; Index++) {
                if ((Image->ImageContext.PdbPointer[Index] == '\\') || (Image->ImageContext.PdbPointer[Index] == '/')) {
                  StartIndex = Index + 1;
                }
              }
              //
              // Copy the PDB file name to our temporary string, and replace .pdb with .efi
              // The PDB file name is limited in the range of 0~255.
              // If the length is bigger than 255, trim the redudant characters to avoid overflow in array boundary.
              //
              for (Index = 0; Index < sizeof (EfiFileName) - 4; Index++) {
                EfiFileName[Index] = Image->ImageContext.PdbPointer[Index + StartIndex];
                if (EfiFileName[Index] == 0) {
                  EfiFileName[Index] = '.';
                }
                if (EfiFileName[Index] == '.') {
                  EfiFileName[Index + 1] = 'e';
                  EfiFileName[Index + 2] = 'f';
                  EfiFileName[Index + 3] = 'i';
                  EfiFileName[Index + 4] = 0;
                  break;
                }
              }

              if (Index == sizeof (EfiFileName) - 4) {
                EfiFileName[Index] = 0;
              }
              DEBUG ((DEBUG_INFO , "%a\n", EfiFileName)); 
              if (AsciiStrCmp(EfiFileName,"Hello.efi")==0) {
                   CpuBreakpoint();
              }
            }
    //LABZDEBUG_End
    //
    // Call the image's entry point
    //
    Image->Started = TRUE;
Image->Status = Image->EntryPoint (ImageHandle, Image->Info.SystemTable);
……省略……

同样的方法可以用于判定当前的Image是什么文件上,简单的说就是向上,找到“MZ”头文件,然后再去查找 PDB 的信息从中确认文件名称。这样的方法对于使用 WinDBG/DCI 调试Windows 驱动同样有效。

Step to UEFI (204)NT32下动态查看 Application(上)

最近在研究 UEFI Application结构相关的内容。除了静态的分析,还需要找到一种观察和调试加载到内存后EFI文件的方法。经过比较和研究, NT32 模拟器是很好的选择。通过它能够方便的进行观察和反编译。

需要解决的第一个问题是:找到跳转到Application Entry Point 处的代码。经过研究入口位于  \MdeModulePkg\Core\Dxe\Image\Image.c  文件中下面这个函数:

EFI_STATUS
EFIAPI
CoreStartImage (
  IN EFI_HANDLE  ImageHandle,
  OUT UINTN      *ExitDataSize,
  OUT CHAR16     **ExitData  OPTIONAL
  )

前面准备妥当后,在下面的语句中跳转到Application Entry来开始执行:

    //
    // Call the image's entry point
    //
    Image->Started = TRUE;
    Image->Status = Image->EntryPoint (ImageHandle, Image->Info.SystemTable);

接下来研究如何实现如何触发,这里采用根据文件大小的方式进行触发。同样的上面这个函数中,申明了下面这个变量:

  LOADED_IMAGE_PRIVATE_DATA     *Image;

这个结构体定义在 \MdePkg\Include\Protocol\LoadedImage.h

typedef struct {
  UINTN                       Signature;
  /// Image handle
  EFI_HANDLE                  Handle;
  /// Image type
  UINTN                       Type;
  /// If entrypoint has been called
  BOOLEAN                     Started;
  /// The image's entry point
  EFI_IMAGE_ENTRY_POINT       EntryPoint;
  /// loaded image protocol
  EFI_LOADED_IMAGE_PROTOCOL   Info;
  /// Location in memory
  EFI_PHYSICAL_ADDRESS        ImageBasePage;
  /// Number of pages
  UINTN                       NumberOfPages;
  /// Original fixup data
  CHAR8                       *FixupData;
  /// Tpl of started image
  EFI_TPL                     Tpl;
  /// Status returned by started image
  EFI_STATUS                  Status;
  /// Size of ExitData from started image
  UINTN                       ExitDataSize;
  /// Pointer to exit data from started image
  VOID                        *ExitData;
  /// Pointer to pool allocation for context save/restore
  VOID                        *JumpBuffer;
  /// Pointer to buffer for context save/restore
  BASE_LIBRARY_JUMP_BUFFER    *JumpContext;
  /// Machine type from PE image
  UINT16                      Machine;
  /// EBC Protocol pointer
  EFI_EBC_PROTOCOL            *Ebc;
  /// Runtime image list
  EFI_RUNTIME_IMAGE_ENTRY     *RuntimeData;
  /// Pointer to Loaded Image Device Path Protocol
  EFI_DEVICE_PATH_PROTOCOL    *LoadedImageDevicePath;
  /// PeCoffLoader ImageContext
  PE_COFF_LOADER_IMAGE_CONTEXT  ImageContext;
  /// Status returned by LoadImage() service.
  EFI_STATUS                  LoadImageStatus;
} LOADED_IMAGE_PRIVATE_DATA;

其中的 EFI_LOADED_IMAGE_PROTOCOL 包含了当前的 Image Size 信息:

///
/// Revision defined in EFI1.1.
///
#define EFI_LOADED_IMAGE_INFORMATION_REVISION    EFI_LOADED_IMAGE_PROTOCOL_REVISION
///
/// Can be used on any image handle to obtain information about the loaded image.
///
typedef struct {
  UINT32            Revision;       ///< Defines the revision of the EFI_LOADED_IMAGE_PROTOCOL structure.
                                    ///< All future revisions will be backward compatible to the current revision.
  EFI_HANDLE        ParentHandle;   ///< Parent image's image handle. NULL if the image is loaded directly from
                                    ///< the firmware's boot manager.
  EFI_SYSTEM_TABLE  *SystemTable;   ///< the image's EFI system table pointer.

  //
  // Source location of image
  //
  EFI_HANDLE        DeviceHandle;   ///< The device handle that the EFI Image was loaded from.
  EFI_DEVICE_PATH_PROTOCOL  *FilePath;  ///< A pointer to the file path portion specific to DeviceHandle
                                        ///< that the EFI Image was loaded from.
  VOID              *Reserved;      ///< Reserved. DO NOT USE.

  //
  // Images load options
  //
  UINT32            LoadOptionsSize;///< The size in bytes of LoadOptions.
  VOID              *LoadOptions;   ///< A pointer to the image's binary load options.

  //
  // Location of where image was loaded
  //
  VOID              *ImageBase;     ///< The base address at which the image was loaded.
  UINT64            ImageSize;      ///< The size in bytes of the loaded image.
  EFI_MEMORY_TYPE   ImageCodeType;  ///< The memory type that the code sections were loaded as.
  EFI_MEMORY_TYPE   ImageDataType;  ///< The memory type that the data sections were loaded as.
  EFI_IMAGE_UNLOAD  Unload;
} EFI_LOADED_IMAGE_PROTOCOL;

我们使用代码中的 Hello.EFI 作为例子,它的大小是7712bytes。最终,代码如下:

  SetJumpFlag = SetJump (Image->JumpContext);
  //
  // The initial call to SetJump() must always return 0.
  // Subsequent calls to LongJump() cause a non-zero value to be returned by SetJump().
  //
  if (SetJumpFlag == 0) {
    RegisterMemoryProfileImage (Image, (Image->ImageContext.ImageType == EFI_IMAGE_SUBSYSTEM_EFI_APPLICATION ? EFI_FV_FILETYPE_APPLICATION : EFI_FV_FILETYPE_DRIVER));
    //LABZDEBUG_Start
        DEBUG ((EFI_D_INFO,"Current size [%d] bytes\n", Image->Info.ImageSize));
        if (Image->Info.ImageSize == 7712) {
                CpuBreakpoint();
        }
    //LABZDEBUG_End
    //
    // Call the image's entry point
    //
    Image->Started = TRUE;
Image->Status = Image->EntryPoint (ImageHandle, Image->Info.SystemTable);

就是说当发现加载的Image 大小是 7712 bytes 的时候自动触发一个 Breakpoint 打开 VS 进行调试:

接下来我们可以跳入Image 的领空进行查看和调试了。但是显而易见,这样的方法并不完美,如果Image 大小有变化,我们就需要重新编译运行 NT32 模拟器。后面会介绍如何使用文件名称来作为触发的判定条件,有兴趣的朋友可以尝试自己先进性研究。

MemTest86 EFI版

最近需要进行memory stress 测试,于是到 MemTest86 网站上进行查看:

https://www.memtest86.com/index.html

目前他们提供了几个版本供下载,具体区别如下:

来自 https://www.memtest86.com/features.htm

可以看出对于普通测试,免费版已经足够。当然,如果有条件还是建议购买 Pro 版本。

运行测试的方法是:

1.从 https://www.memtest86.com/download.htm 下载免费版

2.解压之后的 Boot 目录内容就是可以在 UEFI Shell 下运行的Application

3.根据需求,从 Shell 启动后进入 Boot 目录选择 32位或者 64位的版本执行即可。

特别注意,这个软件开始运行之后比较慢,请耐心等待。

这里提供一个打包的版本,是 MemTest86 v8.2 Free Edition

Step to UEFI (203)CR0 概述

继续研究基础的X86 寄存器。这次研究一下 CR0。在【参考1】可以看到 CR0 的简单介绍如下:

这里我第一次听说 MSW 寄存器,特地去搜索了一下“ Intel在80286 CPU中引入了一个16位的机器状态字寄存器MSW。在80386及其后续的CPU中已经把MSW扩展为4个32位控制寄存器CR0、CR1、CR2和CR3,原来的MSW功能由CR0的低16位来实现。”【参考2】简单的说就是一个历史遗留下来的寄存器,功能已经被 CR0 取代了。

接下来介绍每一个Bit(这部分根据《x86/x64 体系探索及编程》第六章 处理器的控制寄存器,编写),只是大概的介绍,如果想进一步了解推荐购买这本书。

参考:

  1. http://sandpile.org/x86/crx.htm
  2. https://baike.baidu.com/item/MSW/22602373?fr=aladdin

UEFI 下 RU.EFI 工具介绍

Ru 是AMI 的 James Wang(个人主页http://ruexe.blogspot.tw/)推出的系列工具,其中包括IA32 X64版本的Ru.EFI和一个 Windows 版本的 RU.EXE。最新版本可以在https://github.com/JamesAmiTw/ru-uefi 下载到。

本文简单介绍它的功能和使用。

UEFI X64 版本初始界面如下:

  1. File 菜单,提供了 Load
    Save 和 Compare 功能,可以保存和读取当前页面的数据,一般情况下不会使用,主要原因是操作没有 Windows 下方便。

2.Config 菜单用来进行寄存器的访问。

2.1 PCI 可以访问 PCI 设备

选中后输入要查看设备的 Bus Dev Func:

2.2 ISA

ISA IO是 Index/Data 这种形式的访问IO Port:

2.3 ISA IO

选择之后会出现2个选项让你选择:

例如,下面是查看 CMOS 的结果。个人感觉这个和上面的 ISA 选项通过手动输入端口号没有差别。

2.4 IO Space

选择后出现额外菜单如下:

选择 Normal IO Space 后会要求输入要查看的端口。

2.5 IDE Identify。这个功能能够查看当前系统中的SATA接口的硬盘信息(PCIE接口的SSD不行)。

选中后会出现提示继续选择要查看的硬盘。

之后可以查看硬盘信息。

额外说一句:不知道什么原因,这个功能和截图软件(CrScreenshotDxe)有冲突,运行这个功能后,会导致死机。本篇的截图都是使用 HDMI 转 USB 设备完成的。

2.6 ACPI

选中后可以看到系统中的 ACPI Table

能够查看每一项Table

2.7 Memory (Flat)

选择这个项目后输入要查看的内存位置即可:

特别注意,目前只支持查看最高 4G的内存。

2.8 CPU MSR

这个功能只能查看固定几个 MSR 的值。并且在我试验的机台上有问题无法正常看到。如果确实有这方面的需求,建议使用 Rw Everything 这样工具。

2.9 SMBIOS

查看本机的SMBIOS:

2.10 SMBus

选择这个功能后,会要求选择你的PCH。

接下来要选择你要查看的设备。

我的测试机台没有SPD,因此没有进一步试验。

2.11 ATi PCI Space

因为目前没有 ATI 主板所以没有进行试验。

2.12 UEFI variable

这个功能可以用来查看 UEFI 的变量,感觉上似乎不太准,推荐用 Shell 下的命令直接进行查看。

2.13 AHCI MMIO

这个功能在我的Kabylake-U 上无法工作,死机。

2.14 USB MMIO

这个功能这个功能在我的Kabylake-U 上无法工作,调不出来。

3.Go 菜单。

3.1 Next 切换

查看下一个项目,比如:当前显示的是 PCI Bus0 Dev0 FunC 0,那么下一个设备是 Bus:0 Dev 0 Func 2。

3.2 Previous 切换

查看上一个项目。

3.3 Clear all history

不知道什么意思

3.4 Mem/IO space

不知道什么意思。

4. Tools 菜单

 4.1 List PCI devices

这个选项能够列出当前系统中的全部 PCI 设备,快捷键是 F6。

4.2 Toggle 8/16/32 bit

使得当前界面在 1Byte 2Bytes 4bytes 显示之间切换,快捷键是 F7 。在查看寄存器时是很有用的功能。

4.2 Toggle ASCII/Info

切换界面在 ASCII显示和信息显示模式切换,快捷键为 F4。例如:当前是ASCII显示模式

切换为信息显示模式:

4.3 System Info

  显示当前系统基本信息

4.4 Help

显示帮助信息

5.System Group

5.1 Change Color

               切换界面配色

           通常看到的是 Normal 模式,User Defined 是黑白的

  5.2 Toggle Sound

   我的主板上没有 Beeper 所以不得而知是否有效果

5.3Reboot INT19

通过 INT19 启动下一个设备,对于 UEFI 是无效的

5.4 Turn Off system

关机,在 UEFI 下是无效的

6.Quit  退出 RU

从上面也可以看到,对于 X86 来说,访问硬件信息需要的基本操作有:

1. PCI 信息的访问

2. 访问IO Port 直接访问

3. IO Port Index/Data 方式的访问

4. Memory 的访问

5. Memory Index/Data 方式的访问

6. MSR 的访问

掌握了上述的访问方法就可以触碰到 X86 上的全部空间和寄存器了。

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

2025年2月27日

Teensy 打造热成像仪

当我还是一个孩子的时候,荧幕上充斥的还是史泰龙施瓦辛格这样的硬汉,他们用肌肉而不是今天这样的小鲜肉那样用兰花指打败敌人,看起来非常过瘾。一次偶然的机会,在父亲的朋友家看了电视台播出的《铁血战士》。这部影片给我留下很深的印象,神秘的丛林,还有影片中拥有着高科技武器的外星人。譬如说,大反派的铁血战士有一个很酷的带在手腕上能显示当前心率的个人电脑。还有就是他可以切换到红外线模式,看到人类无法看到的红外线。这样的功能在夜战中尽占便宜。最后施瓦辛格州长大人凭借主角光环发现了对手这一特性,用泥巴将自己包裹起来阻挡身体上的红外线躲过搜查实现反杀。

最近发现市面上有一种红外阵列传感器:MLX90640 ,分辨率可以达到 32*24。忽发奇想尝试用它DIY一个简易的热成像仪。MLX90640对内存要求比较高,普通的 Arduino 无法胜任。最终选用  Teensy 3.2 作为主控,主要原因是它性能远超 Arduino ,主频可以达到 120Mhz (Overclock状态)。为了实现在液晶屏上面的显示,需要主控能够快速的将数据发送到屏幕上,否则刷新速度慢非常影响效果。此外,Teensy 3.2内存有 64KB,取得数据和处理数据需要在足够的内存中进行,因此大内存必不可少。之后,从淘宝入手了 320*240 分辨率的SPI 接口的液晶屏作为显示界面。硬件方案确定之后即可着手进行代码编写。

从整体上来说,代码操作需要三部分:第一步,获得传感器数据;第二步,处理数据(因为屏幕明显比传感器大,因此,需要将少量数据变换成大量数据,这里使用的是双线性插值算法);第三步,将数据显示在屏幕上。下面就是详细步骤:

第一步:传感器数据的获得。MLX90640 有对应的 Arduino 库,这使得用户能够方便的获得红外数据。特别需要注意的取得的数据是从右上到左下排列的。

这部分代码很简单,配合调用库函数即可:

  for (byte x = 0 ; x < 2 ; x++){
    uint16_t mlx90640Frame[834];
    int status = MLX90640_GetFrameData(MLX90640_address, mlx90640Frame);

    float vdd = MLX90640_GetVdd(mlx90640Frame, &mlx90640);
    float Ta = MLX90640_GetTa(mlx90640Frame, &mlx90640);

    float tr = Ta - TA_SHIFT; //Reflected temperature based on the sensor ambient temperature
    float emissivity = 0.95;

    MLX90640_CalculateTo(mlx90640Frame, &mlx90640, emissivity, tr, mlx90640To);
  }

第二步,处理数据。传感器获得的实际数据是32X24,最终需要呈现在 320X240的LCD上。可以看到两者之间分辨率差别很大。这里就需要通过一些算法推理补充出更多的数据以便更好的观感。通俗的解释这个过程和“脑补”类似。例如,即便下面的图片上非常模糊,但是观看者还是会认出来这是著名的《蒙娜丽莎》。当人们看到画面之后,大脑会进行处理,处理的结果就是识别出来这幅名画。

这里选择双线性插值算法来完成这个目的。这种算法是假设中间的点收到周围的点均匀的影响。用一维的例子来说明这个算法,比如,已知下面 A 点的值为 100,B点的值为200,那么如果中间有一个C点,算法推理出来 C 点的值应该是 150。

有了上面的经验,推广到二维世界,下面图片中,已知Q(X,Y) 四个点的取值,想推算出 P 的取值。可以先推算出R1 R2的取值

最终再计算出 P 的取值【参考】

代码中使用同样的方法,将4个点插值为100个点,这样整体从 32×24=768个点变换为76800个点。这样的计算量对于 Teensy 来说并不是问题,可以在1ms 内处理完毕。

第三步,数据的现实。代码使用Arduino 的 UcgLib来驱动屏幕。UcgLib 没有提供按照帧发送数据的函数(当然如果有的话,因为数据量太大,也不可能在内存中存放一帧然后完整发送之)。所以使用 DrawPixel 这样的函数一个点一个点的在屏幕上绘制。这样就会牵扯到效率的问题需要进行优化。对此,使用了两个方法:

1.绝对的提升 SPI 传输速度。Teensy 默认频率为 72Mh,超频到 120Mhz 之后,SPI 可以提升到 30Mhz,代码中通过下面这个语句来实现:

  SPI.beginTransaction(SPISettings(30000000, MSBFIRST, SPI_MODE0));

2.相对的减少绘制数据量。通过查看UcgLib 的代码可以看到,绘制每一个点时,都会发送位置信息这对于用户来说是不必要的开销。所谓显示,就是将必要的数据送到LCD显示内存中。因此,代码设计了混合的方式。先用 DrawPixel 绘制Y坐标上的第一个点,之后直接发送这一行上其余各点的颜色信息。从实验数据来看,这样可以节省3/5的时间。此外,默认情况下库使用 18Bits 显示模式,每个点由三个Bytes来给出。此外还可以将屏幕设置为 16Bits显示模式,这样每个点可以由2个Bytes给出。但是相应的屏幕颜色数量会大大减少,经过试验显示效果不佳就放弃了。如果用户对速度有更高的要求,可以考虑使用这种模式。

最终成品如下,用亚克力制作了一个简单的外壳,下面是一个把柄,同时可以放置一个充电宝来为整体进行供电。顺便说一句,我原本的设计是要完全包裹起来的,后来计算失误导致尺寸上的差异才变成现象这个样子的…….

工作的视频可以在知乎专栏看到

https://zhuanlan.zhihu.com/p/76527719

最后,关于视力,讲个有意思的事情“当年,在日本海军部队,有这么一个特殊的群体:每天吃鳗鱼喝鱼肝油,服用特制的维生素A,目的是为了保持一双眼睛炯炯有神。他们是军舰上的瞭望员,夜战之前,他们像猴子一样爬得高高的,用一双肉眼在茫茫大海上搜索敌方舰只的踪影。

  据笔者考证,这个兵种仅仅存在于当年的日本海军。美国海军名将尼米茨特地为他的奇葩对手冠名为“猫眼”(CAT EYES LOOKOUT),因为猫的夜视能力极强嘛,可以异乎寻常地采集摄取到有效的光线,研究发现,猫的瞳孔在昏暗中可扩大至眼球表面的90%,一点微弱的光亮就足够它们觅取猎物。日本海军的瞭望员也练就了“猫眼神功”,在一片漆黑中,有效搜索远方的猎物。

  二战期间,日本政府征召一批作家从军组成“笔部队”,撰写战地报告文学或战地通信稿。“珍珠港事件”后,小说家丹羽文雄随日本海军参加了太平洋战争,他撰写过一篇在当时非常著名的报告文学《海战》,其中就描写了“猫眼神功”:“周围伸手不见五指,就只有手持望远镜的瞭望员的呼喊”左41度,发现一艘舰影”,”左30度,敌舰,敌舰向我而来”……”

  在丹羽文雄的笔下,充满了对这些征战太平洋的“勇士”们的崇敬,更是对猫眼瞭望员钦佩有加。“猫眼神功”有多厉害?据当时记载,他们在黑夜里能够看清万米前后的军舰,然后日军在8千米内以探照灯和水上飞机扔照明弹实施攻击。

不得不承认,日本民族的一大特点是能把事情做到极限,在舰队夜战这一块,日本人曾吹嘘“大日本帝国海军夜战技术誉满全球”,他们倒也有一些吹牛的本钱,日本海军以训练刻苦甚至残酷著称,兵员素质确实相当不错,且不论海军的炮术和操船技术,还是以“猫眼神功”为例,日本人练就了“斜眼”——人类的眼球上,夜间感光细胞的分布在眼球侧面的比眼球正面的多,所以,斜眼能在晚上看得更加清楚。”

起初,猫眼比雷达更厉害:

“美日瓜岛战役历时半年,共爆发数十次海战,争夺瓜岛的第一次海战爆发于1942年8月9日凌晨,夜战,一时许,日舰驶抵战场时,日军猫眼瞭望员确实厉害,先发现了两艘巡逻的美军驱逐舰,而装备新型雷达的美军驱逐舰却未能发现日舰。一直到日舰下达总攻击令十分钟后,美军才反应过来。当时日军实施了轻车熟路的夜战模式:日军的水上飞机投下了照明弹,将美舰照得清清楚楚,日军的炮弹和鱼雷接踵而来。

在瓜岛,这样的海战持续发生,总体而言,猫眼表现得不比雷达差。在1942年11月14日深夜发生的第二次瓜达尔卡纳尔海战中,美军于22点55分以雷达发现了日军舰队,5分钟后,猫眼瞭望员也发现美军舰队,又是一场在漆黑大海上的恶战。此战中,威利斯·李指挥的美军主力战斗舰南达科他号还发生了电力故障,无法使用雷达与火炮控制,自动装弹装置也失效,结果成了活靶子。

  后人分析说,瓜岛战役日军猫眼没怎么输给雷达,原因还是地理环境,当时美军所装备的雷达,虽然在洋面上搜索距离可以达到20海里,但是在瓜岛附近由于受陆地上山包的干扰,搜索距离一下子就降到了6000米。还有一个不可忽略的因素,是当时雷达的技术与雷达操纵者的水平——后者当然无法跟苦练猫眼技术的日本海军瞭望员相比。

  但是,“猫眼神功”再厉害,对于日本海军来说,也是个“然并卵”的结果。在日美双方历时半年多对瓜岛的残酷争夺中,均损失惨重,最终日本因无力进行消耗作战,而选择撤军。美军完全占据瓜岛,而后是整个南太平洋的制海权,因此开始战略反攻。在二战中,瓜岛之战是中途岛之后日本的再次失败,也是日本从战略优势走向劣势的转折点。

  此后日本海军仍然依靠“猫眼神功”与雷达对抗,但是已江河日下,屡战屡败。说来也是,人总有用眼过度的时候,训练有素的猫眼瞭望员也是死一个少一个,但雷达技术却在不断进步之中。”【参考】

所以,从我个人角度来说,更相信武器和物资是决定战争成败的主要因素…….

参考:

1. https://baike.baidu.com/item/%E5%8F%8C%E7%BA%BF%E6%80%A7%E6%8F%92%E5%80%BC  双线性插值

1. http://roll.sohu.com/20150918/n421429328.shtml 日本二战“猫眼神功”PK雷达之鉴(图)

Step to UEFI (202)IoRead 研究

IO Port 的访问是对硬件的最基本操作,在 UEFI Shell 下我们通过 IoLib 中的IoRead8 IoRead16 IoRead32 这样的来进行读取。本文介绍Shell 下的 IoRead8 的具体实现。

首先编写一个代码用于测试:

#include  <Uefi.h>
#include  <Library/UefiLib.h>
#include  <Library/ShellCEntryLib.h>
#include  <Library/IoLib.h>

extern  EFI_SYSTEM_TABLE    *gST;
extern  EFI_BOOT_SERVICES   *gBS;

/***
  Print a welcoming message.

  Establishes the main structure of the application.

  @retval  0         The application exited normally.
  @retval  Other     An error occurred.
***/
INTN
EFIAPI
ShellAppMain (
  IN UINTN Argc,
  IN CHAR16 **Argv
  )
{
        Print(L"Read 80Port=%x\n",IoRead8(0x80));

    return(0);
}

编译之后,在 \Build\AppPkg\DEBUG_VS2015x86\X64\AppPkg\Applications\IOStudy\IOStudy\Makefile 可以看到使用了下面的 Library:

$(BIN_DIR)\MdePkg\Library\BaseIoLibIntrinsic\BaseIoLibIntrinsic\OUTPUT\BaseIoLibIntrinsic.lib

进一步研究,在 \MdePkg\Library\BaseIoLibIntrinsic\IoLibMsc.c 可以看到如下的实现:

/**
  Reads an 8-bit I/O port.

  Reads the 8-bit I/O port specified by Port. The 8-bit read value is returned.
  This function must guarantee that all I/O read and write operations are
  serialized.

  If 8-bit I/O port operations are not supported, then ASSERT().

  @param  Port  The I/O port to read.

  @return The value read.

**/
UINT8
EFIAPI
IoRead8 (
  IN      UINTN                     Port
  )
{
  UINT8                             Value;

  _ReadWriteBarrier ();
  Value = (UINT8)_inp ((UINT16)Port);
  _ReadWriteBarrier ();
  return Value;
}

为了证明这里就是我们要找到的具体实现,可以在  \MdePkg\Library\BaseIoLibIntrinsic\BaseIoLibIntrinsic.inf 加入如下代码:

 [BuildOptions]
  MSFT:*_*_X64_CC_FLAGS  = /FAsc /Od

重新编译(同时还需要删除 \Build\AppPkg\DEBUG_VS2015x86\X64\MdePkg\Library 中的BaseIoLibIntrinsic),之后可以在\Build\AppPkg\DEBUG_VS2015x86\X64\AppPkg\Applications\IOStudy\IOStudy 看到多出来一个IoLibMsc.cod 文件。

IoRead8	PROC						; COMDAT

; 73   : {

$LN3:
  00000	48 89 4c 24 08	 mov	 QWORD PTR [rsp+8], rcx
  00005	48 83 ec 18	 sub	 rsp, 24

; 74   :   UINT8                             Value;
; 75   : 
; 76   :   _ReadWriteBarrier ();
; 77   :   Value = (UINT8)_inp ((UINT16)Port);

  00009	0f b7 54 24 20	 movzx	 edx, WORD PTR Port$[rsp]
  0000e	ec		 in	 al, dx
  0000f	88 04 24	 mov	 BYTE PTR Value$[rsp], al

; 78   :   _ReadWriteBarrier ();
; 79   :   return Value;

  00012	8a 04 24	 mov	 al, BYTE PTR Value$[rsp]

; 80   : }

  00015	48 83 c4 18	 add	 rsp, 24
  00019	c3		 ret	 0
IoRead8	ENDP

在这里可以证明确实是IoLibMsc.c 中实现的。

_inp() 的作用是“从某个端口输入一个字节 (_inp)、一个字 (_inpw) 或一个双字 (_inpd)。” 但是看起来目前已经过时【参考1】。

与这个函数类似,还可以使用__inword 来实现读取某一个 Port,__outword直接写入某一个Port【参考2】。

参考:

  1. https://docs.microsoft.com/zh-cn/cpp/c-runtime-library/inp-inpw-inpd?view=vs-2019这些函数已过时。 从 Visual Studio 2015 开始,CRT 中不再提供这些函数。
  2. https://docs.microsoft.com/zh-cn/cpp/intrinsics/inword?view=vs-2017

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

2024年2月22日 IoLib 提供了如下的函数:

#define IO_LIB_ADDRESS(Segment, Port) \
  ( ((Port) & 0xffff) | (((Segment) & 0xffff) << 16) )

UINT8
EFIAPI
IoRead8 (
  IN      UINTN  Port
  );

UINT8
EFIAPI
IoWrite8 (
  IN      UINTN  Port,
  IN      UINT8  Value
  );

VOID
EFIAPI
IoReadFifo8 (
  IN      UINTN  Port,
  IN      UINTN  Count,
  OUT     VOID   *Buffer
  );

VOID
EFIAPI
IoWriteFifo8 (
  IN      UINTN  Port,
  IN      UINTN  Count,
  IN      VOID   *Buffer
  );

UINT8
EFIAPI
IoOr8 (
  IN      UINTN  Port,
  IN      UINT8  OrData
  );

UINT8
EFIAPI
IoAnd8 (
  IN      UINTN  Port,
  IN      UINT8  AndData
  );

UINT8
EFIAPI
IoAndThenOr8 (
  IN      UINTN  Port,
  IN      UINT8  AndData,
  IN      UINT8  OrData
  );

UINT8
EFIAPI
IoBitFieldRead8 (
  IN      UINTN  Port,
  IN      UINTN  StartBit,
  IN      UINTN  EndBit
  );

UINT8
EFIAPI
IoBitFieldWrite8 (
  IN      UINTN  Port,
  IN      UINTN  StartBit,
  IN      UINTN  EndBit,
  IN      UINT8  Value
  );

UINT8
EFIAPI
IoBitFieldOr8 (
  IN      UINTN  Port,
  IN      UINTN  StartBit,
  IN      UINTN  EndBit,
  IN      UINT8  OrData
  );

UINT8
EFIAPI
IoBitFieldAnd8 (
  IN      UINTN  Port,
  IN      UINTN  StartBit,
  IN      UINTN  EndBit,
  IN      UINT8  AndData
  );

UINT8
EFIAPI
IoBitFieldAndThenOr8 (
  IN      UINTN  Port,
  IN      UINTN  StartBit,
  IN      UINTN  EndBit,
  IN      UINT8  AndData,
  IN      UINT8  OrData
  );

UINT16
EFIAPI
IoRead16 (
  IN      UINTN  Port
  );

UINT16
EFIAPI
IoWrite16 (
  IN      UINTN   Port,
  IN      UINT16  Value
  );

VOID
EFIAPI
IoReadFifo16 (
  IN      UINTN  Port,
  IN      UINTN  Count,
  OUT     VOID   *Buffer
  );

VOID
EFIAPI
IoWriteFifo16 (
  IN      UINTN  Port,
  IN      UINTN  Count,
  IN      VOID   *Buffer
  );

UINT16
EFIAPI
IoOr16 (
  IN      UINTN   Port,
  IN      UINT16  OrData
  );

UINT16
EFIAPI
IoAnd16 (
  IN      UINTN   Port,
  IN      UINT16  AndData
  );

UINT16
EFIAPI
IoAndThenOr16 (
  IN      UINTN   Port,
  IN      UINT16  AndData,
  IN      UINT16  OrData
  );

UINT16
EFIAPI
IoBitFieldRead16 (
  IN      UINTN  Port,
  IN      UINTN  StartBit,
  IN      UINTN  EndBit
  );

UINT16
EFIAPI
IoBitFieldWrite16 (
  IN      UINTN   Port,
  IN      UINTN   StartBit,
  IN      UINTN   EndBit,
  IN      UINT16  Value
  );

UINT16
EFIAPI
IoBitFieldOr16 (
  IN      UINTN   Port,
  IN      UINTN   StartBit,
  IN      UINTN   EndBit,
  IN      UINT16  OrData
  );

UINT16
EFIAPI
IoBitFieldAnd16 (
  IN      UINTN   Port,
  IN      UINTN   StartBit,
  IN      UINTN   EndBit,
  IN      UINT16  AndData
  );

UINT16
EFIAPI
IoBitFieldAndThenOr16 (
  IN      UINTN   Port,
  IN      UINTN   StartBit,
  IN      UINTN   EndBit,
  IN      UINT16  AndData,
  IN      UINT16  OrData
  );

UINT32
EFIAPI
IoRead32 (
  IN      UINTN  Port
  );

UINT32
EFIAPI
IoWrite32 (
  IN      UINTN   Port,
  IN      UINT32  Value
  );

VOID
EFIAPI
IoReadFifo32 (
  IN      UINTN  Port,
  IN      UINTN  Count,
  OUT     VOID   *Buffer
  );

VOID
EFIAPI
IoWriteFifo32 (
  IN      UINTN  Port,
  IN      UINTN  Count,
  IN      VOID   *Buffer
  );

UINT32
EFIAPI
IoOr32 (
  IN      UINTN   Port,
  IN      UINT32  OrData
  );

UINT32
EFIAPI
IoAnd32 (
  IN      UINTN   Port,
  IN      UINT32  AndData
  );

UINT32
EFIAPI
IoAndThenOr32 (
  IN      UINTN   Port,
  IN      UINT32  AndData,
  IN      UINT32  OrData
  );

UINT32
EFIAPI
IoBitFieldRead32 (
  IN      UINTN  Port,
  IN      UINTN  StartBit,
  IN      UINTN  EndBit
  );

UINT32
EFIAPI
IoBitFieldWrite32 (
  IN      UINTN   Port,
  IN      UINTN   StartBit,
  IN      UINTN   EndBit,
  IN      UINT32  Value
  );

UINT32
EFIAPI
IoBitFieldOr32 (
  IN      UINTN   Port,
  IN      UINTN   StartBit,
  IN      UINTN   EndBit,
  IN      UINT32  OrData
  );

UINT32
EFIAPI
IoBitFieldAnd32 (
  IN      UINTN   Port,
  IN      UINTN   StartBit,
  IN      UINTN   EndBit,
  IN      UINT32  AndData
  );

UINT32
EFIAPI
IoBitFieldAndThenOr32 (
  IN      UINTN   Port,
  IN      UINTN   StartBit,
  IN      UINTN   EndBit,
  IN      UINT32  AndData,
  IN      UINT32  OrData
  );

UINT64
EFIAPI
IoRead64 (
  IN      UINTN  Port
  );

UINT64
EFIAPI
IoWrite64 (
  IN      UINTN   Port,
  IN      UINT64  Value
  );

UINT64
EFIAPI
IoOr64 (
  IN      UINTN   Port,
  IN      UINT64  OrData
  );

UINT64
EFIAPI
IoAnd64 (
  IN      UINTN   Port,
  IN      UINT64  AndData
  );

UINT64
EFIAPI
IoAndThenOr64 (
  IN      UINTN   Port,
  IN      UINT64  AndData,
  IN      UINT64  OrData
  );

UINT64
EFIAPI
IoBitFieldRead64 (
  IN      UINTN  Port,
  IN      UINTN  StartBit,
  IN      UINTN  EndBit
  );

UINT64
EFIAPI
IoBitFieldWrite64 (
  IN      UINTN   Port,
  IN      UINTN   StartBit,
  IN      UINTN   EndBit,
  IN      UINT64  Value
  );

UINT64
EFIAPI
IoBitFieldOr64 (
  IN      UINTN   Port,
  IN      UINTN   StartBit,
  IN      UINTN   EndBit,
  IN      UINT64  OrData
  );

UINT64
EFIAPI
IoBitFieldAnd64 (
  IN      UINTN   Port,
  IN      UINTN   StartBit,
  IN      UINTN   EndBit,
  IN      UINT64  AndData
  );

UINT64
EFIAPI
IoBitFieldAndThenOr64 (
  IN      UINTN   Port,
  IN      UINTN   StartBit,
  IN      UINTN   EndBit,
  IN      UINT64  AndData,
  IN      UINT64  OrData
  );

UINT8
EFIAPI
MmioRead8 (
  IN      UINTN  Address
  );

UINT8
EFIAPI
MmioWrite8 (
  IN      UINTN  Address,
  IN      UINT8  Value
  );

UINT8
EFIAPI
MmioOr8 (
  IN      UINTN  Address,
  IN      UINT8  OrData
  );

UINT8
EFIAPI
MmioAnd8 (
  IN      UINTN  Address,
  IN      UINT8  AndData
  );

UINT8
EFIAPI
MmioAndThenOr8 (
  IN      UINTN  Address,
  IN      UINT8  AndData,
  IN      UINT8  OrData
  );

UINT8
EFIAPI
MmioBitFieldRead8 (
  IN      UINTN  Address,
  IN      UINTN  StartBit,
  IN      UINTN  EndBit
  );

UINT8
EFIAPI
MmioBitFieldWrite8 (
  IN      UINTN  Address,
  IN      UINTN  StartBit,
  IN      UINTN  EndBit,
  IN      UINT8  Value
  );

UINT8
EFIAPI
MmioBitFieldOr8 (
  IN      UINTN  Address,
  IN      UINTN  StartBit,
  IN      UINTN  EndBit,
  IN      UINT8  OrData
  );

UINT8
EFIAPI
MmioBitFieldAnd8 (
  IN      UINTN  Address,
  IN      UINTN  StartBit,
  IN      UINTN  EndBit,
  IN      UINT8  AndData
  );

UINT8
EFIAPI
MmioBitFieldAndThenOr8 (
  IN      UINTN  Address,
  IN      UINTN  StartBit,
  IN      UINTN  EndBit,
  IN      UINT8  AndData,
  IN      UINT8  OrData
  );

UINT16
EFIAPI
MmioRead16 (
  IN      UINTN  Address
  );

UINT16
EFIAPI
MmioWrite16 (
  IN      UINTN   Address,
  IN      UINT16  Value
  );

UINT16
EFIAPI
MmioOr16 (
  IN      UINTN   Address,
  IN      UINT16  OrData
  );

UINT16
EFIAPI
MmioAnd16 (
  IN      UINTN   Address,
  IN      UINT16  AndData
  );

UINT16
EFIAPI
MmioAndThenOr16 (
  IN      UINTN   Address,
  IN      UINT16  AndData,
  IN      UINT16  OrData
  );

UINT16
EFIAPI
MmioBitFieldRead16 (
  IN      UINTN  Address,
  IN      UINTN  StartBit,
  IN      UINTN  EndBit
  );

UINT16
EFIAPI
MmioBitFieldWrite16 (
  IN      UINTN   Address,
  IN      UINTN   StartBit,
  IN      UINTN   EndBit,
  IN      UINT16  Value
  );

UINT16
EFIAPI
MmioBitFieldOr16 (
  IN      UINTN   Address,
  IN      UINTN   StartBit,
  IN      UINTN   EndBit,
  IN      UINT16  OrData
  );

UINT16
EFIAPI
MmioBitFieldAnd16 (
  IN      UINTN   Address,
  IN      UINTN   StartBit,
  IN      UINTN   EndBit,
  IN      UINT16  AndData
  );

UINT16
EFIAPI
MmioBitFieldAndThenOr16 (
  IN      UINTN   Address,
  IN      UINTN   StartBit,
  IN      UINTN   EndBit,
  IN      UINT16  AndData,
  IN      UINT16  OrData
  );

UINT32
EFIAPI
MmioRead32 (
  IN      UINTN  Address
  );

UINT32
EFIAPI
MmioWrite32 (
  IN      UINTN   Address,
  IN      UINT32  Value
  );

UINT32
EFIAPI
MmioOr32 (
  IN      UINTN   Address,
  IN      UINT32  OrData
  );

UINT32
EFIAPI
MmioAnd32 (
  IN      UINTN   Address,
  IN      UINT32  AndData
  );

UINT32
EFIAPI
MmioAndThenOr32 (
  IN      UINTN   Address,
  IN      UINT32  AndData,
  IN      UINT32  OrData
  );

UINT32
EFIAPI
MmioBitFieldRead32 (
  IN      UINTN  Address,
  IN      UINTN  StartBit,
  IN      UINTN  EndBit
  );

UINT32
EFIAPI
MmioBitFieldWrite32 (
  IN      UINTN   Address,
  IN      UINTN   StartBit,
  IN      UINTN   EndBit,
  IN      UINT32  Value
  );

UINT32
EFIAPI
MmioBitFieldOr32 (
  IN      UINTN   Address,
  IN      UINTN   StartBit,
  IN      UINTN   EndBit,
  IN      UINT32  OrData
  );

UINT32
EFIAPI
MmioBitFieldAnd32 (
  IN      UINTN   Address,
  IN      UINTN   StartBit,
  IN      UINTN   EndBit,
  IN      UINT32  AndData
  );

UINT32
EFIAPI
MmioBitFieldAndThenOr32 (
  IN      UINTN   Address,
  IN      UINTN   StartBit,
  IN      UINTN   EndBit,
  IN      UINT32  AndData,
  IN      UINT32  OrData
  );

UINT64
EFIAPI
MmioRead64 (
  IN      UINTN  Address
  );

UINT64
EFIAPI
MmioWrite64 (
  IN      UINTN   Address,
  IN      UINT64  Value
  );

UINT64
EFIAPI
MmioOr64 (
  IN      UINTN   Address,
  IN      UINT64  OrData
  );

UINT64
EFIAPI
MmioAnd64 (
  IN      UINTN   Address,
  IN      UINT64  AndData
  );

UINT64
EFIAPI
MmioAndThenOr64 (
  IN      UINTN   Address,
  IN      UINT64  AndData,
  IN      UINT64  OrData
  );

UINT64
EFIAPI
MmioBitFieldRead64 (
  IN      UINTN  Address,
  IN      UINTN  StartBit,
  IN      UINTN  EndBit
  );

UINT64
EFIAPI
MmioBitFieldWrite64 (
  IN      UINTN   Address,
  IN      UINTN   StartBit,
  IN      UINTN   EndBit,
  IN      UINT64  Value
  );

UINT64
EFIAPI
MmioBitFieldOr64 (
  IN      UINTN   Address,
  IN      UINTN   StartBit,
  IN      UINTN   EndBit,
  IN      UINT64  OrData
  );

UINT64
EFIAPI
MmioBitFieldAnd64 (
  IN      UINTN   Address,
  IN      UINTN   StartBit,
  IN      UINTN   EndBit,
  IN      UINT64  AndData
  );

UINT64
EFIAPI
MmioBitFieldAndThenOr64 (
  IN      UINTN   Address,
  IN      UINTN   StartBit,
  IN      UINTN   EndBit,
  IN      UINT64  AndData,
  IN      UINT64  OrData
  );

UINT8 *
EFIAPI
MmioReadBuffer8 (
  IN  UINTN  StartAddress,
  IN  UINTN  Length,
  OUT UINT8  *Buffer
  );

UINT16 *
EFIAPI
MmioReadBuffer16 (
  IN  UINTN   StartAddress,
  IN  UINTN   Length,
  OUT UINT16  *Buffer
  );

UINT32 *
EFIAPI
MmioReadBuffer32 (
  IN  UINTN   StartAddress,
  IN  UINTN   Length,
  OUT UINT32  *Buffer
  );

UINT64 *
EFIAPI
MmioReadBuffer64 (
  IN  UINTN   StartAddress,
  IN  UINTN   Length,
  OUT UINT64  *Buffer
  );

UINT8 *
EFIAPI
MmioWriteBuffer8 (
  IN  UINTN        StartAddress,
  IN  UINTN        Length,
  IN  CONST UINT8  *Buffer
  );

UINT16 *
EFIAPI
MmioWriteBuffer16 (
  IN  UINTN         StartAddress,
  IN  UINTN         Length,
  IN  CONST UINT16  *Buffer
  );

UINT32 *
EFIAPI
MmioWriteBuffer32 (
  IN  UINTN         StartAddress,
  IN  UINTN         Length,
  IN  CONST UINT32  *Buffer
  );

UINT64 *
EFIAPI
MmioWriteBuffer64 (
  IN  UINTN         StartAddress,
  IN  UINTN         Length,
  IN  CONST UINT64  *Buffer
  );