VC Console 读取 WMI 的例子

来自 https://msdn.microsoft.com/en-us/library/aa390423(v=vs.85).aspx

特别注意取得的 WMI 的 String 是 Unicode 的,输出时需要使用 %ls

#include “stdafx.h”

#define _WIN32_DCOM
#include
#include

#pragma comment(lib, “wbemuuid.lib”)

int main(int argc, char **argv)
{
HRESULT hres;

// Step 1: ————————————————–
// Initialize COM. ——————————————

hres = CoInitializeEx(0, COINIT_MULTITHREADED);
if (FAILED(hres))
{
printf(“Failed to initialize COM library. Error code”);
return 1; // Program has failed.
}

// Step 2: ————————————————–
// Set general COM security levels ————————–

hres = CoInitializeSecurity(
NULL,
-1, // COM authentication
NULL, // Authentication services
NULL, // Reserved
RPC_C_AUTHN_LEVEL_DEFAULT, // Default authentication
RPC_C_IMP_LEVEL_IMPERSONATE, // Default Impersonation
NULL, // Authentication info
EOAC_NONE, // Additional capabilities
NULL // Reserved
);

if (FAILED(hres))
{
printf(“Failed to initialize security. Error code = 0x”);
CoUninitialize();
return 1; // Program has failed.
}

// Step 3: —————————————————
// Obtain the initial locator to WMI ————————-

IWbemLocator *pLoc = NULL;

hres = CoCreateInstance(
CLSID_WbemLocator,
0,
CLSCTX_INPROC_SERVER,
IID_IWbemLocator, (LPVOID *)&pLoc);

if (FAILED(hres))
{
printf(“Failed to create IWbemLocator object.”);
CoUninitialize();
return 1; // Program has failed.
}

// Step 4: —————————————————–
// Connect to WMI through the IWbemLocator::ConnectServer method

IWbemServices *pSvc = NULL;

// Connect to the root\cimv2 namespace with
// the current user and obtain pointer pSvc
// to make IWbemServices calls.
hres = pLoc->ConnectServer(
_bstr_t(L”ROOT\\CIMV2″), // Object path of WMI namespace
NULL, // User name. NULL = current user
NULL, // User password. NULL = current
0, // Locale. NULL indicates current
NULL, // Security flags.
0, // Authority (for example, Kerberos)
0, // Context object
&pSvc // pointer to IWbemServices proxy
);

if (FAILED(hres))
{
printf(“Could not connect. Error code = 0x”);
pLoc->Release();
CoUninitialize();
return 1; // Program has failed.
}

printf(“Connected to ROOT\\CIMV2 WMI namespace”);

// Step 5: ————————————————–
// Set security levels on the proxy ————————-

hres = CoSetProxyBlanket(
pSvc, // Indicates the proxy to set
RPC_C_AUTHN_WINNT, // RPC_C_AUTHN_xxx
RPC_C_AUTHZ_NONE, // RPC_C_AUTHZ_xxx
NULL, // Server principal name
RPC_C_AUTHN_LEVEL_CALL, // RPC_C_AUTHN_LEVEL_xxx
RPC_C_IMP_LEVEL_IMPERSONATE, // RPC_C_IMP_LEVEL_xxx
NULL, // client identity
EOAC_NONE // proxy capabilities
);

if (FAILED(hres))
{
printf(“Could not set proxy blanket. Error code = 0x”);
pSvc->Release();
pLoc->Release();
CoUninitialize();
return 1; // Program has failed.
}

// Step 6: ————————————————–
// Use the IWbemServices pointer to make requests of WMI —-

// For example, get the name of the operating system
IEnumWbemClassObject* pEnumerator = NULL;
hres = pSvc->ExecQuery(
bstr_t(“WQL”),
bstr_t(“SELECT * FROM Win32_OperatingSystem”),
WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY,
NULL,
&pEnumerator);

if (FAILED(hres))
{
printf(“Query for operating system name failed.”);
pSvc->Release();
pLoc->Release();
CoUninitialize();
return 1; // Program has failed.
}

// Step 7: ————————————————-
// Get the data from the query in step 6 ——————-

IWbemClassObject *pclsObj = NULL;
ULONG uReturn = 0;

while (pEnumerator)
{
HRESULT hr = pEnumerator->Next(WBEM_INFINITE, 1,
&pclsObj, &uReturn);

if (0 == uReturn)
{
break;
}

VARIANT vtProp;

// Get the value of the Name property
hr = pclsObj->Get(L”Name”, 0, &vtProp, 0, 0);
//wcout << " OS Name : " << vtProp.bstrVal << endl; printf(" OS Name : %ls\n", vtProp.bstrVal); VariantClear(&vtProp); pclsObj->Release();
}

// Cleanup
// ========

pSvc->Release();
pLoc->Release();
pEnumerator->Release();
CoUninitialize();

system(“PAUSE”);
return 0; // Program successfully completed.

}

