入口函数

一个简单的UEFI程序至少包含两个部分

  • C程序源文件 : 用来实现具体功能,在复杂程序中源文件可能包含:C/C++源代码.asm汇编文件(.s汇编文件,uin资源文件(字符串资源文件)和.vfr资源文件)
  • INF元数据文件用来编译代码

入口函数返回值类型EFI_STATUS

  • UEFI中基本所有的返回值类型都是EFI_STATUS,其本质是无符号长整数
  • 最高位为1时其值为错误代码,为0表示正确。听过宏EFI_ERROR(Status)可以判断返回值Status

头文件

  • <Uefi.h>: 定义了UEFI中的基本数据类型核心数据结构
  • <Library/UefiLib.h>: 提供通用的库函数,包括时间简单锁任务优先级驱动管理和字符图形显示输出 等基本功能
  • <Library/BaseLib.h> 字符串处理数学文件路径处理等相关库函数
  • <Library/BaseMemoryLib.h>内存的库函数,包括内存拷贝内存填充内存清空等库函数
  • <Library/DebugLib.h> :提供调试输出功能的库函数

efi文件

.efi文件(UEFI应用程序UEFI驱动程序)加载到内存后生成的对象成为Image映像)。ImageHandleImage对象的句柄,作为模版入口函数参数,它表示模块自身加载到内存后生成的Image对象

UEFI Image通常存储在文件系统中,可以是.efi文件或一个文件夹,包含多个.efi文件,UEFI Image可以包含UEFI BIOS的一些功能模块和驱动程序,也可以是一个UEFI应用程序或操作系统的引导程序。当UEFI启动时,UEFI Loader会加载UEFI Image到内存中,并执行其中的代码来启动系统。

UEFI中,映像通常指的是内存映像,用于表示一个进程或操作系统执行时的内存状态。例如,在UEFI内存取证中,可以通过获取系统的内存映像来分析系统的运行状态和数据。

GUID

typedef struct {
  UINT32    Data1;
  UINT16    Data2;
  UINT16    Data3;
  UINT8     Data4[8];
} GUID;

字符量

/*
__int64的最大值:9223372036854775807
__int64的最小值:-9223372036854775808
unsigned __int64的最大值:18446744073709551615
*/
typedef unsigned __int64  UINT64;
typedef __int64           INT64;
typedef unsigned __int32  UINT32;
typedef __int32           INT32;
/*
 unsigned short [ int ] 的字节数位2,取值范围是 0 ~ 2^16-1,即 0 ~ 65535
*/
typedef unsigned short    UINT16;
typedef unsigned short    CHAR16;
// 短整型 -32768 ~ +32767
typedef short             INT16;
// 无符号字符型 unsigned  0~255
typedef unsigned char     BOOLEAN;
typedef unsigned char     UINT8;
// 字符型char取值范围 -128 ~ +127
typedef char              CHAR8;
// 有符号字符类型 -128到127
typedef signed char       INT8;

工程结构

  • PkgPackage的缩写,的意思
  • EDK2模块化,每个Pkg都是一个解决方案
  • build保存着Pkg编译的结果
  • Conf保存各种配置文件,target.txt编译目标,
  • edksetup.bat脚本
  • dsc是对整个包的描述
  • dec定义公开内容接口,是UEFI接口的实现

启动EDK2

EDK2是实现UEFI的源码

build构建之后,进入对应的如下页面,启动WinHost.exe文件。

D:\edk2\Build\EmulatorX64\DEBUG_VS2019\X64
# U盘的文件系统为FAT32系统I会首先检查下面目录寻找,作为启动
EFI/Boot/BootX64.efi

第一个Hello Word

一个基本的UEFI程序需要包括.c文件,以及.inf文件模块工程文件,描述模块的属性提供依赖库,支持的CPU架构等信息。

存储内容

  • EDK2文件夹下分为两个文件edk2edk2-libc
  • EDK2文件夹下创建mybuild.bat,设置基本环境配置
