菜单

kaoqi
kaoqi
发布于 2026-09-14 / 7 阅读
0
0

基本Shell

经过之前多篇文章的积累,目前终于具备了搭建EFI的自有shell的各种条件。完成这种初步的hello world是后续打印各种协议信息的基础。

需要使用的东西是UefiMain.h所包含的表、SimpleText.h所包含的输入输出协议。

具体的实现思路是:

1、编写EFI_Main的主函数,引用其系统表

2、根据系统表调用ConIn、ConOut两个主要函数

3、添加ConIn中的阻滞函数于while循环中,在接收到键盘事件后,进行具体的动作

4、具体动作初步可以编写为一个string的直白打印

Shell.efi文件参考:

Shell-mzyq.efi

效果是非常的好啊,照片不好看就不发了。

编译杂项

正式进入EFI开发后,很多EDK2项目可以提供的东西最好还是用起来。

有关EFI主要的系统表、入口函数等内容,基本都定义在/edk2/MdePkg/Include/这个目录下面,具体来说,是主要集中在如下三个头文件上:

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

相应的,需要在gcc进行编译时(链接时可以不用头文件),以-I命令把头文件所在的目录加上,由于X86的要求,-I命令除了基本的根目录外还要再指定一个X86的目录。因此,需要再makefile中以参数的形式规定一些头文件检索路径,这里是一个参考:

## 这是makefile中的内容
## 新增参数用以代表检索路径

#######################################################################
# 项目名称
ProjectName = Shell
# 库文件路径参数
LIBFLAGS = -I /edk2/MdePkg/Include/ -I /edk2/MdePkg/Include/AArch64/
# EFI的入口函数
EFIMain = UefiMain
#######################################################################

此外,在引用来自EDK2项目的头文件时,由于其内部根据参数的不同在实例化具体变量结构时会变化,所以这个-I参数必须要对。

此外,EDK2项目本体已经对gST、gBS等尝试做了实例化,但不一定能用,实际最好还是直接以UefiSpec.h这个头文件中比较接地气的直接实例化为准展开变量解析。

有关makefile具体怎么写,这在之前的文章中已经详细谈过,这里不再赘述。那么,解决了最后的工具链问题后,开启真正的shell代码编写吧。

主函数开发

使用了来自MdePkg的头文件后,可以直接复用头文件中的struct。

#include <Uefi.h>
#include <Library/UefiLib.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Protocol/SimpleTextIn.h>

EFI_STATUS
EFIAPI
UefiMain (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
{
  EFI_STATUS      Status;
  EFI_INPUT_KEY   Key;
  BOOLEAN         Running = TRUE;
  UINTN Index;
  EFI_SYSTEM_TABLE    *gST = SystemTable;
  EFI_BOOT_SERVICES   *gBS = gST->BootServices;

  CHAR16          *HelloString = L"Hello, EFI World!\r\n";
  EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *ConOut = gST->ConOut;
  EFI_SIMPLE_TEXT_INPUT_PROTOCOL *ConIn = gST->ConIn;
  CHAR16          OutputStr[2];

  if (ConOut == NULL) {
    return EFI_UNSUPPORTED;
  }

  Status = ConOut->Reset(ConOut,FALSE);
  if (EFI_ERROR(Status)) {
    return Status;
  }

  // 打印欢迎界面
  Status = ConOut->OutputString(ConOut,HelloString);
  if (EFI_ERROR(Status)) {
    return Status;
  }

  // 开启光标可见
  ConOut->EnableCursor(ConOut, TRUE);

  while (Running) {
    // WaitForEvent:
    Status = gBS->WaitForEvent(1, &ConIn->WaitForKey, &Index);
    if (EFI_ERROR(Status)) {
      return Status;
    }

    // ReadKeyStroke: 读取按键数据
    Status = ConIn->ReadKeyStroke(ConIn, &Key);
    
    if (EFI_ERROR(Status)) {
      ConOut->OutputString(ConOut,L"\nError reading key\n");
      return Status;
    }

    // 如果 UnicodeChar 不为0,说明是可打印字符或普通ASCII字符
    if (Key.UnicodeChar != 0) {
      // 处理特殊控制字符,如回车换行
      if (Key.UnicodeChar == CHAR_CARRIAGE_RETURN) {
        ConOut->OutputString(ConOut,L"\r\n");
      } else if (Key.UnicodeChar == CHAR_BACKSPACE) {
        // 简单的退格处理:打印空格覆盖再退格,或者直接忽略,这里选择直接打印以便看到效果
        // 在真实终端中可能需要更复杂的缓冲处理,这里仅做简单回显
        ConOut->OutputString(ConOut,L"\b \b"); 
      } else {
        // 打印输入的字符
        OutputStr[0] = Key.UnicodeChar;
        OutputStr[1] = L'\0';
        ConOut->OutputString(ConOut,OutputStr);
      }
    } else {
      // 处理功能键(如F1-F12, Arrow Keys等),显示其扫描码
      // ScanCode 为 0 表示普通字符,非0表示功能键
      if (Key.ScanCode != 0) {
        ConOut->OutputString(ConOut,L"\nScanCode");
      }
    }
  }

  return EFI_SUCCESS;
}

使用makefile进行编译就可以了。


评论