2
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

linux ド素人だけど OS 自作 みかん 本の環境とか色々 かじってみた

Last updated at Posted at 2022-01-06

お疲れ様です。
先日、突貫で取り急ぎ動かしてみました。
具体的なフローは以下にまとめています。

本当に全くのド素人なので
勉強した内容をまとめていこうと思います。

#0章 UEFI の前に EFI?
UEFI はハードを制御するファームと OS 間の通信の規格です。
有識者の指導はコチラコチラ

ただ、UEFI の前身は EFI らしいので、
EFI の規格書を読んだ方が良いと思われる。
興味がある方はコチラをクリック

EFI driver
要はドライバーだが、制御用の口はハンドル(protocol) と繋がっています。
. An EFI driver is an executable EFI image that installs a variety of protocols of various handles to accomplish its job.

EFI protocol
protocol とは EFI driver への pointer と data 構造 or API のブロックです。
protocol に設定すべき情報(仕様)は各 protocol の仕様で要確認です。
ただ、最低でも GUID を設定してあげる必要があります。
この GUID は "やりたいこと" に紐づけて命名されており、
handl data base から GUID で検索し、各種 Driver を動かしていきます。

つまづきポイント :ハンドルって?基礎知識は コチラ
ハンドルは変数(ポインタ)というイメージですが、
EFI の規格書の中では 1 つ以上の protocol の集合を指します。
前述の handle data base は、ファーム側で保持しており、
その中身はハンドルと protocol で構成されています。
Handles are a collection of one or more protocols,
and protocols are data structures that are named by a GUID.

ファーム側に使いたいドライバーのポインタと、ドライバに放り込みたいデータをまとめてinput し、
それを受け取ったファームは自身の database から該当のドライバを検索し、制御するイメージだと思います。
以上を前提に以下の図を見るとイメージが付きやすいのではないでしょうか?
image1.png

ただし、実際は HandleProtocol() もしくは OpenProtocol() を経由して
各種 Device driver にリンクしていくそうです.
The protocols that a device handle supports are discovered
through the EFI_BOOT_SERVICES.HandleProtocol() Boot Service or
the EFI_BOOT_SERVICES.OpenProtocol() Boot Service.

具体的に呼び出せる Device driver の一覧です。

EFI_LOADED_IMAGE_PROTOCOL EFI_LOADED_IMAGE_DEVICE_PATH_PROTOCOL EFI_DEVICE_PATH_PROTOCOL
EFI_DRIVER_BINDING_PROTOCOL EFI_DRIVER_FAMILY_OVERRIDE_PROTOCOL EFI_PLATFORM_DRIVER_OVERRIDE_PROTOCOL
EFI_BUS_SPECIFIC_DRIVER_OVERRIDE_PROTOCOL EFI_DRIVER_DIAGNOSTICS2_PROTOCOL EFI_COMPONENT_NAME2_PROTOCOL
EFI_SIMPLE_TEXT_INPUT_PROTOCOL EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL EFI_SIMPLE_POINTER_PROTOCOL
EFI_SERIAL_IO_PROTOCOL EFI_LOAD_FILE_PROTOCOL EFI_LOAD_FILE2_PROTOCOL
EFI_SIMPLE_FILE_SYSTEM_PROTOCOL EFI_FILE_PROTOCOL EFI_DISK_IO_PROTOCOL
EFI_BLOCK_IO_PROTOCOL EFI_BLOCK_IO2_PROTOCOL EFI_UNICODE_COLLATION_PROTOCOL
EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL EFI_PCI_IO_PROTOCOL EFI_USB_IO_PROTOCOL
EFI_SIMPLE_NETWORK_PROTOCOL EFI_PXE_BASE_CODE_PROTOCOL EFI_BIS_PROTOCOL
EFI_DEBUG_SUPPORT_PROTOCOL EFI_DEBUGPORT_PROTOCOL EFI_DECOMPRESS_PROTOCOL
EFI_EBC_PROTOCOL EFI_GRAPHICS_OUTPUT_PROTOCOL EFI_NVM_EXPRESS_PASS_THRU_PROTOCOL
EFI_EXT_SCSI_PASS_THRU_PROTOCOL EFI_USB2_HC_PROTOCOL EFI_AUTHENTICATION_INFO_PROTOCOL
EFI_DEVICE_PATH_UTILITIES_PROTOCOL EFI_DEVICE_PATH_TO_TEXT_PROTOCOL EFI_DEVICE_PATH_FROM_TEXT_PROTOCOL
EFI_EDID_DISCOVERED_PROTOCOL EFI_EDID_ACTIVE_PROTOCOL EFI_EDID_OVERRIDE_PROTOCOL
EFI_ISCSI_INITIATOR_NAME_PROTOCOL EFI_TAPE_IO_PROTOCOL EFI_MANAGED_NETWORK_PROTOCOL
EFI_ARP_SERVICE_BINDING_PROTOCOL EFI_ARP_PROTOCOL EFI_DHCP4_SERVICE_BINDING_PROTOCOL
EFI_DHCP4_PROTOCOL EFI_TCP4_SERVICE_BINDING_PROTOCOL EFI_TCP4_PROTOCOL
EFI_IP4_SERVICE_BINDING_PROTOCOL EFI_IP4_PROTOCOL EFI_IP4_CONFIG_PROTOCOL
EFI_IP4_CONFIG2_PROTOCOL EFI_UDP4_SERVICE_BINDING_PROTOCOL EFI_UDP4_PROTOCOL
EFI_MTFTP4_SERVICE_BINDING_PROTOCOL EFI_MTFTP4_PROTOCOL EFI_HASH_PROTOCOL
EFI_HASH_SERVICE_BINDING_PROTOCOL EFI_SD_MMC_PASS_THRU_PROTOCOL

