2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【自分用メモ】OSを自作してみよう〜1ヶ月目~

2
Posted at

1日30分

よく使うコマンド

※「runqm」は「$HOME/osbook/devenv/run_qemu.sh」をエイリアス登録したもの。いちいち書くのがめんどくさいので。

1.ブランチのチェックアウト


# 本来はmikanosだが、自作用ディレクトリ「jisakuos」を指定している
cd $HOME/workspace/jisakuos
git checkout osbook_dayxx

2.ブートローダをビルドしてqemuで実行

cd $HOME/edk2
build
# ブートローダのみ実行
runqm Build/MikanLoaderx64/DEBUG_CLANG38/X64/Loader.efi

# ブートローダとカーネルを実行。本来はmikanosだが、自作用ディレクトリ「jisakuos」を指定している
runqm Build/MikanLoaderX64/DEBUG_CLANG38/X64/Loader.efi $HOME/workspace/jisakuos/kernel/kernel.elf

3.Make(コンパイル&リンク)用の環境変数(CPPFLAGSなど)を設定する

source $HOME/osbook/devenv/buildenv.sh

4.「build」コマンドのパスを通すコマンド

source edksetup.sh

5.コンパイル時にヘッダーファイルを含められるよう「bits/libc-header-start.h」などのパスを通すコマンド

export C_INCLUDE_PATH=/usr/include/aarch64-linux-gnu
export CPLUS_INCLUDE_PATH=/usr/include/aarch64-linux-gnu

1日目~4日目

開発環境準備などを実施したりしていた。

Ubuntuインストール

  • Mac OS(Apple M1)
  • UTM4.6.4
  • Ubuntu22.04

ハマったところ

Mac-UTM間でクリップボード共有ができない。UTMのGithub Issueにも同じような質問が上がってた。スレッドにあった「spice-vdagent はディスプレイサーバーのクリップボード(X11 または Wayland)の実行を必要とするはずです。」という一文がヒントになり、Ubuntu-desktopをインストールしたら解決。ずっとCLIでやろうとしていたのが良くなかった。

5〜6日目

1章実施した。
特筆すべき点はなし。バイナリエディタってこんな風なんだな〜くらい。

7日目

2章「EDK2入門とメモリマップ」に突入。
UEFIブートローダをビルドしようとしたら必要なライブラリがないとのこと。

mikan@mikan-server:~/edk2$ build
Build environment: Linux-5.15.0-143-generic-aarch64-with-glibc2.35
Build start time: 09:35:08, Jul.20 2025

WORKSPACE        = /home/mikan/edk2
EDK_TOOLS_PATH   = /home/mikan/edk2/BaseTools
CONF_PATH        = /home/mikan/edk2/Conf
PYTHON_COMMAND   = /usr/bin/python3


Architecture(s)  = X64
Build target     = DEBUG
Toolchain        = CLANG38

Active Platform          = /home/mikan/edk2/MikanLoaderPkg/MikanLoaderPkg.dsc

Processing meta-data .

build.py...
/home/mikan/edk2/MikanLoaderPkg/MikanLoaderPkg.dsc(...): error 4000: Instance of library class [RegisterFilterLib] is not found
	in [/home/mikan/edk2/MdePkg/Library/BaseLib/BaseLib.inf] [X64]
	consumed by module [/home/mikan/edk2/MikanLoaderPkg/Loader.inf]
 

- Failed -
Build end time: 09:35:08, Jul.20 2025
Build total time: 00:00:00

ゼロからのOS入門サポートサイトに対処方法が載っていた。MikanLoaderPkg.dscRegisterFilterLibを追加して解決。

8日目

引き続き2章でビルドできない問題に対処。
長文になるのでエラー部分のみ抜粋。
Geminiに聞いてみたところ、AARCH64システム上でX64向けのクロスコンパイルを行おうとしていルガ、clangが呼び出しているリンカー(/usr/bin/ld)がAARCH64ネイティブのリンカーであり、X64のバイナリを生成するための「エミュレーションモード」(リンク形式)を認識できないために発生しているとのこと。

mikan@mikan-server:~/edk2$ build

/usr/bin/ld: unrecognised emulation mode: elf_x86_64
Supported emulations: aarch64linux aarch64elf aarch64elf32 aarch64elf32b aarch64elfb armelf armelfb aarch64linuxb aarch64linux32 aarch64linux32b armelfb_linux_eabi armelf_linux_eabi
clang: error: linker command failed with exit code 1 (use -v to see invocation)
make: *** [GNUmakefile:329: /home/mikan/edk2/Build/MikanLoaderX64/DEBUG_CLANG38/X64/MikanLoaderPkg/Loader/DEBUG/Loader.dll] Error 1

この記事に同様のエラーメッセージと解決方法が書かれていた。Conf/tools_def.txtのDEBUG_CLANG38_X64_DLINK_FLAGS-fuse-ld=lldを足して解決できた。
後続のエラーについても上記記事に書かれている手順で解決できた。神。

9日目

2章後半のメモリマップ部分に突入。
メモリマップ取得・保存のためC言語プログラムを写経する。
ポインタが出てきて、初めてまともに理解した。たとえば下記のようなコードがあるとする。

CHAR8* header ="Index, Type, Type(name), PhysicalStart, NumberOfPages, Attribute\n";

変数「header」には、CHAR8型の「I」(※文章の最初の一文字)が格納されている0x7FFC0A3B5670のようなメモリアドレスが格納される。(※変数「header」自体には何文字でも入る。)ポインタを使用すると、データに高速なアクセスができたり、大容量のデータをいちいちメモリにコピーすることがないためメモリ節約が可能。C言語が速い速いと言われるのはこれが理由か。(JavaやPythonでもポインタのような概念はあるが開発者が自由にいじれないよう隠蔽されているらしい。)

10日目

