Step to UEFI (183)SetJump() 和LongJump()

单纯的函数调用从动作上来说,Call 指令可以看作保存寄存器压栈外加 JMP 的过程(复杂的说调用 Call 会有有实模式,保护模式, Near/Far,gated等等差别,在 IA32 手册上描述足有8页之多)。因此,可以使用压栈保存寄存器外加一个跳转来实现。在 UEFI 中可以通过SetJump() 和 LongJump()函数组合来实现。

在 \MdePkg\Include\Library\BaseLib.h 可以看到下面的定义。

/**
  Saves the current CPU context that can be restored with a call to LongJump()
  and returns 0.

  Saves the current CPU context in the buffer specified by JumpBuffer and
  returns 0. The initial call to SetJump() must always return 0. Subsequent
  calls to LongJump() cause a non-zero value to be returned by SetJump().

  If JumpBuffer is NULL, then ASSERT().
  For Itanium processors, if JumpBuffer is not aligned on a 16-byte boundary, then ASSERT().

  NOTE: The structure BASE_LIBRARY_JUMP_BUFFER is CPU architecture specific.
  The same structure must never be used for more than one CPU architecture context.
  For example, a BASE_LIBRARY_JUMP_BUFFER allocated by an IA-32 module must never be used from an x64 module.
  SetJump()/LongJump() is not currently supported for the EBC processor type.

  @param  JumpBuffer  A pointer to CPU context buffer.

  @retval 0 Indicates a return from SetJump().

**/
RETURNS_TWICE
UINTN
EFIAPI
SetJump (
  OUT     BASE_LIBRARY_JUMP_BUFFER  *JumpBuffer
  );

从介绍上来看,SetJump能保存调用处的全部寄存器,然后返回到调用的位置。

/**
  Restores the CPU context that was saved with SetJump().

  Restores the CPU context from the buffer specified by JumpBuffer. This
  function never returns to the caller. Instead is resumes execution based on
  the state of JumpBuffer.

  If JumpBuffer is NULL, then ASSERT().
  For Itanium processors, if JumpBuffer is not aligned on a 16-byte boundary, then ASSERT().
  If Value is 0, then ASSERT().

  @param  JumpBuffer  A pointer to CPU context buffer.
  @param  Value       The value to return when the SetJump() context is
                      restored and must be non-zero.

**/
VOID
EFIAPI
LongJump (
  IN      BASE_LIBRARY_JUMP_BUFFER  *JumpBuffer,
  IN      UINTN                     Value
  );

从介绍上来看,LongJump恢复SetJump函数能保存调用处的全部寄存器,然后返回到调用SetJump函数的下一条指令的位置。

为了验证上面的说法,编写一个Application。主要代码如下:

UINTN RunMark   =       0;

void
ShowString()
{
        Print(L"www.lab-z.com [%d]\n",RunMark);
}