HandleProtocol() or OpenProtocol() ってどっちが良いの?
The HandleProtocol() function is still available for use by old EFI applications and drivers.
However, all new applications and drivers should use EFI_BOOT_SERVICES.OpenProtocol() in place of HandleProtocol().
(・_・D フムフム。 OpenProtocol() を使えって事ですね。了解。

OpenProtocol を少し見てみた。

OpenProtocol_prototype.c
typedef                                    
EFI_STATUS                                 
(EFIAPI *EFI_OPEN_PROTOCOL) (              
     IN EFI_HANDLE Handle,          
     IN EFI_GUID *Protocol,         
     OUT VOID **Interface OPTIONAL, 
     IN EFI_HANDLE AgentHandle,
     IN EFI_HANDLE ControllerHandle,
     IN UINT32 Attributes
);
item description in japanease
Handle The handle for the protocol interface that is being opened. どこに繋がりたいのか
Protocol The published unique identifier of the protocol. It is the caller’s responsibility to pass in a valid GUID.See “Wired For Management Baseline” for a description of valid GUID values. GUID を入力。入力すべき GUID はコチラを参照
Interface Supplies the address where a pointer to the corresponding Protocol Interface is returned. NULL will be returned in *Interface if a structure is not associated with Protocol. This parameter is optional, and will be ignored if Attributes is EFI_OPEN_PROTOCOL_TEST_PROTOCOL. 対応する protocol IF のポインタのポインタを返します。非対応であれば Null を返し、EFI_OPEN_PROTOCOL_TEST_PROTOCOL を入れると、この項目は無視されます。
AgentHandle The handle of the agent that is opening the protocol interface specified by Protocol and Interface. For agents that follow the UEFI Driver Model, this parameter is the handle that contains the EFI_DRIVER_BINDING_PROTOCOL instance that is produced by the UEFI driver that is opening the protocol interface. For UEFI applications, this is the image handle of the UEFI application that is opening the protocol interface. For applications that use HandleProtocol() to open a protocol interface, this parameter is the image handle of the EFI firmware. プロトコルとインターフェースで指定されたプロトコルインターフェースを開いているエージェントのハンドル。UEFIドライバーモデルに従うエージェントの場合、このパラメーターは、プロトコルインターフェイスを開いているUEFIドライバーによって生成されるEFI_DRIVER_BINDING_PROTOCOLインスタンスを含むハンドルです。UEFIアプリケーションの場合、これはプロトコルインターフェイスを開いているUEFIアプリケーションのイメージハンドルです。HandleProtocol()を使用してプロトコルインターフェイスを開くアプリケーションの場合、このパラメーターはEFIファームウェアのイメージハンドルです。
ControllerHandle If the agent that is opening a protocol is a driver that follows the UEFI Driver Model, then this parameter is the controller handle that requires the protocol interface. If the agent does not follow the UEFI Driver Model, then this parameter is optional and may be NULL. プロトコルを開いているエージェントがUEFIドライバーモデルに従うドライバーである場合、このパラメーターはプロトコルインターフェイスを必要とするコントローラーハンドルです。 エージェントがUEFIドライバーモデルに準拠していない場合、このパラメーターはオプションであり、NULLになる可能性があります。
Attributes The open mode of the protocol interface specified by Handle and Protocol. See "Related Definitions" for the list of legal attributes. ハンドル/プロトコルによって指定されたプロトコル IF の Open mode 設定

ではでは、AKpirion が os をいじるのだから、
AKpirios と命名して色々試してみる。

##その他 雑多な MEMO
###EFI_SYSTEM_TABLE って何?
EFI BOOT SERVICE table, EFI RUNTIME SERVICE table , Protocol services を含んだデータ構造。
~ table は良いとして、Protocol servies について補足します。
例えば openprotocol を走らせる場合、何を open しますか?
Grobal uniqe id として何をするかを ID 化してまとめたものが、
Protocol services です。コレ
因みに Protocol は GUID とか IN / OUT をまとめたデータ構造のことです。
紛らわしい(笑)

###image handle って何?
Supports the Loaded Image Protocol。つまり、EFI_LOADED_IMAGE_PROTOCOL で load した handle。

###Handle database って何?
Protocol には GUID / IN / OUT で色々放り込みます、空データとか。
電源投入後 EFI が起動する際に初期値が色々生成されるので、
全部丸々 protocol に突っ込む必要はないようにしてくれるのが Handle database です。
じゃあ、何を何処までやってくれるの??分からん(笑)

###OpenVolume()
The OpenVolume() function opens a volume, and returns a file handle to the volume’s root directory.
This handle is used to perform all other file I/O operations.
The volume remains open until all the file handles to it are closed.
OpenVolume() は(Root の)ボリュームの file を Open します。そして、file のポインタを返します。
そのポインタは、その他全ての file IO 操作で使われます。また closed されない限り
Open 状態は維持されます。

###Open()
The Open() function opens the file or directory referred to by "FileName" relative
to the location of "This" and returns a "NewHandle".
ファイル or ディレクトリを開きます。
"FileName" を検索して、そのポインタを返します。

###gBS->LoadImage()
device driver は gBS->LoadImage() により work memory に展開され、
gBS->StartImage() により呼び出すことが出来ます。
A device driver is loaded into memory with the gBS->LoadImage() Boot Service
and invoked with the gBS->StartImage() Boot Service.

また gBS->LoadImage() は自動で EFI_LOADED_IMAGE_PROTOCOL を埋め込んだ image handle を生成します。
The gBS->LoadImage() seervce automoatically creates an image handle and installs the
EFI_LOADED_IMAGE_PROTOCOL onto the image handle.

EFI_LOADED_IMAGE_PROTOCOL には device driver のメモリの何処に格納されてて、どこで実行されたか記載されています。
The EFI_LOADED_IMAGE_PROTOCOL describes the location where the device driver
was loaded and the location in system memory where the device driver was placed.

###そもそも memory map の作成は必須なの?
コチラ を読んでみると面白いです。
そもそも OS loader は OSPM への memory map の報告が must だそうです。
また書籍では"色々邪魔だから ExitBootServices() を call する" っとありますが、
作者の気分なのか、OS とはそういうものなのか、良く分かりませんでした。
しかし、この記事にはboot service から run-time へ移行する際に
ExitBootService() を実行するとあります。おもろいです。

memory map はどうやって作る?

GetMemoryMap().c
typedef
EFI_STATUS
(EFIAPI *EFI_GET_MEMORY_MAP) (
  IN OUT UINTN *MemoryMapSize,
  OUT EFI_MEMORY_DESCRIPTOR *MemoryMap,
  OUT UINTN *MapKey,
  OUT UINTN *DescriptorSize,
  OUT UINT32 *DescriptorVersion
);

メモリマップをコピーするための容量を input すると、
そのサイズ分のメモリマップ情報のポインタと key を返す。

###main.c では何やってるの?その1

main.c
EFI_STATUS OpenRootDir(EFI_HANDLE image_handle, EFI_FILE_PROTOCOL** root) {
  EFI_STATUS status;
  EFI_LOADED_IMAGE_PROTOCOL* loaded_image;
  /*Can be used on any image handle to obtain information about the loaded image*/
  /*work memory に展開されたアプリケーションへのプロトコル*/

  EFI_SIMPLE_FILE_SYSTEM_PROTOCOL* fs;
  /*EFI_SIMPLE_FILE_SYSTEM_PROTOCOL is used to open a device volume and return 
   an EFI_FILE_PROTOCOL that provides interfaces to access files on a device volume.
   The only function of this interface is to open a handle to the root directory 
   of the file system on the volume.
   つまり、root を開くための interface として fs を使いますって事ね。*/

  status = gBS->OpenProtocol(
      image_handle,                 /*efi_main() のハンドル(ポインタ)*/
      &gEfiLoadedImageProtocolGuid,
      (VOID**)&loaded_image,        /*efi_main() へアクセスしたいから loaded_image を入力*/
      image_handle,                 /*openprotocol の call は誰がしてる? => efi_main().*/
      NULL,
      EFI_OPEN_PROTOCOL_BY_HANDLE_PROTOCOL);
  if (EFI_ERROR(status)) {
    return status;
  }

  status = gBS->OpenProtocol(
      loaded_image->DeviceHandle,        /*EFI image のロード元のデバイスハンドル => root??*/
      &gEfiSimpleFileSystemProtocolGuid,
      (VOID**)&fs,                       /*root への IF を open.root そのものを open したわけではない*/
      image_handle,                      /*openprotocol の call は誰がしてる? => efi_main().*/
      NULL,
      EFI_OPEN_PROTOCOL_BY_HANDLE_PROTOCOL);
  if (EFI_ERROR(status)) {
    return status;
  }

  return fs->OpenVolume(fs, root);
  /*root への IF である fs を放り込むと root を open. 
  これは root のボリュームを確保したとも言い換えられる。
  第二引数の root は output. ココに root へのパスのポインタが入っている*/
}

#1章 Hello world

Hello.c をコンパイルするところから入った。
$HOME/osbook/day01/c に格納されている hello.c を覗いてみる。
取り急ぎ、コード内の Hello world を適当に変えて、
ファイル名も適当に変えて動くか確認。

##before
$ cd $HOME/osbook/day01/c
$ clang -target x86_64-pc-win32-coff -mno-red-zone -fno-stack-protector -fshort-wchar -Wall -c hello.c
$ lld-link /subsystem:efi_application /entry:EfiMain /out:hello.efi hello.o
$ $HOME/osbook/devenv/run_qemu.sh hello.efi

##after
$ cd $HOME/osbook/day01/c
$ clang -target x86_64-pc-win32-coff -mno-red-zone -fno-stack-protector -fshort-wchar -Wall -c try_hello.c
$ lld-link /subsystem:efi_application /entry:EfiMain /out:try_hello.efi try_hello.o
$ $HOME/osbook/devenv/run_qemu.sh try_hello.efi

基本的に hello.c を try_hello.c に変えただけ。
もちろん動いた。

$ cd $HOME/osbook/day01/c
$ clang -target x86_64-pc-win32-coff -mno-red-zone -fno-stack-protector -fshort-wchar -Wall -c try_hello.c
$ lld-link /subsystem:efi_application /entry:EfiMain /out:try_hello.efi try_hello.o
$ $HOME/osbook/devenv/run_qemu.sh try_hello.efi

とりあえず、やってることを言葉にしてみた。

  1. $HOME/osbook/day01/c に移動
  2. try_hello.c をオブジェクトファイル(機械語)に変換。祝 try_hello.o 誕生。
  3. オブジェクトファイルを何かと紐づけて efi にしてるらしい。また今度調べる(予定)
  4. ディスクイメージに efi を埋め込んで起動するコマンド

取り急ぎコメントしてみた。

hello.c
typedef unsigned short CHAR16;
typedef unsigned long long EFI_STATUS;
typedef void *EFI_HANDLE;

//◆ Clear the path for text output ◆
//   Main
//      ↓
//      EFI_SYSTEM_TABLE
//           ↓
//           EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL
//                 ↓
//                 EFI_TEXT_STRING

/*Details of EFI_TEXT_STRING-----*/
struct _EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL;
typedef void (*EFI_TEXT_STRING) (
  struct _EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *This,
  CHAR16 *String);

/*EFI simple text output protocol*/
typedef struct _EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL {
  void             *dummy;
  EFI_TEXT_STRING  OutputString;
} EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL;

/*EFI system table*/
typedef struct {
  char                             dummy[60];
  EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL  *ConOut;
} EFI_SYSTEM_TABLE;

/*Main*/
EFI_STATUS EfiMain(EFI_HANDLE  ImageHandle,EFI_SYSTEM_TABLE SystemTable){
  SystemTable.ConOut->OutputString(SystemTable.ConOut, L"Hello! AKPdddeee\n");
  while (1);
  return 0;
}

// KEY POINT
// --"Details of EFI_TEXT_STRING" is using Functional pointer
//    to determine EFI_TEXT_STRING as function.
// --"EFI system table" use dummy[60]. 
//    because there are 60byte in front of *ConOut.
// --"Main" needs argument "SystemTable" to initialize it.
//    initializtion is needed to work well.

構造体/ポインタについて勉強しなおしました。
C言語の絵本
新・明解C言語 ポインタ完全攻略

#2章 EDK2 で Hello world
linux のコンパイラを卒業して EKD2 へ進学。
まずは使用したコマンドを以下に整理

$ cd $HOME/workspace/mikanos/                     #$HOME/workspace/mikanos/に移動
$ git checkout osbook_day02a                      #osbook_day02a にワークツリーを変更
$ cd ~/edk2                                       #$HOME/edk2 に移動
$ ln -s $HOME/workspace/mikanos/MikanLoaderPkg ./ #$HOME/edk2 の下に MikanLoaderPkg へ直行できるフォルダを生成
$ source edksetup.sh                              #edk2 でコンパイルするための設定ファイルを call

こちらに大変お世話になった。

https://osdev-jp.readthedocs.io/ja/latest/2017/create-uefi-app-with-edk2.html

まぁ、もともと動くソースをいじるのだから、
.dsc , dec , inf とかはファイル名こそ変えど、
中身はコピーで宜しくないか?っということで丸コピー。

具体的には workspace も MikanLoaderPkg に入っている Main.c , MikanLoaderPkg.des , MicanLoarderPkg.dsc, Loarder.inf を、以下にあるように AKpiriosLoaderPkg として新調した作業フォルダに名前を変えてコピー。

$HOME
 ↓
 edk2 → MikanLoarderPkg
      → AKpiriosLoaderPkg

#File names
  #before
   Main.c , MikanLoaderPkg.des , MikanLoarderPkg.dsc, Loarder.inf

  #after
   Main.c , AKpiriosLoaderPkg.des , AKpiriosLoarderPkg.dsc, Loarder.inf

一応、des/dsc/inf ファイルの中身も Mikan ** ~ 的な記述を全て、
AKpirios ** ~ 的な感じに書き換えて対応した。
後は以下のソースを build した。

Main.c
#include  <Uefi.h>
#include  <Library/UefiLib.h>

EFI_STATUS EFIAPI UefiMain(
                    EFI_HANDLE image_handle,
                    EFI_SYSTEM_TABLE *system_table) 
         {
          Print(L"Hello, AKpirios World!\n");
          while (1); 
          return EFI_SUCCESS;
}

###驚くほど記述が減った。なんで?

UefiLibPrint.c
//edk2/MdePkg/Library/UefiLib/UefiLibPrint.c
/*
Source code key point
POINT1:EDK environment is configured to using EFI_SYSTEM_TABLE.
       So user must declare EFI_SYSTEM_TABLE on user program 
       to call all definition of header files. 
POINT2:Print function contains InternalPrint function.
POINT3:InternalPrint's OutputString is the main body of the text display
*/



/** @file
  Mde UEFI library API implementation.
  Print to StdErr or ConOut defined in EFI_SYSTEM_TABLE                  <= POINT1
  Copyright (c) 2007 - 2018, Intel Corporation. All rights reserved.<BR>
  SPDX-License-Identifier: BSD-2-Clause-Patent
**/

//InternalPrint//
UINTN
InternalPrint (
  IN  CONST CHAR16                     *Format,
  IN  EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL  *Console,
  IN  VA_LIST                          Marker
  )
{
  EFI_STATUS  Status;
  UINTN       Return;
  CHAR16      *Buffer;
  UINTN       BufferSize;

  ASSERT (Format != NULL);
  ASSERT (((UINTN)Format & BIT0) == 0);
  ASSERT (Console != NULL);

  BufferSize = (PcdGet32 (PcdUefiLibMaxPrintBufferSize) + 1) * sizeof (CHAR16);

  Buffer = (CHAR16 *)AllocatePool (BufferSize);
  ASSERT (Buffer != NULL);

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

  if ((Console != NULL) && (Return > 0)) {
    //
    // To be extra safe make sure Console has been initialized
    //
    Status = Console->OutputString (Console, Buffer);          //<= POINT3
    if (EFI_ERROR (Status)) {
      Return = 0;
    }
  }

  FreePool (Buffer);

  return Return;
}

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

  VA_START (Marker, Format);

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

  VA_END (Marker);

  return Return;
}


