Step to UEFI (199)如何关闭 Cache

很多年前的BIOS Setup中通常存在 Disable  Cache的选项。时至今日这个选项已经消失的无影无踪,而当你提出这个问题时,人们第一个反应是“为什么要 Disable 它”?不过最近我遇到客户提出这样的需求,只得进行一番研究。

在 “Intel® 64 and IA-32 Architectures Software Developer’s Manual”【参考1】对此有明确表述:

上文提到的操作就是设置 CR0的2个 Bits ,再用 WBINVD 清除缓冲队列,最后Disable MTRR即可,具体代码如下:

AsmDisableCache();        //这个函数自带 WBINVD指令,所以这里无需再写一次
        //
        // Disable MTRRs
        //
AsmWriteMsr64 (MSR_IA32_MTRR_DEF_TYPE, 0);

运行之后会死在Application中。无奈中只得架设起来 DCI 对代码进行调试,起初我以为是在 Disable Cache之后发生了某些异常,但是追踪发现出现问题的时候并没有停在某个死循环上,反而不停有中断进入CommonExceptionHandlerWorker () 要求处理。之后继续试验,在前面提到的函数中加入了Debug Message 然后编译成 Debug 版本的BIOS 发现在 HPET Enable之后不断有中断进入处理,直到Disable Cache之后依然如此,这样的现象着实令人费解。猜测是时间中断导致的问题,但是并没有导致异常,一切看起来仍然按部就班在运行…… 终于,晚上灵光乍现,Cache对于系统来说是透明的,当Disable Cache 之后,系统唯一的变化是处理时间变长,因此有一种可能是:中断处理来不及处理导致中断事件堵塞了CPU,这样系统实际上是活着的没有异常,只是对于用户来说是Hang了。

经过研究,UEFI 的时间中是断EFI_TIMER_ARCH_PROTOCOL 提供的,具体可以由 8254 或者 HPET 来实现。现在的平台大多数都是后者。在 \MdePkg\Include\Protocol\Timer.h 有定义一些函数可供调用:

///
/// This protocol provides the services to initialize a periodic timer
/// interrupt, and to register a handler that is called each time the timer
/// interrupt fires.  It may also provide a service to adjust the rate of the
/// periodic timer interrupt.  When a timer interrupt occurs, the handler is
/// passed the amount of time that has passed since the previous timer
/// interrupt.
///
struct _EFI_TIMER_ARCH_PROTOCOL {
  EFI_TIMER_REGISTER_HANDLER          RegisterHandler;
  EFI_TIMER_SET_TIMER_PERIOD          SetTimerPeriod;
  EFI_TIMER_GET_TIMER_PERIOD          GetTimerPeriod;
  EFI_TIMER_GENERATE_SOFT_INTERRUPT   GenerateSoftInterrupt;
};

第一个试验是直接在 Application 中使用SetTimerPeriod 关闭时间中断,运行之后 Application 可以正常退出到 fs0: 这样的盘符下,但是 Shell 已经无法工作,因此就是说没有了这个时钟源, UEFI 好比失去了心跳;接下来就是先用GetTimerPeriod 取得当前设置的时间中断发生的间隔,直接扩大十倍再用SetTimerPeriod 回写。之后 Shell 仍然可以正常工作。

最后试验进入 OS,感觉一直死机,后来挂上 DCI 不断查看 EIP 是否有变,终于确认没有发生死机,只是慢而已。耐心等待,花费80分钟进入了 OS看到了桌面。随后又测试了几次都是差不多90分钟左右才进入桌面。这样证明能够正常工作。

完整代码如下:

#include  <Uefi.h>
#include  <Library/BaseLib.h>
#include  <Library/UefiLib.h>
#include  <Library/ShellCEntryLib.h>
#include  <Library/IoLib.h>
#include <Library/CpuLib.h>
#include <Protocol/Timer.h>

#define MSR_IA32_MTRR_DEF_TYPE 0x000002FF

extern  EFI_BOOT_SERVICES   *gBS;

EFI_GUID gEfiTimerArchProtocolGuid = { 0x26BACCB3, 0x6F42, 0x11D4, 
                { 0xBC, 0xE7, 0x00, 0x80, 0xC7, 0x3C, 0x88, 0x81 }};