/***
  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
  )
{
        BASE_LIBRARY_JUMP_BUFFER  JumpBuffer;

        SetJump(&JumpBuffer);
        RunMark++;
        ShowString();
        if (RunMark==1) {
                LongJump (&JumpBuffer, (UINTN)-1);
        }
        
        return(0);
}

运行结果如下:

第一次运行 SetJump() 会将当前寄存器存放在 JumpBuffer 中。然后 RunMark 加一,从0变成1.接着,判断这个值等于1,再执行 LongJump,执行的结果会回到  SetJump(); 的下一条指令,也就是 RunMark++。这样,RunMark 变成了2,不会运行 LongJump() 接下来顺利退出了。

最后一个问题,SetJump 和  LongJump的具体实现在哪里?我这边通过 Build 目录里面 MakeFile 内容来确认。

1.找到Application 的 MakeFile 在 \AppPkg\DEBUG_VS2015x86\X64\AppPkg\Applications\JumpCall\JumpCall\Makefile 其中有如下代码

#
# Build Macro
#
STATIC_LIBRARY_FILES =  \
    $(BIN_DIR)\MdePkg\Library\BaseDebugPrintErrorLevelLib\BaseDebugPrintErrorLevelLib\OUTPUT\BaseDebugPrintErrorLevelLib.lib \
    $(BIN_DIR)\MdePkg\Library\BasePrintLib\BasePrintLib\OUTPUT\BasePrintLib.lib \
    $(BIN_DIR)\MdePkg\Library\BasePcdLibNull\BasePcdLibNull\OUTPUT\BasePcdLibNull.lib \
$(BIN_DIR)\MdePkg\Library\BaseLib\BaseLib\OUTPUT\BaseLib.lib \

2.确定 BaseLib.Lib 的生成方式,在C:\BuildBs\201903\Build\AppPkg\DEBUG_VS2015x86\X64\MdePkg\Library\BaseLib\BaseLib\Makefile 可以看到

$(OUTPUT_DIR)\X64\SetJump.obj : $(MAKE_FILE)
$(OUTPUT_DIR)\X64\SetJump.obj : $(WORKSPACE)\MdePkg\Include\Base.h
$(OUTPUT_DIR)\X64\SetJump.obj : $(WORKSPACE)\MdePkg\Include\X64\Nasm.inc
$(OUTPUT_DIR)\X64\SetJump.obj : $(WORKSPACE)\MdePkg\Include\X64\ProcessorBind.h
$(OUTPUT_DIR)\X64\SetJump.obj : $(WORKSPACE)\MdePkg\Include\Library\PcdLib.h
$(OUTPUT_DIR)\X64\SetJump.obj : $(DEBUG_DIR)\AutoGen.h
$(OUTPUT_DIR)\X64\SetJump.obj : $(WORKSPACE)\MdePkg\Library\BaseLib\X64\SetJump.nasm
	"$(PP)" $(PP_FLAGS) $(INC) c:\buildbs\201903\MdePkg\Library\BaseLib\X64\SetJump.nasm > c:\buildbs\201903\Build\AppPkg\DEBUG_VS2015x86\X64\MdePkg\Library\BaseLib\BaseLib\OUTPUT\X64\SetJump.i
	Trim --trim-long --source-code -o c:\buildbs\201903\Build\AppPkg\DEBUG_VS2015x86\X64\MdePkg\Library\BaseLib\BaseLib\OUTPUT\X64\SetJump.iii c:\buildbs\201903\Build\AppPkg\DEBUG_VS2015x86\X64\MdePkg\Library\BaseLib\BaseLib\OUTPUT\X64\SetJump.i
	"$(NASM)" -Ic:\buildbs\201903\MdePkg\Library\BaseLib\X64\ $(NASM_INC) $(NASM_FLAGS) -o c:\buildbs\201903\Build\AppPkg\DEBUG_VS2015x86\X64\MdePkg\Library\BaseLib\BaseLib\OUTPUT\X64\SetJump.obj c:\buildbs\201903\Build\AppPkg\DEBUG_VS2015x86\X64\MdePkg\Library\BaseLib\BaseLib\OUTPUT\X64\SetJump.iii
就是说使用了c:\buildbs\201903\MdePkg\Library\BaseLib\X64\SetJump.nasm 进行编译。

3.具体代码在C:\BuildBs\201903\MdePkg\Library\BaseLib\X64\SetJump.nasm

;------------------------------------------------------------------------------
; UINTN
; EFIAPI
; SetJump (
;   OUT     BASE_LIBRARY_JUMP_BUFFER  *JumpBuffer
;   );
;------------------------------------------------------------------------------
global ASM_PFX(SetJump)
ASM_PFX(SetJump):
    push    rcx
    add     rsp, -0x20
    call    ASM_PFX(InternalAssertJumpBuffer)

完整的例子代码在这里下载

批处理请求管理员权限

当我们使用 RW Everything 这样的软件时,会自动请求管理员权限。

相比之下,我们使用批处理文件调用FITW 刷写 BIOS工具的时候不会出现这样的提示,又忘记使用管理员权限打开 CMD 窗口,这样会导致执行失败。最近看到了一个好用的批处理,可以在批处理文件中直接像 RW 这样来请求管理员权限,这样能够避免忘记使用管理员权限打开的问题。

批处理来自 “在批处理中提升权限 (UAC开启状态下)”【参考1】,根据作者的原文应该是翻译自https://sites.google.com/site/eneerge/home/BatchGotAdmin

@echo off

:: BatchGotAdmin
:-------------------------------------
REM  --> Check for permissions
>nul 2>&1 "%SYSTEMROOT%\system32\cacls.exe" "%SYSTEMROOT%\system32\config\system"

REM --> If error flag set, we do not have admin.
if '%errorlevel%' NEQ '0' (
    echo Requesting administrative privileges...
    goto UACPrompt
) else ( goto gotAdmin )

:UACPrompt
    echo Set UAC = CreateObject^("Shell.Application"^) > "%temp%\getadmin.vbs"
    echo UAC.ShellExecute "%~s0", "", "", "runas", 1 >> "%temp%\getadmin.vbs"

    "%temp%\getadmin.vbs"
    exit /B

:gotAdmin
    if exist "%temp%\getadmin.vbs" ( del "%temp%\getadmin.vbs" )
    pushd "%CD%"
    CD /D "%~dp0"
:--------------------------------------

测试结果:

参考:

1. https://blog.csdn.net/wangjia184/article/details/7488341

Step to UEFI (180)GetVbtData 取得 Vbt

之前的《Shell 下读取 VBT》【参考1】给出了一个读取VBT 的方法。Simon 留言表示可以使用 PLATFORM_GOP_POLICY_PROTOCOL提供的 GetVbtData 。可以在\Vlv2DeviceRefCodePkg\ValleyView2Soc\NorthCluster\Include\Protocol\PlatformGopPolicy.h 中看到如下定义:

#define EFI_PLATFORM_GOP_POLICY_PROTOCOL_GUID \
  { 0xec2e931b, 0x3281, 0x48a5, 0x81, 0x7, 0xdf, 0x8a, 0x8b, 0xed, 0x3c, 0x5d }
#define PLATFORM_GOP_POLICY_PROTOCOL_REVISION_01 0x01
#define PLATFORM_GOP_POLICY_PROTOCOL_REVISION_02 x0222
#pragma pack(1)

typedef enum {
  LidClosed,
  LidOpen,
  LidStatusMax
} LID_STATUS;

typedef enum {
  Docked,
  UnDocked,
  DockStatusMax
} DOCK_STATUS;

typedef
EFI_STATUS
(EFIAPI *GET_PLATFORM_LID_STATUS) (
  OUT LID_STATUS *CurrentLidStatus
  );

typedef
EFI_STATUS
(EFIAPI *GET_VBT_DATA) (
  OUT EFI_PHYSICAL_ADDRESS *VbtAddress,
  OUT UINT32 *VbtSize
  );

#pragma pack()

typedef struct _PLATFORM_GOP_POLICY_PROTOCOL {
  UINT32                             Revision;
  GET_PLATFORM_LID_STATUS            GetPlatformLidStatus;
  GET_VBT_DATA                       GetVbtData;
} PLATFORM_GOP_POLICY_PROTOCOL;

根据上述定义编写代码,首先搜索 PLATFORM_GOP_POLICY_PROTOCOL ,然后调用它提供的GetVbtData 函数。

/** @file
    A simple, basic, EDK II native, "hello" application to verify that
    we can build applications without LibC.

    Copyright (c) 2010 - 2011, 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.

    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/UefiLib.h>
#include  <Library/ShellCEntryLib.h>
#include  <Protocol/SimpleFileSystem.h>
#include  <Library/UefiBootServicesTableLib.h> //global gST gBS gImageHandle
#include  <Library/ShellLib.h>
#include  <Library/IoLib.h>

#include  "PlatformGopPolicy.h"

EFI_GUID gPlatformGOPPolicyGuid      = 
        { 0xec2e931b, 0x3281, 0x48a5, 
                { 0x81, 0x07, 0xdf, 0x8a, 0x8b, 0xed, 0x3c, 0x5d } };

//                        
//Save memory address to a file                        
//
EFI_STATUS 
SaveToFile(
        IN UINT8 *FileData, 
        IN UINTN FileDataLength)
{
    EFI_STATUS          Status;
    EFI_FILE_PROTOCOL   *FileHandle;
    UINTN               BufferSize;
    EFI_FILE_PROTOCOL   *Root;
    EFI_SIMPLE_FILE_SYSTEM_PROTOCOL *SimpleFileSystem;

    Status = gBS->LocateProtocol(
                &gEfiSimpleFileSystemProtocolGuid, 
                NULL,
                (VOID **)&SimpleFileSystem);
                
    if (EFI_ERROR(Status)) {
            Print(L"Cannot find EFI_SIMPLE_FILE_SYSTEM_PROTOCOL \r\n");
            return Status;
    }

    Status = SimpleFileSystem->OpenVolume(SimpleFileSystem, &Root);
    if (EFI_ERROR(Status)) {
        Print(L"OpenVolume error \r\n");
        return Status;
    }
    Status = Root->Open(
                Root, 
                &FileHandle, 
                L"dumpvbt.bin",
                EFI_FILE_MODE_READ |
                EFI_FILE_MODE_WRITE | 
                EFI_FILE_MODE_CREATE, 
                0);
    if (EFI_ERROR(Status)){
        Print(L"Error Open NULL  [%r]\n",Status);
        return Status;
    }
    
    BufferSize = FileDataLength;
    Status = FileHandle->Write(FileHandle, &BufferSize, FileData);
    if (EFI_ERROR(Status)){
        Print(L"Error write [%r]\n",Status);
        return Status;
    }
    else Print(L"VBT has been saved to 'dumpvbt.bin' \n");
    
    FileHandle->Close(FileHandle);
    
    return Status;
}


/***
  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
  )
{
        PLATFORM_GOP_POLICY_PROTOCOL    *mPlatformGopPolicyProtocl;
        EFI_STATUS              Status;
        EFI_PHYSICAL_ADDRESS    VbtAddress;
        UINT32                  VbtSize;
        
        Print(L"*************************************************\n");
        Print(L"* VBT dump tool By Platform GOP Policy Protocol *\n");
        Print(L"*                                 May 4th 2019  *\n");
        Print(L"*                      Powered by www.lab-z.com *\n");
        Print(L"*************************************************\n");
        
        //
        // Locate the Cpu Arch Protocol.
        //
        Status = gBS->LocateProtocol(
                        &gPlatformGOPPolicyGuid, 
                        NULL, 
                        &mPlatformGopPolicyProtocl);
        if (EFI_ERROR (Status)) {
                Print(L"Can't find EFI_PLATFORM_GOP_POLICY_PROTOCOL_\n");
                return Status;
        }        
        
        mPlatformGopPolicyProtocl->GetVbtData(&VbtAddress,&VbtSize);
        SaveToFile((UINT8 *)VbtAddress,VbtSize);
        
        return(0);
}

在 Shell 下运行之后会生成dumpvbt.bin ,内容就是当前使用的 VBT。

完整的代码和 EFI 文件下载:

这个方法和之前相比优点是避免了计算便宜之类的问题更加简单明确,可以在一定程度上克服不同Chipset之间的差别;缺点是:并非公开的 Protcol 在使用的时候可能存在一定风险。

最后,再次感谢 Simon 先生的建议。

参考:

1. http://www.lab-z.com/stu175vbt/  Step to UEFI (175)Shell 下读取 VBT

UEFI Tips: 嵌入 Asm 汇编需要特别注意的地方

之前介绍过,现在的 VS 不支持直接在代码中嵌入汇编,因此需要单独将汇编语句写在独立的 Asm 中然后在编译过程中会 Link 到生成的 EFI 文件中。最近碰到了一个奇怪的问题:明明写好的 Asm 在编译的 Link 过程中会提示无法找到对应的函数。经过研究最终确定是因为Asm 文件的名称导致的。

例如,之前的 “新指令 RDRAND”【参考1】文章中 INF 定义如下

[Sources.IA32]
  IA32/RdRandWord.c
  IA32/AsmRdRand.asm

[Sources.X64]
  X64/RdRandWord.c
  X64/AsmRdRand.asm

如果你手抖,命名成 X64/RdRandWord.asm 就会出现前面提到的问题。并且根据错误提示一直无法确定原因。

Intel CCA/DBC简介

本文根据《软件调试(第2版)》卷1:硬件基础“第七章 JTAG 调试”编写。建议有兴趣的朋友购买一本来学习。对于 Firmware 工程师来说,从底向上学习是一个很好的方向。

现在的X86变得日益复杂,如何进行有效的Debug 也日渐成为一个难题。为此 Intel 在芯片组或者 CPU 上预留了一个称作DCI (Intel® Direct Connect Interface)的Debug 接口。这个接口使用 USB 3.0一模一样的外部连接。CPU 内部有切换器,当Chipset 发现外部有设备和他握手,就将原本的USB信号切换为DCI 的信号。这样无需额外的预留就能实现Debug。

在DCI 出现之前,Intel 使用J-Tag 接口。当时的 Debug 盒子是下面这样,叫做 In-Target Proble,简称 ITP。  当年价格在 3000刀,现在好像没有这么贵了。之前我在的公司买了一个,老板恨不得把它供起来,一年也用不到几次。最后不知道什么原因它的适配器坏掉了。我去询问价格,得到的答案是适配器90刀,然后90天发货……..当然对方也很nice 的告诉我可以去中关村配一个电压相同功率差不多的也能用。后来回报上去之后老板左右不定,不想花钱和时间又怕损坏………最终这个设备束之高阁了。

图片来自【参考1】

为了Debug,主板上同时必须预留下面这样的 J Tag 接口。显而易见,说服HW工程师在主板上预留这样的接口需要花费极大的口舌,至于说服老板在量产板子上焊接这样的接口几乎是不可能的事情。因此,串口一直是BIOS工程师的最爱。

图片来自【参考2】

现在的Debug 盒子长得是下面这样,全称是“Intel SVT Closed Chassis Adapter”,缩写 “CCA”(Intel Silicon View Technology Closed Chassis Adapter),价格390刀。更通俗称作“DCI 蓝盒子”。通过这个设备可以绕开 CPU 直接和硬件打交道。这样,当 CPU 出现问题的时候,比如:CPU Hang了,工程师有机会来读取一些寄存器值…….

来自【参考3】

除此之外还有长得类似 WinDBG 线的 DbC( debug class ) 线,同样是插在 USB 3.0 口上就能进行Debug。

图片来自参考4

相比前面的 CCA ,可以看出 DBC 更见简单便宜,但是在Debug 的时候可能会出现无法连接的情况,比如停得太早或者 XHCI 出现问题直接死掉了。因此,如果有可能尽量首选 CCA。

上述设备的典型应用如下(来自文章开始提到的《软件调试》 7.4.7)

正如本章开头所说的,硬件调试工具通常用来解决软件调试器难以解决的问题,以下是使用 JTAG 方式调试的一些典型场景。

(1)调试系统固件代码,包括BIOS代码,EFI代码以及支持AMT技术的ME(Management Engine)代码。

(2)调试操作系统的启动加载程序(Boot Loader),以及系统临界状态的问题,比如进入睡眠和从睡眠状态恢复过程中发生的问题。

(3)软件调试器无法调试的其他情况,比如开发软件调试器时调试实现调试功能的代码(例如Windows的内核调试引擎),以及调试操作系统内核的中断处理函数,任务调度函数等。

(4)观察CPU的微状态,比如CPU的ACPI 状态(C State)。

作为BIOS工程师难得申请一些设备,所以如果有可能尽量申请一个。毕竟“工具善其事,必先利其器”,工具简单直观能省去很多麻烦。

参考:

  1. https://habrahabr.ru/company/pt/blog/341946/
  2. https://minnowboard.org/add-ons/debugger-lure
  3. https://designintools.intel.com/Silicon_View_Technology_Closed_Chassis_Adapter_p/itpxdpsvt.htm
  4. https://designintools.intel.com/product_p/itpdciamcm1mu.htm

C# 取得本机 IP

通常情况下,一台电脑不止一个 IP,因此需要考虑枚举出所有的 IP.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net;
using System.Net.Sockets;

namespace tmpshowip
{
    class Program
    {
        static void Main(string[] args)
        {
            string hostName = Dns.GetHostName(); // Retrive the Name of HOST  
            for (int i = 0; i < Dns.GetHostByName(hostName).AddressList.Length; i++)
            {
                // Get the IP  
                string myIP = Dns.GetHostByName(hostName).AddressList[i].ToString();
                Console.WriteLine("My IP Address is :" + myIP);
            }
            Console.ReadKey();
        }
    }
}

参考:

1.https://www.codeproject.com/questions/399318/how-to-get-user-ip-address-using-csharp

Step to UEFI (179)Shell下 EC Ram 读取工具

最近工作需要在 UEFI  Shell 下Check EC Ram 的设定,发现手上只有 Windows 下的读取工具(RW Everything)。于是研究了一下如何在Shell 读取 EC Ram。

 根据【参考1】读取的流程如下:

Port 66 Commands

There are also some EC commands that use ports 0x66 and 0x62. Some of these are standard ACPI commands as defined in the external ACPI spec, others are custom.

The port 66 protocol is essentially the standard ACPI EC interface protocol.

1. Wait for port66.IBF = 0

2. Write command byte to port 66.

3. For each outgoing data or address byte:

3a. Wait for port66.IBF = 0

3b. Write data or address byte to port 62.

4. For each incoming data byte:

4a. Wait for port66.OBF = 1

4b. Read data byte from port 62.

5. If the command requires no data or address bytes, you can determine when the command was accepted/executed by waiting for port66.IBF=0.

同时 ACPI 定义的通用 Command如下:

ACPI-defined port 66 commands

0x80 Read EC (write 0x80 to port 66, write address byte to port 62, read data byte from port 62)

0x81 Write EC (write 0x81 to port 66, write address byte to port 62, write data byte to port 62)

0x82 Burst Enable (write 0x82 to port 66, read data byte from port 62 – the data byte is “burst ACK”, value 0x90)

0x83 Burst Disable (write 0x83 to port 66, wait for port66.IBF=0)

0x84 Query EC (i.e. read SCI event queue) (write 0x84 to port 66, read data byte from port 62). When the data byte is 0, it means that the SCI event queue is empty.

最终根据上述资料,编写一个 Application 如下:

#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;

#define TIMEOUT         0xFFFF
#define ECCOMMAND       0x66
#define ECSTATUS        0x66
#define ECDATA          0x62
#define EC_S_OBF           BIT0
#define EC_S_IBF           BIT1
#define ECREADCMD          0x80

UINT8   MemBuffer[16][16];

void   WaitIBF() {
  UINT32 Status;
  UINTN Count;

  Count = 0;
  Status = 0;

  Status = IoRead8 (ECSTATUS);
  while (((Status & EC_S_IBF) != 0)||(Count>TIMEOUT)) {
    Status = IoRead8 (ECSTATUS);
    Count++;
  }
}

void    WaitOBF() {
  UINT32 Status;
  UINTN Count;

  Count = 0;
  Status = 0;

  Status = IoRead8 (ECSTATUS);
  while (((Status & EC_S_OBF) == 0)||(Count>TIMEOUT)) {
    Status = IoRead8 (ECSTATUS);
    Count++;
  }
}

UINT8 ReadECRam(UINT8 Index) {
  WaitIBF();               //1
  IoWrite8(ECCOMMAND,0x80);//2
  WaitIBF();               //3a
  IoWrite8(ECDATA, Index); //3b
  WaitOBF();               //4a
  return IoRead8(ECDATA); //4b
}

void GetData()
{
        UINT8   i,j;
        for (i=0;i<16;i++)
          for (j=0;j<16;j++) {
             MemBuffer[i][j]=ReadECRam(i*16+j);
          }
}
void ShowData()
{
        UINT8   i,j;
        Print(L"    ");
        for (i=0;i<16;i++) Print(L"%02X ",i);
        Print(L"\n");
        for (i=0;i<16;i++) {
                Print(L"%02X: ",i);
           for (j=0;j<16;j++) {
                Print(L"%02X ",MemBuffer[i][j]);
           }
           Print(L"\n");
        }
        Print(L"\n");
}
/***
  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_INPUT_KEY         Key;

  Key.ScanCode=SCAN_NULL;
  while (SCAN_UP!=Key.ScanCode)
    {
        gST->ConOut->ClearScreen(gST->ConOut);
        GetData();
        ShowData();
        gST -> ConIn -> ReadKeyStroke(gST->ConIn,&Key);   
        Print(L"Press Arrow-Up to exit\n");
        gBS->Stall(1000000UL);
    }

    return(0);
}

在实体机上运行结果如下(按向上键退出):

源代码和Application(X64)下载:

参考:

1. http://wiki.laptop.org/go/Ec_specification

Arduino Leonardo 自带的“显示屏

电子的巨大魅力在于无限的可能性。比如说用3个IO端口驱动6个 LED 或者用三极管、电感、电阻制作能榨干电池剩余电力的“焦耳小偷”。百思不得其解之后看到最终的解决方案总会有醍醐灌顶的感觉,也会非常钦佩第一个想到这样用法的人。和解决数学问题之后的快乐不同,因为电子和生活息息相关,学会了这样的招数转头也可以用在自己的设计上。

本文的起因是某天在网上看到有人用 Teensy 2.X 制作的摄像头【参考1】,2.0版本使用32u4也是Leonardo同款主控芯片,因此,这个项目完全可以使用在Leonardo上。

Arduino Leonardo 是很常见的Arduino开发板,它使用了 32U4 的主控芯片,其中带有了USB Device,因此我们有机会将视频直接投送到PC上,而具体的方法就是将设备报告为 USB Camera,再将要显示的内容生成视频发送出去。Windows 内置了 USB Mass Storage 驱动,因此用户可以直接使用 U盘而无需额外安装驱动。同样的,目前的 Win10 内置了 UVC(USB video device class)的驱动,对于符合这个协议定义的USB 设备可以直接在“摄像头”程序中显示出来。

上面介绍了基本原理,接下来就是具体的实验。为了能够更好的展现内容,实验的目标是滚动显示“祝新年快乐”字样。在实验验证上有很多经验之谈,比如:不要用4个字做实验,因为4刚好是2的2倍,同时也是2的平方。很多适用于此的技巧实际上只是巧合。因此,这次使用5个字。另外还有就是测试音频设备尽量不要使用纯音乐而要使用歌曲,后者更容易让测试人员得知当前的音调是否正常。

先研究一下字模的问题。为了将汉字的字形显示输出,汉字信息处理系统还需要配有汉字字模库,也称字形库,它集中了全部汉字的字形信息。需要显示汉字时,根据汉字内码向字模库检索出该汉字的字形信息,然后输出,再从输出设备得到汉字。汉字点阵字模有16*16点、24*24点、32*32点,48*48点几种,每个汉字字模分别需要32、72、128、288个字节存放,点数愈多,输出的汉字愈美观。从经验上来说,16×16是普通人能够接受的最小字形,虽然这个尺寸的字形信息也有缺少笔画的问题(比如:“量”字,在这个尺寸下会丢掉上面 “曰”的最下面一横),但是少于16X16的汉字字形信息会让观看者有明显的缺少笔画的的观感,有如“第二次简化字”死灰复燃。24×24 的字形信息则是完全不丢失笔画的最小尺寸。但是缺点很明显,每个汉字要比16×16的字形多花1倍的空间来进行存储。这对于内存和处理能力有限的单片机来说,着实是一个负担。因此,大多数情况下,单片机使用最多的是 16×16的自字形库HZK16。这个字库是符合GB2312国家标准的16×16点阵字库,HZK16的GB2312-80支持的汉字有6763个,符号682个。其中一级汉字有 3755个,按声序排列,二级汉字有3008个,按偏旁部首排列。取得字形的过程如下:

  1. 取得欲查询汉字的GB2312编码,每个汉字由2个Byte 进行编码,第一个字节被称作              “区码”;第二个字节被称作“位码”;
  2. 接下来计算汉字在HZK16中的绝对偏移位置:offset = (94*(区码-1)+(位码-1))*32
  3. 读取 HZK16 中 offset 给出的位置连续 32 Bytes 即可得到字形信息

例如:查询得到“奈”的区位码为3646,那么计算  offset=(94*(36-1)+(46-1))*32=106720(D)=0x1A0E0

○○○○○○○●○○○○○○○○   →   0x01,0x00
○○○○○○○●○○○○●○○○   →   0x01,0x08
○●●●●●●●●●●●●●○○   →   0x7F,0xFC
○○○○○○●○●○○○○○○○   →   0x02,0x80
○○○○○●○○○●○○○○○○   →   0x04,0x40
○○○○●○○○○○●●○○○○   →   0x08,0x30
○○○●○○○○○●○○●●●○   →   0x10,0x4E
●●●○●●●●●●●○○●○○   →   0xEF,0xE4
○○○○○○○○○○○○○○○○   →   0x00,0x00
○○○○○○○○○○○●○○○○   →   0x00,0x10
○○●●●●●●●●●●●○○○   →   0x3F,0xF8
○○○○○○○●○○○○○○○○   →   0x01,0x00
○○○○●○○●○○●○○○○○   →   0x09,0x20
○○○●○○○●○○○●●○○○   →   0x11,0x18
○●●○○●○●○○○○●○○○   →   0x65,0x08
○○○○○○●○○○○○○○○○   →   0x02,0x00

有了上面的知识,可以很容易的从字库中取得汉字的字形,比如,用程序取得”祝”字的字形信息:

其中的key[32] = {0x20,0x08,0x13,0xFC,0x12,0x08,0x02,0x08,0xFE,0x08,0x0A,0x08,0x12,0x08,0x3B,0xF8,0x56,0xA8,0x90,0xA0,0x10,0xA0,0x11,0x20,0x11,0x22,0x12,0x22,0x14,0x1E,0x18,0x00}; 就是我们需要的字形信息。

用同样的方法,可以依次取得“新年快乐”的字形信息。滚动的原理可以想象成一个窗户,不断在向右侧滑动。窗户的大小为  16 Bits ,落在这个里面的内容就是需要显示出来的内容。

出现在窗口中的数值有两种情况:

  1. 刚好是一个完整的字,那么直接输出这个字的信息即可;
  2. 介于第一个字和第二个字之间,需要取得第一个字形的高位信息,然后和第二个字的低位信息拼接即可;

上面解决了显示内容的问题,下面就是如何显示。模拟出来的摄像头将一帧帧的图像发送给PC,其中采用的是YV12的编码格式。通常我们接触到的都是 RGB 格式,每一个点由Red/Green/Blue 三个颜色信息组成,最常见的是每一个颜色各占1个字节。YV12 则使用的是另外的颜色表示方法,使用Y 命令度(Luminance),U色度(Chrominance)和 浓度(Chroma)。这种表示方法是 是历史原因导致的,它出现在Y’UV的发明是由于彩色电视与黑白电视的过渡时期。黑白视频只有Y(Luma,Luminance)视频,也就是灰阶值。到了彩色电视规格的制定,是以YUV的格式来处理彩色电视图像,把UV视作表示彩度的C(Chrominance或Chroma),如果忽略C信号,那么剩下的Y(Luma)信号就跟之前的黑白电视频号相同,这样一来便解决彩色电视机与黑白电视机的兼容问题。另外,这种编码方式也会使用视觉特性来节省空间。每一个点的 Y 信号是独立的,但是相邻的四个点会共享同一个U和同一个V信息。比如:之前空间上存在相邻的四个点 (Ri,Gi,Bi) 通过某种算法变换后得到 (Yi,U1,V1)这样的四个点信息。之前存放四个点需要 3*4=12Byte;变化之后只需要存储 Y1/Y2/Y3/Y4/U1/V1 6Byte的信息即可。减少了一半的数据量。

R1,G1,B1 R2,G2,B2 Y1,U1,V1 Y2,U1,V1
R3,G3,B3 R4,G4,B4 Y3,U1,V1 Y4,U1,V1

将上面的过程放在一个帧的图像上来看是下面这样

代码很长,但大部分都只是框架,send_yv12_frame() 是最关键的函数,具体的输出帧(借用计算机图形学的概念可以说是“渲染的过程”)是在其中完成计算的:

static inline void send_yv12_frame(void)
{
    uint16_t h,w,lastw;
    uint8_t write_hdr = 1;
    uint8_t hdr;
    uint8_t color, colcnt;
    //uint8_t br = LSB(brightness);
    uint8_t board[16][16];
    int c,low,high;
    char *p;
  
    usb_wait_in_ready();

    if (k==(sizeof(wordaddr)/2-1)*16) {k=0;}
    else k++;
    
    //显示一个完整的字形
    for (int i=0;i<16;i++)
    {
      //k给出当前要显示的窗口起始位置      
      if (k%16==0)  //如果当前指针刚好在一个字上,那么直接取出这个字进行显示
        {
          //指向要显示的汉字起始地址
          p=wordaddr[k/16];
          //取出这个字的一行信息
          c=((*(p+i*2)&0xFF)<<8)+(*(p+i*2+1)&0xFF);
        }
      else //指针不在一个字上,就要用两个字来拼成一个字进行显示
        {
          //指向要显示的第一个汉字
          p=wordaddr[k/16];
          //取出第一个汉字的一行
          low=(((*(p+i*2)&0xFF)<<8)+(*(p+i*2+1)&0xFF))&0xFFFF;           
          //指向要显示的第二个汉字
          p=wordaddr[(k/16+1)%sizeof(wordaddr)];
          //取出第二个汉字的一行
          high=(((*(p+i*2)&0xFF)<<8)+(*(p+i*2+1)&0xFF));
          //用取得的信息拼出来要显示的一行
          c=low<<(k%16)|(high&0xFFFF)>>(16-k%16);
        }

    for (int j=0;j<16;j++)
      {
        if ((c&0x8000)==0) {
              board[j][i]=0;//这个点位置有信息
          }
        else  board[j][i]=1;//这个点位无信息
        c=c<<1;
      }        
     }
     
    //下面发送一帧信息 
    /* Y plane (h*w bytes) */ 
    for(h=0; h < HEIGHT; h++) {
        w=0;
        color = cur_start_col;
        colcnt = COLUMN_WIDTH - cur_col_offset;
        do {
            if(!usb_rw_allowed()) {
                usb_ack_bank();                
                if(usb_wait_in_ready_timeo(10) < 0)                
                    return;                
                usb_ack_in();
                lastw = w;
                write_hdr = 1;
            } else {
                if(write_hdr) {
                    /* Write header */
                    hdr = UVC_PHI_EOH | (fid&1);
                    UEDATX = 2; // write header len
                    UEDATX = hdr;
                    write_hdr = 0;
                }
                //为了美观,上下各空出12
                if ((h<12)||(h>107)) {
                       UEDATX=0; 
                }
                else {
                   //检查当前的点阵信息输出黑或者白  
                   //Y:255 U:128 V:128 是白色
                   //Y:0   U:128 V:128 是黑色
                   if (board[w/10][(h-12)/6]==1) {UEDATX=0xFF;} // Y                
                   else {UEDATX=0;}
                }
                w++;
                if(--colcnt == 0) {
                    color = next_color(color);
                    colcnt = COLUMN_WIDTH;
                }
            }            
        } while(w < WIDTH);
    }

    /* U plane (h/2*w/2 bytes) */ 
    for(h=0; h < HEIGHT/2; h++) {
        w=0;
        color = cur_start_col;
        colcnt = COLUMN_WIDTH - cur_col_offset;
        do {
            if(!usb_rw_allowed()) {
                usb_ack_bank();                
                if(usb_wait_in_ready_timeo(10) < 0)                
                    return;                
                usb_ack_in();
                lastw = w;
                write_hdr = 1;
            } else {
                if(write_hdr) {
                    /* Write header */
                    hdr = UVC_PHI_EOH | (fid&1);
                    UEDATX = 2; // write header len
                    UEDATX = hdr;
                    write_hdr = 0;
                }
                UEDATX=128;                
                w++;
                if(colcnt <= 2) {
                    color = next_color(color);
                    colcnt = COLUMN_WIDTH;
                } else {
                   colcnt -= 2; 
                }

            }            
        } while(w < WIDTH/2);
    }

    /* V plane (h/2*w/2 bytes) */ 
    for(h=0; h < HEIGHT/2; h++) {
        w=0;
        color = cur_start_col;
        colcnt = COLUMN_WIDTH - cur_col_offset;
        do {
            if(!usb_rw_allowed()) {
                usb_ack_bank();                
                if(usb_wait_in_ready_timeo(10) < 0)                
                    return;                
                usb_ack_in();
                lastw = w;
                write_hdr = 1;
            } else {
                if(write_hdr) {
                    /* Write header */
                    hdr = UVC_PHI_EOH | (fid&1);
                    if(h==HEIGHT/2-1 && w < UVC_TX_SIZE)
                        hdr |= UVC_PHI_EOF; 
                    UEDATX = 2; // write header len
                    UEDATX = hdr;
                    write_hdr = 0;
                }
                UEDATX=128;
                w++;
                if(colcnt <= 2) {
                    color = next_color(color);
                    colcnt = COLUMN_WIDTH;
                } else {
                    colcnt-=2;
                }
            }            
        } while(w <WIDTH/2);
    }

    if(lastw != w) {
        usb_ack_bank();
    }
    fid = ~fid; // flip frame id bit
}