引き続き写経する。
VOID**というポインタが出てきた。は型を指定せずにメモリアドレスを格納できる万能ポインタVOID*のメモリアドレスを格納する役割を持つらしい。
これができてなにが嬉しいのかはよくわからなかった。そのうち理解できればいいか。今は先に進もう。

11日目

引き続き写経。写経完了。
メモリマップを取得し、書き込んで保存するプログラムが完成した。

#include  <Uefi.h>
#include  <Library/UefiLib.h>
#include  <Library/UefiBootServicesTableLib.h>
#include  <Library/PrintLib.h>
#include  <Protocol/LoadedImage.h>
#include  <Protocol/SimpleFileSystem.h>
#include  <Protocol/DiskIo2.h>
#include  <Protocol/BlockIo.h>

// #@@range_begin(struct_memory_map)
struct MemoryMap {
  UINTN buffer_size;
  VOID* buffer;
  UINTN map_size;
  UINTN map_key;
  UINTN descriptor_size;
  UINT32 descriptor_version;
};
// #@@range_end(struct_memory_map)

// #@@range_begin(get_memory_map)
EFI_STATUS GetMemoryMap(struct MemoryMap* map) {
  if (map->buffer == NULL) {
    return EFI_BUFFER_TOO_SMALL;
  }

  map->map_size = map->buffer_size;
  return gBS->GetMemoryMap(
      &map->map_size,
      (EFI_MEMORY_DESCRIPTOR*)map->buffer,
      &map->map_key,
      &map->descriptor_size,
      &map->descriptor_version);
}
// #@@range_end(get_memory_map)

// #@@range_begin(get_memory_type)
const CHAR16* GetMemoryTypeUnicode(EFI_MEMORY_TYPE type) {
  switch (type) {
    case EfiReservedMemoryType: return L"EfiReservedMemoryType";
    case EfiLoaderCode: return L"EfiLoaderCode";
    case EfiLoaderData: return L"EfiLoaderData";
    case EfiBootServicesCode: return L"EfiBootServicesCode";
    case EfiBootServicesData: return L"EfiBootServicesData";
    case EfiRuntimeServicesCode: return L"EfiRuntimeServicesCode";
    case EfiRuntimeServicesData: return L"EfiRuntimeServicesData";
    case EfiConventionalMemory: return L"EfiConventionalMemory";
    case EfiUnusableMemory: return L"EfiUnusableMemory";
    case EfiACPIReclaimMemory: return L"EfiACPIReclaimMemory";
    case EfiACPIMemoryNVS: return L"EfiACPIMemoryNVS";
    case EfiMemoryMappedIO: return L"EfiMemoryMappedIO";
    case EfiMemoryMappedIOPortSpace: return L"EfiMemoryMappedIOPortSpace";
    case EfiPalCode: return L"EfiPalCode";
    case EfiPersistentMemory: return L"EfiPersistentMemory";
    case EfiMaxMemoryType: return L"EfiMaxMemoryType";
    default: return L"InvalidMemoryType";
  }
}
// #@@range_end(get_memory_type)

// #@@range_begin(save_memory_map)
EFI_STATUS SaveMemoryMap(struct MemoryMap* map, EFI_FILE_PROTOCOL* file) {
  CHAR8 buf[256];
  UINTN len;

  CHAR8* header =
    "Index, Type, Type(name), PhysicalStart, NumberOfPages, Attribute\n";
  len = AsciiStrLen(header);
  file->Write(file, &len, header);

  Print(L"map->buffer = %08lx, map->map_size = %08lx\n",
      map->buffer, map->map_size);

  EFI_PHYSICAL_ADDRESS iter;
  int i;
  for (iter = (EFI_PHYSICAL_ADDRESS)map->buffer, i = 0;
       iter < (EFI_PHYSICAL_ADDRESS)map->buffer + map->map_size;
       iter += map->descriptor_size, i++) {
    EFI_MEMORY_DESCRIPTOR* desc = (EFI_MEMORY_DESCRIPTOR*)iter;
    len = AsciiSPrint(
        buf, sizeof(buf),
        "%u, %x, %-ls, %08lx, %lx, %lx\n",
        i, desc->Type, GetMemoryTypeUnicode(desc->Type),
        desc->PhysicalStart, desc->NumberOfPages,
        desc->Attribute & 0xffffflu);
    file->Write(file, &len, buf);
  }

  return EFI_SUCCESS;
}
// #@@range_end(save_memory_map)

EFI_STATUS OpenRootDir(EFI_HANDLE image_handle, EFI_FILE_PROTOCOL** root) {
  EFI_LOADED_IMAGE_PROTOCOL* loaded_image;
  EFI_SIMPLE_FILE_SYSTEM_PROTOCOL* fs;

  gBS->OpenProtocol(
      image_handle,
      &gEfiLoadedImageProtocolGuid,
      (VOID**)&loaded_image,
      image_handle,
      NULL,
      EFI_OPEN_PROTOCOL_BY_HANDLE_PROTOCOL);

  gBS->OpenProtocol(
      loaded_image->DeviceHandle,
      &gEfiSimpleFileSystemProtocolGuid,
      (VOID**)&fs,
      image_handle,
      NULL,
      EFI_OPEN_PROTOCOL_BY_HANDLE_PROTOCOL);

  fs->OpenVolume(fs, root);

  return EFI_SUCCESS;
}

EFI_STATUS EFIAPI UefiMain(
    EFI_HANDLE image_handle,
    EFI_SYSTEM_TABLE* system_table) {
  Print(L"Hello, Mikan World!\n");

  // #@@range_begin(main)
  CHAR8 memmap_buf[4096 * 4];
  struct MemoryMap memmap = {sizeof(memmap_buf), memmap_buf, 0, 0, 0, 0};
  GetMemoryMap(&memmap);

  EFI_FILE_PROTOCOL* root_dir;
  OpenRootDir(image_handle, &root_dir);

  EFI_FILE_PROTOCOL* memmap_file;
  root_dir->Open(
      root_dir, &memmap_file, L"\\memmap",
      EFI_FILE_MODE_READ | EFI_FILE_MODE_WRITE | EFI_FILE_MODE_CREATE, 0);

  SaveMemoryMap(&memmap, memmap_file);
  memmap_file->Close(memmap_file);
  // #@@range_end(main)

  Print(L"All done\n");

  while (1);
  return EFI_SUCCESS;
}