set WORKSPACE=%CD%
set EDK_TOOLS_PATH=%CD%\edk2\BaseTools
set CONF_PATH=%CD%\edk2\Conf
set PACKAGES_PATH=%CD%\edk2;%CD%\edk2-lib
  • 在根目录的AppPkg目录Applications目录下创建目录
  • 分别创建.c.inf文件
  • Application同级目录的AppPkg.dsc文件中添加文件.inf模块工程文件内容
[Components]
  AppPkg/Applications/ShellApp_Main/ShellApp_Main.inf
  AppPkg/Applications/Stdlib_Main/Stdlib_Main.inf    
  AppPkg/Applications/Uefi_Main/Uefi_Main.inf

入口函数

Uefi_Main.c

/** @file
  This is Sample of UEFI aplication. 
 * **/
#include <Uefi.h>
#include <Library/UefiLib.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.
  EFI_STATUS   typedef UINT64 UINTN;
  @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,  //Image-Handle指向模版自身加载到内存的Image对象,也就是Image对象的句柄
  IN EFI_SYSTEM_TABLE  *SystemTable  //SystemTable是UEFI应用与UEFI内核交互的桥梁,通过它可以获得UEFI提供的各种服务,包括BS服务,和RT服务
  )
{
  EFI_TIME curTime; //时间函数结构体
  Print(L"Hello,this is Entry of UefiMain!\n"); //使用Print方法输出

  //使用BootService和RuntimeService
  SystemTable->BootServices->Stall(2000);  //延时2秒
  /*
    调用了在RuntimeServices里面的GetTime函数指针 (&curTime,NULL)为参数
    RuntimeServices为结构体
    将得到的时间结构体赋值给了curTime结构体
  */
  SystemTable->RuntimeServices->GetTime(&curTime,NULL);
  //通过结构体输出
  Print(L"Current Time: %d-%d-%d %02d:%02d:%02d\n",curTime.Year,curTime.Month,curTime.Day,curTime.Hour,curTime.Minute,curTime.Second);
  Print(L"NanoTime:%d\n",curTime.Nanosecond);
  Print(L"TimeZone:%d\n",curTime.TimeZone);
  Print(L"Pad1:%d\n",curTime.Pad1);
  Print(L"Pad2:%d\n",curTime.Pad2);
  //使用SystemTable
  // 使用SystemTable的输出函数调用进行输出
  SystemTable->ConOut->OutputString(SystemTable->ConOut,L"Test SystemTable...\n\r");
  return EFI_SUCCESS; //return 0
}

Uefi_Main.inf

[Defines]
  INF_VERSION =      0x00010005
  BASE_NAME =        Hello
  FILE_GUID =        f35a7352-2cc1-44c0-9ba6-4c3b5f4dbe42
  MODULE_TYPE =      UEFI_APPLICATION 
  VERSION_STRING =   1.0
  ENTRY_POINT =      UefiMain
[sources]
  Hello.c
[Packages]
  MdePkg/MdePkg.dec
  #MdeModulePkg/MdeModulePkg.dec
[LibraryClasses]
  UefiApplicationEntryPoint
  UefiLib

ShellApp_Main.c