和之前的项目一样,这个需要基于 Lufa 库支持,使用  WinAvr 进行编译。使用 make 即可完成编译。

使用和Arduino刷新一样的的命令,要根据你的Arduino板子重启时占用的串口号调整 –PCOMn 参数。同时,每次刷新时,需要先按下 Arduino Reset键,然后运行刷写命令。

D: \arduino-1.8.4\hardware\tools\avr\bin\avrdude -v –d:\arduino-1.8.4\hardware\tools\avr\etc\avrdude.conf -patmega32u4 -cavr109 -PCOM7 -b57600 -D -V -Uflash:w:./uvc.hex:i

烧写成功后,运行 Camera 即可查看结果:

为了简单起见,只实现了黑白显示,有兴趣的朋友可以发挥想象力显示彩色的汉字,相信这只是一个开始,后面会有更多的玩法。

参考:

1. https://github.com/avivgr/teensy_uvc

Step to UEFI (178)UEFI 下面的 Openssl 签名

人类常用权威的话语来证明自己的观点,这种方式可以说是一种签名。当然,这种方式存在一个严重的缺陷:无法证明那个生成的名人确实说过这句话。


同样的,在互联网上,我们需要用一种方式来验证确认身份信息。其中的一个方法就是数字签名。

前面介绍过 RSA 算法,因为它能够实现非对称的加密所以还可以用来进行数字签名。比如:我在网站上公布了自己的公钥,然后每次发送消息的时候会附上使用私钥对这个消息签名结果。这样,别人就无法冒充我发布消息。这次展示使用 OpenSSL 实现UEFI下面的签名和验证。