/***
  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
  )
{
        EFI_TIMER_ARCH_PROTOCOL *gTimer         = NULL;
        UINT64                  t;
        
        // Get  Timer Arch Protocol
        gBS->LocateProtocol(
                        &gEfiTimerArchProtocolGuid, 
                        NULL, 
                        (VOID **)&gTimer);
        // Disable Timer
        gTimer->GetTimerPeriod (gTimer, &t);  
        gTimer->SetTimerPeriod (gTimer, 0);        
        AsmDisableCache();        
        //
        // Disable MTRRs
        //
        AsmWriteMsr64 (MSR_IA32_MTRR_DEF_TYPE, 0);
        Print(L"Cache disabled\n");

        gTimer->SetTimerPeriod (gTimer, t*10); 
        Print(L"[CR0]=%lX\n",AsmReadCr0());  
        gTimer->GetTimerPeriod (gTimer, &t); 
        Print(L"Set a new timer for Shell\n");         
        return(0);
}

下载:

参考:

1. Intel® 64 and IA-32 Architectures Software Developer’s Manual Combined Volumes:  1, 2A, 2B, 2C, 2D, 3A, 3B, 3C, 3D and 4

Step to UEFI (198)Shell 下中断和异常的处理代码

前面介绍了 IDT ,这里继续研究Shell 下是如何处理 Exception的。

同样,前面提到过,Shell下当一个中断发生之后会从 IDT 中查找Vector入口,然后跳转进去执行。

1.代码在\UefiCpuPkg\Library\CpuExceptionHandlerLib\X64\ExceptionHandlerAsm.nasm 可以看作用是在尽量短的代码中跳转到对应的处理代码

AsmIdtVectorBegin:
%rep  32
    db      0x6a        ; push  #VectorNum
    db      ($ - AsmIdtVectorBegin) / ((AsmIdtVectorEnd - AsmIdtVectorBegin) / 32) ; VectorNum
    push    rax
    mov     rax, strict qword 0 ;    mov     rax, ASM_PFX(CommonInterruptEntry)
    jmp     rax
%endrep
AsmIdtVectorEnd:

2.同一个文件中CommonInterruptEntry   函数,在堆栈中有当前的 Vector Number

;---------------------------------------;
; CommonInterruptEntry                  ;
;---------------------------------------;
; The follow algorithm is used for the common interrupt routine.
; Entry from each interrupt with a push eax and eax=interrupt number
; Stack frame would be as follows as specified in IA32 manuals:
;
; +---------------------+ <-- 16-byte aligned ensured by processor
; +    Old SS           +
; +---------------------+
; +    Old RSP          +
; +---------------------+
; +    RFlags           +
; +---------------------+
; +    CS               +
; +---------------------+
; +    RIP              +
; +---------------------+
; +    Error Code       +
; +---------------------+
; +   Vector Number     +
; +---------------------+
; +    RBP              +
; +---------------------+ <-- RBP, 16-byte aligned
; The follow algorithm is used for the common interrupt routine.
global ASM_PFX(CommonInterruptEntry)
ASM_PFX(CommonInterruptEntry):
    cli
    pop     rax
    ;
    ; All interrupt handlers are invoked through interrupt gates, so
    ; IF flag automatically cleared at the entry point
    ;
    xchg    rcx, [rsp]      ; Save rcx into stack and save vector number into rcx
    and     rcx, 0xFF
    cmp     ecx, 32         ; Intel reserved vector for exceptions?
    jae     NoErrorCode
    bt      [ASM_PFX(mErrorCodeFlag)], ecx
jc      HasErrorCode
 ……省略…….
    ;
    ; Per X64 calling convention, allocate maximum parameter stack space
    ; and make sure RSP is 16-byte aligned
    ;
    sub     rsp, 4 * 8 + 8
    call    ASM_PFX(CommonExceptionHandler)
    add     rsp, 4 * 8 + 8
……省略…….

3.对于 Shell 下,会跳转到\UefiCpuPkg\Library\CpuExceptionHandlerLib\DxeException.c 这个文件中。 所有的 Shell 下中断都会经过这里。

/**
  Common exception handler.

  @param ExceptionType  Exception type.
  @param SystemContext  Pointer to EFI_SYSTEM_CONTEXT.
**/
VOID
EFIAPI
CommonExceptionHandler (
  IN EFI_EXCEPTION_TYPE          ExceptionType,
  IN EFI_SYSTEM_CONTEXT          SystemContext
  )
{
  CommonExceptionHandlerWorker (ExceptionType, SystemContext, &mExceptionHandlerData);
}

4.真正工作的是 \UefiCpuPkg\Library\CpuExceptionHandlerLib\PeiDxeSmmCpuException.c

