Step to UEFI (40) ----- 从Shell下向Windows传值

很早之前,BIOS如果想把一些值传递给OS,通常都是使用CMOS的。但是这样的方法有着下面的缺点:复杂度高,需要和使用位置的人协商,否则没人知道你放在CMOS中什么地方
不确定性大,可能会在代码中冲突,无法确定别人是否也用到你选择的那个CMOS位
每次传递值少,CMOS一般也就128 BYTES,很容易耗尽。

现在进化到了UEFI的时代,可以在 Shell 上将一些内容存放在内存中,然后轻松的传输到Windows中。这里只是演示这种做法,总体来说还是非常简单的。

最关键的函数就是下面这个:

Status = pBS->AllocatePool(EfiReservedMemoryType, Hdr1->Length, &Ptr);

 

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

#include  <stdio.h>
#include  <stdlib.h>
#include  <wchar.h>
#include <Protocol/EfiShell.h>
#include <Library/ShellLib.h>

#include <Protocol/SimpleFileSystem.h>
#include <Protocol/BlockIo.h>
#include <Library/DevicePathLib.h>
#include <Library/HandleParsingLib.h>
#include <Library/SortLib.h>
#include <Library/MemoryAllocationLib.h>
#include <Library/BaseMemoryLib.h>

extern EFI_SYSTEM_TABLE			 *gST;
extern EFI_BOOT_SERVICES         *gBS;

int
EFIAPI
main (                                         
  IN int Argc,
  IN char **Argv
  )
{
  EFI_STATUS                Status;	
  UINT8                		*Buffer;  
  
  Status = gBS -> AllocatePool (EfiACPIMemoryNVS, 0x100 , & Buffer);

  * Buffer    = 'L';
  *(Buffer+1) = 'A';  
  *(Buffer+2) = 'B';  
  *(Buffer+3) = '-';  
  *(Buffer+4) = 'Z';  
  *(Buffer+5) = '.';  
  *(Buffer+6) = 'C';  
  *(Buffer+7) = 'O';  
  *(Buffer+8) = 'M';    
  *(Buffer+9) = '1';   
  printf("Memory1 [%x]\n",Buffer);
  
  //Status = gBS -> FreePool (Buffer);

    Status = gBS -> AllocatePool (EfiReservedMemoryType, 0x100 , & Buffer);

  * Buffer    = 'L';
  *(Buffer+1) = 'A';  
  *(Buffer+2) = 'B';  
  *(Buffer+3) = '-';  
  *(Buffer+4) = 'Z';  
  *(Buffer+5) = '.';  
  *(Buffer+6) = 'C';  
  *(Buffer+7) = 'O';  
  *(Buffer+8) = 'M';    
  *(Buffer+9) = '2';   
  printf("Memory2 [%x]\n",Buffer);
  
  return EFI_SUCCESS;
}

 

运行结果(虚拟机下面的测试)
mo1

直接查看,可以看到确实在内存中写入了
mo2

mo3

再检查一下E820 Table,可以看到放置字符串的内存已经被标记为占用
mo4

之后,我们在实体机器上运行,首先还是shell下面写,然后到Windows中读取对应的内存。可以看到我们在对应的内存中能够看到写入的String.
mo5

mo6

因此,这个方法是可行的。

代码下载
Mem2OS

Step to UEFI (39) ----- 编写一个GetCursorPosition函数

我们知道,可以使用 ConOut 下面的 SetCursorPosition 来设置光标输出的位置【参考1】。

setcursorposition

但是,找了一圈也没有找到 GetCursorPosition ,我怎么知道当前光标在哪呢?查查C手册,通常使用conio.h 中的 wherex,wherey 来完成这个功能。问题是, clib 连这个头文件都没有........

经过一番查找,在【参考2】中有人提到可以使用 Mode 中的 CursorColumn 和 CursorRow 来完成这个功能。于是,编写简单的程序验证一下,程序非常简单,随机生成一个位置显示X,然后在它后面输出当前这个X的位置:

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

#include  <stdio.h>
#include  <stdlib.h>
#include  <wchar.h>
#include <Protocol/EfiShell.h>
#include <Library/ShellLib.h>

#include <Protocol/SimpleFileSystem.h>
#include <Protocol/BlockIo.h>
#include <Library/DevicePathLib.h>
#include <Library/HandleParsingLib.h>
#include <Library/SortLib.h>
#include <Library/MemoryAllocationLib.h>
#include <Library/BaseMemoryLib.h>

extern EFI_SYSTEM_TABLE			 *gST;

/**
  Get the position of cursor
**/
VOID GetCursorPosition(
	IN	UINTN	*x,
	IN	UINTN	*y
)
{
	*x = gST->ConOut->Mode->CursorColumn;
	*y = gST->ConOut->Mode->CursorRow;
	
	return;
}