第一步,生成RSA密钥对(我不清楚为什么文章都喜欢将这个直接称作“私钥”,实际上这样生成的结果是包括公钥部分的,索性这里我称之为密钥包)。

openssl genrsa -out rsa_private_key.pem 1024

生成的密钥包内容如下:

-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQDTiExu8F6X/VWPqPmzBc3mqUfMvOB0xtaTZAQbSfyt+uqxu7GN
aStGpdJvCuOFL7+FFXSFLJfmZsiYRaiOSpPDDuQLBkG0m/ABk8pLtobEy6UgJPpS
6Oiku1618+uwYKcZpPj2Ftu5d+FFMiS23SCI8zhLcF3fM2J7/IXudCOErQIDAQAB
AoGABbgtuOIu7JUg9x1ugvSpOI9jLZn9x6qIqruNkN9TQbEDH4Mfrd8mGGbrZa05
saQ03XhTCjbGdKhazCM2B4Lks9jb0ovA2CUvuX+sxUHXdgEFekEv/VVIzem3qt0F
kMV+lSX76F/CkV4XXOO4H1rYja7BObl/jFMjTpRgPh0g4AECQQD3MCGCwqqQvGBV
Lw8cJHj7E9bMK8NWauONJfYL0D+9/ylas+xS73iqLcSpWcchWDjbM3T2tYAeZLrf
i0VmHm4BAkEA2xLFKytC9dbIDJS+0316jZvK/AWsSDbKo+Fn+DnO9x6ZhstX062t
7WEDRHT3ZR0vQTS7f9SayxN3MMwSB88urQJBAOI2ofRQwleCjYZncqSGnFDqbwCa
bEGBwI1D2FAnXK47/VSMpBGiJgNXr0psZtgVLLMt/DRrFby64moBwpkZ8AECQQC3
7zWOfj81S8UhEw55YYQxO1odaeHxq9dN62Yw+tBzmcSLcVVnTA6ZHPfyVUaWJf/T
/qNiu63PzaMoXF7TIbftAkBcpDvw4T3kynR1mXyuC1jmvenYZGtfdhtCgqBRt4Z2
MFeMsqkFNDzuJbHdq1vdDUl2Oh4XyVyOGNc5hxiFrx8a
-----END RSA PRIVATE KEY-----

