UEFI上でDHCPを使ってIPアドレスを取得できたので、今度はTFTPクライアントを動かしてみます。

まずはサーバーの用意。自分はtftpd64を使いました。

$ cat /path/to/tftp_root/hello.txt
hello world

このhello.txtをUEFIから取得してみます。

UEFIにはEFI_MTFTP4(or 6)_PROTOCOLが用意されているので、簡単にTFTP接続を行うことができます。

プロトコルの開き方はDHCPのときとほぼ同じで、まずはEFI_DHCP4_SERVICE_BINDING_PROTOCOLを開き、そこからEFI_DHCP4_PROTOCOLHandleCreateChild()します。

今回は、このEFI_SERVICE_BINDING_PROTOCOLを探してきてCreateChild()する一連の流れが何回か出て来る(上にかなり面倒)ので、この処理を以下のようにまとめました。

/*
 * ServiceBindingProtocolGuid で指定される EFI_SERVICE_BINDING_PROTOCOL を開き、
 * それを使って ProtocolGuid で指定されるプロトコルを開く。
 */
EFI_STATUS
EFIAPI
LocateProtocolByServiceBindingProtocol (
  IN EFI_GUID *ServiceBindingProtocolGuid,
  IN EFI_GUID *ProtocolGuid,
  OUT EFI_SERVICE_BINDING_PROTOCOL **ServiceBinding,
  OUT VOID **Interface,
  OUT EFI_HANDLE *Handle
  )
{
  EFI_STATUS Status;
  EFI_HANDLE *Handles;
  UINTN NoHandles;
  Status = gBS->LocateHandleBuffer(
    ByProtocol,
    ServiceBindingProtocolGuid,
    NULL,
    &NoHandles,
    &Handles
    );
  if (Status != EFI_SUCCESS) {
    return Status;
  }

  // 見つけた Handle を順番に試してみて、最初にうまく行ったやつを返す
  UINTN ProtocolFound = FALSE;
  for (UINTN i = 0; i < NoHandles; i++) {
    *Handle = Handles[i];
    Status = gBS->OpenProtocol(
      *Handle,
      ServiceBindingProtocolGuid,
      ServiceBinding,
      gIH,
      NULL,
      EFI_OPEN_PROTOCOL_GET_PROTOCOL
      );
    if (Status != EFI_SUCCESS) {
      continue;
    }

    Status = (*ServiceBinding)->CreateChild(*ServiceBinding, Handle);
    if (Status != EFI_SUCCESS) {
      gBS->CloseProtocol(*Handle, ServiceBindingProtocolGuid, gIH, NULL);
      continue;
    }

    Status = gBS->OpenProtocol(
      *Handle,
      ProtocolGuid,
      Interface,
      gIH,
      NULL,
      EFI_OPEN_PROTOCOL_GET_PROTOCOL
      );
    if (Status != EFI_SUCCESS) {
      (*ServiceBinding)->DestroyChild(*ServiceBinding, *Handle);
      gBS->CloseProtocol(*Handle, ServiceBindingProtocolGuid, gIH, NULL);
      continue;
    }

    ProtocolFound = TRUE;
    break;
  }

  if (!ProtocolFound) {
    return EFI_NOT_FOUND;
  }

  gBS->FreePool(Handles);

  return EFI_SUCCESS;
}

TFTPプロトコルによるファイルの取得はEFI_MTFTP4_PROTOCOL.ReadFile()で行います。が、Cで非同期処理を行っているので準備が大変。ReadFile()を呼ぶ前に、以下のすべてを準備しないといけません。

  1. TFTP接続の設定を表すEFI_MTFTP4_CONFIG_DATAを作り、それを使ってConfigure()する
  2. TFTP接続が終わったあとに発火されるイベントを作る
  3. 接続がタイムアウトしたときのコールバック関数を作る
  4. パケットのチェック関数を作る
  5. 通信の内容を表すEFI_MTFTP4_TOKENを作る

これらを実際にどうやって行うかはUEFIの仕様書を見ればわかるので詳しくは説明しませんが、以下のコードを見てもらえればだいたい分かるかと思います。

#include <Uefi.h>
#include <Library/UefiApplicationEntryPoint.h>
#include <Library/UefiLib.h>
#include <Library/PrintLib.h>
#include <Protocol/ServiceBinding.h>
#include <Protocol/Dhcp4.h>
#include <Protocol/Mtftp4.h>

EFI_HANDLE gIH;
EFI_SYSTEM_TABLE *gST;
EFI_BOOT_SERVICES *gBS;

EFI_GUID gEfiDhcp4ProtocolGuid = EFI_DHCP4_PROTOCOL_GUID;
EFI_GUID gEfiDhcp4ServiceBindingProtocolGuid = EFI_DHCP4_SERVICE_BINDING_PROTOCOL_GUID;
EFI_GUID gEfiMtftp4ProtocolGuid = EFI_MTFTP4_PROTOCOL_GUID;
EFI_GUID gEfiMtftp4ServiceBindingProtocolGuid = EFI_MTFTP4_SERVICE_BINDING_PROTOCOL_GUID;

#define MTFTP_BUF_SIZE 128

EFI_EVENT MtftpConnectionDone;

/*
 * TFTPはUDPなので、送られてきたパケットが壊れていないか手動でチェックする必要がある。
 * 今回はただのサンプルなので、特に何もせず EFI_SUCCESS を返している。
 */
EFI_STATUS
EFIAPI
MtftpCheckPacket (
  IN EFI_MTFTP4_PROTOCOL *This,
  IN EFI_MTFTP4_TOKEN *Token,
  IN UINT16 PacketLen,
  IN EFI_MTFTP4_PACKET *Packet
  )
{
  return EFI_SUCCESS;
}