蛇足ですけど、 while(1) の役割について。
これがないと UefiMain は EFI_SUCCESS を return して終了となる。
そのため、黒い画面から変な設定画面に飛ぶ(笑)。

無限ループを置くことで、return EFI_SUCCESS に行かずに、
その場にとどまっているから、hello world の画面のまま静止することが出来ます。
はい、横道にそれました。

QEMU を起動するときは以下のコマンドで対応しました。

$HOME/osbook/devenv/run_qemu.sh $HOME/edk2/Build/AKpiriosLoaderX64/DEBUG_CLANG38/X64/Loader.efi

結果はこんな感じ。

hello_world.png

##osbook_day02b を自分の環境で動かす
まずは、AKpiriosLoaderPkg でビルドして
動作するまで持っていかないと話が始まらない。

そんな中、出会った有識者のノウハウ。

https://zenn.dev/nnabeyang/scraps/fd9caa55515852

記事にあるように Loader.inf をいじらないと
osbook_day02b は上手く動かないので注意
Main.c の中身を議論する前に有識者の見解を探す。。あった。

https://mir3636.hatenablog.com/

##わくわく Memory Map 取得のソースコード
その前に基礎知識。ボリュームって何だっけ?
=>コチラ or コチラ

本体の UefiMain を覗いてイメージを作ってみる。