上記プログラムをUEFIブートローダとしてqemuで実行し、作成されたメモリマップが下記になる。
‥‥結局なにやってるかよくわからなかった。

Index, Type, Type(name), PhysicalStart, NumberOfPages, Attribute
0, 3, , 00000000, 1, F
1, 7, , 00001000, 9F, F
2, 7, , 00100000, 700, F
3, A, , 00800000, 8, F
4, 7, , 00808000, 8, F
5, A, , 00810000, F0, F
6, 4, , 00900000, B00, F
7, 7, , 01400000, 3AB36, F
8, 4, , 3BF36000, 20, F
9, 7, , 3BF56000, 2726, F
10, 1, , 3E67C000, 2, F
11, 4, , 3E67E000, A, F
12, 9, , 3E688000, 1, F
13, 4, , 3E689000, 1F7, F
14, 3, , 3E880000, B4, F
15, A, , 3E934000, 12, F
16, 0, , 3E946000, 1C, F
17, 3, , 3E962000, 10A, F
18, 6, , 3EA6C000, 5, F
19, 5, , 3EA71000, 5, F
20, 6, , 3EA76000, 5, F
21, 5, , 3EA7B000, 5, F
22, 6, , 3EA80000, 5, F
23, 5, , 3EA85000, 7, F
24, 6, , 3EA8C000, 8F, F
25, 4, , 3EB1B000, 6F7, F
26, 7, , 3F212000, 4, F
27, 4, , 3F216000, 6, F
28, 7, , 3F21C000, 1, F
29, 4, , 3F21D000, 7FE, F
30, 7, , 3FA1B000, 1, F
31, 3, , 3FA1C000, 17F, F
32, 5, , 3FB9B000, 30, F
33, 6, , 3FBCB000, 24, F
34, 0, , 3FBEF000, 4, F
35, 9, , 3FBF3000, 8, F
36, A, , 3FBFB000, 4, F
37, 4, , 3FBFF000, 201, F
38, 7, , 3FE00000, 8D, F
39, 4, , 3FE8D000, 20, F
40, 3, , 3FEAD000, 20, F
41, 4, , 3FECD000, 9, F
42, 3, , 3FED6000, 1E, F
43, 6, , 3FEF4000, 84, F
44, A, , 3FF78000, 88, F
45, 6, , FFC00000, 400, 1

12日目

  • ポインタについての補足説明を読む。
  • ポインタのポインタという概念がどんな時に嬉しいのか結局よくわからなかった。
  • 無理やり3章に突入。QUMEモニタのデバッグ方法を学ぶ。よくわからない言葉がいろいろ出てきたが先に進む
  • CPUの記憶領域であるレジスタには2種類あるらしい。
    • 汎用レジスタ‥計算に使用する。メインメモリより容量小さくて読み書きが速い
    • 特殊レジスタ‥メモリアドレスを保持したりフラグが入っていたりCPUの重要な設定が入っていたりする
  • 書いてあることがマジで1ミリも理解できないので、とにかく手を動かしていく。次はカーネルの作成

13日目

  • カーネルのコンパイルとリンクを行った。
    • コンパイル‥人間用のソースコードを機械が読めるように「オブジェクトファイル」(1と0の組み合わせのファイル)に変換する。1つのソースコードは1つのオブジェクトファイルになる。
    • リンク‥オブジェクトファイルを結合して一つの実行ファイルにする

今回で言うと下記のようなイメージ。

[ソースコード]main.cpp →(コンパイル)→ [オブジェクトファイル]main.o →(リンク) → [実行ファイル]kernel.elf

次に、ブートローダからカーネルを呼び出すような動作を行う。
また写経する。
写経完了。とりあえずブートローダからkernel呼び出すところまでは実行できた。

14日目

  • 「3.3 初めてのカーネル」が完了。コードの解説を読み終わった。カーネルファイルを読み込む→ブートローダは邪魔なのでカーネル起動前に停止する→カーネルのエントリポイント(あるプログラムの入り口。今回で言うとKernelMain()が該当)を呼び出す‥という流れ。
  • どんなことをやるにしても、そのプログラムファイルを実行するためのメモリを確保する必要があるんだなと思った。大変そう。
  • git checkoutで、あるコミット時点のものとローカルファイルを一緒にできる。git stashで、ローカルに加えた変更を破棄できる。git checkoutでローカルに落としてきたファイルを変更している場合、そのままgit checkoutで別コミットに移ることはできないのでgit stashを実行してあげる必要がある。

カーネルファイルを読み込む部分。gBS->AllocatePagesの部分でメモリを確保

  // #@@range_begin(read_kernel)
  EFI_FILE_PROTOCOL* kernel_file;
  root_dir->Open(
    root_dir, &kernel_file, L"\\kernel.elf",EFI_FILE_MODE_READ, 0);

  UINTN file_info_size = sizeof(EFI_FILE_INFO) + sizeof(CHAR16) * 12;
  UINT8 file_info_buffer[file_info_size];
  kernel_file->GetInfo(
    kernel_file, &gEfiFileInfoGuid,&file_info_size, file_info_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(
    AllocateAddress, EfiLoaderData,
    (kernel_file_size + 0xfff) / 0x1000,&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);
  // #@@range_end(read_kernel)

15日目

  • 画面を白く塗りつぶしてみる。まずはブートローダで実行。
  • Main.cのこの部分。255が白を指すらしい。
/ #@@range_begin(gop)
  EFI_GRAPHICS_OUTPUT_PROTOCOL* gop;
  OpenGOP(image_handle, &gop);

  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);

  UINT8* frame_buffer = (UINT8*)gop->Mode->FrameBufferBase;
  for (UINTN i = 0; i < gop->Mode->FrameBufferSize; ++i){
    frame_buffer[i] = 255;
  }
  // #@@range_end(gop)
  • この章から急に、efiファイルのビルドコマンド〜qemu実行コマンドが記載されなくなっていたので「よく使うコマンド」の章を設置。メモしておく。

  • C言語において、Include <>で指定したライブラリはコンパイル時に読み込めるようにする必要があるらしい。今回は筆者が用意してくれたスクリプトを下記のように実行して環境変数にライブラリのパスを読み込ませ、コンパイル・リンク時に上記環境変数を指定するようにしている。


