Step to UEFI (209)subst 命令

Subst 命令是一个很老的 DOS命令,通过它可以将一个目录映射为一个盘符。

例如,我本机当前只有一个盘符c:

通过 subst d: 201903, 即可将 buildbs\201903 这个目录映射为 D:

再次查看会多出一个 d: 盘符

对于我们BIOS编译来说,有时候会发生BUILD Debug版本的时候 Code 爆掉了,超过容量大小的限制。其中很大原因是目录过长。代码中很多DEBUG宏使用了 __FILE__ 定义,在编译中会展开为当前文件的完整路径,凭空要多出十几个字节。累积下来数量可观。为此,可以将你的编路径映射为一个盘符然后进入盘符编译。这样编译过程中路径是 D: 这样可以减少尺寸:

使用完毕后,可以使用 subst 盘符 /d 来取消映射。

台湾宝工(Pro’sKit)真的是一家玩具公司了

前一段时间参加创客嘉年华,惊奇的发现他们在这个活动上有一个摊位,更惊奇的是他们的摊位主打竟然是玩具。然后和他们聊了两句发现他们这些年转型向玩具市场进军。

转过天,我修理东西,拿出了他们家生产的螺丝刀,发现竟然生锈了。

基本上每一个刀头都没有幸免…….
这套工具用的很少
外包装还是完好的

除了这一套螺丝刀,我还有一个游标卡尺也是他们家的

检查发现也是生锈的

工具一直存放在家里,看起他们他们用的材质是很有问题的。如果非要加一个条件的话那就是:宝工产品不适合南方地区使用(作为东北人,过了山海关都算是南方)。

Step to UEFI (208)谁改动了我的文件头?

前面介绍了如何使用汇编语言直接编写UEFI Application,这里我偶然发现一个问题:用汇编生成的EFI 文件中,DOS 头部看起来多了一些东西。比如十六进制查看之前的 NasmUEFI.EFI 文件:

对比 Hello.EFI:

可以看到有些内容被清空为0x00. 首先怀疑的是编译过程中有工具来完成这个动作。于是,重新编译 Hello.EFI 观察到在末期有下面的操作:

Generating code
Finished generating code
        "GenFw" -e UEFI_APPLICATION -o c:\buildbs\201903\Build\AppPkg\DEBUG_VS2015x86\X64\AppPkg\Applications\Hello\Hello\DEBUG\Hello.efi c:\buildbs\201903\Build\AppPkg\DEBUG_VS2015x86\X64\AppPkg\Applications\Hello\Hello\DEBUG\Hello.dll

这个操作输入的是hello.dll 输出为 hello.efi,观察发现DLL 中还存在一些字符串,到了EFI 中会消失。接下来在窗口中直接运行 GenFw 来从 DLL生成EFI文件,最终确认是这个工具来完成移除多余的字符工作的,准确的说只是用0xFF覆盖,并没有引起文件长度的变化。

接下来查看 GenFW 的代码, \BaseTools\Source\C\GenFw\GenFw.c 其中下面就是我们需要找到的位置:

//
  // Zero all unused fields of the DOS header
  //
  if (DosHdr != NULL) {
    memcpy (&BackupDosHdr, DosHdr, sizeof (EFI_IMAGE_DOS_HEADER));
    memset (DosHdr, 0, sizeof (EFI_IMAGE_DOS_HEADER));
    DosHdr->e_magic  = BackupDosHdr.e_magic;
    DosHdr->e_lfanew = BackupDosHdr.e_lfanew;
    for (Index = sizeof (EFI_IMAGE_DOS_HEADER); Index < (UINT32 ) DosHdr->e_lfanew; Index++) {
      FileBuffer[Index] = (UINT8) DosHdr->e_cp;
    }
  }

特别提醒:目前提供的编译辅助工具都是 X86 版本的,因此需要在 x86 Native Tools Command Prompt 窗口下编译,如果使用X64会出现编译报错。

本文只是为了简单试验,并没有考虑一些特殊情况。针对上面的代码进行修改,插入一句话即可:

strcpy(&FileBuffer[sizeof (EFI_IMAGE_DOS_HEADER)],"www.lab-z.com");

这样,再次使用 GenFw 来进行DLL到EFI的转换,可以看到 EFI 文件中DOS Header后面被插入了我们期望的字符串。

通过上述的方法可以在 EFI 文件中自动加入自定义的字符串,通过这样的方法可以制作一些特殊的 EFI 文件,比如只能在特别BIOS上运行的 EFI文件。

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