第二步,从这个密钥包分离出来公钥。

Openssl rsa -in rsa_private_key.pem -pubout -out rsa_public_key.pem

公钥文件名是rsa_public_key.pem,内容如下:

-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDTiExu8F6X/VWPqPmzBc3mqUfM
vOB0xtaTZAQbSfyt+uqxu7GNaStGpdJvCuOFL7+FFXSFLJfmZsiYRaiOSpPDDuQL
BkG0m/ABk8pLtobEy6UgJPpS6Oiku1618+uwYKcZpPj2Ftu5d+FFMiS23SCI8zhL
cF3fM2J7/IXudCOErQIDAQAB
-----END PUBLIC KEY-----

这个文件是可以公开的。前面提到的密钥包是需要保密的。

到这步,已经准备好了公钥和私钥,可以用

openssl rsa -in rsa_private_key.pem -text

解析出我们要定义在代码中的内容

Private-Key: (1024 bit)
modulus:   //这是公钥RsaN[]  需要特别注意,下面是 127 Byte,开头多了一个 00
    00:d3:88:4c:6e:f0:5e:97:fd:55:8f:a8:f9:b3:05:
    cd:e6:a9:47:cc:bc:e0:74:c6:d6:93:64:04:1b:49:
    fc:ad:fa:ea:b1:bb:b1:8d:69:2b:46:a5:d2:6f:0a:
    e3:85:2f:bf:85:15:74:85:2c:97:e6:66:c8:98:45:
    a8:8e:4a:93:c3:0e:e4:0b:06:41:b4:9b:f0:01:93:
    ca:4b:b6:86:c4:cb:a5:20:24:fa:52:e8:e8:a4:bb:
    5e:b5:f3:eb:b0:60:a7:19:a4:f8:f6:16:db:b9:77:
    e1:45:32:24:b6:dd:20:88:f3:38:4b:70:5d:df:33:
    62:7b:fc:85:ee:74:23:84:ad