int
EFIAPI
main (                                         
  IN int Argc,
  IN char **Argv
  )
{
	UINTN wherex;
	UINTN wherey;
	UINTN i;
	
	for (i=0; i<5; i++)
	  {
		gST -> ConOut -> SetCursorPosition(gST -> ConOut,rand()%70,rand() % 20);
		GetCursorPosition(&wherex,&wherey);
		Print(L"x[%02d]:[%02d] ",wherex, wherey);
		
	  }
  
  return EFI_SUCCESS;
}

 

运行结果:

Getcur

完整代码下载:

GetCursor

参考:

1.UEFI Spec 2.4 P460
2.http://biosren.com/viewthread.php?tid=4164&highlight=%B9%E2%B1%EA EFI下对应wherex()作用的FUN是哪个?该怎么用?

Step to UEFI (37) ----- SetTimer 设定定时器(上)

众所周知:UEFI中没有中断(UEFI唯一一个中断int 0,timer )【参考1】,如果想实现一个定时器的功能,必须使用 Event。

实现的思路是:

1. CreateEvent 创建 Timer Event
2. SetTimer 设定 Periodic 触发
3. SetTimer 关闭定时器
4. CloseEvent 销毁 Timer Event

首先研究 CreateEvent ,这个函数是Boot Service中提供的【参考3】

settimer1

第一个参数给出创建的类型,我们要选择EVT_TIMER;第二个参数是优先级,对我们来说影响不大;第三个参数给出当Event发生时对应的处理函数;第四个参数我的理解是自定义的数据;第五个参数是创建出来的Event。

接下来再看看SetTimer函数,同样也是 Boot Service 中提供的服务

settimer2

第一个参数是你创建的Event;然后是Timer的类型,比如:周期性触发;最后是设定Timer的时间,多久触发一次,单位是100ns。

CloseEvent就很简单了

settimer3

程序还参考了 ShellPkg\Library\UefiShellNetwork1CommandsLib\Ping.c 的代码。

最终代码如下

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

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

#include <Protocol/EfiShell.h>
#include <Library/ShellLib.h>

#include <Protocol/SimpleFileSystem.h>
#include <Protocol/BlockIo.h>
#include <Library/DevicePathLib.h>
#include <Library/HandleParsingLib.h>
#include <Library/SortLib.h>
#include <Library/MemoryAllocationLib.h>
#include <Library/BaseMemoryLib.h>

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

extern EFI_SHELL_ENVIRONMENT2    *mEfiShellEnvironment2;
extern EFI_HANDLE				 gImageHandle;

STATIC CONST UINTN SecondsToNanoSeconds = 10000000;

UINTN	Counter = 0;
/**
  The callback function for the timer event used to get map.

  @param[in] Event    The event this function is registered to.
  @param[in] Context  The context registered to the event.
**/
VOID
EFIAPI
Timeout (
  IN EFI_EVENT      Event,
  IN VOID           *Context
  )
{
  Print(L"www.lab-z.com [%d]\r\n",++ Counter);
  return ;
}

int
EFIAPI
main (                                         
  IN int Argc,
  IN char **Argv
  )
{
  EFI_STATUS                Status;
  EFI_HANDLE                TimerOne = NULL;
  BOOLEAN					ExitMark=FALSE;
  
  Status  = gBS->CreateEvent (
                    EVT_NOTIFY_SIGNAL | EVT_TIMER,
                    TPL_CALLBACK,
                    Timeout,
                    NULL,
                    &TimerOne
                    );
    
    if (EFI_ERROR (Status)) {
        Print(L"Create Event Error! \r\n");
		return ;
    }

    Status = gBS->SetTimer (
                   TimerOne,
                   TimerPeriodic,
                   MultU64x32 (SecondsToNanoSeconds, 1)
                   );
    
    if (EFI_ERROR (Status)) {
        Print(L"Set Timer Error! \r\n");
		return ;
    }

	while (!ExitMark)
	{
		if (mEfiShellEnvironment2 -> GetExecutionBreak()) {ExitMark=TRUE;}
	}
    gBS->SetTimer (TimerOne, TimerCancel, 0);
    gBS->CloseEvent (TimerOne);	

  return EFI_SUCCESS;
}

 

运行结果如下
TimerTest

完整代码下载
TimerTest

后记:这部分对我来说还是比较复杂,在描述上定义概念可能会有偏差,如果阅读中发现,欢迎通知我及时改正。

参考:

1. http://blog.csdn.net/celiaqianhj/article/details/7180783 UEFI Events
2. http://biosren.com/viewthread.php?tid=2095&highlight=%B6%A8%CA%B1 什么是EFI Events?
3. UEFI Spec 2.4 P118

Step to UEFI (36) ----- 枚举Shell下的全部盘符

目标:写一个小程序来枚举当前系统中的盘符。比如:FS0: FS1: 等等。