#include  <Uefi.h>
#include  <Library/UefiLib.h>
#include  <Library/ShellCEntryLib.h>
#include <Library/UefiBootServicesTableLib.h>  //gST,gBs
#include <Library/UefiRuntimeServicesTableLib.h> //gRT
/***
  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_TIME curTime;
  Print(L"Hello,this is Entry of ShellAppMain!\n");

  //使用BootService和RuntimeService
  // gST->BootServices->Stall(2000);  //延时2秒
  gBS->Stall(2000);
  // gST->RuntimeServices->GetTime(&curTime,NULL);
  gRT->GetTime(&curTime,NULL);
  Print(L"Current Time: %d-%d-%d %02d:%02d:%02d\n",curTime.Year,curTime.Month,curTime.Day,curTime.Hour,curTime.Minute,curTime.Second);

  //使用SystemTable
  gST->ConOut->OutputString(gST->ConOut,L"Test SystemTable...\n\r");

  return(0);
}

ShellApp_Main.inf

## @file
#  Sample UEFI Application Reference EDKII Module.
#
#  This is a sample shell application that will print Hello,this is Entry of ShellAppMain!" to the
#  UEFI Console.
#
##

[Defines]
  INF_VERSION                    = 0x00010006
  BASE_NAME                      = ShellApp_Main
  FILE_GUID                      = a912f198-7f0e-4813-b918-b757b106ec83
  MODULE_TYPE                    = UEFI_APPLICATION
  VERSION_STRING                 = 0.1
  ENTRY_POINT                    = ShellCEntryLib

#
#  VALID_ARCHITECTURES           = IA32 X64
#

[Sources]
  ShellApp_Main.c

[Packages]
  MdePkg/MdePkg.dec
  ShellPkg/ShellPkg.dec

[LibraryClasses]
  UefiLib
  ShellCEntryLib

Stdlib_Main.c


#include  <Uefi.h>
#include  <Library/UefiLib.h>
#include  <Library/ShellCEntryLib.h>
#include <Library/UefiBootServicesTableLib.h>  //gST,gBs
#include <Library/UefiRuntimeServicesTableLib.h> //gRT

#include  <stdio.h>
#include  <stdlib.h>

/***
  Demonstrates basic workings of the main() function by displaying a
  welcoming message.

  Note that the UEFI command line is composed of 16-bit UCS2 wide characters.
  The easiest way to access the command line parameters is to cast Argv as:
  wchar_t **wArgv = (wchar_t **)Argv;

  @param[in]  Argc    Number of argument tokens pointed to by Argv.
  @param[in]  Argv    Array of Argc pointers to command line tokens.

  @retval  0         The application exited normally.
  @retval  Other     An error occurred.
***/
int
main (
  IN int Argc,
  IN char **Argv
  )
{
  EFI_TIME curTime;
  Print(L"Hello,this is Entry of ShellAppMain!\n");
  printf("Hello,this is Entry of main!\n");

  //使用BootService和RuntimeService
  gST->BootServices->Stall(2000);  //延时2秒
  gBS->Stall(2000); //调用全局变量
  //获取时间,真实的函数只有一个,而实现函数的函数指针有多个
  gST->RuntimeServices->GetTime(&curTime,NULL);
  gRT->GetTime(&curTime,NULL);
  printf("Current Time: %d-%d-%d %02d:%02d:%02d\n",curTime.Year,curTime.Month,curTime.Day,curTime.Hour,curTime.Minute,curTime.Second);

  //使用SystemTable
  gST->ConOut->OutputString(gST->ConOut,L"Test SystemTable...\n\r");

  return 0;
}

Stdlib_Main.inf

## @file
#  Sample UEFI Application Reference EDKII Module.
#
#  This is a sample shell application that will print Hello,this is Entry of main!" to the
#  UEFI Console.
#
##
[Defines]
  INF_VERSION                    = 0x00010006
  BASE_NAME                      = Stdlib_Main
  FILE_GUID                      = 4ea97c46-1491-4dfd-b412-747010f31e5f
  MODULE_TYPE                    = UEFI_APPLICATION
  VERSION_STRING                 = 0.1
  ENTRY_POINT                    = ShellCEntryLib

#
#  VALID_ARCHITECTURES           = IA32 X64
#

[Sources]
  Stdlib_Main.c

[Packages]
  StdLib/StdLib.dec
  MdePkg/MdePkg.dec
  ShellPkg/ShellPkg.dec

[LibraryClasses]
  LibC
  LibStdio

DSC添加编译

将添加到AppPkg目录下的文件添加到Components目录下

[Components]
  AppPkg/Applications/ShellApp_Main/ShellApp_Main.inf
  AppPkg/Applications/Stdlib_Main/Stdlib_Main.inf    
  AppPkg/Applications/Uefi_Main/Uefi_Main.inf

修改输出位置

为了方便EDK2运行环境执行,固将AppPkg.dsc输出的目录修改为EDK2编译环境的执行位置,方便测试执行

修改下面对应的

