51. UEFI APP에 메뉴얼 추가하기(shell의 -?와 help 옵션)

- EFI_SHELL_PROTOCOL의 EFI_SHELL_GET_HELP_TEXT 함수

UEFI shell의 모든 명령어를 보면, -? 옵션을 추가하면, 도움말 메시지를 출력할 수 있다. 예를 들어,

Shell> reset -?
Resets the system.

RESET [-w [string]]
RESET [-s [string]]
RESET [-c [string]]

  -s     - Performs a shutdown.
  -w     - Performs a warm boot.
  -c     - Performs a cold boot.
  string - Describes a reason for the reset.

NOTES:
  1. This command resets the system.
  2. The default is to perform a cold reset unless the -w parameter is
     specified.
  3. If a reset string is specified, it is passed into the Reset()
     function, and the system records the reason for the system reset.

이번 장에서는 help/man 기능을 애플리케이션에 추가하는 방법을 알아볼 것이다.

man finding 및 파싱을 담당하는 Shell 모듈은 ShellManParser이다.

ProcessManFile 함수를 자세히 보면, 다음과 같은 기능들이 있다.

  • gEfiHiiPackageListProtocolGuid에 의한 개방형 이미지 프로토콜

  • 이를 찾아내면, 받은 패키지 목록을 gHiiDatabase -> NewPackageList에 등록한다.

  • HiiGetString의 도움으로 가능한 모든 문자열 ID를 검토한다.

  • ManFileFindTitleSection 함수가 일부 문자열에 대해 true를 반환하면, ManFileFindSections를 실행한다.

ManFileFineTitleSection은 기본적으로 특별한 man 형식의 문자열을 검색한다.

UEFI shell 규격에서 이 형식에 대해 자세히 알아볼 수 있다. https://uefi.org/sites/default/files/resources/UEFI_Shell_Spec_2_0.pdf

최소한의 manual로 앱 만들기

manual이 있는 애플리케이션을 만들어 보자.

shell 스크립트를 사용하여, 템플릿을 통해 애플리케이션 만들기

./createNewApp.sh HIIStringsMan

새로 만든 애플리케이션을 UefiLessonsPkg.dsc에 추가한다.

$ vi UefiLessonsPkg/UefiLessonsPkg.dsc
---
[Components]
  UefiLessonsPkg/HIIStringsMan/HIIStringsMan.inf

기억할 것이라 생각하지만, ShellManParsergEfiHiiPackageListProtocolGuid 프로토콜 데이터에서 maunal 문자열을 검색했다. 따라서 manual 문자열을 이미지 리소스 Section(HIIStringsMan.inf)에 직접 포함해야 한다.

$ cat UefiLessonsPkg/HIIStringsMan/HIIStringsMan.inf
---
[Defines]
  ...
  UEFI_HII_RESOURCE_SECTION      = TRUE

[Sources]
  ...
  Strings.uni

다음은 문자열 파일에 최소화된 내용만을 담은 코드이다.

$ cat UefiLessonsPkg/HIIStringsMan/Strings.uni
---
#langdef en-US "English"

#string STR_HELP        #language en-US ""
".TH HIIStringsMan 0 "Simple application with a manual inside."\r\n"
".SH NAME\r\n"
"HIIStringsMan application.\r\n"

애플리케이션을 빌드하고, 실행하려고 하면 더 이상 아무런 도움이 되지 않는다는 것을 볼 수 있다.

FS0:\> HIIStringsMan.efi -?
No help could be found for command 'HIIStringsMan.efi'.
FS0:\>

이는 프로그램이 문자열 토큰을 참조하지 않고, 빌드 프로세스(HIIStringsManStrDefs.h)에서 최적화되었기 때문이다.

//
//Unicode String ID
//
// #define $LANGUAGE_NAME                                       0x0000 // not referenced
// #define $PRINTABLE_LANGUAGE_NAME                             0x0001 // not referenced
// #define STR_HELLO_WORLD_HELP_INFORMATION                     0x0002 // not referenced