EFI_STATUS
EFIAPI
UefiMain (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
{
  gIH = ImageHandle;
  gST = SystemTable;
  gBS = SystemTable->BootServices;

  /*
   * DHCP関連
   * このへんは前のやつと同じ
   */
  EFI_HANDLE Dhcp4Handle;
  EFI_SERVICE_BINDING_PROTOCOL *Dhcp4Binding;
  EFI_DHCP4_PROTOCOL *Dhcp4;
  LocateProtocolByServiceBindingProtocol(
    &gEfiDhcp4ServiceBindingProtocolGuid,
    &gEfiDhcp4ProtocolGuid,
    &Dhcp4Binding,
    &Dhcp4,
    &Dhcp4Handle
    );

  EFI_DHCP4_CONFIG_DATA Dhcp4Config = {
    .DiscoverTryCount = 0,
    .DiscoverTimeout = NULL,
    .RequestTryCount = 0,
    .RequestTimeout = NULL,
    .ClientAddress = { 0, 0, 0, 0 },
    .Dhcp4Callback = NULL,
    .CallbackContext = NULL,
    .OptionCount = 0,
    .OptionList = NULL
  };
  Dhcp4->Configure(Dhcp4, &Dhcp4Config);
  Dhcp4->Start(Dhcp4, NULL);

  EFI_DHCP4_MODE_DATA ModeData;
  Dhcp4->GetModeData(Dhcp4, &ModeData);


  Print(L"ipv4 address:\n");
  Print(
    L"%d.%d.%d.%d\n",
    ModeData.ClientAddress.Addr[0],
    ModeData.ClientAddress.Addr[1],
    ModeData.ClientAddress.Addr[2],
    ModeData.ClientAddress.Addr[3]
    );

  /*
   * ここからTFTPの処理
   */

  EFI_HANDLE Mtftp4Handle;
  EFI_SERVICE_BINDING_PROTOCOL *Mtftp4Binding;
  EFI_MTFTP4_PROTOCOL *Mtftp4;
  LocateProtocolByServiceBindingProtocol(
    &gEfiMtftp4ServiceBindingProtocolGuid,
    &gEfiMtftp4ProtocolGuid,
    &Mtftp4Binding,
    &Mtftp4,
    &Mtftp4Handle
    );

  // TFTP接続が終わった時に発火されるイベント
  gBS->CreateEvent(
    0,
    TPL_CALLBACK,
    NULL,
    NULL,
    &MtftpConnectionDone
    );

  EFI_IPv4_ADDRESS ServerAddress = SERVER_ADDRESS;

  // TFTP接続の設定
  EFI_MTFTP4_CONFIG_DATA Mtftp4Config;
  Mtftp4Config.UseDefaultSetting = FALSE;
  Mtftp4Config.StationIp = ModeData.ClientAddress;
  Mtftp4Config.SubnetMask = ModeData.SubnetMask;
  Mtftp4Config.LocalPort = 1234; // クライアントのポートは適当に
  Mtftp4Config.GatewayIp = ModeData.RouterAddress;
  Mtftp4Config.ServerIp = ServerAddress;
  Mtftp4Config.InitialServerPort = 69;
  Mtftp4Config.TryCount = 5;
  Mtftp4Config.TimeoutValue = 30;

  Mtftp4->Configure(Mtftp4, &Mtftp4Config);

  CHAR8 *FileBuffer;
  gBS->AllocatePool(
    EfiBootServicesData,
    MTFTP_BUF_SIZE,
    &FileBuffer
    );

  CHAR16 *UnicodeBuffer;
  gBS->AllocatePool(
    EfiBootServicesData,
    MTFTP_BUF_SIZE * 2,
    &UnicodeBuffer
    );

  EFI_MTFTP4_TOKEN ReadFileToken;
  ReadFileToken.Event = MtftpConnectionDone;
  ReadFileToken.OverrideData = NULL;
  ReadFileToken.ModeStr = "netascii"; // "netascii" or "octet"
  ReadFileToken.Filename = "hello.txt";
  ReadFileToken.OptionCount = 0;
  ReadFileToken.OptionList = NULL;
  ReadFileToken.BufferSize = MTFTP_BUF_SIZE - 1; // 後で末尾に '\0' を追加するため
  ReadFileToken.Buffer = FileBuffer;
  ReadFileToken.Context = NULL;
  ReadFileToken.CheckPacket = MtftpCheckPacket;
  ReadFileToken.TimeoutCallback = NULL; // 面倒なのでコールバック関数は指定していない
  ReadFileToken.PacketNeeded = NULL;

  Mtftp4->ReadFile(Mtftp4, &ReadFileToken);

  UINTN EventIndex;
  gBS->WaitForEvent(1, &MtftpConnectionDone, &EventIndex);

  if (ReadFileToken.Status != EFI_SUCCESS) {
    Print(L"an error occurred during mtftp connection\n");
    return ReadFileToken.Status;
  }

  FileBuffer[ReadFileToken.BufferSize] = L'\0';
  UnicodeSPrintAsciiFormat(UnicodeBuffer, MTFTP_BUF_SIZE * 2, FileBuffer);

  Print(L"content:\n");
  Print(L"%s", UnicodeBuffer);

  gBS->FreePool(FileBuffer);
  gBS->FreePool(UnicodeBuffer);
  gBS->CloseEvent(MtftpConnectionDone);
  Dhcp4->Release(Dhcp4);
  Mtftp4Binding->DestroyChild(Mtftp4Binding, Mtftp4Handle);
  Dhcp4Binding->DestroyChild(Dhcp4Binding, Dhcp4Handle);
  Print(L"Done.\n");
  return EFI_SUCCESS;
}

実際に実行してみると、hello.txtの中身が表示されているのがわかります。

/img/post/2017-07-04-tftp.png