[Defines]
	OUTPUT_DIRECTORY               = Build/EmulatorX64/DEBUG_VS2019/X64  # 输出到指定的位置

EDK2编译

# 编译mybuild.bat
mybuild.bat
edk2\edksetup.bat
# 编译edk2的UEFI模拟器
build -p edk2\EmulatorPkg\EmulatorPkg.dsc -t VS2019 -a X64
# 编译UEFI程序
build -p edk2-libc\AppPkg\AppPkg.dsc -t VS2019 -a X64
    
# 清理编译结果,如果不清空,存在无法覆盖问题
build clean -p edk2\EmulatorPkg\EmulatorPkg.dsc -t VS2019 -a X64
    
# 清理edk2-lib2
build clean -p edk2-libc\AppPkg\AppPkg.dsc -t VS2019 -a X64
  • 编译完成之后会创建Build目录
  • 运行build即可对刚才输入的程序进行编译efi可执行文件
  • 进入WinHost.exe运行对应的文件名即可运行成功
# 输入如下指令,进入根目录
FS0:
# 输入Hello点回车自动补全,即可进入HelloWorld.efi输出UEFI Hello World!
Hello.efi

第一个HelloWorld

运行结束后,执行build clean清空缓存,以防止再次编译无法覆盖问题。

相关指令

# 启动UI的BIOS页面
UiApp.efi

编译运行

在edk2的跟页面启动CMD,运行

edksetup.bat
# 设置之后修改 conf 中的target.txt
edksetup.bat rebuild 
# 通过build构建,此方法构建的是默认conf设置的包路径
build

鼠标抓取小项目

首先将鼠标键盘时间绑定events事件

  • 通过while(1)循环
  • 等待鼠标或键盘事件发生,0鼠标,其它为键盘
  • 当为表示时获取当前鼠标的状态,根据其状态输出其对应的位置点击时间,输出左键或右键
  • 键盘的话,判断其对应的是否为q,若为qbreak结束循环
  • 若键盘输入不为k输出其值

后续可逐步进化到鼠标控制UEFI界面

小Bug

  • 键盘上输入字母,需要点一下鼠标才能显示出来。

Mouse_Main.c

#include <Uefi.h>
#include <Library/UefiLib.h>
#include <Library/UefiBootServicesTableLib.h> //gST,gBs
#include <Library/UefiRuntimeServicesTableLib.h>
#include <Protocol/SimplePointer.h> //mouse point include Protocol

// Include/Protocol/SimplePointer 
EFI_GUID gEfiSimplePointerProtocolGuid = { 0x31878C87, 0x0B75, 0x11D5, { 0x9A, 0x4F, 0x00, 0x90, 0x27, 0x3F, 0xC1, 0x4D }};