Main.c
EFI_STATUS EFIAPI UefiMain(
    EFI_HANDLE image_handle,        //UefiMain のポインタ        
    EFI_SYSTEM_TABLE* system_table) //EFI_SYSTEM_TABLE のポインタ 
  {
   CHAR8 memmap_buf[4096 * 4];//メモリマップ用に workmemory に用意する領域
   struct MemoryMap memmap = {sizeof(memmap_buf), memmap_buf, 0, 0, 0, 0};//メモリマップ生成用のパラメータ一括生成
   GetMemoryMap(&memmap);//指定の先頭アドレスを start として memmap_buf 分だけ使用してメモリマップを workmemory にメモ

   EFI_FILE_PROTOCOL* root_dir;
   OpenRootDir(image_handle, &root_dir);//root 領域を Open!! file IO access を可能にする

   EFI_FILE_PROTOCOL* memmap_file;      /*EFI_FILE_PROTOCOLをインスタンス。ココにメモリマップを書き込む*/
                                        /*PROTOCOL は色々な関数を call も出来るが自由帳としてもイメージ*/

   root_dir->Open(root_dir,             /*open した ROOT を基準に...*/
                  &memmap_file,         /*↓の結果のポインタを返す*/
                  L"\\memmap",          /*root_dir で memmap を検索して open*/
                  EFI_FILE_MODE_READ | EFI_FILE_MODE_WRITE | EFI_FILE_MODE_CREATE, 0);/*memmap は無いから新規で作成*/

  SaveMemoryMap(&memmap, memmap_file); /*memmap_file にmemory mapをメモして save*/
  memmap_file->Close(memmap_file);     /*file closed*/
  Print(L"All done\n");
  while (1);
  return EFI_SUCCESS;
 }