이를 수정하려면, 다음 코드를 HIIStringsMan.c에 추가하면 된다.

$ vi UefiLessonsPkg/HIIStringsMan/HIIStringsMan.c
---
GLOBAL_REMOVE_IF_UNREFERENCED EFI_STRING_ID mStringHelpTokenId = STRING_TOKEN(STR_HELP);

현재 문자열이 최적화되지 않았음을 빌드 이후에 확인할 수 있다.

//
//Unicode String ID
//
// #define $LANGUAGE_NAME                                       0x0000 // not referenced
// #define $PRINTABLE_LANGUAGE_NAME                             0x0001 // not referenced
#define STR_HELP                                             0x0002

애플리케이션을 실행하고 나면, 다음과 같은 결과를 확인할 수 있다.

FS0:\> HIIStringsMan.efi -?
HIIStringsMan application.

우리의 Manual을 확장하기

manual에 들어갈 수 있는 모든 섹션을 추가해보자.

#langdef en-US "English"

#string STR_HELP        #language en-US ""
".TH HIIStringsMan 0 "Simple application with a manual inside."\r\n"
".SH NAME\r\n"
"HIIStringsMan application.\r\n"
".SH SYNOPSIS\r\n"
"This is the synopsis section.\r\n"
".SH DESCRIPTION\r\n"
"This is the description section.\r\n"
".SH OPTIONS\r\n"
"This is the options section.\r\n"
".SH RETURN VALUES\r\n"
"This is the return values section.\r\n"
".SH ENVIRONMENT VARIABLES\r\n"
"This is the section for used environment variables\r\n"
".SH FILES\r\n"
"This is the section for files associated with the subject.\r\n"
".SH EXAMPLES\r\n"
"This is the section for examples and suggestions.\r\n"
".SH ERRORS\r\n"
"This is the section for errors reported by the command.\r\n"
".SH STANDARDS\r\n"
"This is the section for conformance to applicable standards.\r\n"
".SH BUGS\r\n"
"This is the section for errors and caveats.\r\n"
".SH CATEGORY\r\n"
"This is the section for categories.\r\n"
".SH CUSTOMSECTION\r\n"
"This is an example of a custom section.\r\n"

여기까지 빌드하고, 실행하면 다음 내용이 출력되는 것을 확인할 수 있다.

FS0:\> HIIStringsMan.efi -?
HIIStringsMan application.
This is the synopsis section.
This is the description section.
This is the options section.
This is the section for examples and suggestions.

보다시피, 넣어준 모든 섹션에 대한 print가 되지 않았다는 것을 알 수 있다. 그 이유에 대해 알아보자.

먼저 명령어에 -? 옵션을 추가해주면, 어떤 일이 일어나는지 알아보도록 하자. shell의 소스코드를 보면 shell이 -?를 명령 인수 중 하나로, command 및 나머지 모든 인수를 Shell.c의 도움말 명령으로 리다이렉트한다.

$ cat ShellPkg/Application/Shell/Shell.c
---
/**
  Reprocess the command line to direct all -? to the help command.
  if found, will add "help" as argv[0], and move the rest later.
  @param[in,out] CmdLine        pointer to the command line to update
**/
EFI_STATUS
DoHelpUpdate(
  IN OUT CHAR16 **CmdLine
  )
{
  ...
      if (StrStr(CurrentParameter, L"-?") == CurrentParameter) {
        CurrentParameter[0] = L' ';
        CurrentParameter[1] = L' ';
        NewCmdLineSize = StrSize(L"help ") + StrSize(*CmdLine);
        NewCommandLine = AllocateZeroPool(NewCmdLineSize);
        if (NewCommandLine == NULL) {
          Status = EFI_OUT_OF_RESOURCES;
          break;
        }

        //
        // We know the space is sufficient since we just calculated it.
        //
        StrnCpyS(NewCommandLine, NewCmdLineSize/sizeof(CHAR16), L"help ", 5);
        StrnCatS(NewCommandLine, NewCmdLineSize/sizeof(CHAR16), *CmdLine, StrLen(*CmdLine));
        SHELL_FREE_NON_NULL(*CmdLine);
        *CmdLine = NewCommandLine;
        break;
      }
  ...
}