mikan@mikan-server:~/workspace/mikanos/kernel$ cat $HOME/osbook/devenv/buildenv.sh
# Usage: source buildenv.sh

BASEDIR="$HOME/osbook/devenv/x86_64-elf"
EDK2DIR="$HOME/edk2"

if [ ! -d $BASEDIR ]
then
    echo "$BASEDIR \u304c\u5b58\u5728\u3057\u307e\u305b\u3093\u3002"
    echo "\u4ee5\u4e0b\u306e\u30d5\u30a1\u30a4\u30eb\u3092\u624b\u52d5\u3067\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u3057\u3001$(dirname $BASEDIR)\u306b\u5c55\u958b\u3057\u3066\u304f\u3060\u3055\u3044\u3002"
    echo "https://github.com/uchan-nos/mikanos-build/releases/download/v2.0/x86_64-elf.tar.gz "
else
    export CPPFLAGS="\
    -I$BASEDIR/include/c++/v1 -I$BASEDIR/include -I$BASEDIR/include/freetype2 \
    -I$EDK2DIR/MdePkg/Include -I$EDK2DIR/MdePkg/Include/X64 \
    -nostdlibinc -D__ELF__ -D_LDBL_EQ_DBL -D_GNU_SOURCE -D_POSIX_TIMERS \
    -DEFIAPI='__attribute__((ms_abi))'"
    export LDFLAGS="-L$BASEDIR/lib"
fi

mikan@mikan-server:~/workspace/mikanos/kernel$ source $HOME/osbook/devenv/buildenv.sh
mikan@mikan-server:~/workspace/mikanos/kernel$ clang++ $CPPFLAGS -O2 --target=x86_64-elf -fno-exceptions -ffreestanding -c main.cpp
mikan@mikan-server:~/workspace/mikanos/kernel$ ld.lld $LDFLAGS --entry KernelMain -z norelro --image-base 0x100000 --static -o kernel.elf main.o

16日目

急にbuildコマンドが使用できなくなって焦った。

mikan@mikan-server:~/edk2$ build
Command 'build' not found, did you mean:
  command 'pbuild' from deb pbuilder-scripts (22)
  command 'guild' from deb guile-2.2-dev (2.2.7+1-6build2)
  command 'guild' from deb guile-3.0-dev (3.0.7-1)
  command 'sbuild' from deb sbuild (0.81.2ubuntu6.1)
  command 'xbuild' from deb mono-xbuild (6.8.0.105+dfsg-3.2)
  command 'buildd' from deb buildd (0.81.2ubuntu6.1)
  command 'obuild' from deb ocaml-obuild (0.1.10-3build1)

どうやらUTM上のUbuntu再起動?などによりPATHが通らなくなってしまったらしい。
下記コマンドを実行後、buildコマンドが正常に実行できるようになった。

eishu@mikan-server:~/edk2$ source edksetup.sh
Loading previous configuration from /home/mikan/edk2/Conf/BuildEnv.sh
Using EDK2 in-source Basetools
WORKSPACE: /home/mikan/edk2
EDK_TOOLS_PATH: /home/mikan/edk2/BaseTools
CONF_PATH: /home/mikan/edk2/Conf

あとはUbuntuの画面がスタックしたのでUTM上から停止→起動しようとしたら、Ubuntu22.04が「開始中」のままとなってしまい、どうしようもなくなって発狂していた。option + command + esc同時押しでUTMそのものを強制終了後、再び起動したら無事に復旧できた。
スクリーンショット 2025-07-30 1.14.53.JPG

上記の対応をしていたので今日は進捗なし。「3.5 カーネルからピクセルを描く」を完了したくらい。明日は「3.6 エラー処理をしよう」をやっていく。

17日目

  • 3章は消化不良で終わった感がある
  • 「3.6 エラー処理をしよう」のところは写経しようかと思ったが流石にやっていることがわかったので読むだけにとどめた。
  • 「ポインタのキャスト」は脳が理解することを諦めた。
  • 「ポインタとアセンブリ言語」についてはアドレスを入れたり出したりしているんだね‥。ということはわかったがそれ以上は脳が理解することを諦めた。
  • このままだと挫折しそうなので先に進む。とりあえず重要なのは最後まで行くことだと思うので。そもそもこの分厚い本をやってるだけ偉いやろの精神。最悪2週目やればいいか(フロム脳)

18日目

  • 無理やり「4章 ピクセル描画とmake入門」に入った
  • Makefileを使用すると、今まで手打ちで長いコマンドを打ってコンパイル・リンクしていたのを一つのファイルにまとめ、短いコマンドで実行することができる
  • 「make」コマンドを実行したところライブラリが見つからないエラーになったがこの記事を読んで解決

19日目

  • 画面描画できるように下記を実施
    • ブートローダのプログラム「Main.c」にてframebufferの情報をkernelに渡すよう修正
    • kernelのプログラム「main.cpp」にてframebufferを用いて画面描画するよう処理追加←ここの写経している時に時間切れ

今日写経したもの