各プロトコルを使い分けて何をしたいかを明確にしてから
逆転してプログラムを読むと上記のような推察になった(今後、修正 update の可能性あり)。
参照 URL => コチラ
ディレクトリって何だ?? => コチラ
UEFI ファームウェアにアクセスしメモリマップを取得について => コチラ
ブートローダーって何? => コチラ

#3章カーネル

実行コマンドを念のためメモ。

$HOME/osbook/devenv/run_qemu.sh $HOME/edk2/Build/AKpiriosLoaderX64/DEBUG_CLANG38/X64/Loader.efi $HOME/workspace/mikanos/kernel/kernel.elf

前章でメモリマップを作った続きとして、
以下の記述が続き、カーネルをブートさせているらしいです。

Main.c
EFI_FILE_PROTOCOL* kernel_file;
root_dir->Open(
    root_dir,          /*open した ROOT を基準に...*/
    &kernel_file,      /*↓の結果のポインタを返す*/
    L"\\kernel.elf",   /*root_dir で kernel.elf を検索し、Open*/
    EFI_FILE_MODE_READ,/*read で kernel.elf を開く*/
    0);

UINTN file_info_size = sizeof(EFI_FILE_INFO) + sizeof(CHAR16) * 12;
UINT8 file_info_buffer[file_info_size];

