Translate

Archives

Problems With UEFI Shell Options

Recently I needed to work down at the UEFI Shell level. The platform I was on was a BioStar TZ77XE4 which, by the way, was recently certified for Microsoft Windows 8 Client Family, x64. No UEFI Shell was installed, so I UEFI-installed Fedora 17 x64 and copied the pre-compiled UEFI Shell from the UDK 2010 to /boot/efi, i.e. the mount point of the ESP filesystem. Note that while BioStar say that a UEFI Shell should be named ShellX64.efi, I found that Shell.efi also worked and that is what I used.

The UEFI Shell itself is just another UEFI application. It takes command-line options that are null-terminated UCS-2 (2-byte Universal Character Set) encoded strings. The syntax is:

shell.efi [ShellOpt-options] [options] [file-name [file-name-options]]


The command-line options are separated by the space or tab character. Options are processed left-to-right. The following two tables describe the standard command-line options.

OPTIONDESCRIPTION
file-name
The name of a UEFI shell application or script to be executed after initialization is complete. By default, if file-name is specified, then -nostartup is implied. Scripts are not supported by level 0 support.
file-name-options
The command-line options that are passed to file-name when it is invoked.
options
Options (from the table below) which control the initialization behavior of the shell.
ShellOpt-options
Options (from the table below) which control the initialization behaviour of the shell. These options are read from the EFI global variable ShellOpt and are processed before options or file-name.

[table "33" seems to be empty /]

While the command line options worked as expected, I wished to use the ShellOpt global variable to save repeatedly typing in various command line options. This turned out not to work as expected. The set command did not work because it cannot accept variable values that start with a ‘-‘. So, frustrated by what should have been a simple task, I ended up digging through the EDK2 codebase to see when was going on.

The relevant code is in …/ShellPkg/Application/Shell/ShellParametersProtocol.c. What I found is that the code relating to ShellOpt could never have worked for two reasons:

  • Incorrect use of &FullCommandLine instead of FullCommandLine as one of the arguments to the GetVariable Runtime Service defined in ShellEnvVar.h
  • The contents of the FullCommandLine buffer are replaced with the LoadOptions string instead of pre-pending the LoadOptions string to whatever is already in the FullCommandLine buffer.

Here are the diffs to ShellParametersProtocol.c which contain the fixes for the two problems described above:

$ diff ShellParametersProtocol.c ShellParametersProtocol.c.org
40d239
< UINT32                    Attr;
290d288
< 
294,298c292,297
<   Attr = EFI_VARIABLE_NON_VOLATILE|EFI_VARIABLE_BOOTSERVICE_ACCESS;
<   Status = SHELL_GET_ENVIRONMENT_VARIABLE_AND_ATTRIBUTES(L"ShellOpt", &Attr, &Size, FullCommandLine);
<   if (Status == EFI_BUFFER_TOO_SMALL) {
<      FullCommandLine = AllocateZeroPool(Size + LoadedImage->LoadOptionsSize);
< Status = SHELL_GET_ENVIRONMENT_VARIABLE_AND_ATTRIBUTES(L"ShellOpt", &Attr, &Size, FullCommandLine);
---
>
> 
>    Status = SHELL_GET_ENVIRONMENT_VARIABLE(L"ShellOpt", &Size, &FullCommandLine);
>    if (Status == EFI_BUFFER_TOO_SMALL) {
>       FullCommandLine = AllocateZeroPool(Size + LoadedImage->LoadOptionsSize);
>       Status = SHELL_GET_ENVIRONMENT_VARIABLE(L"ShellOpt", &Size, &FullCommandLine);
300c299,304
<   if (EFI_ERROR(Status) && Status != EFI_NOT_FOUND) {
---
>   if (Status == EFI_NOT_FOUND) {
>     //
>     // no parameters via environment... ok
>     //
>   } else {
>   if (EFI_ERROR(Status)) {
301a306
>   }
303a309
> 
312d317
< 314,326c319,320
<     if (LoadedImage->LoadOptionsSize != 0) {
<       //
<       // insert LoadOptions in front of ShellOpt options
<       //
<       CHAR16 *f = FullCommandLine;
<       CHAR16 *l = LoadedImage->LoadOptions;
<       CHAR16 *p = f + StrLen(f);
<       CHAR16 *q = p + StrLen(l);
<
<       while (p >= f)
<        *q-- <= *p--;
<       while (*l)
<        *f++ = *l++;
---
>     if (LoadedImage->LoadOptionsSize != 0){
>       StrCpy(FullCommandLine, LoadedImage->LoadOptions);


Even with my fixes in a newly built UEFI Shell, I was still faced with the problem of how to create the ShellOpt global variable. Rather than dig into the set command and modify it so that it accepted arguments in the form of -arg (moderately difficult – probably requiring a POSIX-like — option), I decided to roll my own application to set and unset the ShellOpt global variable.

This uncovered another interesting issue. The UEFI Shell Specification describes ShellOpt as a global variable. However, the source code does not use EFI_GLOBAL_VARIABLE; instead it uses EFI_SHELL_INTERFACE_GUID. I suspect this is another coding error but I choose not to change that particular code in case I broke code elsewhere. Instead, I choose to use EFI_SHELL_INTERFACE_GUID in my application.

Here is the simple EFI application that I wrote to enable me to set and unset ShellOpt:

//
//   Copyright (c) 2012  Finnbarr P. Murphy.   All rights reserved.
//

#include <efi.h>
#include <efilib.h>

#define EFI_SHELL_INTERFACE_GUID \
   (EFI_GUID) {0x47c7b223, 0xc42a, 0x11d2, {0x8e,0x57,0x00,0xa0,0xc9,0x69,0x72,0x3b}}

// There is some confusion here - the EDK2 shell source code uses SHELL_VARIABLE_GUID whereas 
// the Shell 2.0 specification mandates EFI_GLOBAL_VARIABLE.  See Section 3.2, Table 2.
#define EFI_GLOBAL_VARIABLE \
   (EFI_GUID) {0x8BE4DF61, 0x93CA, 0x11d2, {0xAA, 0x0D, 0x00, 0xE0, 0x98, 0x03, 0x2B, 0x8C }}

#define SHELL_VARIABLE_GUID \
   (EFI_GUID) {0x158def5a, 0xf656, 0x419c, {0xb0,0x27,0x7a,0x31,0x92,0xc0,0x79,0xd2}}

#define BUFSIZE 2048

typedef enum {
    ARG_NO_ATTRIB         = 0x0,
    ARG_IS_QUOTED         = 0x1,
    ARG_PARTIALLY_QUOTED  = 0x2,
    ARG_FIRST_HALF_QUOTED = 0x4,
    ARG_FIRST_CHAR_IS_ESC = 0x8
} EFI_SHELL_ARG_INFO_TYPES;

struct _EFI_SHELL_ARG_INFO {
    UINT32 Attributes;
} __attribute__((packed)) __attribute__((aligned (1)));
typedef struct _EFI_SHELL_ARG_INFO EFI_SHELL_ARG_INFO;
G
struct _EFI_SHELL_INTERFACE {
    EFI_HANDLE           ImageHandle;
    EFI_LOADED_IMAGE    *Info;
    CHAR16             **Argv;
    UINTN                Argc;
    CHAR16             **RedirArgv;
    UINTN                RedirArgc;
    EFI_FILE            *StdIn;
    EFI_FILE            *StdOut;
    EFI_FILE            *StdErr;
    EFI_SHELL_ARG_INFO  *ArgInfo;
    BOOLEAN              EchoOn;
} __attribute__((packed)) __attribute__((aligned (1)));
typedef struct _EFI_SHELL_INTERFACE EFI_SHELL_INTERFACE;

static struct {
    CHAR16 *name;
    UINTN len;
} shellopts[] = {
    { L"-nomap", 6 },
    { L"-nostartup", 10 },
    { L"-startup", 8 },
    { L"-noversion", 10 },
    { L"-noconsolein", 12 },
    { L"-noconsoleout", 13 },
    { L"-nointerrupt", 12 },
    { L"-delay", 6 },
    { NULL, 0 }
};


static EFI_STATUS
get_args(EFI_HANDLE image, UINTN *argc, CHAR16 ***argv)
{
    EFI_STATUS status;
    EFI_SHELL_INTERFACE *shell;
    EFI_GUID gEfiShellInterfaceGuid = EFI_SHELL_INTERFACE_GUID;

    status = uefi_call_wrapper(BS->OpenProtocol, 6,
                               image, &gEfiShellInterfaceGuid,
                               (VOID **)&shell, image, NULL,
                               EFI_OPEN_PROTOCOL_GET_PROTOCOL);
    if (EFI_ERROR(status))
        return status;

    *argc = shell->Argc;
    *argv = shell->Argv;

    status = uefi_call_wrapper(BS->CloseProtocol, 4, image,
                               &gEfiShellInterfaceGuid, image, NULL);
    return status;
}

static BOOLEAN
is_number(CHAR16* str)
{
    CHAR16 *s = str;

    while (*s) {
        if (*s  < L'0' || *s > L'9')
            return FALSE;
        s++;
    }

    return TRUE;
}


static void
usage(void)
{
    Print(L"Usage: shellopt -s|--set [-nomap] [-nostartup | -startup] [-noversion] \\\n");
    Print(L"                         [-noconsolein] [-noconsoleout] [-nointerrupt] \\\n");
    Print(L"                         [-delay[:n]]\n");
    Print(L"       shellopt -c|--clear\n");
}
 

EFI_STATUS
efi_main (EFI_HANDLE image, EFI_SYSTEM_TABLE *systab)
{
    EFI_STATUS status;
    EFI_GUID gEfiGlobalVariableGuid = SHELL_VARIABLE_GUID;
    CHAR16 buffer[BUFSIZE];
    CHAR16 *bufptr = buffer;
    UINTN bufsize = 0;
    UINTN argc;
    CHAR16 **argv;
    int i, j, Match;

    InitializeLib(image, systab);

    status = get_args(image, &argc, &argv);
    if (EFI_ERROR(status)) {
        Print(L"ERROR: Parsing command line arguments: %d\n", status);
       return status;
    }
    if (argc < 2) {
        usage();
        return EFI_SUCCESS;
    } else if (argc == 2) {
        if (!StrCmp(argv[1], L"--help") ||
            !StrCmp(argv[1], L"/help") ||
            !StrCmp(argv[1], L"-h") ||
            !StrCmp(argv[1], L"-?")) {
            usage();
            return EFI_SUCCESS;
        }
        if (!StrCmp(argv[1], L"-c") ||
            !StrCmp(argv[1], L"--clear")) {
            bufsize = 0;
            bufptr  = (CHAR16 *)NULL;
        }
    } else if (argc > 2 && (!StrCmp(argv[1], L"-s") || !StrCmp(argv[1], L"--set"))) {
    } else if (argc > 2 && (!StrCmp(argv[1], L"-s") || !StrCmp(argv[1], L"--set"))) {
        ZeroMem(bufptr, BUFSIZE);
        for (i = 2; i < argc; i++) {
            StrLwr(argv[i]);
            Match = 0;
            for (j = 0; shellopts[j].name != NULL; j++) {
                if (!StrCmp(shellopts[j].name, argv[i])) {
                    StrCat(buffer, argv[i]);
                    StrCat(buffer, L" ");
                    Match = 1;
                    // handle delay number if one entered on command line
                    if (!StrCmp(L"-delay", argv[i]) && i < argc && is_number(argv[i+1])) {
                        i++;
                        StrCat(buffer, argv[i]);
                        StrCat(buffer, L" ");
                    }
                    break;
                }
             }
             if (!Match) {
                 Print(L"ERROR: Invalid shell startup option: %s\n", argv[i]);
                 return EFI_INVALID_PARAMETER;
             }
         }
         // hack - but it works!
         bufsize = StrLen(buffer) * 2;
     } else {
         usage();
         return EFI_INVALID_PARAMETER;
     }

     status = uefi_call_wrapper(RT->SetVariable, 5, L"ShellOpt", &gEfiGlobalVariableGuid,
                                EFI_VARIABLE_NON_VOLATILE | EFI_VARIABLE_BOOTSERVICE_ACCESS,
                                bufsize, bufptr);
     if (status != EFI_SUCCESS && status != EFI_NOT_FOUND)
         Print(L"ERROR: SetVariable: %d\n", status);

     return status;
}


Assuming you have Peter Jone‘s gnu-efi package installed, here is a suitable Makefile for Fedora 17 which will correctly build the shellopt application.

SRCDIR     = .
PREFIX     := /usr
HOSTARCH   = $(shell uname -m | sed s,i[3456789]86,ia32,)
ARCH       := $(shell uname -m | sed s,i[3456789]86,ia32,)
INCDIR     = -I.
CPPFLAGS   = -DCONFIG_$(ARCH)
CFLAGS     = $(ARCH3264) -g -O0 -fpic -Wall -fshort-wchar -fno-strict-aliasing -fno-merge-constants --std=gnu99 -D_GNU_SOURCE
ASFLAGS    = $(ARCH3264)
LDFLAGS    = -nostdlib
INSTALL    = install

CC         = gcc
AS         = as
LD         = ld.bfd
AR         = ar
RANLIB     = ranlib
OBJCOPY    = objcopy

ifeq ($(ARCH), ia32)
  LIBDIR := $(PREFIX)/lib
  ifeq ($(HOSTARCH), x86_64)
    ARCH3264 := -m32
  endif
endif

ifeq ($(ARCH), x86_64)
  CFLAGS += -mno-red-zone
  LIBDIR := $(PREFIX)/lib64
  ifeq ($(HOSTARCH), ia32)
    ARCH3264 := -m64
  endif
endif

FORMAT=efi-app-$(HOSTARCH)
LDFLAGS = -nostdlib -T $(LIBDIR)/gnuefi/elf_$(HOSTARCH)_efi.lds -shared -Bsymbolic $(LIBDIR)/gnuefi/crt0-efi-$(HOSTARCH).o -L$(LIBDIR)
LIBS=-lefi -lgnuefi $(shell $(CC) -print-libgcc-file-name)
CCLDFLAGS =
CFLAGS = -I/usr/include/efi/ -I/usr/include/efi/$(HOSTARCH)/ -I/usr/include/efi/protocol -fpic -fshort-wchar -fno-reorder-functions -fno-strict-aliasing -fno-merge-constants -mno-red-zone -Wimplicit-function-declaration


TARGETS = shellopt.efi

all : $(TARGETS)

clean : 
        @rm -rf *.o *.a *.so $(TARGETS)

.PHONY: all clean install

%.efi : %.so
        $(OBJCOPY) -j .text -j .sdata -j .data -j .dynamic -j .dynsym -j .rel \
                -j .rela -j .reloc --target=$(FORMAT) $*.so $@

%.so: %.o
        $(LD) $(LDFLAGS) -o $@ $^ $(LIBS)

%.o: %.c
        $(CC) $(INCDIR) $(CFLAGS) $(CPPFLAGS) -D__UEFI__ -c $< -o $@

%.S: %.c
        $(CC) $(INCDIR) $(CFLAGS) $(CPPFLAGS) -D__UEFI__ -S $< -o $@

%.E: %.c
        $(CC) $(INCDIR) $(CFLAGS) $(CPPFLAGS) -D__UEFI__ -E $< -o $@


You can download the above two files from Github. You can also watch a YouTube video on how to use the shellopt application to set and unset the ShellOpt variable.

A number of bloggers have recently stated that the UEFI codebase is larger than the current Linux kernel codebase (which is over 15 million LOC.) This is erroneous as CLOC reports just over 2 million LOC in the current EDK2 trunk. Even so, the UEFI codebase is very large and not that many eyeballs have examined the source code in detail. I spotted numerous other lines of iffy code while hunting down and resolving the ShellOpt issue. I suspect that there numerous latent bugs in the codebase which will only come to light as UEFI becomes mainstream.

Comments are closed.