◾️Main.cのmain()の一部分で、kernelにframebufferの情報を渡すところ

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

  // #@@range_begin(pass_frame_buffer_config)
  struct FrameBufferConfig config = {
    (UINT8*)gop->Mode->FrameBufferBase,
    gop->Mode->Info->PixelsPerScanLine,
    gop->Mode->Info->HorizontalResolution,
    gop->Mode->Info->VerticalResolution,
    0
  };

  switch (gop->Mode->Info->PixelFormat) {
    case PixelRedGreenBlueReserved8BitPerColor:
      config.pixel_format = kPixelRGBResv8BitPerColor;
      break;
    case PixelBlueGreenRedReserved8BitPerColor:
      config.pixel_format = kPixelBGRResv8BitPerColor;
      break;
    default:
      Print(L"Unimplemented pixel format: %d\n", gop->Mode->Info->PixelFormat);
      Halt();
  }

  typedef void EntryPointType(const struct FrameBufferConfig*);
  EntryPointType* entry_point = (EntryPointType*)entry_addr;
  entry_point(&config);
  // #@@range_end(pass_frame_buffer_config)
  // #@@range_end(pas_frame_buffer_config)

◾️ヘッダーファイル「frame_buffer_config.hpp」
フレームバッファ(コンピュータの画面表示に用いられる、表示内容を記憶しておくためのメモリ領域)を定義する。
最初はなぜこの定義だけ独立させるのかわからなかったが、Main.cだけでなくmain.cppからもincludeされていたので、何らかの定義を共通化したい場合に便利だということがわかった。

#pragma once

#include <stdint.h>

enum PixelFormat{
    KPixelRGBResv8BitPerColor,
    KPixelBGRResv8BitPerColor,
};

struct FrameBufferConfig{
    uint8_t* frame_buffer;
    uint32_t pixels_per_scan_line;
    uint32_t horizontal_resolution;
    uint32_t vertical_resolution;
    enum PixelFormat pixel_format;
};

20日目

In file included from /usr/lib/llvm-14/lib/clang/14.0.0/include/stdint.h:52:
/usr/include/stdint.h:26:10: fatal error: 'bits/libc-header-start.h' file not found
#include <bits/libc-header-start.h>
         ^~~~~~~~~~~~~~~~~~~~~~~~~~

今日写経したもの

◾️カーネルプログラム「main.cpp」

/**
* @ file main.cpp
* 
*
*/

# include <cstdint>
# include <cstddef>

# include "frame_buffer_config.hpp"

// #@@range_begin(write_pixel)
struc PixelColor {
    uint8_t r, g, b;
};

/** WritePixelは1つの点を描画します.
 * @retval 0   成功
 * @retval 非0 失敗
 */
int WritePixel(const FrameBufferConfig& config,
               int x, int y, const PixelColor& c){
    const int pixel_position = config.pixels_per_scan_line * y + x;
    if (config.pixel_format == kPixelRGBResv8BitPerColor) {
        uint8_t* p = &config.frame_buffer[4 * pixel_position];
        p[0] = c.r;
        p[1] = c.g;
        p[2] = c.b;
    } 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;
}
// #@@range_end(write_pixel)

// #@@range_begin(call_write_pixel)
extern "C" void KernelMain(const FrameBufferConfig& frame_buffer_config) {
    for (int x = 0: x < frame_buffer_config.horizontal_rezolution; ++x) {
        for (int y = 0; y < frame_buffer_config.vertical_resolution; ++y) {
            WritePixel(frame_buffer_config, x, y, (255, 255, 255);)
        }
    }
    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");
}
// #@@range_end(call_write_pixel)

21日目

export C_INCLUDE_PATH=/usr/include/aarch64-linux-gnu
export CPLUS_INCLUDE_PATH=/usr/include/aarch64-linux-gnu
  • 無事ビルドできたブートローダとカーネルを実行したが、表示される画面が本で示されているもの(白地の画面に緑の四角形が描画されるはず)と違った。全く同じ事象がIssudeで報告されており解決策も記載されていたのでこれを元に対処する

image.png

22日目

  • リンカのバージョンが上がっていることで、kernel.elfが0x100000に読み込まれずズレてしまうことで挙動がおかしくなっているらしい
  • ld.lldに-z separate-codeを追加することで解決するらしいが、肝心の対象ファイルが文字化けしておりどうすればいいんだ‥となったがld.lld自体はバイナリファイルなので意味がなかった。
  • 実際は「ld.lld」コマンドでリンクする時に、オプションに-z separate-codeを追加するという意味だった。
  • $HOME/workspace/mikanos/kernel/Makefileを下記のように改修
TARGET = kernel.elf
OBJS = main.o

CXXFLAGS += -O2 -Wall -g --target=x86_64-elf -ffreestanding -mno-red-zone \
            -fno-exceptions -fno-rtti -std=c++17
LDFLAGS  += --entry KernelMain -z norelro --image-base 0x100000 --static


.PHONY: all
all: $(TARGET)

.PHONY: clean
clean:
	rm -rf *.o

kernel.elf: $(OBJS) Makefile
	ld.lld $(LDFLAGS) -o kernel.elf -z separate-code $(OBJS)

%.o: %.cpp Makefile
	clang++ $(CPPFLAGS) $(CXXFLAGS) -c $<
  • 改修後、再びMake、Buildを実行してqemuを起動したら期待した動作になっていた!
    image.png

  • -z separate-codeは、リンクの際に命令とデータを別のメモリページに配置するオプションらしい。このオプションをつけることで、kernel.elfが0x100000に近いところで読み込まれるようだ。下記のように実際に確認できた。(ぶっちゃけ、解決策実施後もそこまで0x100000に近いところに配置されているとは思えないが、とにかくこの差が大事らしい)

  • この話は「4.5 ローダを改良する」でも出てくるとのこと。


・解決策実施前(Entry pointが0x101180)

mikan@mikan-server:~/workspace/mikanos$ readelf -l  kernel/kernel.elf