和这个需求最相近的参考文件是 map 功能,每次启动shell的时候他都会展示一下当前系统中的全部盘符。这个功能的代码在 ShellPkg\Library\UefiShellLevel2CommandsLib\Map.c 。大概研究了一下,实现的方法是分别枚举有 Simple File Protocol 和 Block IO Protocol 的Handle (在 PerformMappingDisplay 函数中),然后取每的 Device Path Protocol(在 PerformSingleMappingDisplay 函数中),最后从这个Protocol中获取对应的盘符。

map.c中使用 gEfiShellProtocol->GetMapFromDevicePath 功能取得名称,但是在实际测试过程中我的程序中取得到的 gEfiShellProtocol 不知为何一直为0. 最后只得使用 mEfiShellEnvironment2 -> GetFsName 来完实现这个功能。

ShellPkg\Include\Protocol\EfiShellEnvironment2.h 有 EFI_SHELL_ENVIRONMENT2的定义

/// EFI_SHELL_ENVIRONMENT2 protocol structure.
typedef struct {
  SHELLENV_EXECUTE                        Execute;
  SHELLENV_GET_ENV                        GetEnv;
  SHELLENV_GET_MAP                        GetMap;
  SHELLENV_ADD_CMD                        AddCmd;
  SHELLENV_ADD_PROT                       AddProt;
  SHELLENV_GET_PROT                       GetProt;
                          CurDir;
  SHELLENV_FILE_META_ARG                  FileMetaArg;
  SHELLENV_FREE_FILE_LIST                 FreeFileList;

  //
  // The following services are only used by the shell itself.
  //
  SHELLENV_NEW_SHELL                      NewShell;
  SHELLENV_BATCH_IS_ACTIVE                BatchIsActive;

  SHELLENV_FREE_RESOURCES                 FreeResources;

  //
  // GUID to differentiate ShellEnvironment2 from ShellEnvironment.
  //
  EFI_GUID                                SESGuid;
  //
  // Major Version grows if shell environment interface has been changes.
  //
  UINT32                                  MajorVersion;
  UINT32                                  MinorVersion;
  SHELLENV_ENABLE_PAGE_BREAK              EnablePageBreak;
  SHELLENV_DISABLE_PAGE_BREAK             DisablePageBreak;
  SHELLENV_GET_PAGE_BREAK                 GetPageBreak;

  SHELLENV_SET_KEY_FILTER                 SetKeyFilter;
  SHELLENV_GET_KEY_FILTER                 GetKeyFilter;

  SHELLENV_GET_EXECUTION_BREAK            GetExecutionBreak;
  SHELLENV_INCREMENT_SHELL_NESTING_LEVEL  IncrementShellNestingLevel;
  SHELLENV_DECREMENT_SHELL_NESTING_LEVEL  DecrementShellNestingLevel;
  SHELLENV_IS_ROOT_SHELL                  IsRootShell;

  SHELLENV_CLOSE_CONSOLE_PROXY            CloseConsoleProxy;
  HANDLE_ENUMERATOR                       HandleEnumerator;
  PROTOCOL_INFO_ENUMERATOR                ProtocolInfoEnumerator;
  GET_DEVICE_NAME                         GetDeviceName;
  GET_SHELL_MODE                          GetShellMode;
  SHELLENV_NAME_TO_PATH                   NameToPath;
  SHELLENV_GET_FS_NAME                    GetFsName;
  SHELLENV_FILE_META_ARG_NO_WILDCARD      FileMetaArgNoWildCard;
  SHELLENV_DEL_DUP_FILE                   DelDupFileArg;
  SHELLENV_GET_FS_DEVICE_PATH             GetFsDevicePath;
} EFI_SHELL_ENVIRONMENT2;

 

同一个文件中

/**
  Converts a device path into a file system map name.

  If DevPath is NULL, then ASSERT.

  This function looks through the shell environment map for a map whose device
  path matches the DevPath parameter.  If one is found the Name is returned via
  Name parameter.  If sucessful the caller must free the memory allocated for
  Name.

  This function will use the internal lock to prevent changes to the map during
  the lookup operation.

  @param[in] DevPath                The device path to search for a name for.
  @param[in] ConsistMapping         What state to verify map flag VAR_ID_CONSIST.
  @param[out] Name                  On sucessful return the name of that device path.

  @retval EFI_SUCCESS           The DevPath was found and the name returned
                                in Name.
  @retval EFI_OUT_OF_RESOURCES  A required memory allocation failed.
  @retval EFI_UNSUPPORTED       The DevPath was not found in the map.
**/
typedef
EFI_STATUS
(EFIAPI *SHELLENV_GET_FS_NAME) (
  IN EFI_DEVICE_PATH_PROTOCOL     * DevPath,
  IN BOOLEAN                      ConsistMapping,
  OUT CHAR16                      **Name
  );

 

根据上面的函数,编写程序如下

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

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