프로그램에 직접 help 명령을 사용하면, 결과가 동일하게 출력되는지 확인할 수 있다.

FS0:\> help HIIStringsMan.efi
HIIStringsMan application.
This is the synopsis section.
This is the description section.
This is the options section.
This is the section for examples and suggestions.

이제 도움말 명령 소스 코드(Help.c)를 살펴보자.

$ cat ShellPkg/Library/UefiShellLevel3CommmandsLib/Help.c
---
SHELL_STATUS
EFIAPI
ShellCommandRunHelp (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
  ...
        //
        // Get the section name for the given command name
        //
        if (ShellCommandLineGetFlag(Package, L"-section")) {
          StrnCatGrow(&SectionToGetHelpOn, NULL, ShellCommandLineGetValue(Package, L"-section"), 0);
        } else if (ShellCommandLineGetFlag(Package, L"-usage")) {
          StrnCatGrow(&SectionToGetHelpOn, NULL, L"NAME,SYNOPSIS", 0);
        } else if (ShellCommandLineGetFlag(Package, L"-verbose") || ShellCommandLineGetFlag(Package, L"-v")) {
        } else {
          //
          // The output of help <command> will display NAME, SYNOPSIS, OPTIONS, DESCRIPTION, and EXAMPLES sections.
          //
          StrnCatGrow (&SectionToGetHelpOn, NULL, L"NAME,SYNOPSIS,OPTIONS,DESCRIPTION,EXAMPLES", 0);
        }
  ...
}

여기에서 help를 넣어줄 인수를 파싱하는 방법을 찾아볼 수 있다.

  • 기본적으로 NAME, SYNOPSIS, OPTIONS, DESCRIPTION, EXAMPLES 섹션이 프린트된다.

  • -section <SECTION NAME> 인수를 사용해 특정 섹션을 출력할 수 있다.

  • -verbose 혹은 -v 인수를 사용해 모든 섹션들을 출력할 수 있다.

Shell에서 이 내용을 직접 확인해 볼 수 있다.

FS0:\> help HIIStringsMan.efi -v
HIIStringsMan application.
This is the synopsis section.
This is the description section.
This is the options section.
This is the return values section.
This is the section for used environment variables
This is the section for files associated with the subject.
This is the section for examples and suggestions.
This is the section for errors reported by the command.
This is the section for conformance to applicable standards.
This is the section for errors and caveats.
This is the section for categories.
This is an example of a custom section.
FS0:\> help HIIStringsMan.efi -section BUGS
This is the section for errors and caveats.

-? 옵션도 마찬가지다.

FS0:\> HIIStringsMan.efi -? -section "RETURN VALUES"
This is the return values section.
FS0:\> HIIStringsMan.efi -? -verbose
HIIStringsMan application.
This is the synopsis section.
This is the description section.
This is the options section.
This is the return values section.
This is the section for used environment variables
This is the section for files associated with the subject.
This is the section for examples and suggestions.
This is the section for errors reported by the command.
This is the section for conformance to applicable standards.
This is the section for errors and caveats.
This is the section for categories.
This is an example of a custom section.

ShellManParser가 호출되는 방식

ShellManParser가 애플리케이션 문자열을 파싱한 것이라는 가정으로, 이번 장을 시작했었다. 도움말 프로그램이 이 모듈을 사용하여, 어떻게 끝나는 지 조사해보자.

모든 걸 따져보면, ShellCommandRunHelpShellPrintHelp 함수(Help.c)를 호출한다.

$ cat ShellPkg/Library/UefiShellLevel3CommandsLib/Help.c
---
SHELL_STATUS
EFIAPI
ShellCommandRunHelp (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
  ...
  Status = ShellPrintHelp(CommandToGetHelpOn, SectionToGetHelpOn, FALSE);
  ...
}