Elf file type is EXEC (Executable file)
Entry point 0x101180
There are 4 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000100040 0x0000000000100040
                 0x00000000000000e0 0x00000000000000e0  R      0x8
  LOAD           0x0000000000000000 0x0000000000100000 0x0000000000100000
                 0x0000000000000120 0x0000000000000120  R      0x1000
  LOAD           0x0000000000000120 0x0000000000101120 0x0000000000101120
                 0x0000000000000133 0x0000000000000133  R E    0x1000
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     0x0

 Section to Segment mapping:
  Segment Sections...
   00     
   01     
   02     .text 
   03     
readelf: Warning: Unrecognized form: 0x22
readelf: Warning: Unrecognized form: 0x22
readelf: Warning: Unrecognized form: 0x22
readelf: Warning: Unrecognized form: 0x22
readelf: Warning: Unrecognized form: 0x22
readelf: Warning: Unrecognized form: 0x22
readelf: Warning: Unrecognized form: 0x22
readelf: Warning: Unrecognized form: 0x22
readelf: Warning: Unrecognized form: 0x23
readelf: Warning: Unrecognized form: 0x22
readelf: Warning: Unrecognized form: 0x22
readelf: Warning: Unrecognized form: 0x22
readelf: Warning: Unrecognized form: 0x22
readelf: Warning: Unrecognized form: 0x22

・解決策実施後(Entry pointは0x101060)

mikan@mikan-server:~/workspace/mikanos$ readelf -l  kernel/kernel.elf

Elf file type is EXEC (Executable file)
Entry point 0x101060
There are 4 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000100040 0x0000000000100040
                 0x00000000000000e0 0x00000000000000e0  R      0x8
  LOAD           0x0000000000000000 0x0000000000100000 0x0000000000100000
                 0x0000000000000120 0x0000000000000120  R      0x1000
  LOAD           0x0000000000001000 0x0000000000101000 0x0000000000101000
                 0x0000000000001000 0x0000000000001000  R E    0x1000
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     0x0

 Section to Segment mapping:
  Segment Sections...
   00     
   01     
   02     .text 
   03     
readelf: Warning: Unrecognized form: 0x22
readelf: Warning: Unrecognized form: 0x22
readelf: Warning: Unrecognized form: 0x22
readelf: Warning: Unrecognized form: 0x22
readelf: Warning: Unrecognized form: 0x22
readelf: Warning: Unrecognized form: 0x22
readelf: Warning: Unrecognized form: 0x22
readelf: Warning: Unrecognized form: 0x22
readelf: Warning: Unrecognized form: 0x23
readelf: Warning: Unrecognized form: 0x22
readelf: Warning: Unrecognized form: 0x22
readelf: Warning: Unrecognized form: 0x22
readelf: Warning: Unrecognized form: 0x22
readelf: Warning: Unrecognized form: 0x22

23日目

  • 「4.3 C++の機能を使って書き直す」に突入
  • main.cppの写経をした。抽象クラス「PixelWriter」を作成。
  • 抽象クラスで処理を定義し、それを継承するクラスにも処理を行わせるよう強制することでそれぞれのクラスで行う処理がバラバラにならないようにする。今回の例で言うと、データ形式が「RGB」と「BGR」のものでそれぞれクラスを作り、抽象クラスを継承でもさせるのだろう。
  • PixelWriter(const FrameBufferConfig& config): config_{config}はコンストラクタを表し、メモリ上にインスタンスを配置する役割をになっている。ついでにメンバ変数を初期化している。
  • ~PixelWriter~はデストラクタであることを示している。
  • virtualをつけると「仮想関数」となり、基底クラスのメソッドではなく、子どものクラスのメソッドを呼び出すようにすることができる。
  • virtual void Write(int x, int y, const PixelColor& c) = 0;は純仮想関数と言い、子どものクラスでこの関数の処理を行うことを強制できるらしい。抽象クラスとなる条件のうちの一つが、この関数があることのようだ。
 struct PixelColor {
    uint8_t r, g, b;
 };

 // #@@range_begin(pixel_writer)
 class PixelWriter {
    public:
     PixelWriter(const FrameBufferConfig& config): config_{config} {
     }

     virtual ~PixelWriter() = default;
     virtual void Write(int x, int y, const PixelColor& c) = 0;

    protected:
     uint8_t* PixelAt(int x, int y){
        return config_.frame_buffer + 4 * (config_.pixels_per_scan_line * y + x);
     }
    Private:
     const FrameBufferConfig& config_;
 };
 // #@@range_end(pixel_writer)

24日目

* 昨日かいた基底クラスを継承する2種類のクラスを定義する

  • using PixelWriter::PixelWriter;がコンストラクタの役割を果たしている。
// #@@range_begin(derived_pixel_writer)
class RGBResv8BitPerColorPixelWriter : public PixelWriter{
   public:
    using PixelWriter::PixelWriter;
    virtual void Write(int x, int y, const PixelColor& c) override{
       auto p = PixelAt(x, y);
       p[0] = c.r;
       p[1] = c.g;
       p[2] = c.b;
    }
};
class BGRResv8BitperColorPixeWriter : public PixelWriter {
   public:
    using PixelWriter::PixelWriter;
    
    virtual void Write(int x, int y, const PixelColor& c) override {
      auto p = PixelAt(x, y);
      p[0] = c.b;
      p[1] = c.g;
      p[2] = c.r;
    }
};
// #@@range_end(derived_pixel_writer)
  • これは配置newという書き方らしい。まだOSがメモリを管理できていないので、一般的なnewは使えない(ヒープ領域に確保するためプログラムがOSにメモリ管理要求を出すが、OS側は応えられない)ので、保存して欲しいメモリ領域を明示的に指定するnewを行なっているらしい。
// #@@range_begin(placement_new)
void* operator new(size_t size, void* buf) {
    return buf;
}

void operator delete(void* obj) noexcept {
}
// #@@range_end(placement_new)

  • メイン処理の部分はクラスを使うことで前よりもすっきりした
  • 相変わらず細かいところはなにやっているかわからないが、大まかな処理の流れはわかるようになってきた気がする。成長。
  • 明日は実際にコンパイル、リンク、ビルドして動かし、「4.4 vtable」に突入しよう