#include <Protocol/EfiShell.h>
#include <Library/ShellLib.h>

#include <Protocol/SimpleFileSystem.h>
#include <Protocol/BlockIo.h>
#include <Library/DevicePathLib.h>
#include <Library/HandleParsingLib.h>
#include <Library/SortLib.h>
#include <Library/MemoryAllocationLib.h>
#include <Library/BaseMemoryLib.h>

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

extern EFI_SHELL_ENVIRONMENT2    *mEfiShellEnvironment2;
extern EFI_HANDLE				 gImageHandle;

EFI_STATUS
EFIAPI
PerformSingleMappingDisplay(
  IN CONST EFI_HANDLE Handle
  )
{
  EFI_DEVICE_PATH_PROTOCOL  *DevPath;
  EFI_DEVICE_PATH_PROTOCOL  *DevPathCopy;
  CHAR16                    *CurrentName;

  CurrentName = NULL;
  DevPath = DevicePathFromHandle(Handle);
  DevPathCopy = DevPath;
  mEfiShellEnvironment2->GetFsName(DevPathCopy,FALSE,&CurrentName);

  Print (L"%s \r\n", CurrentName);  
	
  if ((CurrentName) != NULL) { FreePool((CurrentName)); CurrentName = NULL; }

  return EFI_SUCCESS;
}

int
EFIAPI
main (                                         
  IN int Argc,
  IN char **Argv
  )
{
  EFI_STATUS                Status;
  EFI_HANDLE                *HandleBuffer=NULL;
  UINTN                     BufferSize=0;
  UINTN                     LoopVar;
  BOOLEAN                   Found;
  //Copy from ShellLibConstructorWorker in \ShellPkg\Library\UefiShellLib\UefiShellLib.c
  //
  // UEFI 2.0 shell interfaces (used preferentially)
  //
  Status = gBS->OpenProtocol(
    gImageHandle,
    &gEfiShellProtocolGuid,
    (VOID **)&gEfiShellProtocol,
    gImageHandle,
    NULL,
    EFI_OPEN_PROTOCOL_GET_PROTOCOL
   );
   
  if (EFI_ERROR(Status)) {
    //
    // Search for the shell protocol
    //
    Status = gBS->LocateProtocol(
      &gEfiShellProtocolGuid,
      NULL,
      (VOID **)&gEfiShellProtocol
     );
    if (EFI_ERROR(Status)) {
      gEfiShellProtocol = NULL;
     }
  }
  
  //
  // Look up all SimpleFileSystems in the platform
  //
  Status = gBS->LocateHandle(
    ByProtocol,
    &gEfiSimpleFileSystemProtocolGuid,
    NULL,
    &BufferSize,
    HandleBuffer);
	
  if (Status == EFI_BUFFER_TOO_SMALL) {
		HandleBuffer = AllocateZeroPool(BufferSize);
		if (HandleBuffer == NULL) {
			return (SHELL_OUT_OF_RESOURCES);
		}
		Status = gBS->LocateHandle(
			ByProtocol,
			&gEfiSimpleFileSystemProtocolGuid,
			NULL,
			&BufferSize,
			HandleBuffer);
   }

  //
  // Get the map name(s) for each one.
  //
  for ( LoopVar = 0, Found = FALSE
      ; LoopVar < (BufferSize / sizeof(EFI_HANDLE)) && HandleBuffer != NULL
      ; LoopVar ++
     ) {
    Status = PerformSingleMappingDisplay(HandleBuffer[LoopVar]);
    if (!EFI_ERROR(Status)) {
      Found = TRUE;
    }
  }
  
  FreePool(HandleBuffer);
	
  return EFI_SUCCESS;
}

 

运行结果,和 Map命令的对比

ShowMap

完整的代码下载
ShowMap

Step to UEFI (35) ----- How to build Shell.efi

【特别提醒:下面的全部操作都是在UDK2014中完成,具体代码会与2010有差别】

第一个问题:我们运行的模拟环境(NT32)中的Shell是来自哪里?

回答:在 \Nt32Pkg\Nt32Pkg.fdf 中你可以看到下面的定义

################################################################################
#
# FILE statements are provided so that a platform integrator can include
# complete EFI FFS files, as well as a method for constructing FFS files
# using curly "{}" brace scoping. The following three FILEs are
# for binary shell, binary fat and logo module.
#
################################################################################
INF EdkShellBinPkg/FullShell/FullShell.inf

INF FatBinPkg/EnhancedFatDxe/Fat.inf