//ファイル情報を読みたいときに getinfo
kernel_file->GetInfo(
   kernel_file,       /*読みたいファイルのポインタ*/ 
   &gEfiFileInfoGuid, /*file type ID => EDK2 のライブラリで extern 宣言してくれている see 参考情報1*/
   &file_info_size,   /*data 入れる前のバケツ*/
   file_info_buffer); /*data 入れた後の buffer ポインタ*/

  EFI_FILE_INFO* file_info = (EFI_FILE_INFO*)file_info_buffer;
  UINTN kernel_file_size = file_info->FileSize;

  EFI_PHYSICAL_ADDRESS kernel_base_addr = 0x100000;
  gBS->AllocatePages(                        /*system からメモリページを割り当てる*/
      AllocateAddress,                       /*AllocateAddress 直入力で OK */
      EfiLoaderData,                         /*memory type は EfiLoaderData 直入力で OK*/
      (kernel_file_size + 0xfff) / 0x1000,   /*page 数 : 1page 当たり 4 KiB*/
      &kernel_base_addr);                    /*メモリ確保 開始アドレス*/

  kernel_file->Read(kernel_file, &kernel_file_size, (VOID*)kernel_base_addr);
  Print(L"Kernel: 0x%0lx (%lu bytes)\n", kernel_base_addr, kernel_file_size);

  EFI_STATUS status;
  status = gBS->ExitBootServices(            /*terminates all boot service*/
                image_handle,                /*UefiMain に紐づく boot service を止める*/
                memmap.map_key);             /*lastest memory map の key*/
  
  if (EFI_ERROR(status)) {
    status = GetMemoryMap(&memmap);
    if (EFI_ERROR(status)) {
      Print(L"failed to get memory map: %r\n", status);
      while (1);
    }
    status = gBS->ExitBootServices(image_handle, memmap.map_key);
    if (EFI_ERROR(status)) {
      Print(L"Could not exit boot service: %r\n", status);
      while (1);
    }
  }

  UINT64 entry_addr = *(UINT64*)(kernel_base_addr + 24);

  typedef void EntryPointType(void);
  EntryPointType* entry_point = (EntryPointType*)entry_addr;
  entry_point();

参考情報1 :&gEfiFileInfoGuid

https://github.com/tianocore/edk2/blob/master/MdePkg/Include/Guid/FileInfo.h

https://monozukuri-c.com/langc-funclist-extern/

カーネルの呼び出しっと考えると重たいですが、
workmemory に割り当てたソフト(関数)を呼び出して
実行するイメージであれば考えやすいです。

プログラムの解説に関しては書籍を購入してご確認を。

##ブートローダーからピクセル編集(osbook_day03b)
UEFI の規格書を覗くと見かける pool。pool??