publicExponent: 65537 (0x10001)  
privateExponent:  //这是私钥部分,RsaD[]
    05:b8:2d:b8:e2:2e:ec:95:20:f7:1d:6e:82:f4:a9:
    38:8f:63:2d:99:fd:c7:aa:88:aa:bb:8d:90:df:53:
    41:b1:03:1f:83:1f:ad:df:26:18:66:eb:65:ad:39:
    b1:a4:34:dd:78:53:0a:36:c6:74:a8:5a:cc:23:36:
    07:82:e4:b3:d8:db:d2:8b:c0:d8:25:2f:b9:7f:ac:
    c5:41:d7:76:01:05:7a:41:2f:fd:55:48:cd:e9:b7:
    aa:dd:05:90:c5:7e:95:25:fb:e8:5f:c2:91:5e:17:
    5c:e3:b8:1f:5a:d8:8d:ae:c1:39:b9:7f:8c:53:23:
4e:94:60:3e:1d:20:e0:01

第三步,有了上面的密钥包(其实是其中的私钥)即可对消息进行签名。例如,我们使用“This message from lab-z.com”作为被签名的字符串,将这一段存放在 string.txt 文件中。然后对其签名:

openssl dgst -sha1 -sign rsa_private_key.pem –out string.sign string.txt