이 ShellPrintHelp 함수는 UefiShellLib.c에 정의되어 있다. 주로 EFI_SHELL_PROTOCOL에서 GetHelpText 함수를 호출한다.

EFI_STATUS
EFIAPI
ShellPrintHelp (
  IN CONST CHAR16     *CommandToGetHelpOn,
  IN CONST CHAR16     *SectionToGetHelpOn,
  IN BOOLEAN          PrintCommandText
  )
{
  ...
  Status = gEfiShellProtocol->GetHelpText (CommandToGetHelpOn, SectionToGetHelpOn, &OutText);
  ...
}

다음은 UEFI Shell 규격에서의 위 함수에 대한 설명이다.

EFI_SHELL_PROTOCOL.GetHelpText()

Summary:
Return help information about a specific command.

Prototype:
typedef
EFI_STATUS
(EFIAPI *EFI_SHELL_GET_HELP_TEXT) (
 IN CONST CHAR16 *Command,
 IN CONST CHAR16 *Sections,
 OUT CHAR16 **HelpText
 );

Parameters:
Command		Points to the null-terminated UEFI Shell command name.
Sections	Points to the null-terminated comma-delimited section names to return. If NULL, then all sections will be returned.
HelpText	On return, points to a callee-allocated buffer containing all specified help text.  

EDKII의 EFI_SHELL_PROTOCOL structure에 대한 프로토타입은 Shell.h에 나와있다.

$ cat MdePkg/Include/Protocol/Shell.h
---
typedef struct _EFI_SHELL_PROTOCOL {
  ...
  EFI_SHELL_GET_HELP_TEXT                   GetHelpText;
  ...
} EFI_SHELL_PROTOCOL;

이 프로토콜에 대한 초기화는 ShellProtocol.c에 있다.

$ cat ShellPkg/Application/Shell/ShellProtocol.c
---
EFI_SHELL_PROTOCOL         mShellProtocol = {
  ...
  EfiShellGetHelpText,
  ...
}

이 함수는 위의 동일한 파일에 정의되어 있으며, 정의를 읽어보면 ProcessManFile 모듈에서 함수를 호출하고 있음을 알 수 있다.

$ cat ShellPkg/Application/Shell/ShellProtocol.c
---
EFIAPI
EfiShellGetHelpText (
  IN CONST CHAR16  *Command,
  IN CONST CHAR16  *Sections OPTIONAL,
  OUT CHAR16       **HelpText
 )
{
  CONST CHAR16  *ManFileName;
  CHAR16        *FixCommand;
  EFI_STATUS    Status;

  ASSERT (HelpText != NULL);
  FixCommand = NULL;

  ManFileName = ShellCommandGetManFileNameHandler (Command);

  if (ManFileName != NULL) {
    return (ProcessManFile (ManFileName, Command, Sections, NULL, HelpText));
  } else {
    if (  (StrLen (Command) > 4)
       && ((Command[StrLen (Command)-1] == L'i') || (Command[StrLen (Command)-1] == L'I'))
       && ((Command[StrLen (Command)-2] == L'f') || (Command[StrLen (Command)-2] == L'F'))
       && ((Command[StrLen (Command)-3] == L'e') || (Command[StrLen (Command)-3] == L'E'))
       && (Command[StrLen (Command)-4] == L'.')
          )
    {
      FixCommand = AllocateZeroPool (StrSize (Command) - 4 * sizeof (CHAR16));
      if (FixCommand == NULL) {
        return EFI_OUT_OF_RESOURCES;
      }

      StrnCpyS (
        FixCommand,
        (StrSize (Command) - 4 * sizeof (CHAR16))/sizeof (CHAR16),
        Command,
        StrLen (Command)-4
        );
      Status = ProcessManFile (FixCommand, FixCommand, Sections, NULL, HelpText);
      FreePool (FixCommand);
      return Status;
    } else {
      return (ProcessManFile (Command, Command, Sections, NULL, HelpText));
    }
  }
}

Last updated