https://wa3.i-3-i.info/word18163.html

main.c

/*以下の記述は理解すべきか不明。いつか調べるかも(笑)*/
EFI_STATUS OpenGOP(EFI_HANDLE image_handle,
                   EFI_GRAPHICS_OUTPUT_PROTOCOL** gop) {
  UINTN num_gop_handles = 0;
  EFI_HANDLE* gop_handles = NULL;
  gBS->LocateHandleBuffer(
      ByProtocol,                      /*SearchKey を除く全ての handle で検索*/
      &gEfiGraphicsOutputProtocolGuid, /*参考1*/
      NULL,
      &num_gop_handles,
      &gop_handles);

  gBS->OpenProtocol(
      gop_handles[0],
      &gEfiGraphicsOutputProtocolGuid,
      (VOID**)gop,
      image_handle,
      NULL,
      EFI_OPEN_PROTOCOL_BY_HANDLE_PROTOCOL);

  FreePool(gop_handles);

  return EFI_SUCCESS;
}

  EFI_GRAPHICS_OUTPUT_PROTOCOL* gop;
  OpenGOP(image_handle, &gop);

/*以下の Print を call しているが、その後に塗りつぶしているので結局表示されない(笑)*/
  /*Print(L"Resolution: %ux%u, Pixel Format: %s, %u pixels/line\n",
      gop->Mode->Info->HorizontalResolution,
      gop->Mode->Info->VerticalResolution,
      GetPixelFormatUnicode(gop->Mode->Info->PixelFormat),
      gop->Mode->Info->PixelsPerScanLine);
  Print(L"Frame Buffer: 0x%0lx - 0x%0lx, Size: %lu bytes\n",
      gop->Mode->FrameBufferBase,
      gop->Mode->FrameBufferBase + gop->Mode->FrameBufferSize,
      gop->Mode->FrameBufferSize);*/

  /*画面を塗りつぶす記述*/
  UINT8* frame_buffer = (UINT8*)gop->Mode->FrameBufferBase;
  for (UINTN i = 0; i < gop->Mode->FrameBufferSize; ++i) {
    frame_buffer[i] = 200;
  }

参考1

https://dox.ipxe.org/GraphicsOutput_8h_source.html

hello_world.png

カーネルから行く場合は、どうする??っとワクワク。

##カーネルからピクセル編集(osbook_day03c)

entry point からカーネル(プログラム)を call してるようです。
ただし、call に伴い 変数を 2 つ渡しています。

main.c
typedef void EntryPointType(UINT64, UINT64);
EntryPointType* entry_point = (EntryPointType*)entry_addr;
entry_point(gop->Mode->FrameBufferBase, gop->Mode->FrameBufferSize);

osbook_day03b でやってる塗りつぶし作業をカーネル(man.cpp) に任せているイメージ。