签名结果如下:

第四步,签名的校验。使用下面的命令,这次用到的是公钥,然后针对 string.txt 校验签名结果是否和 string.sign内容相同。

openssl dgst -verify rsa_public_key.pem -sha1 -signature string.sign string.txt

如果结果相同,会输出Verified OK ,否则输出Verification Failure。

上面就是使用 OpenSSL.exe 来完成 RSA 签名校验的过程。接下来介绍使用 UEFI 完成这些操作。代码改编自 UDK2017 中的 CryptoPkg 下面 Application 的例子(但是从 UDK2018开始这部分代码被移除了)

/** @file
  Application for Cryptographic Primitives Validation.

Copyright (c) 2009 - 2016, 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/BaseMemoryLib.h>
#include <Library/MemoryAllocationLib.h>
#include <Library/UefiLib.h>
#include <Library/UefiApplicationEntryPoint.h>
#include <Library/DebugLib.h>
#include <Library/BaseCryptLib.h>

#define  RSA_MODULUS_LENGTH  1024

//
// Public Modulus of RSA Key
//
GLOBAL_REMOVE_IF_UNREFERENCED CONST UINT8 RsaN[] = {
    0xd3,0x88,0x4c,0x6e,0xf0,0x5e,0x97,0xfd,0x55,0x8f,0xa8,0xf9,0xb3,0x05,
    0xcd,0xe6,0xa9,0x47,0xcc,0xbc,0xe0,0x74,0xc6,0xd6,0x93,0x64,0x04,0x1b,0x49,
    0xfc,0xad,0xfa,0xea,0xb1,0xbb,0xb1,0x8d,0x69,0x2b,0x46,0xa5,0xd2,0x6f,0x0a,
    0xe3,0x85,0x2f,0xbf,0x85,0x15,0x74,0x85,0x2c,0x97,0xe6,0x66,0xc8,0x98,0x45,
    0xa8,0x8e,0x4a,0x93,0xc3,0x0e,0xe4,0x0b,0x06,0x41,0xb4,0x9b,0xf0,0x01,0x93,
    0xca,0x4b,0xb6,0x86,0xc4,0xcb,0xa5,0x20,0x24,0xfa,0x52,0xe8,0xe8,0xa4,0xbb,
    0x5e,0xb5,0xf3,0xeb,0xb0,0x60,0xa7,0x19,0xa4,0xf8,0xf6,0x16,0xdb,0xb9,0x77,
    0xe1,0x45,0x32,0x24,0xb6,0xdd,0x20,0x88,0xf3,0x38,0x4b,0x70,0x5d,0xdf,0x33,
    0x62,0x7b,0xfc,0x85,0xee,0x74,0x23,0x84,0xad
  };

//
// Public Exponent of RSA Key

GLOBAL_REMOVE_IF_UNREFERENCED CONST UINT8 RsaE[] = { 0x01, 0x00,0x01 };

//
// Private Exponent of RSA Key
//
GLOBAL_REMOVE_IF_UNREFERENCED CONST UINT8 RsaD[] = {
    0x05,0xb8,0x2d,0xb8,0xe2,0x2e,0xec,0x95,0x20,0xf7,0x1d,0x6e,0x82,0xf4,0xa9,
    0x38,0x8f,0x63,0x2d,0x99,0xfd,0xc7,0xaa,0x88,0xaa,0xbb,0x8d,0x90,0xdf,0x53,
    0x41,0xb1,0x03,0x1f,0x83,0x1f,0xad,0xdf,0x26,0x18,0x66,0xeb,0x65,0xad,0x39,
    0xb1,0xa4,0x34,0xdd,0x78,0x53,0x0a,0x36,0xc6,0x74,0xa8,0x5a,0xcc,0x23,0x36,
    0x07,0x82,0xe4,0xb3,0xd8,0xdb,0xd2,0x8b,0xc0,0xd8,0x25,0x2f,0xb9,0x7f,0xac,
    0xc5,0x41,0xd7,0x76,0x01,0x05,0x7a,0x41,0x2f,0xfd,0x55,0x48,0xcd,0xe9,0xb7,
    0xaa,0xdd,0x05,0x90,0xc5,0x7e,0x95,0x25,0xfb,0xe8,0x5f,0xc2,0x91,0x5e,0x17,
    0x5c,0xe3,0xb8,0x1f,0x5a,0xd8,0x8d,0xae,0xc1,0x39,0xb9,0x7f,0x8c,0x53,0x23,
    0x4e,0x94,0x60,0x3e,0x1d,0x20,0xe0,0x01
  };

//
// Known Answer Test (KAT) Data for RSA PKCS#1 Signing
//
GLOBAL_REMOVE_IF_UNREFERENCED CONST CHAR8 RsaSignData[] = "This message from lab-z.com";

//
// Known Signature for the above message, under SHA-1 Digest
//
GLOBAL_REMOVE_IF_UNREFERENCED CONST UINT8 RsaPkcs1Signature[] = {
0x61,0xA2,0xDD,0x93,0x1A,0xA6,0x1B,0x46,0x2A,0x84,0xC8,0x7A,0x74,0xBB,0x23,0x44,
0x43,0xD6,0xE9,0x2A,0x30,0xAF,0x2D,0x13,0x0A,0x74,0x26,0x78,0xF6,0x42,0x23,0x4D,
0x55,0x85,0xC9,0x42,0xA8,0x4A,0xAC,0x2C,0x46,0x76,0xF5,0x34,0xC7,0x57,0x3B,0x2F,
0x4B,0xF9,0x42,0x03,0x1F,0x80,0xCE,0xF2,0xD7,0xA4,0x8C,0xBB,0xBF,0x37,0x60,0x4A,
0x32,0x3A,0xF2,0x82,0x95,0xF3,0x11,0x40,0x2E,0x45,0x4B,0x2E,0x02,0xBA,0xAA,0xFC,
0x29,0x8D,0xEC,0x56,0xC6,0xCD,0x97,0x06,0xDE,0x52,0x85,0xDB,0x1B,0x17,0xF1,0x39,
0xBB,0x6B,0x8C,0xAA,0xFE,0xEC,0xD4,0xA7,0x96,0x6F,0x22,0xCD,0x4B,0x6D,0x01,0x0B,
0x00,0xA1,0xDF,0x7F,0xA4,0xA0,0xD2,0xC4,0x09,0x0C,0xB0,0x4A,0x7A,0xA2,0xE2,0x93
};

/**
  Entry Point of Cryptographic Validation Utility.

  @param  ImageHandle  The image handle of the UEFI Application.
  @param  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
CryptestMain (
  IN     EFI_HANDLE                 ImageHandle,
  IN     EFI_SYSTEM_TABLE           *SystemTable
  )
{
  VOID     *Rsa;
  UINT8    HashValue[SHA1_DIGEST_SIZE];
  UINTN    HashSize;
  UINTN    CtxSize;
  VOID     *Sha1Ctx;
  UINT8    *Signature;
  UINTN    SigSize;
  BOOLEAN  Status;
  UINTN i;
  UINT8    sign[SHA1_DIGEST_SIZE];

  RandomSeed (NULL, 0);

  //
  // SHA-1 Digest Message for PKCS#1 Signature 
  //
  Print (L"Hash Original Message ...\n");
  HashSize = SHA1_DIGEST_SIZE;
  ZeroMem (HashValue, HashSize);
  CtxSize = Sha1GetContextSize ();
  Sha1Ctx = AllocatePool (CtxSize);

  Status  = Sha1Init (Sha1Ctx);
  if (!Status) {
    Print (L"[Fail]");
    return EFI_ABORTED;
  }

  Status  = Sha1Update (Sha1Ctx, RsaSignData, AsciiStrLen (RsaSignData));
  if (!Status) {
    Print (L"[Fail]");
    return EFI_ABORTED;
  }

  Status  = Sha1Final (Sha1Ctx, HashValue);
  if (!Status) {
    Print (L"[Fail]");
    return EFI_ABORTED;
  }
  
  for (i=0;i<HashSize;i++) {Print (L"%02X  ",HashValue[i]);}
  
  FreePool (Sha1Ctx);

  //
  // Sign RSA PKCS#1-encoded Signature
  //
  Print (L"PKCS#1 Signature ...\n");

  Rsa = RsaNew ();
  if (Rsa == NULL) {
    Print (L"[Fail]t\n");
    return EFI_ABORTED;
  }

  Status = RsaSetKey (Rsa, RsaKeyN, RsaN, sizeof (RsaN));
  if (!Status) {
    Print (L"[Fail]p\n");
    return EFI_ABORTED;
  }

  Status = RsaSetKey (Rsa, RsaKeyE, RsaE, sizeof (RsaE));
  if (!Status) {
    Print (L"[Fail]h\n");
    return EFI_ABORTED;
  }
  //Private Key
  Status = RsaSetKey (Rsa, RsaKeyD, RsaD, sizeof (RsaD));
  if (!Status) {
    Print (L"[Fail]q\n");
    return EFI_ABORTED;
  }

  SigSize = 0;
  Status  = RsaPkcs1Sign (Rsa, HashValue, HashSize, sign, &SigSize);
  if (Status || SigSize == 0) {
    return EFI_ABORTED;
  }
  Signature = AllocatePool (SigSize);
  Status  = RsaPkcs1Sign (Rsa, HashValue, HashSize, Signature, &SigSize);
  if (!Status) {
    Print (L"[Fail]y\n");
    return EFI_ABORTED;
  }

  for (i=0;i<SigSize;i++) { Print(L"%02X  ",Signature[i]);} Print(L" \n");
  
  if (SigSize != sizeof (RsaPkcs1Signature)) {
    Print (L"[Fail]\n");
    return EFI_ABORTED;
  }

  if (CompareMem (Signature, RsaPkcs1Signature, SigSize) != 0) {
    Print (L"[Fail]a2\n");
    return EFI_ABORTED;
  }

  //
  // Release Resources
  //
  RsaFree (Rsa);
  Print (L"Release RSA Context ... [Pass]");

  Print (L"\n");

  return EFI_SUCCESS;
}

运行结果如下:

可以看到代码是先进行了一次 SHA1 运算,然后再用私钥进行签名的。我猜测这样操作的原因是为了避免 RSA运算缓慢和安全性方面的考虑。

完整的代码下载

参考:

  1. https://www.cnblogs.com/gordon0918/p/5382541.html openssl 摘要和签名验证指令dgst使用详解
  2. https://www.cnblogs.com/liliyang/p/9738964.html (4) openssl rsa/pkey(查看私钥、从私钥中提取公钥、查看公钥)