FILE FREEFORM = PCD(gEfiIntelFrameworkModulePkgTokenSpaceGuid.PcdLogoFile) {
    SECTION RAW = MdeModulePkg/Logo/Logo.bmp

如果你把上面的 FullShell.inf 替换成 EdkShellBinPkg\MinimumShell 下面的MinimumShell.inf 再次编译之后会发现使用的是Mini版本的Shell. 例如: Hexedit 这个命令只在Full版本中才有,Mini版本下不支持。(实验时候特别注意,如果你 Build了 MinimumShell.inf, 在 fsnt1: 下面有一个 Hexedit.efi)

2.如何重新Build Shell.efi?

根据 ShellBinPkg 目录下的 Readme.txt (没错,我也不知道为什么是在这个目录而不是ShellPkg下面的 ReadMe.txt) ,可以使用下面的命令进行Build:

build -a IA32 -p ShellPkg\ShellPkg.dsc -b RELEASE

3.实际操作。这是我们最常见到的Shell的模样,我下面要尝试给他添加一段String.

full

在 \ShellPkg\Application\Shell\Shell.c 可以看到输出版本信息的语句

    //
    // Display the version
    //
    if (!ShellInfoObject.ShellInitSettings.BitUnion.Bits.NoVersion) {
      ShellPrintHiiEx (
        0,
        gST->ConOut->Mode->CursorRow,
        NULL,
        STRING_TOKEN (STR_VER_OUTPUT_MAIN_SHELL),
        ShellInfoObject.HiiHandle,
        SupportLevel[PcdGet8(PcdShellSupportLevel)],
        gEfiShellProtocol->MajorVersion,
        gEfiShellProtocol->MinorVersion
       );

      ShellPrintHiiEx (
        -1,
        -1,
        NULL,
        STRING_TOKEN (STR_VER_OUTPUT_MAIN_SUPPLIER),
        ShellInfoObject.HiiHandle,
        (CHAR16 *) PcdGetPtr (PcdShellSupplier)
       );

      ShellPrintHiiEx (
        -1,
        -1,
        NULL,
        STRING_TOKEN (STR_VER_OUTPUT_MAIN_UEFI),
        ShellInfoObject.HiiHandle,
        (gST->Hdr.Revision&0xffff0000)>>16,
        (gST->Hdr.Revision&0x0000ffff),
        gST->FirmwareVendor,
        gST->FirmwareRevision
       );
    }

    //
    // Display the version
    //
    if (!ShellInfoObject.ShellInitSettings.BitUnion.Bits.NoVersion) {
      ShellPrintHiiEx (
        0,
        gST->ConOut->Mode->CursorRow,
        NULL,
        STRING_TOKEN (STR_VER_OUTPUT_MAIN_SHELL),
        ShellInfoObject.HiiHandle,
        SupportLevel[PcdGet8(PcdShellSupportLevel)],
        gEfiShellProtocol->MajorVersion,
        gEfiShellProtocol->MinorVersion
       );

      ShellPrintHiiEx (
        -1,
        -1,
        NULL,
        STRING_TOKEN (STR_VER_OUTPUT_MAIN_SUPPLIER),
        ShellInfoObject.HiiHandle,
        (CHAR16 *) PcdGetPtr (PcdShellSupplier)
       );

      ShellPrintHiiEx (
        -1,
        -1,
        NULL,
        STRING_TOKEN (STR_VER_OUTPUT_MAIN_UEFI),
        ShellInfoObject.HiiHandle,
        (gST->Hdr.Revision&0xffff0000)>>16,
        (gST->Hdr.Revision&0x0000ffff),
        gST->FirmwareVendor,
        gST->FirmwareRevision
       );
    }

	//LabZDebug_Start 加入我们定义的String
	ShellPrintHiiEx (
        -1,
        -1,
        NULL,
        STRING_TOKEN (STR_LABZ_UEFI),
        ShellInfoObject.HiiHandle
       );
	//LabZDebug_End

    //
    // Display the mapping
    //

    if (PcdGet8(PcdShellSupportLevel) >= 2 && !ShellInfoObject.ShellInitSettings.BitUnion.Bits.NoMap) {
      Status = RunCommand(L"map", NULL);
      ASSERT_EFI_ERROR(Status);
    }

同时在 \ShellPkg\Application\Shell\Shell.uni 加入我们自定义的字符串

#string STR_LABZ_UEFI		          #language en-US "www.lab-z.com 2014/12/12 build.....\r\n"

最后的结果如下,可以看到多出来一行我们自己定义的字符串:

modify

特别提醒:请注意文章中两张图片,实际上盘符是有差别的,一个是 FSNTx: 一个是 FSx。就是说在虚拟环境下模拟出来的盘符还是有差别的。目前我不清楚这个差别是如何导致的。

========================================================================
2015年3月31日补充

1.build shell.efi 之后生成的文件在 Build\Shell\RELEASE_MYTOOLS\IA32下面。同样他的子目录中也能找到Shell.efi (总共三个),他们内容相同

2.有一种情况是你替换了 Shell_Full.efi 之后,模拟器无法进入 shell,始终停留在Setup中。这种情况请检查输出的Log信息,我遇到的情况是因为改动有问题,导致 Shell.efi 是损坏的,无法正常Load起来。

Step to UEFI (33) ----- FindFile 枚举目录下所有文件

研究一下如何枚举目录下的全部文件,查到了2个函数 ShellFindFirstFile 和 ShellFindNextFile。下面就研究如何使用这两个函数。

这两个函数在 \ShellPkg\Include\Library\ShellLib.h 中有定义

/** Retrieve first entry from a directory.

  This function takes an open directory handle and gets information from the
  first entry in the directory.  A buffer is allocated to contain
  the information and a pointer to the buffer is returned in *Buffer.  The
  caller can use ShellFindNextFile() to get subsequent directory entries.

  The buffer will be freed by ShellFindNextFile() when the last directory
  entry is read.  Otherwise, the caller must free the buffer, using FreePool,
  when finished with it.

  @param[in]  DirHandle         The file handle of the directory to search.
  @param[out] Buffer            The pointer to the buffer for the file's information.

  @retval EFI_SUCCESS           Found the first file.
  @retval EFI_NOT_FOUND         Cannot find the directory.
  @retval EFI_NO_MEDIA          The device has no media.
  @retval EFI_DEVICE_ERROR      The device reported an error.
  @retval EFI_VOLUME_CORRUPTED  The file system structures are corrupted.
  @return Others                Status of ShellGetFileInfo, ShellSetFilePosition,
                                or ShellReadFile.

  @sa ShellReadFile
**/
EFI_STATUS
EFIAPI
ShellFindFirstFile (
  IN      SHELL_FILE_HANDLE       DirHandle,
     OUT  EFI_FILE_INFO         **Buffer
  );


/** Retrieve next entries from a directory.

  To use this function, the caller must first call the ShellFindFirstFile()
  function to get the first directory entry.  Subsequent directory entries are
  retrieved by using the ShellFindNextFile() function.  This function can
  be called several times to get each entry from the directory.  If the call of
  ShellFindNextFile() retrieved the last directory entry, the next call of
  this function will set *NoFile to TRUE and free the buffer.

  @param[in]  DirHandle         The file handle of the directory.
  @param[out] Buffer            The pointer to buffer for file's information.
  @param[out] NoFile            The pointer to boolean when last file is found.

  @retval EFI_SUCCESS           Found the next file, or reached last file
  @retval EFI_NO_MEDIA          The device has no media.
  @retval EFI_DEVICE_ERROR      The device reported an error.
  @retval EFI_VOLUME_CORRUPTED  The file system structures are corrupted.
**/
EFI_STATUS
EFIAPI
ShellFindNextFile(
  IN SHELL_FILE_HANDLE                      DirHandle,
  OUT EFI_FILE_INFO              *Buffer,
  OUT BOOLEAN                    *NoFile
  )
{
  //
  // pass to file handle lib
  //
  return (FileHandleFindNextFile(DirHandle, Buffer, NoFile));
}

 

阅读说明可以发现,调用之后会分配一个Buffer,然后由调用者来负责释放。

开始写程序之后很快问题就来了,下面这样很简单的程序会让我的模拟器一次次崩溃。

int
EFIAPI
main (
  IN int Argc,
  IN char **Argv
  )
{
  EFI_FILE_HANDLE   DirHandle;
  RETURN_STATUS     Status;
  EFI_FILE_INFO     *FileInfo = NULL;
  BOOLEAN			NoFile=FALSE;
  
  Status = ShellOpenFileByName(L"fsnt0:\\testz", (SHELL_FILE_HANDLE *)&DirHandle,
                               EFI_FILE_MODE_READ , 0);

  if(Status != RETURN_SUCCESS) {
        Print(L"OpenFile failed!\n");
		return EFI_SUCCESS;
      }							   

  ShellFindFirstFile(DirHandle,&FileInfo);
  PrintFileInfo(FileInfo);
  free(FileInfo);
  while (FALSE==NoFile) 
    {
		Status=ShellFindNextFile(DirHandle,FileInfo,&NoFile);
		if (EFI_SUCCESS == Status)
			{
			}
		Print(L"No File[%d]\n",NoFile);	
	}
 
  return EFI_SUCCESS;
}

 

百思不得其解,在程序中加入输出函数,跟踪一下。出错的部分在ShellFindNextFile(DirHandle,FileInfo,&NoFile); 中。追踪进入 \ShellPkg\Library\UefiFileHandleLib\UefiFileHandleLib.c 的 FileHandleFindNextFile。

/** Retrieve next entries from a directory.

  To use this function, the caller must first call the FileHandleFindFirstFile()
  function to get the first directory entry.  Subsequent directory entries are
  retrieved by using the FileHandleFindNextFile() function.  This function can
  be called several times to get each entry from the directory.  If the call of
  FileHandleFindNextFile() retrieved the last directory entry, the next call of
  this function will set *NoFile to TRUE and free the buffer.

  @param[in]  DirHandle         The file handle of the directory.
  @param[out] Buffer            The pointer to buffer for file's information.
  @param[out] NoFile            The pointer to boolean when last file is found.

  @retval EFI_SUCCESS           Found the next file, or reached last file
  @retval EFI_NO_MEDIA          The device has no media.
  @retval EFI_DEVICE_ERROR      The device reported an error.
  @retval EFI_VOLUME_CORRUPTED  The file system structures are corrupted.
**/
EFI_STATUS
EFIAPI
FileHandleFindNextFile(
  IN EFI_FILE_HANDLE          DirHandle,
  OUT EFI_FILE_INFO          *Buffer,
  OUT BOOLEAN                *NoFile
  )
{
  EFI_STATUS    Status;
  UINTN         BufferSize;

  //
  // ASSERTs for DirHandle or Buffer or NoFile poitners being NULL
  //
  ASSERT (DirHandle != NULL);
  ASSERT (Buffer    != NULL);
  ASSERT (NoFile    != NULL);

  //
  // This BufferSize MUST stay equal to the originally allocated one in GetFirstFile
  //
  BufferSize = FIND_XXXXX_FILE_BUFFER_SIZE;

  //
  // read in the info about the next file
  //
  Status = FileHandleRead (DirHandle, &BufferSize, Buffer);
  ASSERT(Status != EFI_BUFFER_TOO_SMALL);
  if (EFI_ERROR(Status)) {
    return (Status);
  }

  //
  // If we read 0 bytes (but did not have erros) we already read in the last file.
  //
  if (BufferSize == 0) {
    FreePool(Buffer);
    *NoFile = TRUE;
  }

  return (EFI_SUCCESS);
}

 

确定是 FreePool(Buffer); 导致的。看到这个代码感觉很奇怪,明明是让 caller 负责释放,为什么他在里面要干这事情?再阅读函数说明“ If the call of
FileHandleFindNextFile() retrieved the last directory entry, the next call of
this function will set *NoFile to TRUE and free the buffer.” 原来这个函数设计的意思是:调用FileHandleFindNextFile 不断取得文件时,如果你中间停止了,那么请自行释放Buffer;但是如果你一直调用到了最后一个文件,那么函数本身会帮你释放掉而不需要你来做。

最终的程序不复杂

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

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

#include <Protocol/EfiShell.h>
#include <Library/ShellLib.h>

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

extern EFI_SHELL_PROTOCOL        *gEfiShellProtocol;

void PrintFileInfo(EFI_FILE_INFO     *FileInfo)
{
  Print(L"Filesize [%ld] bytes\n",FileInfo-> FileSize);
  Print(L"PhysicalSize [%ld] bytes\n",FileInfo-> PhysicalSize);  

  Print(L"File Create date [%d-%d-%d %d-%d-%d]\n",
		FileInfo-> CreateTime.Year,
		FileInfo-> CreateTime.Month,
		FileInfo-> CreateTime.Day,
		FileInfo-> CreateTime.Hour,
		FileInfo-> CreateTime.Minute,
		FileInfo-> CreateTime.Second);    
		
  Print(L"File last accessed date [%d-%d-%d %d-%d-%d]\n",
		FileInfo-> LastAccessTime.Year,
		FileInfo-> LastAccessTime.Month,
		FileInfo-> LastAccessTime.Day,
		FileInfo-> LastAccessTime.Hour,
		FileInfo-> LastAccessTime.Minute,
		FileInfo-> LastAccessTime.Second);   		
		
  Print(L"File last modification date [%d-%d-%d %d-%d-%d]\n",
		FileInfo-> ModificationTime.Year,
		FileInfo-> ModificationTime.Month,
		FileInfo-> ModificationTime.Day,
		FileInfo-> ModificationTime.Hour,
		FileInfo-> ModificationTime.Minute,
		FileInfo-> ModificationTime.Second);   		

  Print(L"File Name [%s]\n",&FileInfo->FileName[0]);  
}

int
EFIAPI
main (
  IN int Argc,
  IN char **Argv
  )
{
  EFI_FILE_HANDLE   DirHandle;
  RETURN_STATUS     Status;
  EFI_FILE_INFO     *FileInfo = NULL;
  BOOLEAN			NoFile=FALSE;
  
  Status = ShellOpenFileByName(L"fsnt0:", (SHELL_FILE_HANDLE *)&DirHandle,
                               EFI_FILE_MODE_READ , 0);

  if(Status != RETURN_SUCCESS) {
        Print(L"OpenFile failed!\n");
		return EFI_SUCCESS;
      }							   

  ShellFindFirstFile(DirHandle,&FileInfo);
  while (TRUE!=NoFile) 
    {
	    PrintFileInfo(FileInfo);
		Status=ShellFindNextFile(DirHandle,FileInfo,&NoFile);
	}
  return EFI_SUCCESS;
}

 

运行结果

findfile

完整代码下载

FindFile

Step to UEFI (24) ----- Print的换行问题

前面一篇介绍了 ConOut 的换行,然后问题就来了:为什么 Print 的String不需要 \n \r 呢?

这里继续分析:

首先看一下ClsTest.map

 0001:0000006d       _DebugAssert               000002cd f   BaseDebugLibNull:DebugLib.obj
 0001:0000006e       _DebugAssertEnabled        000002ce f   BaseDebugLibNull:DebugLib.obj
 0001:00000071       _InternalPrint             000002d1 f   UefiLib:UefiLibPrint.obj
 0001:000000b1       _Print                     00000311 f   UefiLib:UefiLibPrint.obj
 0001:000000cc       _InternalAllocatePool      0000032c f   UefiMemoryAllocationLib:MemoryAllocationLib.obj
 0001:000000f3       _UnicodeVSPrint            00000353 f   BasePrintLib:PrintLib.obj
 0001:00000112       _BasePrintLibFillBuffer    00000372 f   BasePrintLib:PrintLibInternal.obj

就是说 Print 是来自 UefiLibPrint.Obj,接下来搜索 UefiLibPrint 能找到2个,用实验的方法确定我们需要的是在 \MdePkg\Library\UefiLib\UefiLibPrint.c

INTN
EFIAPI
Print (
  IN CONST CHAR16  *Format,
  ...
  )
{
  VA_LIST Marker;
  UINTN   Return;

  VA_START (Marker, Format);

  Return = InternalPrint (Format, gST->ConOut, Marker);

  VA_END (Marker);

  return Return;
}

继续追 InternalPrint 发现它调用下面的语句

Return = UnicodeVSPrint (Buffer, BufferSize, Format, Marker);

而这个函数在 \MdePkg\Library\BasePrintLib\PrintLib.c 中

UINTN
EFIAPI
UnicodeVSPrint (
  OUT CHAR16        *StartOfBuffer,
  IN  UINTN         BufferSize,
  IN  CONST CHAR16  *FormatString,
  IN  VA_LIST       Marker
  )
{

  ASSERT_UNICODE_BUFFER (StartOfBuffer);
  ASSERT_UNICODE_BUFFER (FormatString);
  return BasePrintLibSPrintMarker ((CHAR8 *)StartOfBuffer, BufferSize >> 1, FORMAT_UNICODE | OUTPUT_UNICODE, (CHAR8 *)FormatString, Marker, NULL);
}

继续追踪 BasePrintLibSPrintMarker 发现他在 \MdePkg\Library\BasePrintLib\PrintLibInternal.c

其中有一个程序段,如下

    case '\r':
      Format += BytesPerFormatCharacter;
      FormatCharacter = ((*Format & 0xff) | (*(Format + 1) << 8)) & FormatMask;
      if (FormatCharacter == '\n') {
        //
        // Translate '\r\n' to '\r\n'
        //
        ArgumentString = "\r\n";
      } else {
        //
        // Translate '\r' to '\r'
        //
        ArgumentString = "\r";
        Format   -= BytesPerFormatCharacter;
      }
      break;

    case '\n':
      //
      // Translate '\n' to '\r\n' and '\n\r' to '\r\n'
      //
      ArgumentString = "\r\n";
      Format += BytesPerFormatCharacter;
      FormatCharacter = ((*Format & 0xff) | (*(Format + 1) << 8)) & FormatMask;
      if (FormatCharacter != '\r') {
        Format   -= BytesPerFormatCharacter;
      }
      break;

就是说,实际上他在检查字符串是否有 \n 和 \r如果有,那么用 \n \r 替换之(文件中有2处干这个事情的,第一个是在分析 "%",第二个才是我们想要的)。为了验证,我们将上面这段替换的代码删除,重新编译,运行结果如下:

24

上面一次运行结果是修改之前,下面是修改之后。可以看到,当我们去掉那段自己添加 \n \r做结尾的代码之后,同样会出现只换行不移动到行首的问题。

结论:Print 之所以 \n 直接就能换行移动到行首,是因为他代码中有特殊处理。

Step to UEFI (8)----显示版本号

UEFI 2.4 SPEC 第四章介绍 EFI System Table 如下

getvendora

看到其中 Vendor 和 Revision信息便着手写了一个小程序来验证,核心代码如下

EFI_SYSTEM_TABLE	*gST;

//
// Entry point function - ShowVersion
//
EFI_STATUS
EFIAPI
UefiMain (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
{

  gST = SystemTable;

  Print(L"Firmware Vendor  :  %s \n", gST->FirmwareVendor);
  Print(L"Firmware Revision:  %d \n", gST->FirmwareRevision);

  return EFI_SUCCESS;
}

编译运行之后得到如下结果:

ShowVersion

手边暂时没有其他EFI的主板所以没有进一步实验。

下载 ShowVersion