main.cpp
#include <cstdint>
extern "C" void KernelMain(uint64_t frame_buffer_base,
                           uint64_t frame_buffer_size) {
  uint8_t* frame_buffer = reinterpret_cast<uint8_t*>(frame_buffer_base);
  for (uint64_t i = 0; i < frame_buffer_size; ++i) {
    frame_buffer[i] = i % 256;
  }
  while (1) __asm__("hlt");

ほぇーー。

#4章 make/ピクセル描写
##hello make
GNU make 第3版を読むのは、ひとまず置いておきます(笑)
とりあえず、ビルドとリンクをまとめてくれる Makefile を呼び出してくれる良い奴です。

[before]
$ clang -target x86_64-pc-win32-coff -mno-red-zone -fno-stack-protector -fshort-wchar -Wall -c hello.c
$ lld-link /subsystem:efi_application /entry:EfiMain /out:hello.efi hello.o

[after]
$ make

##ピクセル描写
基本は同じ。投げるパラメータを増やしてる。

main.c
 /*投げるパラメータを構造体で定義*/
 struct FrameBufferConfig config = {
    (UINT8*)gop->Mode->FrameBufferBase,
    gop->Mode->Info->PixelsPerScanLine,
    gop->Mode->Info->HorizontalResolution,
    gop->Mode->Info->VerticalResolution,
    0
  };
 //:
 /*中略*/
 //:
  typedef void EntryPointType(const struct FrameBufferConfig*);
  EntryPointType* entry_point = (EntryPointType*)entry_addr;
  entry_point(&config);/*先ほど定義した構造体 config 込みで entry_point を call*/

次に投げられたカーネルは何をするのか。

main.cpp
extern "C" void KernelMain(const FrameBufferConfig& frame_buffer_config) {
  /*画面一杯を白に塗る*/
  for (int x = 0; x < frame_buffer_config.horizontal_resolution; ++x) {
    for (int y = 0; y < frame_buffer_config.vertical_resolution; ++y) {
      WritePixel(frame_buffer_config, x, y, {255, 255, 255});
    }
  }
  /*0<x<200, 0<y<100 の領域を緑で上書き*/
  for (int x = 0; x < 200; ++x) {
    for (int y = 0; y < 100; ++y) {
      WritePixel(frame_buffer_config, 100 + x, 100 + y, {0, 255, 0});
    }
  }
  while (1) __asm__("hlt");
}

この WritePixel() って何ぞ?

main.cpp
int WritePixel(const FrameBufferConfig& config,
               int x, int y, const PixelColor& c) {
  const int pixel_position = config.pixels_per_scan_line * y + x;
  /*data format check: is it RGB?*/
  if (config.pixel_format == kPixelRGBResv8BitPerColor) {
    uint8_t* p = &config.frame_buffer[4 * pixel_position];/*fram_buffer を P とする*/
    p[0] = c.r;                                           /*red   値を直書き*/
    p[1] = c.g;                                           /*green 値を直書き*/
    p[2] = c.b;                                           /*blue  値を直書き*/
   /*data format check: is it BGR?*/
  } else if (config.pixel_format == kPixelBGRResv8BitPerColor) {
    uint8_t* p = &config.frame_buffer[4 * pixel_position];
    p[0] = c.b;
    p[1] = c.g;
    p[2] = c.r;
  } else {
    return -1;
  }
  return 0;
}

loader のカスタムについては、また今度。

#5章文字を書く
A は以下のように表現しているらしい、すげー(笑)

main.cpp
const uint8_t kFontA[16] = {
  0b00000000, //
  0b00011000, //    **
  0b00011000, //    **
  0b00011000, //    **
  0b00011000, //    **
  0b00100100, //   *  *
  0b00100100, //   *  *
  0b00100100, //   *  *
  0b00100100, //   *  *
  0b01111110, //  ******
  0b01000010, //  *    *
  0b01000010, //  *    *
  0b01000010, //  *    *
  0b11100111, // ***  ***
  0b00000000, //
  0b00000000, //
};

これを使ってどうやって書いてるか。

main.cpp
                                    /*文字の X,Y 座標, 表示文字,文字の色*/
void WriteAscii(PixelWriter& writer, int x, int y, char c, const PixelColor& color) {
  /*入力が A じゃなきゃ終了*/
  if (c != 'A') {
    return;
  }
  for (int dy = 0; dy < 16; ++dy) {     /*kFontA[16] を [0],[1]...[16]で取り出す*/
    for (int dx = 0; dx < 8; ++dx) {    /*kFontA[0][7],[0][6].. を順番に 1 を掛けて 1 なら塗る*/
      if ((kFontA[dy] << dx) & 0x80u) { /*[0][7]的な考え方をするのは私が python 出身だからです、すいません*/
        writer.Write(x + dx, y + dy, color);/*座標(x,y) を基準に塗る*/
      }
    }
  }
}

WriteAscii(*pixel_writer, 50, 50, 'A', {0, 0, 0});
WriteAscii(*pixel_writer, 58, 50, 'A', {0, 0, 0});

##分割コンパイル
main.cpp に分割したファイルを #include で宣言しておく。

main.cpp
#include "frame_buffer_config.hpp"
#include "graphics.hpp"
#include "font.hpp"

##フォントを増やそう
txt に書かれている 1 文字当たりのデータを
16Byte のバイナリデータに変換してから扱うそうです。
久々の python にホッとする(笑)

makefont.py
import argparse
import collections
import functools
import re
import sys

BITMAP_PATTERN = re.compile(r'([.*@]+)')

def compile(src: str) -> bytes:
    src = src.lstrip()# 先頭を削除
    result = []

    for line in src.splitlines():
        m = BITMAP_PATTERN.match(line)
        if not m:
            continue

        bits = [(0 if x == '.' else 1) for x in m.group(1)]
        bits_int = functools.reduce(lambda a, b: 2*a + b, bits)
        result.append(bits_int.to_bytes(1, byteorder='little'))

    return b''.join(result)

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('font', help='path to a font file')                     #hankaku.txt
    parser.add_argument('-o', help='path to an output file', default='font.out')#hankaku.bin
    ns = parser.parse_args()

    with open(ns.o, 'wb') as out, open(ns.font) as font:
        src = font.read()
        out.write(compile(src))


if __name__ == '__main__':
    main()

argparse??? なんだそりゃ(笑)
引数をコマンドラインで取れるのね、へー。
image.jpg

なんか option で "font" としてるの?って思ったけど、
只の naming のようだ。

https://docs.python.org/3/library/argparse.html#the-add-argument-method

だけど、この nameing を使って file open をやってる、ワクワク(笑)

https://note.nkmk.me/python-file-io-open-with/

取り急ぎ以下のコマンドで bin ファイルが出来た。

python test.py hankaku.txt -o hankaku.bin

事前に用意されている Makefile を覗くと、
hankaku.txt を bin への変換(上記の python の実行)と
リンクに必要なのりしろ付きオブジェクトファイルの生成までやっているようです。

2
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?