// #@@range_begin(call_pixel_writer)
extern "C" void KernelMain(const FrameBufferConfig& frame_buffer_config) {
  switch (frame_buffer_config.pixel_format) {
    case kPixelRGBResv8BitPerColor:
      pixel_writer = new(pixel_writer_buf)
        RGBResv8BitPerColorPixelWriter{frame_buffer_config};
      break;
    case kPixelBGRResv8BitPerColor:
      pixel_writer = new(pixel_writer_buf)
        BGRResv8BitperColorPixeWriter{frame_buffer_config};
      break;
  }

  for (int x = 0; x < frame_buffer_config.horizontal_resolution; ++x) {
    for (int y = 0; y < frame_buffer_config.vertical_resolution; ++y) {
        pixel_writer->Write(x, y, {255, 255, 255});
    }
  }
  for (int x = 0; x < 200; ++x){
    for (int y = 0; y < 100; ++y) {
        pixel_writer->Write(x, y, {0, 255, 0});
    }
  }
  while (1) __asm__("hlt");
}
// #@@range_end(call_pixel_writer)

25日目

  • 写経したコードを使用してピクセル描画することができた。
  • 写経時に色々とタイポしていてその修正だけで時間が潰れた
  • 写経だけして雰囲気掴むだけでもいいかも‥。わざわざ実行の必要あるのかな?

image.png

26日目

  • ひたすらよくわからんことが書いてあった
  • とりあえず、メモリを読み込む処理に不具合がある状態なので修正が必要らしい
  • 難しい言葉だらけで処理が追いつかない‥。とりあえず写経しつつ、Geminiに意味を聞いていってる。

27日目

  • そもそもELFについて何かわからなくなってきたので調べた。同じように悩んでいた人が解説記事を出してくれていたので参考にした。
    • Executable and Linking Formatの略。C言語のプログラムをコンパイル&リンクすると出来上がる実行ファイルがこれ。(別にUEFIブートローダ用の実行ファイル形式というわけではない)
    • .elfのみだけでなく、.O(コンパイルによって作成されるオブジェクトファイル)などもELFに該当する
    • 本で説明されていたファイル構造も説明されていた。
      • ファイルヘッダー‥ファイル全体の情報
      • プログラムヘッダー‥ローダ向けの情報
      • セクション本体
      • セクションヘッダー‥リンカ向けの情報
  • 概念は何となくわかったので、引き続き調べる

ファイルヘッダー

mikan@mikan-server:~/workspace/mikanos/kernel$ readelf -h kernel.elf
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x101020
  Start of program headers:          64 (bytes into file)
  Start of section headers:          15288 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         5
  Size of section headers:           64 (bytes)
  Number of section headers:         18
  Section header string table index: 16

プログラムヘッダー

mikan@mikan-server:~/workspace/mikanos/kernel$ readelf -l kernel.elf

Elf file type is EXEC (Executable file)
Entry point 0x101020
There are 5 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000100040 0x0000000000100040
                 0x0000000000000118 0x0000000000000118  R      0x8
  LOAD           0x0000000000000000 0x0000000000100000 0x0000000000100000
                 0x00000000000001a8 0x00000000000001a8  R      0x1000
  LOAD           0x0000000000001000 0x0000000000101000 0x0000000000101000
                 0x00000000000001c9 0x00000000000001c9  R E    0x1000
  LOAD           0x0000000000002000 0x0000000000102000 0x0000000000102000
                 0x0000000000000000 0x0000000000000018  RW     0x1000
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     0x0

 Section to Segment mapping:
  Segment Sections...
   00     
   01     .rodata 
   02     .text 
   03     .bss 
   04

セクションヘッダー

mikan@mikan-server:~/workspace/mikanos/kernel$ readelf -S kernel.elf
There are 18 section headers, starting at offset 0x3bb8:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .rodata           PROGBITS         0000000000100158  00000158
       0000000000000050  0000000000000000   A       0     0     8
  [ 2] .text             PROGBITS         0000000000101000  00001000
       00000000000001c9  0000000000000000  AX       0     0     16
  [ 3] .bss              NOBITS           0000000000102000  00002000
       0000000000000018  0000000000000000  WA       0     0     16
  [ 4] .debug_loclists   PROGBITS         0000000000000000  00002000
       00000000000000cb  0000000000000000           0     0     1
  [ 5] .debug_abbrev     PROGBITS         0000000000000000  000020cb
       00000000000002d5  0000000000000000           0     0     1
  [ 6] .debug_info       PROGBITS         0000000000000000  000023a0
       0000000000000690  0000000000000000           0     0     1
  [ 7] .debug_rnglists   PROGBITS         0000000000000000  00002a30
       0000000000000033  0000000000000000           0     0     1
  [ 8] .debug_str_o[...] PROGBITS         0000000000000000  00002a63
       00000000000001cc  0000000000000000           0     0     1
  [ 9] .debug_str        PROGBITS         0000000000000000  00002c2f
       0000000000000656  0000000000000001  MS       0     0     1
  [10] .debug_addr       PROGBITS         0000000000000000  00003285
       0000000000000088  0000000000000000           0     0     1
  [11] .comment          PROGBITS         0000000000000000  0000330d
       0000000000000042  0000000000000001  MS       0     0     1
  [12] .debug_frame      PROGBITS         0000000000000000  00003350
       0000000000000160  0000000000000000           0     0     8
  [13] .debug_line       PROGBITS         0000000000000000  000034b0
       00000000000001ee  0000000000000000           0     0     1
  [14] .debug_line_str   PROGBITS         0000000000000000  0000369e
       000000000000019b  0000000000000001  MS       0     0     1
  [15] .symtab           SYMTAB           0000000000000000  00003840
       0000000000000150  0000000000000018          17     2     8
  [16] .shstrtab         STRTAB           0000000000000000  00003990
       00000000000000c4  0000000000000000           0     0     1
  [17] .strtab           STRTAB           0000000000000000  00003a54
       0000000000000164  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  D (mbind), l (large), p (processor specific)