运行结果 (VS2015 Windows 10 RS1)

Capture

Step to UEFI (127)RDRAND 再研究

前面提到过 RDRAND指令,最近再拿出来看一下,思考了更多问题。
前情提要:
具体的RDRAND指令是通过内嵌汇编实现的,在ASM中直接定义如下

        ; rdrand   ax                  ; generate a 16 bit RN into ax, CF=1 if RN generated ok, otherwise CF=0
        db     0fh, 0c7h, 0f0h         ; rdrand r16:  "0f c7 /6  ModRM:r/m(w)"

 

第一个问题是:这些数字是哪里来的,如何对应到这个指令上的?这种事情必须查阅 Intel 手册了,在“Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 2 (2A, 2B, 2C & 2D): Instruction Set Reference, A-Z” 有如下介绍:

rd1

从上面可以看到 0F C7 /6 就是 RDRAND指令。
接下来是第二个问题:这组数字是如何决定指令使用的寄存器的?意思是:如何确定当前机器代码是rdrand ax 还是rdrand bx?
这个问题就涉及到了指令编码 ModR/M 的知识。在研究了下面的资料之后
http://www.mouseos.com/x64/index.html x86/x64 指令编码内幕(适用于 AMD/Intel)
http://blog.csdn.net/aap159951/article/details/48706207 自己的反汇编引擎–Intel指令编码 (2)
配合下面的表格:0F C7 /6 中的 /6 决定列,然后红色框中的是对应的寄存器

rd2

具体来说0F C7 F1 表示 RDRAND ecx;0F C7 F7 表示 RDRAND edi。
接下来就是如何验证上面的猜想。我们使用 0f 0c7 f3 来验证是否为 RDRAND EBX,使用 0f 0c7 f7 来验证是否为 RDRAND EDI。
为了配合内嵌汇编,我在INF 中设置打开生成的 COD文件,这样可以方便的看清楚参数的传递。

[BuildOptions]
  MSFT:*_*_X64_CC_FLAGS = /Oi- /FAcs /Od

 

这样在编译的时候会生成 cod文件,可以在\Build\AppPkg\DEBUG_VS2013x86\X64\AppPkg\Applications\ 的对应目录下找到
从里面可以看到,通过 rcx传递进去地址,返回值也会在对应的地址中,同时判断 eax是否成功。