EFI_STATUS
EFIAPI
UefiMain(
    IN EFI_HANDLE ImageHandle, 
    IN EFI_SYSTEM_TABLE *SystemTable 
)
{
    
    EFI_TIME curTime; 
    Print(L"mouse clicked grap!\n");
    SystemTable->ConOut->OutputString(SystemTable->ConOut,L"mouse clicked grap\n");
   
    gRT->GetTime(&curTime,NULL);
    Print(L"Clicked Time: %d-%d-%d %d-%d-%d-%d\n",curTime.Year,curTime.Month,curTime.Day,curTime.Hour,curTime.Minute,curTime.Second,curTime.Nanosecond);
    // Enable Cursor


    EFI_STATUS Status;
    EFI_SIMPLE_POINTER_PROTOCOL* mounse = 0;
    EFI_SIMPLE_POINTER_STATE State;
    EFI_EVENT events[2];  //键盘鼠标事件
    // 显示光标
    gST->ConOut->EnableCursor(gST->ConOut,TRUE);
    // 根据GUID值找出鼠标设备
    Status = gBS->LocateProtocol(
        &gEfiSimplePointerProtocolGuid,
        NULL,
        (VOID*)&mounse
    );
    // 错误判断
    if(EFI_ERROR(Status))
    {
        Print(L"Unable to initialize EFI_SIMPLE_POINTER_PROTOCOL protocol interface!\n");
    }

    // 重置鼠标设置
    Status = mounse->Reset(mounse,TRUE);
    // 错误判断
    if(EFI_ERROR(Status))
    {
        Print(L"Unable to initialize EFI_SIMPLE_POINTER_PROTOCOL protocol interface!\n");
    }

    // 将鼠标事件放到等待事件数组
    events[0] = mounse->WaitForInput; 
    // 将键盘也放到等待数组中
    events[1] = mounse->WaitForInput;

    //使用循环读取
    while (1)
    {
        /* code */
        EFI_INPUT_KEY Key;
        UINTN index = 0;
        //等待events中的任一事件发生,根据时间判断键盘还是鼠标
        Status = gBS->WaitForEvent(2,events,&index);
        // 根据index的不同值会传出不同的事件
        if(index == 0)
        {
            if(State.LeftButton)
            {
                Print(L"Pressed Left\n");
            }
            else if(State.RightButton)
            {
                Print(L"Pressed Right\n");
            }
            //获取鼠标状态并输出
            Status = mounse->GetState(mounse,&State);
            gRT->GetTime(&curTime,NULL);
            Print(L"Clicked Time minuter second  N-Second:%d - %d - %d\t",curTime.Minute,curTime.Second,curTime.Nanosecond);
            Print(L"X:%d Y:%d Z:%d L:%d R:%d\n",
            State.RelativeMovementX,
            State.RelativeMovementY,
            State.RelativeMovementZ);
        }
        else
        {
            //按键盘事件发生后读取键盘
            Status = gST->ConIn->ReadKeyStroke(gST->ConIn,&Key);
            // 错误判断
            if(EFI_ERROR(Status))
            {
                // Print(L"95 Read Key Stroke Error\n"); //莫名多输入
            }
            //按q键退出
            if(Key.UnicodeChar == 'q')
            {
                
                Print(L"Key Board %c Was Pressed\n",Key.UnicodeChar);
                // SystemTable->ConOut->OutputString(SystemTable->ConOut,L"Key input:%s\n",Key.UnicodeChar);
                // Print(L"Key input:%s\n",Key.UnicodeChar);
                break;
            }
            else if(Key.UnicodeChar) //当键盘有数据输入时,对其进行输入,若没有不操作
            {
                Print(L"Key Board %c Was Pressed\n",Key.UnicodeChar);
            }
        }

    }
    return EFI_SUCCESS;
}

Mouse_Main.inf

[Defines]
    INF_VERSION = 0x00010007  
    BASE_NAME = Mouse_Main 
    FILE_GUID = b579648b-5331-46a4-93ce-22423e567f55
    VERSION_STRING = 0.1
    MODULE_TYPE = UEFI_APPLICATION
    ENTRY_POINT = UefiMain


[Sources]
    Mouse_Main.c 

[Packages]
    MdePkg/MdePkg.dec
    MdeModulePkg/MdeModulePkg.dec

[LibraryClasses]
    UefiApplicationEntryPoint
    UefiLib

应用程序加载过程

一个应用程序被编译成.efi文件,整体上分成三步

  • UefiMain.c被编译成目标文件UefiMain.obj(程序编译后的二进制代码)
  • 连接器将目标文件UefiMain.obj其他库连接成UefiMain.dll(Dynamic Link LIbrary``动态链接库)
  • GenFW工具UefiMain.dl转换成UefiMain.efi

UEFI编译过程

报错解决思路

问题error 7000

error 7000: Failed to execute command Vc\bin\nmake.exe /nologo tbuild

error 7000

解决

通过UltraEdit查看中文注释是否存在乱码等行为.

解决乱码就OK啦,本身使用VS Code输入的莫名乱码,查Bug查了好久。

file read failture

inf模版工程文件不可有中文,包括注释中文会报错

注意性报错

输出字符串要加上L

fatal error LNK1120

链接失败,可能为在引用GUID地址时,没有当前函数对其进行赋值

在引用gEfiSimplePointerProtocolGuid是在文件顶部添加其对应的GUID唯一编码。

链接GUID