28日目

  • 引き続きELF調べつつ、写経した下記コードの理解を進める。
  • 言葉の意味はわかったけど実感ができていない。先に進もう
// #@@range_begin(calc_addr_func)
void CalcLoadAddressRange(f64_Ehdr* ehdr, UINT64* first, UINT64* last) {
  // ELFヘッダからプログラムヘッダテーブルのアドレスを計算する。ehdr->e_phoffはファイル先頭からプログラム
  Elf64_Phdr* phdr = (Elf64_Phdr*)((UINT64)ehdr + ehdr->e_phoff);
  *first = MAX_UINT64;
  *last = 0;
  for (Elf64_Half i = 0; i < ehdr->e_phnum; ++i){
    if(phdr[i].p_type != PT_LOAD) continue;

    *first = MIN(*first, phdr[i].p_vaddr);
    *last = MAX(*last, phdr[i].p_vaddr + phdr[i].p_memsz);
  }
}
// #@@range_end(calc_addr_func)

29日目

  • 写経を続けている
  • 相変わらずわからない表現ばかりだが、CalcLoadAddressRange(kernel_ehdr, &kernel_first_addr, &kernel_last_addr);のところでは、kernel_first_addrkernel_first_addrのポインタを渡し、渡した先の関数で計算結果をポインタに代入することで、実データのやりとりをしないで済んでいる(ポインタを介してやりとりしている)ことが直感的にわかった。前よりは成長しているはず‥。
// #@@range_begin(copy_segm_func)
 void CopyLoadSegments(Elf64_Ehdr* ehdr){
    Elf64_Phdr phdr = (Elf64_Phdr*)((UINT64)ehdr + ehdr->e_phoff);
  // e_phnum(プログラムヘッダテーブルのエントリ数)分だけ繰り返し処理する
    for (Elf64_Half i = 0; i < ehdr->e_phnum; ++i) {
    // p_type(セグメント種別)がPT_LOAD(ロード可能なセグメント種別)なら処理続行。それ以外は処理中断。
    if(phdr[i].p_type != PT_LOAD) continue;

    // phdr[i].p_offsetは、ファイル先頭からセグメントのデータまでのオフセット
    // ロード対象となるセグメントが、ファイル内のどこに位置しているかを計算
    UINT64 segm_in_file = (UINT64)ehdr + phdr[i].p_offset;
    
    // ファイルからメモリへのセグメントのコピーを行う。
    // p_vaddrセグメントがロードされるべきメモリ上の仮想アドレス
    // segm_in_fileは、ファイル内のセグメントの開始アドレス
    CopyMem((VOID*)phdr[i].p_vaddr, (VOID*)segm_in_file, phdr[i].p_filesz);

    // BSSセグメント(初期化されていないデータ)のサイズを計算。p_memszがp_fileszよりも大きい場合、その差分がBSSセグメントでありゼロで埋める必要がある。
    UINTN remain_bytes = phdr[i].p_memsz - phdr[i].p_filesz;
    // BSSセグメントのゼロクリアを行う。
    setMem((VOID*)(phdr[i].p_vaddr + phdr[i].p_filesz), remain_bytes, 0);
  }
}
// #@@range_end(copy_segm_func)
// #@@range_begin(alloc_pages)
Elf64_Ehdr* kernel_ehdr = (Elf64_Ehdr*)kernel_buffer;
UINT64 kernel_first_addr, kernel_last_addr;
CalcLoadAddressRange(kernel_ehdr, &kernel_first_addr, &kernel_last_addr);

UINTN num_pages = (kernel_last_addr - kernel_first_addr + 0xfff) / 0x1000;
status = gBS->AllocatePages(AllocatedAddress, EfiLoaderData,
                            num_pages, &kernel_first_addr);
if (EFI_ERROR(status)){
    Print(L"failed to allocate pages: %r\n",status);
    Halt();
}
// #@@range_end(alloc_pages)

30日目

  • とりあえず写経は終わらせた。この章についてもう嫌になったので先に進むことにした。
  • いつか強くなったワイが読み解いてくれるだろう‥。
  • せっかく写経したファイルだが、タイポによってコンパイルエラー発生して修正して進み遅くなるのもしんどいのでとりあえずosbook_day04dのコード群をmakeして動かせるところまでを確認する
  • とりあえずやってるだけ偉いので。
  • 5章「文字表示とコンソールクラス」に突入した
  • 「AA」と表示するだけで、8 * 16ピクセルで表現するための下記のようなコードが必要になるらしい。「普通に文字表示すればええやん」と思ったけどそもそも文字表示機能とかないところからやっているので全て一からやらなければならないことが実感できた。
  • 何やっているかはある程度直感的にわかるのでここは写経しない。

image.png

↓文字ひとつ表示するだけで大変‥

main.cpp
// #@@range_begin(font_a)
const uint8_t kFontA[16] = {
  0b00000000, //
  0b00011000, //    **
  0b00011000, //    **
  0b00011000, //    **
  0b00011000, //    **
  0b00100100, //   *  *
  0b00100100, //   *  *
  0b00100100, //   *  *
  0b00100100, //   *  *
  0b01111110, //  ******
  0b01000010, //  *    *
  0b01000010, //  *    *
  0b01000010, //  *    *
  0b11100111, // ***  ***
  0b00000000, //
  0b00000000, //
};
// #@@range_end(font_a)

// (中略)


// #@@range_begin(write_ascii)
void WriteAscii(PixelWriter& writer, int x, int y, char c, const PixelColor& color) {
  if (c != 'A') {
    return;
  }
  for (int dy = 0; dy < 16; ++dy) {
    for (int dx = 0; dx < 8; ++dx) {
      if ((kFontA[dy] << dx) & 0x80u) {
        writer.Write(x + dx, y + dy, color);
      }
    }
  }
}
// #@@range_end(write_ascii)

2
1
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
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?