; 143  :     if (RdRand32StepEDI (Rand)) {

  00046	48 8b 4c 24 40	 mov	 rcx, QWORD PTR Rand$[rsp]
  0004b	e8 00 00 00 00	 call	 RdRand32StepEDI
  00050	0f b6 c0	 movzx	 eax, al
  00053	85 c0		 test	 eax, eax
  00055	74 04		 je	 SHORT $LN1@RdRand32ED

; 144  :       return EFI_SUCCESS;

 

有了上面的实验,即可编写代码。需要特别注意三点:一个是不要破坏寄存器,否则可能出现不可预料的后果;另外一个是当函数接收参数只有一个时,会使用rcx来传递参数【这部分的详细资料目前没有查到,有了解的朋友欢迎在留言中补充】;最后的结果会从 rcx 传出,在这个例子中是传输到 rcx 给出的地址中。
最终的代码如下:

;------------------------------------------------------------------------------
;  Generate a 32 bit random number
;    Return TRUE if Rand generated successfully, or FALSE if not
;
;  BOOLEAN EFIAPI RdRand32Step (UINT32 *Rand);   RCX
;------------------------------------------------------------------------------
RdRand32StepEBX  PROC
    ; rdrand   ebx                 ; generate a 32 bit RN into eax, CF=1 if RN generated ok, otherwise CF=0
    push   rbx
    db     0fh, 0c7h, 0f3h         ; rdrand r32:  "0f c7 /6  ModRM:r/m(w)"
    jb     rn32_ok                 ; jmp if CF=1
    xor    ebx, ebx                ; reg=0 if CF=0
    pop    rbx
    ret                            ; return with failure status
rn32_ok:
    mov    [rcx], ebx
    mov    rax,  1
    pop    rbx
    ret
RdRand32StepEBX ENDP

;------------------------------------------------------------------------------
;  Generate a 32 bit random number
;    Return TRUE if Rand generated successfully, or FALSE if not
;
;  BOOLEAN EFIAPI RdRand32Step (UINT32 *Rand);   RCX
;------------------------------------------------------------------------------
RdRand32StepEDI  PROC
    ; rdrand   edi                 ; generate a 32 bit RN into eax, CF=1 if RN generated ok, otherwise CF=0
    push   rdi
    db     0fh, 0c7h, 0f7h         ; rdrand r32:  "0f c7 /6  ModRM:r/m(w)"
    jb     rn32_ok                 ; jmp if CF=1
    xor    edi, edi                ; reg=0 if CF=0
    pop    rdi
    ret                            ; return with failure status
rn32_ok:
    mov    [rcx], edi
    mov    rax,  1
    pop    rdi
    ret
RdRand32StepEDI ENDP

 

运行结果如下:

rd3

完整的代码下载:

RdRand2

最后还有第三个问题:从上面可以看到0fh, 0c7h, 0f7h 可以对应 RDRAND eax 也可以对应到 RDRAND ax,那么是如何进行区别的呢?
我猜测是根据当前CPU 的状态决定的,当现在的代码段是32位,这个机器码操作的目标是 EAX,如果是16位,操作目标就是ax。 当然,对于这一点我现在无法确认,后面会想办法进行验证。

Step to UEFI (126)ReturnAddress 研究

最近查看代码发现 Base.h 中新添加了一个ReturnAddress的函数(称作宏更恰当)。对应代码如下:

#if defined(_MSC_EXTENSIONS) && !defined (__INTEL_COMPILER) && !defined (MDE_CPU_EBC)
  void * _ReturnAddress(void);
  #pragma intrinsic(_ReturnAddress)
  /**
    Get the return address of the calling function.

    Based on intrinsic function _ReturnAddress that provides the address of
    the instruction in the calling function that will be executed after
    control returns to the caller.

    @param L    Return Level.

    @return The return address of the calling function or 0 if L != 0.

  **/
  #define RETURN_ADDRESS(L)     ((L == 0) ? _ReturnAddress() : (VOID *) 0)
#elif defined(__GNUC__)
  void * __builtin_return_address (unsigned int level);
  /**
    Get the return address of the calling function.

    Based on built-in Function __builtin_return_address that returns
    the return address of the current function, or of one of its callers.

    @param L    Return Level.

    @return The return address of the calling function.

  **/
  #define RETURN_ADDRESS(L)     __builtin_return_address (L)
#else
  /**
    Get the return address of the calling function.

    @param L    Return Level.

    @return 0 as compilers don't support this feature.

  **/
  #define RETURN_ADDRESS(L)     ((VOID *) 0)
#endif

 

为了一探究竟,编写一个Application ,将上面的内容直接移植到上面:

/** @file
  Support routines for RDRAND instruction access.

Copyright (c) 2013, Intel Corporation. All rights reserved.<BR>
This program and the accompanying materials
are licensed and made available under the terms and conditions of the BSD License
which accompanies this distribution.  The full text of the license may be found at
http://opensource.org/licenses/bsd-license.php

THE PROGRAM IS DISTRIBUTED UNDER THE BSD LICENSE ON AN "AS IS" BASIS,
WITHOUT WARRANTIES OR REPRESENTATIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED.

**/
#include <Uefi.h>
#include <Library/BaseLib.h>
#include <Library/UefiLib.h>

  #pragma intrinsic(_ReturnAddress)
  /**
    Get the return address of the calling function.

    Based on intrinsic function _ReturnAddress that provides the address of
    the instruction in the calling function that will be executed after
    control returns to the caller.

    @param L    Return Level.

    @return The return address of the calling function or 0 if L != 0.

  **/
  #define RETURN_ADDRESS(L)     ((L == 0) ? _ReturnAddress() : (VOID *) 0)

/**
  The user Entry Point for Application. The user code starts with this function
  as the real entry point for the application.

  @param[in] ImageHandle    The firmware allocated handle for the EFI image.
  @param[in] SystemTable    A pointer to the EFI System Table.

  @retval EFI_SUCCESS       The entry point is executed successfully.
  @retval other             Some error occurs when executing this entry point.

**/
EFI_STATUS
EFIAPI
UefiMain (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
{
        Print (L"return here [%X] \n",UefiMain);
        Print (L"return here [%X] \n",RETURN_ADDRESS(0));
        
        return EFI_SUCCESS;
}

 

为了更方便的研究编译之后的结果,在INF中打开生成汇编代码的功能:

 [BuildOptions]
  MSFT:*_*_X64_CC_FLAGS = /Oi- /FAcs /Od

 

上面的代码运行结果如下(NT32模拟器,X64编译)

rtadd

首先查看MAP文件,生成的代码中UefiMain 的地址是4dc,这就是运行结果中 41DB4DC的来源

\Build\AppPkg\DEBUG_VS2013x86\X64\AppPkg\Applications\ReturnAddress\returnaddress\OUTPUT\returnadd.map
0001:000001f8       ProcessModuleEntryPointList 00000000000004b8 f   returnadd:AutoGen.obj
 0001:0000021c       UefiMain                   00000000000004dc f   returnadd:returnaddress.obj
 0001:00000258       DebugPrint                 0000000000000518 f   BaseDebugLibNull:DebugLib.obj

 

进一步观察 COD 文件,UefiMain 编译的结果如下,就是:48 89 54 24 10 48 89 4c 24 08……

\Build\AppPkg\DEBUG_VS2013x86\X64\AppPkg\Applications\ReturnAddress\returnaddress\returnaddress.cod

UefiMain PROC						; COMDAT

; 50   : {

$LN3:
  00000	48 89 54 24 10	 mov	 QWORD PTR [rsp+16], rdx
  00005	48 89 4c 24 08	 mov	 QWORD PTR [rsp+8], rcx
  0000a	48 83 ec 28	 sub	 rsp, 40			; 00000028H

; 51   :         Print (L"return here [%X] \n",UefiMain);

  0000e	48 8d 15 00 00
	00 00		 lea	 rdx, OFFSET FLAT:UefiMain
  00015	48 8d 0d 00 00
	00 00		 lea	 rcx, OFFSET FLAT:??_C@_1CG@PNEGEHON@?$AAr?$AAe?$AAt?$AAu?$AAr?$AAn?$AA?5?$AAh?$AAe?$AAr?$AAe?$AA?5?$AA?$FL?$AA?$CF?$AAX?$AA?$FN?$AA?5?$AA?6?$AA?$AA@
  0001c	e8 00 00 00 00	 call	 Print

 

打开十六进制编辑软件,可以看到 0x4DC偏移处就是上面的机器码:

rtadd2

我们再查看调用 UefiMain的地方,可以在 AutoGen.C 中找到

\Build\AppPkg\DEBUG_VS2013x86\X64\AppPkg\Applications\ReturnAddress\returnaddress\AutoGen.cod

ProcessModuleEntryPointList PROC			; COMDAT

; 224  : {

$LN3:
  00000	48 89 54 24 10	 mov	 QWORD PTR [rsp+16], rdx
  00005	48 89 4c 24 08	 mov	 QWORD PTR [rsp+8], rcx
  0000a	48 83 ec 28	 sub	 rsp, 40			; 00000028H

; 225  :   return UefiMain (ImageHandle, SystemTable);

  0000e	48 8b 54 24 38	 mov	 rdx, QWORD PTR SystemTable$[rsp]
  00013	48 8b 4c 24 30	 mov	 rcx, QWORD PTR ImageHandle$[rsp]
  00018	e8 00 00 00 00	 call	 UefiMain

 

我们再使用上面生成的机器码进行查找,可以发现是从 0x4B8开始的

rtadd3

0x4D5 对应的位置在下面

  0001d	48 83 c4 28	 add	 rsp, 40			; 00000028H
  00021	c3		 ret	 0
ProcessModuleEntryPointList ENDP

 

就是 call UefiMain 之后的位置。
因此 Return_Address 的作用就是返回当前所在函数返回的位置。比如:下面的示例代码中 Return_Address() 取得的地址就是 RT: 的地址。

Int Foo()
{
Return_Address()
}
Call Foo()
Rt:

 

本文提到的完整代码在这里:
ReturnAddress

参考:
1. https://msdn.microsoft.com/en-us/library/64ez38eh.aspx

Step to UEFI (125)CPUBreakPoint 研究

最近在研究 Intel ITP,这是可以进行源码级别调试的硬件工具。刚使用就遇到一个有意思的问题:如何让代码在需要的时候停下来。最直接的想法就是编写死循环,但是很快发现因为编译器的优化,当它发现你编写的代码是死循环,那么它会自动优化掉后面的它认为跑不到的代码。因此需要另辟蹊径找到更标准的方法。
找人请教了一下,对方指点说有一个 CpuBreakpoint() 函数可以使用。首先看这个函数的原型,在\MdePkg\Include\Library\BaseLib.h 中如下:

/**
  Generates a breakpoint on the CPU.

  Generates a breakpoint on the CPU. The breakpoint must be implemented such
  that code can resume normal execution after the breakpoint.

**/
VOID
EFIAPI
CpuBreakpoint (
  VOID
  );

 

具体实现代码可以在 \MdePkg\Library\BaseLib\X64\CpuBreakpoint.c 中找到

void __debugbreak ();

#pragma intrinsic(__debugbreak)

/**
  Generates a breakpoint on the CPU.

  Generates a breakpoint on the CPU. The breakpoint must be implemented such
  that code can resume normal execution after the breakpoint.

**/
VOID
EFIAPI
CpuBreakpoint (
  VOID
  )
{
  __debugbreak ();
}

 

上面代码使用到了 Intrinsic ,对于这个关键字,我的理解是 VS 编译器插入汇编的方法(X64 是不可以直接使用 __ASM进行内嵌汇编的)。这个问题在之前的文章中涉及到,下面用实验来说明,在Shell Application,加入如下代码:

  __asm{
          int 3;
  }

 

在 -a IA32 的情况下,可以编译通过;在 -a X64 的情况下,编译期有如下错误提示:

d:\udk2017\AppPkg\Applications\BreakPointDemo\BreakPointDemo.c(45) : error C4235
: nonstandard extension used : ‘_asm’ keyword not supported on this architecture

d:\udk2017\AppPkg\Applications\BreakPointDemo\BreakPointDemo.c(46) : error C2143
: syntax error : missing ‘;’ before ‘constant’
d:\udk2017\AppPkg\Applications\BreakPointDemo\BreakPointDemo.c(46) : warning C40
91: ‘ ‘ : ignored on left of ‘int’ when no variable is declared
NMAKE : fatal error U1077: ‘”C:\Program Files (x86)\Microsoft Visual Studio 12.0
\Vc\bin\x86_amd64\cl.exe”‘ : return code ‘0x2’
Stop.
为此, VS 定义了一个关键字Intrinsic 用来内嵌汇编。“大多数的函数是在库中,Intrinsic Function却内嵌在编译器中(built in to the compiler)” 【参考1】。
为了进一步验证,需要看到编译之后的结果,我们在 \MdePkg\Library\BaseLib\BaseLib.inf 中加入如下指令:

[BuildOptions]
  MSFT:*_*_X64_CC_FLAGS = /Oi- /FAcs /Od

 

重新编译,会生成cpubreakpoint.c对应的 cod 文件。

PUBLIC	CpuBreakpoint
; Function compile flags: /Odsp
; File d:\udk2017\mdepkg\library\baselib\x64\cpubreakpoint.c
;	COMDAT CpuBreakpoint
_TEXT	SEGMENT
CpuBreakpoint PROC					; COMDAT

; 36   : {

  00000	66 90		 npad	 2

; 37   :   __debugbreak ();

  00002	cc		 int	 3

; 38   : }

  00003	c3		 ret	 0
CpuBreakpoint ENDP
_TEXT	ENDS
END

 

可见,产生了一个 int 3 。

下面是完整的测试代码
运行之后会在 Shell 下面 Hang 住,运行调试工具,发现是停在PeiDxeSmmCpuException.c 文件中。这个让我很意外,因为在之前的调试工具中,看到 Int3 都是会停在调用 Int 3 的位置。

cpb1

之后,仔细研读了一下代码,发现停的这个位置实际上是 Int 3 的处理函数。这个处理函数的结尾使用 CpuDeadLoop 来让代码停下来。
ITP 支持直接修改内存,因此,我们把 CPUDeadLoop 的汇编判断修改为 NOP 指令之后可以返回到调用处。

cbp2

留下一个很类似的函数 CpuDeadLoop ,有兴趣的朋友可以去研究代码的具体实现。
ITP 是非常有力的武器,但是用起来还是非常麻烦,绝大多数情况下,串口输出信息依然是我们的首选。

参考:
1.http://www.cnblogs.com/wangguchangqing/p/5466301.html

Delphi 版的 KeyPressed 函数

有时候,我们需要在 console 中检查是否有按键按下,对于c语言来说,有 kbhit 和 keypressed, 但是Delphi库中没有对应的现成函数。

经过搜索,最终找到了一段代码【参考1】,根据这段代码,编写下面的测试程序:

program Project5;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils,Windows;


function KeyPressed:Boolean;
var
  lpNumberOfEvents     : DWORD;
  lpBuffer             : TInputRecord;
  lpNumberOfEventsRead : DWORD;
  nStdHandle           : THandle;
begin
  Result:=false;
  //get the console handle
  nStdHandle := GetStdHandle(STD_INPUT_HANDLE);
  lpNumberOfEvents:=0;
  //get the number of events
  GetNumberOfConsoleInputEvents(nStdHandle,lpNumberOfEvents);
  if lpNumberOfEvents<> 0 then
  begin
    //retrieve the event
    PeekConsoleInput(nStdHandle,lpBuffer,1,lpNumberOfEventsRead);
    if lpNumberOfEventsRead <> 0 then
    begin
      if lpBuffer.EventType = KEY_EVENT then //is a Keyboard event?
      begin
        if lpBuffer.Event.KeyEvent.bKeyDown then //the key was pressed?
          Result:=true
        else
          FlushConsoleInputBuffer(nStdHandle); //flush the buffer
      end
      else
      FlushConsoleInputBuffer(nStdHandle);//flush the buffer
    end;
  end;
end;

var
  i:integer;

  begin
  while (keypressed()=false) do
    begin
      write('waiting ');
      writeln(i);
      sleep(1000);
      inc(i);
    end;
end.

 

运行结果:

dkp

参考:
1.https://stackoverflow.com/questions/5845080/how-i-can-implement-a-iskeypressed-function-in-a-delphi-console-application

Step to UEFI (124) Ready_To_Boot Event

很早之前的代码,如果想完成在引导之前的一些工作,需要在Log中查找确定跳入引导代码之前的代码。直观,但是没有规范,同一个项目很容易出现好多代码都塞到同一处的情况。到了 UEFI ,规范中给出了一个规范的做法:使用 ReadyToBoot Event。对于这个,在《UEFI 原理与编程》第6章 事件有简单的介绍:

EFI_EVENT_GROUP_READY_TO_BOOT
GUID: gEfiEventReadyToBootGuid

Boot Manager 加载并且执行一个启动项时触发该组内所有 Event。

对应函数原型可以在 \MdePkg\Include\Library\UefiLib.h 看到:

/**
  Create an EFI event in the Ready To Boot Event Group and allows
  the caller to specify a notification function.  
  
  This function abstracts the creation of the Ready to Boot Event.
  The Framework moved from a proprietary to UEFI 2.0 based mechanism.
  This library abstracts the caller from how this event is created to prevent
  to code form having to change with the version of the specification supported.
  If ReadyToBootEvent is NULL, then ASSERT().

  @param  NotifyTpl         The task priority level of the event.
  @param  NotifyFunction    The notification function to call when the event is signaled.
  @param  NotifyContext     The content to pass to NotifyFunction when the event is signaled.
  @param  ReadyToBootEvent  Returns the EFI event returned from gBS->CreateEvent(Ex).

  @retval EFI_SUCCESS       The event was created.
  @retval Other             The event was not created.

**/
EFI_STATUS
EFIAPI
EfiCreateEventReadyToBootEx (
  IN  EFI_TPL           NotifyTpl,                  //事件优先级
  IN  EFI_EVENT_NOTIFY  NotifyFunction,  OPTIONAL   //事件 Notification 函数
  IN  VOID              *NotifyContext,  OPTIONAL  //传递给事件 Notification 函数的参数
  OUT EFI_EVENT         *ReadyToBootEvent   //生成的事件
  );

 

为此,在 UDK2017 上做一个简单的实验,在GraphicsConsole.c 文件中加入创建事件和 Notification函数的代码:

1. 创建事件

//LABZ_DEBUG_Start
  //
  // Register the event to reclaim variable for OS usage.
  //
  EfiCreateEventReadyToBootEx (
    TPL_NOTIFY,              
    OnMyReadyToBoot, 
    NULL,
    &ReadyToBootEvent  
    );  
//LABZ_DEBUG_End

 

2. 事件 Notification 函数

//LABZ_Debug_Start
/**
  On Ready To Boot Services Event notification handler.

  Notify SMM variable driver about the event.

  @param[in]  Event     Event whose notification function is being invoked
  @param[in]  Context   Pointer to the notification function's context

**/
VOID
EFIAPI
OnMyReadyToBoot (
  IN      EFI_EVENT                         Event,
  IN      VOID                              *Context
  )
{

  DEBUG ((EFI_D_INFO, "Invoke OnMyReadyToBoot\n"));
  
  gBS->CloseEvent (Event);
}
//LABZ_Debug_End

 

运行结果中可以看到,这个函数会被 Invoke 2次

event1

研究代码发现,这是因为创建的函数运行了两次,因此创建了2个不同的事件

event2

附件是修改后的GraphicsConsole.c,有兴趣的朋友可以自己动手实验。实验使用的编译命令是:

build -a X64
build -a X64 run

Step to UEFI (123)NOOPT 编译问题

之前我们在介绍 Source Level Debug 的文章中使用过 Noopt 的选项。后来找人请教了一下这个编译目标的含义:关闭一切编译优化。我猜测这样做的目的是为了让编译结果很容易实现汇编和 Source Code的一一对应。但是,很多情况下,编写EDK2代码的人都会忘记测试这个选项,于是,直接下载的代码会碰到X64 IA32 一切正常,唯独=在这个编译目标上发生问题。
比如,最近我又碰到了关于这个编译目标的问题,下面是简化之后的代码,可以帮助展示这个问题:

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

/**
  The user Entry Point for Application. The user code starts with this function
  as the real entry point for the application.

  @param[in] ImageHandle    The firmware allocated handle for the EFI image.  
  @param[in] SystemTable    A pointer to the EFI System Table.
  
  @retval EFI_SUCCESS       The entry point is executed successfully.
  @retval other             Some error occurs when executing this entry point.

**/
EFI_STATUS
EFIAPI
UefiMain (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
{
        UINTN  *z;
        UINT64 x;
  z=(UINTN  *)1024;  //取内存 1024处的值,这里只是为了演示没有实际意义
                                     //如果直接赋值常数,在编译过程中会被优化掉
  x=*z;

  Print (L"%d",x / 1024);
  return EFI_SUCCESS;
}

 

上面的代码在下面的编译命令时

Build –a IA32 –p MdeModulePkg/MdeModulePkg.dsc –b NOOPT 

 

会出现这样的报错

“C:\Program Files (x86)\Microsoft Visual Studio 14.0\Vc\bin\link.exe” /OUT:c:\buildbs\test\Build\MdeModule\NOOPT_VS2015x86\IA32\MdeModulePkg\Application\NooptTest\NooptTest\DEBUG\NooptTest.dll /NOLOGO /NODEFAULTLIB /IGNORE:4001 /OPT:REF /OPT:ICF=10 /MAP /ALIGN:32 /SECTION:.xdata,D /SECTION:.pdata,D /MACHINE:X86 /LTCG /DLL /ENTRY:_ModuleEntryPoint /SUBSYSTEM:EFI_BOOT_SERVICE_DRIVER /SAFESEH:NO /BASE:0 /DRIVER /DEBUG @c:\buildbs\test\Build\MdeModule\NOOPT_VS2015x86\IA32\MdeModulePkg\Application\NooptTest\NooptTest\OUTPUT\static_library_files.lst
NooptTest.lib(NooptTest.obj) : error LNK2001: unresolved external symbol __aulldiv
c:\buildbs\test\Build\MdeModule\NOOPT_VS2015x86\IA32\MdeModulePkg\Application\NooptTest\NooptTest\DEBUG\NooptTest.dll : fatal error LNK1120: 1 unresolved externals
NMAKE : fatal error U1077: ‘”C:\Program Files (x86)\Microsoft Visual Studio 14.0\Vc\bin\link.exe”‘ : return code ‘0x460’
Stop.

build…
: error 7000: Failed to execute command
C:\Program Files (x86)\Microsoft Visual Studio 14.0\Vc\bin\nmake.exe /nologo tbuild [c:\buildbs\test\Build\MdeModule\NOOPT_VS2015x86\IA32\MdeModulePkg\Application\NooptTest\NooptTest]

具体原因我不清楚,-b Release 以及 –b Debug 都不存在问题,可以确定和 NOOPT 模式有关。
标准的解决方法是使用 BaseLib.h 中的DivU64x32函数来代替直接除法运算避免这个问题。同样的,如果针对64位变量进行左移右移也会遇到类似的问题,同样在
BaseLib.h 中可以找到替代的函数。

Step to UEFI Tips :打印 256 个字节

一个很简单的例子,打印256个字节

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


extern EFI_BOOT_SERVICES         *gBS;
extern EFI_SYSTEM_TABLE			 *gST;
extern EFI_RUNTIME_SERVICES 	 *gRT;

static UINT32 next = 1;
/** Expands to an integer constant expression that is the maximum value
    returned by the rand function.
**/
#define RAND_MAX  0x7fffffff

/** Compute a pseudo-random number.
  *
  * Compute x = (7^5 * x) mod (2^31 - 1)
  * without overflowing 31 bits:
  *      (2^31 - 1) = 127773 * (7^5) + 2836
  * From "Random number generators: good ones are hard to find",
  * Park and Miller, Communications of the ACM, vol. 31, no. 10,
  * October 1988, p. 1195.
**/
int
rand()
{
  INT32 hi, lo, x;

  /* Can't be initialized with 0, so use another value. */
  if (next == 0)
    next = 123459876;
  hi = next / 127773;
  lo = next % 127773;
  x = 16807 * lo - 2836 * hi;
  if (x < 0)
    x += 0x7fffffff;
  return ((next = x) % ((UINT32)RAND_MAX + 1));
}

void
srand(unsigned int seed)
{
  next = (UINT32)seed;
}


int
EFIAPI
main (
  IN int Argc,
  IN CHAR16 **Argv
  )
{
	UINT32	Buffer[256];
	UINT32	i;
	UINT32	j;
	
	srand(0);
	for (i=0;i<256;i++)
	{
		Buffer[i]=(UINT8)(rand()%256);
	}
	Print(L" \\|");
	for (i=0;i<16;i++)
		{
			Print(L" %2X|",i);
		}	
	Print(L"\n");
	for (j=0;j<16;j++)
	{
		Print(L"%2X| ",j);
		for (i=0;i<16;i++)
		{
			Print(L"%2X  ",Buffer[i*16+j]);
		}
		Print(L"\n");
	}
  
  return EFI_SUCCESS;
}

运行结果:

simpletest

完整的程序

 

ShowData

CurieNano 直接播放声音

前一段拿到了 DFRobot的CurieNano控制板。除了控制器,还有两个徽章以及多功能便携工具卡:

220537naqkmwaaihumzhak

可以看到这次的主角 Curie Nano 非常小巧,但是功能和全尺寸的 Arduino 101 相比毫不逊色。接下来我就使用这块小板子完成直接播放声音的功能,之前我在【参考1】中介绍过。 唯一的问题是直接寄存器仿佛古文一般面目可憎,又好比英文小说,每一个字母都认识,但是放在一起变成了天书。 仔细捋了一下原理,重新编写了一次。
从原理出发,声音播放就是在特定的频率下(通常大于8000Hz),将数字量化后的信号按照模拟的方式输出。而对于Arduino 来说,最简单的模拟输出就是用PWM。参考1的代码用了一个定时器,8000Hz触发一次,这样整体的代码非常复杂。我们修改为循环,输出之后做足够的延时(1/8000=125us),实际上的输出也是8000Hz,但是整体变得直观和容易理解多了。此外,默认的 PWM输出频率过小(关于 Curie的PWM我会另外介绍),这会导致analogWrite输出的PWM还没有来得及输出完整的一个周期下面的数据就过来。因此,需要升高PWM的频率。对于101 来说,使用analogWriteFrequency(Pin,Frequency);即可指定输出的 PWM 频率(意想不到的简单)。
首先,我们要大概估计一下空间,编写下面这样的简单代码:

void setup() {
analogWriteFrequency(OUTPIN, 64000);
}
void loop() {
}

 

剩余空间差不多有 155648-48016=107632 Bytes,大概可以存放107632/8000=13.4秒的声音(实际上还有额外库函数的开销,实际写代码根据编译结果删除了很多数据)。
220537ll4xtkt883x45pxn

硬件连接上非常简单:Uno 的地接在喇叭负极标志处上,然后随便选一个 支持PWM的引脚作为输出,这里我选择的是Pin9接在喇叭的正极标记处。

220538gm9mnu44d8f4440p

播放的内容是罗大佑的《恋曲1990》,首先用工具将音频转化为 8000Hz,8Bits,然后用【参考4】的工具转化数据为 C的格式,放置在代码中即可。
不完整的程序如下:

#define OUTPIN 9

const unsigned char sample[] PROGMEM =
{
        0x80, 0x80, 0x84, 0x85, 0x85, 0x83, 0x83, 0x81, 0x82, 0x83, 0x85, 0x82, 0x80, 0x7F, 0x7F, 0x80, 0x83, 0x81, 0x81, 0x80, 0x7F, 0x85, 0x87, 0x87, 0x86, 0x84, 0x84, 0x87, 0x84, 0x82, 0x81, 0x80,
        0x82, 0x87, 0x88, 0x88, 0x89, 0x84, 0x80, 0x7F, 0x80, 0x81, 0x82, 0x82, 0x83, 0x82, 0x83, 0x84, 0x83, 0x82, 0x80, 0x7F, 0x7D, 0x7A, 0x78, 0x75, 0x74, 0x72, 0x72, 0x74, 0x75, 0x73, 0x70, 0x70,
//很多很多数据
        0x7C, 0x76, 0x76, 0x7C, 0x76, 0x72, 0x77, 0x79, 0x77, 0x7B, 0x7F, 0x84, 0x87, 0x85, 0x7F, 0x85, 0x88, 0x86, 0x82, 0x81, 0x83, 0x82, 0x80, 0x7F, 0x82, 0x80, 0x7A, 0x74, 0x73, 0x74, 0x77, 0x7A,
        0x84, 0x88, 0x87, 0x83, 0x85, 0x8B, 0x90, 0x91, 0x8F, 0x8A, 0x8A, 0x88, 0x88, 0x82, 0x7F, 0x7D, 0x74, 0x6D, 0x6A, 0x67, 0x6B, 0x6E, 0x74, 0x75, 0x74, 0x6F, 0x70, 0x73, 0x77, 0x7B, 0x7B, 0x7B,
        }
        void setup()
        {
                analogWriteFrequency(OUTPIN, 64000);
        }

        void loop()
        {
                for (unsigned int i = 0; i < sizeof(sample); i++)
                {
                        analogWrite(OUTPIN, pgm_read_byte(&sample));
                        delayMicroseconds(125);
                }
        }

 

整个程序源代码500K。

220538s16xtttm8ltsusqu

完整代码下载:

1990s

针对这种代码的调试上有一些建议:

1. 最好使用歌曲作为素材,理由是单纯的音乐通常无法分清楚当前的播放是否正常。当然,如果你的喇叭质量不好,使用<<月亮之上>>这样的也可以掩盖破音(这也是为什么山寨机最喜欢放这首曲子的原因);
2. 为了安全起见,建议在喇叭上串联一个电阻,防止电流超过40ma;
3. 因为数据被定义在 Flash中,所以一定要使用pgm_read_byte(&sample)这种形式读取出来,切忌直接使用sample.这个问题让我头晕了很久.
4.
有了这样的方法, 你可以尝试在自己的作品中加入声音的功能。 后面我还会尝试使用板载SPI芯片来扩展存储更多的音频。

参考:
1. http://www.lab-z.com/arduinosound/使用Arduino直接发声
2. http://www.diy-robots.com/?p=852Arduino系列教程之 – PWM的秘密(下)
3. http://www.diy-robots.com/?p=814Arduino系列教程之 – PWM的秘密(上)
4. http://www.arduino.cn/thread-46025-1-1.html一个wave 转h 文件的工具