/**
  Internal worker function for common exception handler.

  @param ExceptionType         Exception type.
  @param SystemContext         Pointer to EFI_SYSTEM_CONTEXT.
  @param ExceptionHandlerData  Pointer to exception handler data.
**/
VOID
CommonExceptionHandlerWorker (
  IN EFI_EXCEPTION_TYPE          ExceptionType,
  IN EFI_SYSTEM_CONTEXT          SystemContext,
  IN EXCEPTION_HANDLER_DATA      *ExceptionHandlerData
  )
{
  EXCEPTION_HANDLER_CONTEXT      *ExceptionHandlerContext;
  RESERVED_VECTORS_DATA          *ReservedVectors;
  EFI_CPU_INTERRUPT_HANDLER      *ExternalInterruptHandler;

  ExceptionHandlerContext  = (EXCEPTION_HANDLER_CONTEXT *) (UINTN) (SystemContext.SystemContextIa32);
  ReservedVectors          = ExceptionHandlerData->ReservedVectors;
  ExternalInterruptHandler = ExceptionHandlerData->ExternalInterruptHandler;
……省略……

这里,可以使用 DEBUG宏输出调试信息,可以用串口输出看到完整的信息。

上面的流程使用 DCI 确认过,有兴趣的朋友可以同样尝试使用 DCI来观察。需要注意的是,有时候调用无法使用单步跟踪指令完成,推荐使用在要跳转到的位置下断点的方式来进行追踪。

此外,IBV 的代码可能会对文件进行 Override,就是同一个代码中出现两个同样名称的文件,两者大部分代码相同,但是会存在一些细节差别,这在追踪过程中需要特别注意。

Step to UEFI (197)EFI 文件的研究(2)

前面介绍了静态条件下的分析,下面研究一下EFI 文件加载在内存中的情况。

用一个图来说明情况:

左侧是文件,右侧是加载到内存后的情况,可以看到对于头是照搬到内存中。对于Section 的话,看起来就比较麻烦,一般情况下内存的对齐要求会比PE文件要求的大。比如:PE 中按照 16Bytes对齐,在内存中可能要求按照 64Bytes对齐,相比是因为 PE文件希望紧凑一些,内存的数据希望读取更快所以要做成这样的。

用一个GenCRC32.exe为例:

加载到内存中的是按照 0x1000对齐,数据在文件中存放是按照 0x200对齐。

继续查看 .text Section 可以看到 RVA=0x1000,意思是:当这个PE被加载到内存后,会放在 BaseAddress+0x1000的内存地址;Pointer to Raw Data 给出 0x400意思是这个段在文件中的位置是从 0x400开始的。

下面我们查看之前的 es.efi ,可以看到文件对齐和内存对齐是相同的。

再查看 Section,可以看到 RVA 和 Pointer to Raw Data 是相同的:

有兴趣的可以多查看几个EFI和 Section ,和上面是相同的。为此,我们再做一个实验,在\MdePkg\Library\UefiApplicationEntryPoint\ApplicationEntryPoint.c 加入一个中断:

EFI_STATUS
EFIAPI
_ModuleEntryPoint (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
{
  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;
    }
  }
CpuBreakpoint(); //LABZ_Debug
  //
  // Call constructor for all libraries.
  //
  ProcessLibraryConstructorList (ImageHandle, SystemTable);

重新编译一次es.efi(注意不要重新编译 NT32Pkg)。执行之后报错可以用 VS2015断下来:

跳出int中断就来到我们EFI 代码的领空:

对照ApplicationEntryPoint.cod 查看停在CpuBreakpoint() 之后的语句了:

_ModuleEntryPoint PROC					; COMDAT

; 45   : {

$LN25:
  00000	48 89 5c 24 08	 mov	 QWORD PTR [rsp+8], rbx
  00005	48 89 74 24 10	 mov	 QWORD PTR [rsp+16], rsi
  0000a	57		 push	 rdi
  0000b	48 83 ec 20	 sub	 rsp, 32			; 00000020H
  0000f	48 8b fa	 mov	 rdi, rdx
  00012	48 8b f1	 mov	 rsi, rcx

; 46   :   EFI_STATUS                 Status;
; 47   : 
; 48   :   if (_gUefiDriverRevision != 0) {
; 49   :     //
; 50   :     // Make sure that the EFI/UEFI spec revision of the platform is >= EFI/UEFI spec revision of the application.
; 51   :     //
; 52   :     if (SystemTable->Hdr.Revision < _gUefiDriverRevision) {
; 53   :       return EFI_INCOMPATIBLE_VERSION;
; 54   :     }
; 55   :   }
; 56   : CpuBreakpoint(); //LABZ_Debug

  00015	e8 00 00 00 00	 call	 CpuBreakpoint

; 57   :   //
; 58   :   // Call constructor for all libraries.
; 59   :   //
; 60   :   ProcessLibraryConstructorList (ImageHandle, SystemTable);

  0001a	48 8b d7	 mov	 rdx, rdi
  0001d	48 8b ce	 mov	 rcx, rsi
  00020	e8 00 00 00 00	 call	 ProcessLibraryConstructorList

当前的位置是 0x 26BABD252DA,那么这个 .text 段是在26BABD252C0 开始的,我们还可以推理出Image 加载位置(BaseAddress)0x26BABD252C0 – 0x2c0=0x26BABD25000:

.text 段在 0x26BABD25000 + 0x2C0

.rdata段在0x26BABD25000 + 0x1700

.data段在0x26BABD25000 + 0x1F20

无名段在0x26BABD25000 +  0x1F60

.xdata段在0x26BABD25000 + 0x2020

.reloc段在0x26BABD25000 + 0x20C0

我们只验证一下 .reloc 段:

可见他们是完全相同的,就是说 EFI 文件加载到内存之后依然是相同的对齐。从设计上说,这样可以使得文件更加紧凑,能够化简加载动作,同时方便调试。

参考:

1. https://www.cnblogs.com/gd-luojialin/p/11306135.html

2019年上海创客嘉年华

2019年10月19-20日,本站会参加位于上海市杨浦区五角场市级副中心国和路 346 号江湾体育场举办的创客嘉年华活动,展位编号 M10。主要展示 Arduino 设计相关内容。

展览中出售的模块如下:

  1. USB Host Mini http://www.lab-z.com/cuhm/
  2. USB Host Shield http://www.lab-z.com/arduinousb1/
  3. Leonardo2UNO Shield http://www.lab-z.com/l2u/
  4. ProtoShield V1 http://www.lab-z.com/prototype-shield-v3/
  5. ProtoShield V3 http://www.lab-z.com/prototype-shield-v3/

欢迎新老朋友前来捧场。

Step to UEFI (196)Pause指令

很多年前我去 DELL 面试,里面的BIOS工程师问如何实现一个delay。我讲了一些使用硬件Timer的方法来实现精确的delay,他都一直摇头。最后我实在忍不住问他正确答案是什么。他的回答是 90h 也就是 NOP指令。当然,NOP是最简单的方法,但是这种方法密切和CPU速度相关的,在不同的CPU上实现的效果不同。

最近查看UEFI 发现其中实现了一个delay使用的是 PAUSE 指令看起来很有意思,于是做了一番研究。具体代码在 \MdePkg\Library\BaseLib\Ia32\CpuPause.c 和\MdePkg\Library\BaseLib\X64\CpuPause.nasm 中。殊途同归,最终都是通过pause 指令来实现。

PAUSE Spin Loop Hint

Opcode Mnemonic           Description

F3 90     PAUSE   Gives hint to processor that improves performance of spin-wait loops.

Description

Improves the performance of spin-wait loops. When executing a “spin-wait loop,” a Pentium 4 or Intel Xeon processor suffers a severe performance penalty when exiting the loop because it detects a possible memory order violation. The PAUSE instruction provides a hint to the processor that the code sequence is a spin-wait loop. The processor uses this hint to avoid the memory order violation in most situations, which greatly improves processor performance. For this reason, it is recommended that a PAUSE instruction be placed in all spin-wait loops.

An additional function of the PAUSE instruction is to reduce the power consumed by a Pentium 4 processor while executing a spin loop. The Pentium 4 processor can execute a spinwait loop extremely quickly, causing the processor to consume a lot of power while it waits for the resource it is spinning on to become available. Inserting a pause instruction in a spinwait loop greatly reduces the processor’s power consumption.

This instruction was introduced in the Pentium 4 processors, but is backward compatible with all IA-32 processors. In earlier IA-32 processors, the PAUSE instruction operates like a NOP instruction. The Pentium 4 and Intel Xeon processors implement the PAUSE instruction as a pre-defined delay. The delay is finite and can be zero for some processors. This instruction does not change the architectural state of the processor (that is, it performs essentially a delaying noop operation). 【参考1】

从上面的资料来看 Pause 的机器码和 NOP 的非常像。因此,这样可以实现兼容之前的 CPU。

上面一段话翻译如下:

PAUSE指令提升了自旋等待循环(spin-wait loop)的性能。当执行一个循环等待时,Intel P4Intel Xeon处理器会因为检测到一个可能的内存顺序违规(memory order violation)而在退出循环时使性能大幅下降。PAUSE指令给处理器提了个醒:这段代码序列是个循环等待。处理器利用这个提示可以避免在大多数情况下的内存顺序违规,这将大幅提升性能。因为这个原因,所以推荐在循环等待中使用PAUSE指令。

PAUSE的另一个功能就是降低Intel P4在执行循环等待时的耗电量。Intel P4处理器在循环等待时会执行得非常快,这将导致处理器消耗大量的电力,而在循环中插入一个PAUSE指令会大幅降低处理器的电力消耗。”【参考2】

参考:

  1. https://c9x.me/x86/html/file_module_x86_id_232.html
  2. https://blog.csdn.net/misterliwei/article/details/3951103

测试 Arduino Pro Micro 的 SPI 接口

Arduino Pro Micro Pinout

首先测试1M 速率, 测试D15(SCK). 因为先是发送0x90,然后再发送一个 0x00(也是读取),所以会送2次 SCK.

查看 MOSI 上面的信号

再测试8M,这是 32U4可以达到的最高速度(16Mhz的一半)

信号幅度是5V,因此,如果和 3.3V设备通讯,必须进行电平转换.

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

2023年2月1日

使用 USBTinyISP 烧写 Booloader的方法:

1:MISO 接 D14

2.VCC 接 VIN

3.SCK接D15

4.MOSI接D16

5.RESET接RST

6.GND接GND

之后,直接使用 Arduino IDE 自带的Bootloader功能烧录即可。

Step to UEFI (195)谁动了我的 RET

前面的文章“EFI 文件研究(1)”提到了入口地方有一个奇怪的现象,直接从ProcessLibraryConstructorList 函数跳到了ProcessModuleEntryPointList。百思不得其解之后咨询天杀,他提到有编译器有一种优化方式,针对“连续两个函数的调用 ,在优化后可能会将第一个函数的尾部返回优化成对第二个函数的跳转,然后由第二个函数来进行返回”。经过这样的提醒后,我在代码中查找,找到了非常类似的代码。

在\MdePkg\Library\UefiApplicationEntryPoint\ApplicationEntryPoint.c 定义了Application 的入口:

/**
  Entry point to UEFI Application.

  This function is the entry point for a UEFI Application. This function must call
  ProcessLibraryConstructorList(), ProcessModuleEntryPointList(), and ProcessLibraryDestructorList().
  The return value from ProcessModuleEntryPointList() is returned.
  If _gUefiDriverRevision is not zero and SystemTable->Hdr.Revision is less than _gUefiDriverRevison,
  then return EFI_INCOMPATIBLE_VERSION.

  @param  ImageHandle                The image handle of the UEFI Application.
  @param  SystemTable                A pointer to the EFI System Table.

  @retval  EFI_SUCCESS               The UEFI Application exited normally.
  @retval  EFI_INCOMPATIBLE_VERSION  _gUefiDriverRevision is greater than SystemTable->Hdr.Revision.
  @retval  Other                     Return value from ProcessModuleEntryPointList().

**/
EFI_STATUS
EFIAPI
_ModuleEntryPoint (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
{
  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;
    }
  }
  //
  // Call constructor for all libraries.
  //
  ProcessLibraryConstructorList (ImageHandle, SystemTable);
  //
  // Call the module's entry point
  //
  Status = ProcessModuleEntryPointList (ImageHandle, SystemTable);

  //
  // Process destructor for all libraries.
  //
  ProcessLibraryDestructorList (ImageHandle, SystemTable);

  //
  // Return the return status code from the driver entry point
  //
  return Status;
}

因此,前面_gUefiDriverRevision == 0 在编译期内部代码直接会被优化掉,剩下的就是连续两次调用ProcessLibraryConstructorList 和ProcessModuleEntryPointList 函数。

为了证明这一点,我在 Inf 文件中加入关闭优化的指令 /Od:

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

加入之后再次编译,

在 \Build\AppPkg\DEBUG_VS2015x86\X64\AppPkg\Applications\EFIStudy\EFIStudy\ApplicationEntryPoint.cod 中可以看到完整的图景:

_ModuleEntryPoint PROC					; COMDAT

; 45   : {

$LN25:
  00000	48 89 5c 24 08	 mov	 QWORD PTR [rsp+8], rbx
  00005	48 89 74 24 10	 mov	 QWORD PTR [rsp+16], rsi
  0000a	57		 push	 rdi
  0000b	48 83 ec 20	 sub	 rsp, 32			; 00000020H
  0000f	48 8b fa	 mov	 rdi, rdx
  00012	48 8b f1	 mov	 rsi, rcx

; 46   :   EFI_STATUS                 Status;
; 47   : 
; 48   :   if (_gUefiDriverRevision != 0) {
; 49   :     //
; 50   :     // Make sure that the EFI/UEFI spec revision of the platform is >= EFI/UEFI spec revision of the application.
; 51   :     //
; 52   :     if (SystemTable->Hdr.Revision < _gUefiDriverRevision) {
; 53   :       return EFI_INCOMPATIBLE_VERSION;
; 54   :     }
; 55   :   }
; 56   : 
; 57   :   //
; 58   :   // Call constructor for all libraries.
; 59   :   //
; 60   :   ProcessLibraryConstructorList (ImageHandle, SystemTable);

  00015	e8 00 00 00 00	 call	 ProcessLibraryConstructorList

; 61   : 
; 62   :   //
; 63   :   // Call the module's entry point
; 64   :   //
; 65   :   Status = ProcessModuleEntryPointList (ImageHandle, SystemTable);

  0001a	48 8b d7	 mov	 rdx, rdi
  0001d	48 8b ce	 mov	 rcx, rsi
  00020	e8 00 00 00 00	 call	 ProcessModuleEntryPointList

; 66   : 
; 67   :   //
; 68   :   // Process destructor for all libraries.
; 69   :   //
; 70   :   ProcessLibraryDestructorList (ImageHandle, SystemTable);

  00025	48 8b d7	 mov	 rdx, rdi
  00028	48 8b ce	 mov	 rcx, rsi
  0002b	48 8b d8	 mov	 rbx, rax
  0002e	e8 00 00 00 00	 call	 ProcessLibraryDestructorList

; 71   : 
; 72   :   //
; 73   :   // Return the return status code from the driver entry point
; 74   :   //
; 75   :   return Status;
; 76   : }

  00033	48 8b 74 24 38	 mov	 rsi, QWORD PTR [rsp+56]
  00038	48 8b c3	 mov	 rax, rbx
  0003b	48 8b 5c 24 30	 mov	 rbx, QWORD PTR [rsp+48]
  00040	48 83 c4 20	 add	 rsp, 32			; 00000020H
  00044	5f		 pop	 rdi
  00045	c3		 ret	 0
_ModuleEntryPoint ENDP

这里可以清楚的看到分别调用了2个函数,并且和我们上面找到的位置代码是一致的。

从上面的试验可以得知:

  1. _ModuleEntryPoint ()是每个 Application 的起点,如果有需要可以在其中添加代码;
  2. 编译器有时候会将连续的2个函数调用优化。

Step to UEFI (194)EFI 文件研究(1)

EFI 文件使用的是 PE 格式(PE文件的全称是Portable Executable,意为可移植的可执行的文件,常见的EXE、DLL、OCX、SYS、COM都是PE文件,PE文件是微软Windows操作系统上的程序文件【参考1】),所以很多关于PE文件的知识在 EFI 文件上仍然是通用的。

我们编写一个代码来进行研究。功能非常简单,如果运行时加入 a 参数,那么打印一串字符,否则无任何动作。具体代码如下:

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

/***
  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
  )
{
        if ((Argc>1)&&(Argv[1][0]=='a')) {
                Print(L"Hello there fellow Programmer.\n");
        }
  
  return(0);
}

此外在对应的 inf 文件加入下面一段:

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

编译之后,可以在\Build\AppPkg\DEBUG_VS2015x86\X64\AppPkg\Applications\EFIStudy\EFIStudy\DEBUG目录下看到 es.efi 和 es.map 文件。打开 es.map 文件,有如下  Section:

Start         Length     Name                   Class
 0001:00000000 000011aeH .text$mn                CODE
 0002:00000000 00000654H .rdata                  DATA
 0002:00000654 00000114H .rdata$zzzdbg           DATA
 0003:00000000 00000020H .data                   DATA
 0003:00000020 00000020H .bss                    DATA
 0004:00000000 00000084H .pdata                  DATA
 0005:00000000 0000007cH .xdata                  DATA

这些 Section 的作用是:

.rdata – 保存常量数据的节。这个可以对应C语言中的常数和常量字符串,同上面一样的原因,他们的初值被保存到了PE文件的次Section中,而不是在运行时被赋值。

.data – 保存数据的节,这个对应C语言中以初始化的全局变量数据。想想为什么你在源码里初始化一个全局变量后运行时这个变量的值正是你想要的那个?int a = 12;并不意味着CRT为你执行了一个赋值语句,而是a在PE文件中保存的位置已经被硬编码了一个12的值。这样loader加载程序时,你给的初值被从PE文件读取到了内存中变量a的位置,这样才使你的变量a有了初值。

.bss – (Block Start with Symbol) 这个section对应C程序中的全局未初始化变量。啥?你说C中未初始化的全局变量实际上全被初始化成了0?这是因为实际上操作系统是这样干的——你的全局未初始化变量由于没有初值,所以不需要将值像上面两个一样保存到PE文件中(所以.bss节除了描述信息之外不占据磁盘空间),但是.bss会描述一段内存区域,loader在加载.bss section时直接开辟这么一块包括所有未初始化数据的内存区域,然后直接将这区域清零。这就是C中全局未初始化数据之所以为零的原因了。

.pdata和 .xdata都存放的是异常处理相关的内容。

.text 是最重要的执行代码段 。如果我们想直接修改 EFI 文件中的代码,需要直接在 .text section 中查找修改。【参考2】【参考3】

使用 NikPEViwer(这是一个 Windows PE 文件查看工具) 打开 es.efi。在左侧的 .text 双击可以直接定位到 TEXT Section。

同样的,我们可以使用这个工具找到这个 EFI文件的EntryPoint为 0x2C0,具体是在  NT Headers-> Optional header –> AddressOfEntryPoint【参考5】

在编译之后的结果中查找,第一个条语句在

\Build\AppPkg\DEBUG_VS2015x86\X64\AppPkg\Applications\EFIStudy\EFIStudy\AutoGen.cod 文件中(从实习结果来看,这样生成的COD文件中有很多注释是不对的,所以具体要根据C语句和汇编语言对照来进行分析)

ProcessLibraryConstructorList 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

其中COD 文件列出的 0004 开始的 48 89 05 00 00 00 00 末尾的4字节00 是Link时才会确定的取值,因此和实际 48 89 05 19 00 00 00 是有差别的。

此外这个函数的末尾有一个 ret,但是最后生成的代码中是 E9 03 00 00 00 这个字样。

  0001d	48 89 15 00 00
	00 00		 mov	 QWORD PTR gST, rdx

; 208  : }

  00024	c3		 ret	 0
ProcessLibraryConstructorList ENDP
_TEXT	ENDS

反编译结果如下【参考4】:

就是说在编译过程中 ret 被替换成跳转指令,跳转的位置是下一个函数。

ProcessModuleEntryPointList PROC			; COMDAT
; 232  : {
$LN27:
  00000	4c 8b dc	 mov	 r11, rsp
  00003	49 89 5b 08	 mov	 QWORD PTR [r11+8], rbx
  00007	49 89 73 20	 mov	 QWORD PTR [r11+32], rsi
  0000b	57		 push	 rdi

我们 C 代码中的判断条件if ((Argc>1)&&(Argv[1][0]==’a’)) { 也在这个 COD文件中,可以看到 000a2 处有一个 cmp ,之后000a6 会根据结果进行跳转:

; 33   :         if ((Argc>1)&amp;&amp;(Argv[1][0]=='a')) {

  0008e	48 8b 44 24 50	 mov	 rax, QWORD PTR EfiShellInterface$2[rsp]
  00093	48 83 78 18 01	 cmp	 QWORD PTR [rax+24], 1
  00098	76 1a		 jbe	 SHORT $LN18@ProcessMod
  0009a	48 8b 40 10	 mov	 rax, QWORD PTR [rax+16]
$LN25@ProcessMod:
  0009e	48 8b 48 08	 mov	 rcx, QWORD PTR [rax+8]
  000a2	66 83 39 61	 cmp	 WORD PTR [rcx], 97	; 00000061H
  000a6	75 0c		 jne	 SHORT $LN18@ProcessMod

对于我们来说,只要修改  jne 为 je (机器码74)即可完成目标。前面图片展示过,ProcessModuleEntryPointList  函数从0x2ec开始,因此,0006a 在文件中的偏移是 0x2ec+0xa6=0x392.直接查看这里是 75 修改为 74 (JE),保存为 esM.efi

修改之后可以直接在 Nt32 环境中测试:

可以看到,修改之后输入 a 参数不会输出,反而输入其他参数会输出字符。

本文提到的源代码和 EFI 文件下载(注意:EFI文件并非试验中使用的,具体偏移可能有差别,只供参考)

通过上述试验我们可以了解下面的知识:

  1. 一个 EFI Application 从\MdePkg\Library\UefiBootServicesTableLib 中的UefiBootServicesTableLibConstructor 函数开始,到\ShellPkg\Library\UefiShellCEntryLib\UefiShellCEntryLib.c 中的ShellCEntryLib 函数;
  2. 如何通过 COD 文件计算一个代码在 EFI 文件中的偏移。

参考:

  1. https://baike.baidu.com/item/pe%E6%96%87%E4%BB%B6/6488140?fr=aladdin
  2. https://docs.microsoft.com/en-us/windows/win32/debug/pe-format  权威,建议以此为准
  3. https://blog.csdn.net/lj94093/article/details/50503964 windows PE结构解析
  4. https://onlinedisassembler.com/odaweb/ 一个在线反编译工具很好用
  5. The PE format is documented (in the loosest sense of the word) in the WINNT.H header file. About midway through WINNT.H is a section titled “Image Format.” This section starts out with small tidbits from the old familiar MS-DOS MZ format and NE format headers before moving into the newer PE information. WINNT.H provides definitions of the raw data structures used by PE files, but contains only a few useful comments to make sense of what the structures and flags mean. Whoever wrote the header file for the PE format (the name Michael J. O’Leary keeps popping up) is certainly a believer in long, descriptive names, along with deeply nested structures and macros. When coding with WINNT.H, it’s not uncommon to have expressions like this:

pNTHeader->

OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_DEBUG].VirtualAddress;

上述资料来自 https://docs.microsoft.com/en-us/previous-versions/ms809762(v=msdn.10)?redirectedfrom=MSDN

Teensy 听音辨数

有一千个读者便有一千个哈姆雷特。又好比鲁迅先生评价《红楼梦》 “经学家看见《易》,道学家看见淫,才子看见缠绵,革命家看见排满,流言家看见宫闱秘事。” 再比如经常被人批判的《古惑仔》系列电影,觉得它教坏小朋友,我觉得并不是。在我看起来应该属于青春励志题材。展现了“以陈浩南为首的香港下层青年人不甘平庸,通过打拼努力向上”的故事。当然,还可以说他是批判现实主义题材的作品,也包含了做人的道理。比如,影片中靓坤转眼间就被几个小时前羞辱的小警察射杀。

再比如3Q大战之时,记者采访了红衣教主,没成想有牛人直接从采访的视频声音中分析出来了他的电话【参考1】。谁能想到流氓软件之父竟然栽倒这样的事情中。

这次我们使用 Teensy 和数字麦克风来实现这个功能。

先介绍一下原理。电话实现拨号有两种方式:脉冲拨号和音频拨号。

脉冲拨号是一种时域处理方法,它用脉冲的个数来表示号码数字。脉冲拨号方式对脉冲的宽度、大小、间距、形状都有着严格的要求,如果由于线路的干扰或其他原因而使得这些参数发生了变化,则可能引起号码接收的错误。另一方面,由于每个脉冲都占有一定的时间(一般每个脉冲占用的时间为100ms),而使得这种拨号方式比较慢。当拨号时,用户通常会听到一串拨号音,老式的转盘电话就使用脉冲拨号。比如,拨号“0”时,电路“断”、“续”10次,代表数字“0”。可以看到,如果号码较长拨号耗时也会很长。因此,这种拨号方式逐渐为音频拨号所取代。

我们常用的音频拨号是双音多频 DTMF(Dual Tone Multi Frequency),双音多频,由高频群和低频群组成,低频群包含3个频率,高频群包含4个频率。一个高频信号和一个低频信号叠加组成一个组合信号,代表一个数字。

 120913361447
697123
770456
852789
941*0#

比如,用频率为770Hz 的正弦波加到1366Hz 的正弦波合成一个声音表示数字“5”

这个合成过程用Matlab 模拟如下:

首先是 770Hz 的正弦波:

这是 1366Hz 的正弦波

二者相加的结果是

对我们来说,目标是将上面这个合成后的结果分解得到具体是哪两个信号合成的。自然而然想到使用傅立叶变换来处理:

放大,可以看到是 770 和 1366 合成的。

这次选择 Teensy+SPH0645LM4H实现声音的采集和分析,最终结果显示在一个 1602 LCD上。

接线方式:

Teensy 3.2     SPH0645LM4H

        GND             SEL

        D23              LRCL

         D13             DOUT

         D9               BCLK

         GND           GND

         3.3V            3V

Teensy 3.2     LCD1602

         GND           GND

         Vin              VCC     

         Pin19          SCL

         Pin18          SDA

代码上分析获得频率并没有通过傅立叶变换,而是直接使用 Teensy 的音频库中的AudioAnalyzeToneDetect 函数。这个函数使用的是Goertzel算法,该算法的主要思想是检查音频数据中是否包含某一个给定的频率【参考2】。因此,对于我们来说就是不断测试当前的信号中是否有697到1477这些频率,如果存在的话就转化为对应的数字。

完整代码如下:

// Dial Tone (DTMF) decoding

 

#include &lt;Audio.h>

#include &lt;Wire.h>

#include &lt;SPI.h>

#include &lt;LiquidCrystal_I2C.h>

 

LiquidCrystal_I2C lcd(0x3f,20,4);

 

// Create the Audio components.  These should be created in the

// order data flows, inputs/sources -> processing -> outputs

//

AudioInputI2S            audioIn;

AudioAnalyzeToneDetect   row1;     // 7 tone detectors are needed

AudioAnalyzeToneDetect   row2;     // to receive DTMF dial tones

AudioAnalyzeToneDetect   row3;

AudioAnalyzeToneDetect   row4;

AudioAnalyzeToneDetect   column1;

AudioAnalyzeToneDetect   column2;

AudioAnalyzeToneDetect   column3;

 

// Create Audio connections between the components

//

AudioConnection patchCord01(audioIn, 0, row1, 0);

AudioConnection patchCord02(audioIn, 0, row2, 0);

AudioConnection patchCord03(audioIn, 0, row3, 0);

AudioConnection patchCord04(audioIn, 0, row4, 0);

AudioConnection patchCord05(audioIn, 0, column1, 0);

AudioConnection patchCord06(audioIn, 0, column2, 0);

AudioConnection patchCord07(audioIn, 0, column3, 0);

 

int charpos=0;

 

void setup() {

  // Audio connections require memory to work.  For more

  // detailed information, see the MemoryAndCpuUsage example

  AudioMemory(12);

 

  lcd.init();

  lcd.backlight();

 

 

  while (!Serial);

  delay(100);

  Serial.println("Start decoding");

 

  // Configure the tone detectors with the frequency and number

  // of cycles to match.  These numbers were picked for match

  // times of approx 30 ms.  Longer times are more precise.

  row1.frequency(697, 21);

  row2.frequency(770, 23);

  row3.frequency(852, 25);

  row4.frequency(941, 28);

  column1.frequency(1209, 36);

  column2.frequency(1336, 40);

  column3.frequency(1477, 44);

}

 

const float row_threshold = 0.009;

const float column_threshold = 0.009;

char lastdigit=0;

 

void loop() {

  float r1, r2, r3, r4, c1, c2, c3;

  char digit=0;

 

  // read all seven tone detectors

  r1 = row1.read();

  r2 = row2.read();

  r3 = row3.read();

  r4 = row4.read();

  c1 = column1.read();

  c2 = column2.read();

  c3 = column3.read();

 

/*

  // print the raw data, for troubleshooting

  Serial.print("tones: ");

  Serial.print(r1);

  Serial.print(", ");

  Serial.print(r2);

  Serial.print(", ");

  Serial.print(r3);

  Serial.print(", ");

  Serial.print(r4);

  Serial.print(",   ");

  Serial.print(c1);

  Serial.print(", ");

  Serial.print(c2);

  Serial.print(", ");

  Serial.println(c3);

*/

  // check all 12 combinations for key press

  if (r1 >= row_threshold) {

    if (c1 > column_threshold) {

      digit = '1';

    } else if (c2 > column_threshold) {

      digit = '2';

    } else if (c3 > column_threshold) {

      digit = '3';

    }

  } else if (r2 >= row_threshold) {

    if (c1 > column_threshold) {

      digit = '4';

    } else if (c2 > column_threshold) {

      digit = '5';

    } else if (c3 > column_threshold) {

      digit = '6';

    }

  } else if (r3 >= row_threshold) {

    if (c1 > column_threshold) {

      digit = '7';

    } else if (c2 > column_threshold) {

      digit = '8';

    } else if (c3 > column_threshold) {

      digit = '9';

    }

  } else if (r4 >= row_threshold) {

    if (c1 > column_threshold) {

      digit = '*';

    } else if (c2 > column_threshold) {

      digit = '0';

    } else if (c3 > column_threshold) {

      digit = '#';

    }

  }

 

  // print the key, if any found

  if ((digit > 0)&amp;&amp;(lastdigit!=digit)) {

  //if (digit > 0) {

    Serial.print("  --> Key: ");

    Serial.println(digit);

   

    lcd.setCursor(charpos % 16, (charpos / 16)%2);

    lcd.print(digit);

    charpos++;  

  }

  lastdigit=digit;

 

 

 

  // uncomment these lines to see how much CPU time

  // the tone detectors and audio library are using

  //Serial.print("CPU=");

  //Serial.print(AudioProcessorUsage());

  //Serial.print("%, max=");

  //Serial.print(AudioProcessorUsageMax());

  //Serial.print("%   ");

 

}

工作的照片:

工作的视频:

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

很明显,你的按键信息包含在了发出的声音中,这是一个安全隐患。现实生活中,自动柜员机(ATM)也有一个键盘。仔细观察能发现每个按键会发出相同的声音。但是在十几年前,那个键盘的数字对应着的是不同的声音………..

参考:

1. https://www.cnblogs.com/emouse/archive/2012/09/01/2666308.html?utm_source=debugrun&utm_medium=referral 转:技术宅逆天了!如何从按键音中听出周鸿祎的手机号码

2. https://blog.csdn.net/silent123go/article/details/54022037