osdev-jp core へようこそ

本サイト “osdev-jp core” は、osdev-jp のコアメンバが執筆した技術文書を掲載する場所です。OS 開発に役立つ記事ばかりを扱っています。

メインコンテンツ

資料集

OS開発をする上で参考になる資料

These documents will help your operating system development.

Intel CPU

Intel Chipset

Intel NIC

PCI

ACPI

  • ACPI Spec

    ACPIは電源管理のみならず、MP TablesやPCI、各種デバイスの情報を取る事ができる。

HPET Timer

UEFI

  • UEFI Specifications

    BIOSの後継となるファームウエアインターフェイス。UEFI搭載PCでブートローダを書く際には必須

AHCI

  • Serial ATA AHCI: Specification, Rev. 1.3.1

    IDE に代わる SATA 用のコントローラ。IDE でも SATA を読み書きすることは可能だが、より新しい規格である AHCI を使えば間違いないだろう。

ハードウエア全般

対象読者:複数人でOS開発をやっているプロジェクト、環境が変わった時に毎回OS開発環境を構築し直すのが面倒な人

livaです。 OSの開発環境を作るのって、面倒ですよね。一度環境構築した方法が新しいパソコンで使えなかったり、特に共同開発になるとWindows,Mac,Linuxといろいろな環境に対応させなければならないので。

私が主導しているプロジェクトでは、Vagrantを使って、どんなPCでも「コマンド一つで」手軽にOS開発環境を構築できるようになっているので、今回はVagrantの導入方法について解説していきたいと思います。

Vagrant

そもそもVagrantって何だ、という人も多いかもしれません。 では、VirtualBoxならどうでしょう?これならご存知ですかね。

一応説明しておくと、VirtualBoxは、今お使いのOSの上で別のOSを立ち上げる事ができるソフトウェア(仮想マシン)です。OS開発を始めると、デバッグにqemuやBochsといったエミュレータを使う事になると思うのですが、VirtualBoxもこれらのエミュレータの仲間です。

仮にVirtualBoxの上でOS開発を行うようにすれば、あなたが使っているパソコンのOSがWindowsであろうと、Macであろうと、Linuxであろうと、そのVirtualBox上で動かしているOSさえ同じなら、同じ開発環境構築手法を使ってどんなパソコン上でもOS開発ができる、という事になります。しかも、これならうっかり手元のOSを壊してしまう心配もありません。便利そうですね。

とはいえ、VirtualBox上で開発するのは意外と不便です。自分の使ってるOSの上に新しくVirtualBoxのGUIが立ち上がって、その上でエディタを開いてOSを開発する、みたいな形になるのですが、実際に使ってみるとGUIが重かったりするなど、とにかく使いづらいのです。仮想マシンにsshサーバーを立てて、手元から繋いでCUI環境にするという方法もありますが、ssh環境の整備やポートを適切に設定しなければいけなかったりと中々面倒です。

そこで登場するのがVagrantです。VagrantはVirtualBoxのラッパーみたいな物で、ターミナルからvagrantコマンドを叩くと裏でVirtualBox仮想マシンを立ち上げ、そこにssh接続をしてくれます。 ssh絡みの面倒な部分も全てVagrantが面倒を見てくれるので、環境整備をする必要もありません。世の中便利になったものです。

Vagrantを使ってみる

まず、VirtualBoxとVagrantをインストールしましょう。どちらも公式サイトからインストールできます。

次に、Vagrantfileと呼ばれるファイルを作ります。ターミナルで以下のコマンドを実行してみてください。 (基本的に説明にはUNIX環境を用いますが、Windowsでも同様の方法でできるはずです)

$ vagrant init ubuntu14.04

これでVagrantfileが出来ます。(中身はただのテキストファイルです) コマンドから分かる通り、このVagrantfileはubuntu14.04ベースの仮想マシンを作ってくれます。指定の仕方を変えれば、CentOS等、好きなOSをインストールできます。もちろん、ubuntu14.04ベースの仮想マシンなので、OS開発の際はubuntuに用意されているツールは全て使う事ができます。

OS開発環境を自力でいろいろ整えたいと思うと、Linuxを使うのが一番簡単なので、特にこだわりが無ければ何も考えずにこのまま進むと良いでしょう。これ以降の解説もubuntu14.04で仮想マシンを作成したとして行います。

Vagrantfileが作成されたのを確認したら、以下のコマンドを実行してみてください。

$ vagrant up

このコマンドは、シャットダウンされている仮想マシンを立ち上げるための物です。仮に仮想マシンが存在しなければ、Vagrantfileを元に自動で仮想マシンを作成してくれます。(逆に言うと、vagrant upはVagrantfileのあるディレクトリでやってくださいね)

最初は仮想マシンイメージのダウンロード等があるので、少し時間が掛かるかと思います。

仮想マシンが立ち上がったら、ssh接続してみましょう。

$ vagrant ssh

ubuntuからのメッセージが表示されたら、成功です。

パソコンを再起動した時は、もう一度vagrant upを実行して、vagrant sshすればssh接続できます。既に仮想マシンが作成されているので、vagrant upで仮想マシンイメージをダウンロードしたりする事はないので安心してください。

余談ですが、Vagrantを使う上で以下のコマンドを覚えておくと良いでしょう。

$ vagrant halt // 仮想マシンのシャットダウン
$ vagrant reload // 仮想マシンの再起動

プロビジョニングレシピの作成

ubuntuが起動したらその上で開発環境を構築していく(必要なツールをインストールしたり)事になると思うのですが、その際に実行したコマンドをメモっておきましょう。

一通り開発環境構築が終了したとして、ここからは別のパソコンでも同様の環境構築を手作業で行わなくても良いようにする(Vagrantにやらせる)方法を説明します。

まずはホストOS(あなたのパソコンにインストールされているOS、仮想マシン上のOSではないですよ)のVagrantfileがあるディレクトリに、bootstrap.shというファイルを作成しましょう。これは名前の通りシェルスクリプトで、あなたが開発環境を構築した際に実行したコマンドをシェルスクリプト化したものです。つまり、シェルスクリプトにしておく事で、次に環境構築する際は、Vagrantが代わりに実行してくれるという事です。

基本的には実行したコマンドをそのまま羅列してシェルスクリプトにしてしまえば良いのですが、一つだけおまじないを。

このシェルスクリプトは、後でVagrantfileを編集する事で、初回のvagrant up時、及びvagrant provisionをした時に実行されるように設定します。ただ、vagrant provisionが実行される度に環境構築は何回も走って欲しくない(冪等性が担保されている場合は別)ので、初回のみ実行されるようにしてしまいましょう。

#!/bin/sh
test -f /etc/bootstrapped && exit

// ここに環境構築時に実行したコマンドを羅列する
sudo apt-get install -y gcc g++ make

date > /etc/bootstrapped

こうすれば、初回の仮想マシン作成時にのみ実行される環境構築シェルスクリプトになります。

次に、Vagrantfileに以下のような行を追加しましょう。場所としては、config.vm.box = “ubuntu14.04”等と書かれている所の直下が良いでしょう。

config.vm.provision :shell, :path => "bootstrap.sh", :privileged => true

後は、Vagrantfileとbootstrap.shをコピーして、そのマシンでvagrant upとすれば(もちろん、事前にVagrantとVirtualboxはインストールする事)、自動でOS開発環境が構築できます。

qemu

OS開発環境構築は終わったのですが、実は一つだけ問題があります。 仮想マシン上でqemuデバッグをする事があると思うのですが、その際、仮想マシンへのssh接続だと、qemuの画面(VGA出力)を表示できないのです。これではデバッグが捗らないので、我がプロジェクトで使用している方法を書いておきます。参考にしてみてください。

まず、Vagrantfile内にconfig.vm.networkに関する設定を追加しましょう。

config.vm.network "forwarded_port", guest: 5900, host: 15900

これは仮想マシンの5900ポートをホストOSの15900にマッピングするという設定です。 次に、QEMUの画面出力をvnc(デフォルトはポート番号5900)にして立ち上げます。 つまり、ネットワーク経由でqemu上で立ち上がってるOSを遠隔操作できるようにする事で、VirtualBoxの壁を超えるという事になります。

# qemu -hda $(IMAGE) -vnc 0.0.0.0:0,password

次にqemuのコンソールでvncのパスワードを設定します。

(qemu) set_password vnc hogehoge

最後に、ホストOSから15900ポートに対してvnc接続し(ホストOS上では適当なvncクライアントを使ってください)、パスワードに先ほど設定したhogehogeを入れると、qemuのVGA出力画面が表示されるはずです。

ただ、この方法だとqemuにvnc接続する際に毎回パスワードを求められてしまって面倒ですよね。vncを使わずにX11転送で接続することもできるらしいのですが、ごめんなさい、それについては検証していないので、次の章では全く別の方法を紹介させてください。

共有フォルダ

ソースコードの共有等でホストOSとゲストOSの間でファイルを共有したくなる事があります。Macでは(恐らくWindowsやLinuxでも)ホストOSのVagrantfileのあるフォルダがゲストOSの/vagrant以下と同期されているので、これを利用すればできます。(Hyper-Vを使う場合はよく分からないのですが、どうやら共有フォルダはサポートされてないようです。その場合、自前でsambaサーバーを建てたりしないといけないかもしれません。ここではホストOSとゲストOSの間でファイル共有ができたとして話を進めます)

共有フォルダはソースコード共有に使うだけでなく、作成したOSのディスクイメージを共有する事にも使えます。つまり、仮想マシン上でディスクイメージを作ったらそれをホストOSに引っ張ってきてホストOS上のqemuで起動すれば、vncの設定といった面倒な事をしなくてもOSのデバッグが行えます。

お疲れ様でした

Vagrantを使って簡単にOS開発環境を構築する方法を書いてみました。これでパソコンを買い換えても、プロジェクトメンバーが増えても安心ですね!

ちなみに、ここでは基本的な事しか説明していませんが、Vagrantを上手く活用すると、いろいろな事ができるはずです。皆さんいろいろ試してみて、自分好みの環境を作ってみてください。

実際に自分で環境を作ってみた!という方は、osdev-jpオープンコミュニティの方に専用のスペースを設けたので、是非そこに書いてください!

2016/04/07 by uchan

Visual Studio Code で OS 開発をする

Visual Studio Code(以下 VSCode)は Microsoft が 2015 年 4 月にリリースした、ソースコード編集用のテキストエディタです。同社の有名な統合開発環境 Visual Studio は全部入りですが、VSCode はシンプルです。”Visual Studio” という名前ではありますが、Linux でも Mac OS X でも動きます。しかもオープンソースで開発されていて、最近の Microsoft の凄さを感じます。さらに、起動がとても軽いのにソースコードの補完機能を使えたりします。

ソースコードの補完機能というのは、変数名の一部を入力するだけで全体を補完してくれたり、構造体型の変数の直後で .-> を入力するとメンバの一覧を出してくれるような機能のことです。こんな機能があったらとても便利ではないでしょうか。VSCode でそれができますので(しかもとても快適に!)、試してみる価値はあるはずです。

対象読者

VSCode を使って補完を効かせながら OS のソースコードを書きたい方を対象とします。Linux の基本的なコマンドを知っていることを前提としています。Linux 上で VSCode を動かしたい方はもちろんですが、Windows 上の VSCode から Linux に置いてあるソースコードを編集したい方も対象です。

Linux 上で直接ソースコードを編集する方法

この記事では、まず Linux (記事の動作確認は Ubuntu 15.10 で行っています)に VSCode をセットアップし、補完を効かせながらコーディングするための設定を説明します。OS 開発に慣れてきて、いろいろな言語、いろいろなツールで開発しようとすると、やはり Linux を使いたくなってくると思います。その時に本記事が役に立つと思います。

そうはいっても Windows の完成度の高い GUI が使いたい、普段使いは Windows なので Windows から Linux を操作したい、というような筆者のような方も多いことでしょう。記事の後半では、Windows の VSCode から Linux に置いたソースコードを編集する方法を説明します。

VSCode のセットアップ

VSCode をセットアップし、きちんと補完が効くように設定する手順を説明します。順に VSCode と Clang のセットアップ、VSCode へのプラグインのインストール、各種設定をやります。まずは VSCode のセットアップから。

公式のセットアップ手順が Setting up Visual Studio Code に載っています。Mac OS X、Linux、Windows それぞれの手順が書いてあるので読んでください。

参考までに、筆者が実行したコマンド列を紹介します。

$ unzip ~/Downloads/VSCode-linux-x64-stable.zip
$ sudo mv ~/Downloads/VSCode-linux-x64 /usr/local/
$ sudo ln -s /usr/local/VSCode-linux-x64/code /usr/local/bin/vscode

手順書では VSCode のアーカイブをどこかに展開することになっています。迷ったら /usr/local に置けば良いでしょう。この手順を実行すると、好きなディレクトリで vscode . などと実行できるようになります。

Clang のセットアップ

Clang は C/C++ 用のコンパイラです。といっても Clang で OS をビルドしよう!ということではなく、ソースコードの補完機能のために使うだけです(後で導入する VSCode の補完用プラグインが Clang を要求します)。OS 自体のビルドは、お好みのコンパイラでどうぞ。もちろん Clang を使ってもいいですよ。

Clang のセットアップは Ubuntu なら APT を使うだけなので簡単です。Windows での導入方法は後ほど説明します。

$ sudo apt install clang-3.7

執筆時点で Ubuntu 15.01 の APT に登録されている最新版は clang-3.7 でした。基本的に新しいほど新しい言語規格に対応しているので、こだわりがなければ最新版を入れるといいでしょう。

プラグインのインストール

いよいよソースコード補完のためのプラグイン “vscode-clang” をインストールします。VSCode を立ち上げて(code をダブルクリックするか、ターミナルで vscode と入力します)、Ctrl-P で Command Palette を表示させます。Command Palette で次のコマンドを実行すると vscode-clang をインストールできます。

ext install vscode-clang

しばらくしてインストールが完了すると、有効にするために VSCode を再起動するか聞かれますので、再起動してください。

コラム:ソースコード補完の技術

C/C++ ソースコードを補完するための VSCode プラグインには、執筆時点で次の 3 つがあります。

  1. C/C++ Clang (by Yasuaki MITANI)
  2. ClangComplete (by kube)
  3. C++ Intellisense (by austin)

1 と 2 はどちらも Clang を用いてソースコード補完を行うものです。3 は GNU Global を用いて補完を行います。

どちらの方法も、ソースコードを実行することなく静的に解析してソースコードの補完を行います。好みの問題かもしれませんが、筆者は Clang を使った補完をおすすめします。なぜなら Clang はもともとコンパイラであるため、GNU Global より高精度にソースコードを解析できるからです。今回は、Clang を使ったプラグインのうち、ダウンロード数の多い “C/C++ Clang” (vscode-clang) を使うことにしました。

Clang の他に有名な(Linux でよく使われる) C/C++ コンパイラには GCC があります。コンパイラとしては両者ほぼ同じように使えますが、Clang にはソースコードの解析情報を外部から取得しやすいという、GCC にない特徴があります。そのため、Clang はソースコード補完技術のバックエンドとしてよく使われます。

実はこれ以外に “C/C++” という、マイクロソフトが作っているプラグインがあります。試していませんので詳しくは分かりませんが、デバッグ機能などが充実しているようです。いかにもソースコード補完ができそうな気がしてきますが、残念ながら 2016/04/10 の時点ではソースコード補完機能はないようです。しかも、今のところ動作環境に対する制限が多く、簡単に動くのは Ubuntu 14.04 (x64) だけのようです。Windows に至ってはまったく対応していません。

Clang コマンド名の設定

vscode-clang が Clang コマンドを見つけられるように設定します。VSCode の File > Preferences > User Settings を開き、次のように設定します(実際はセットアップした Clang のコマンド名に合わせてください)。

{
    "clang.executable": "clang++-3.7"
}

この設定が済んで VSCode を再起動させれば、とりあえずはソースコード補完ができるようになります。ただし、場合によってはきちんと補完ができません。それは、ソースコードからインクルードしているヘッダファイルを Clang が探せない場合です。

インクルードパスの設定

システムの標準的な場所にインストールされているヘッダファイルや、相対パスで指定してあるヘッダファイルであれば、Clang が自動で探してくれます。しかし、自作 OS のビルド時に -I オプションなどでインクルードパスを追加している場合、そのパスを明示的に Clang に伝える必要があります。

その設定は VSCode の設定の clang.cxxflags で行えます。

この設定はワークスペースの設定ファイルに書くので、まずワークスペースを開きます。VSCode における「ワークスペース」は、あるディレクトリを指す言葉です。ワークスペースは普段仕事をする場所、みたいな意味合いがあります。VSCode を再起動しても、前回開いていたワークスペースは開かれたままになります。私は自作 OS のソースコードが置いてあるトップディレクトリをワークスペースとしました。

一旦ワークスペースとなるディレクトリを開いたら、VSCode の File > Preferences > Workspace Settings を開き、次のように設定します。(自作 OS 用のヘッダファイルが /home/uchan/bitnos/include にあるとします)

{
    "clang.cxxflags": ["-I/home/uchan/bitnos/include"]
}

ここまでで User Settings と Workspace Settings の 2 つの設定ファイルが登場しました。User Settings は $HOME/.config/Code/User/settings.json に、Workspace Settings はワークスペース直下の .vscode/settings.json に書かれます。

どちらにも同じ設定を書けますが、ワークスペースごとに変える必要のない設定は User Settings に、ワークスペース固有の設定は Workspace Settings に書くのが良いでしょう。Clang のコマンド名は自作 OS には関係ないので前者に、インクルードパスは自作 OS 専用なので後者に設定することにしました。

Windows から Linux 上のソースコードを編集する方法

いよいよ Windows の VSCode から Linux 上のソースコードをいじる方法を説明します。やり方は大きく次の 2 つがあります。

  • Windows から、Linux で動いている VSCode に接続して編集する
  • Windows で動いている VSCode から Linux のファイルを編集する

前者は VNC などのリモートデスクトップ接続技術を使って Linux のデスクトップ画面を Window から見たり、X11 転送を用いて Linux 上のアプリケーションの画面描画だけを Windows でおこなったりして実現します。

後者は Samba を用いて Linux と Windows でファイルを共有することで実現します。この亜種として、Linux と Windows 双方から NAS を参照するという方法もあります。ただし、この方法はファイルが NAS に置かれてしまうため、Linux ノートを持って外出し、外出先でビルドする、というのができません。

VNC は手堅いですが、デスクトップ全体の画像データをやり取りするので重かったり、カクカクすることがあります。X11 転送はその画面の情報だけをやり取りするので比較的軽いですが、残念なことに筆者の環境では VSCode で X11 転送ができませんでした。ということで、筆者が職場でも実践している Samba を用いた方法を説明します。

Samba のセットアップ

Samba はごく一般的なソフトウェアなのでパッケージになっているはずです。

$ sudo apt install samba

ホームディレクトリを Windows と共有するための設定を書きます。

/etc/samba/smb.conf に次の設定を追加します。Ubuntu の場合、これらの設定はコメントアウトされている状態で書かれていることがあります。(sudo gedit /etc/samba/smb.conf とすれば設定ファイルを開けます)

[homes]
   read only = no

設定を変えたら Samba をリスタートして、Windows から Samba に接続する際のアカウントを追加します。

$ sudo service smbd restart
$ sudo smbpasswd -a YOUR_NAME

リスタートとアカウント追加はどちらが先でも大丈夫なはずです。smbpasswd に指定した -a オプションは、ユーザがいなければ追加し、既にいればそのユーザのパスワードを変更する意味があります。

Windows でディレクトリをマウント

今しがた共有設定をしたホームディレクトリを Windows でマウントします。エクスプローラを開いて次のアドレスをアドレスバーに入力します。

\\HOST_NAME\YOUR_NAME

HOST_NAME は IP アドレスでも大丈夫です。

Windows から Linux のホームディレクトリを開く

ホームディレクトリが開けたら、ディレクトリを右クリックし「ネットワーク ドライブの割り当て」を選択します。適当なドライブ文字を割り当てます。

ドライブ文字を付けなくてもファイルの読み書きはできますが、ドライブ文字を付けておくといろいろなツールから扱いやすくなります。開発ツールによっては直接ネットワーク上のディレクトリを見に行けないものもある(IntelliJ IDEA とか)ので、ドライブ文字を割り当てておくと便利です。

ネットワークドライブの割り当て

Clang for windows の導入

Windows の VSCode で補完機能を使うには、Windows に Clang をインストールしなければなりません。

まず http://llvm.org/releases/download.html から “Clang for Windows (64-bit)” をダウンロードします。ビット数はお使いの OS のビット数に合わせれば良いですが、分からなければ 32-bit 版を使うとどちらの環境でも動くはずです。Clang のインストールとは全く関係ないですが、自作 OS で 64 ビット対応したら恰好良いでしょうねえ。どなたか記事をお願いします!(他力本願)

インストールは LLVM Clang の Windows へのインストールと使い方 - プログラマーズ雑記帳 あたりを参考にしてください。インストーラをクリックしていくだけです。ただし、記事によれば事前に Visual Studio (おそらく VSCode ではダメ)をインストールしておく必要があるようです。筆者のパソコンにはすでに Visual Studio 2015 がインストールされていたので、検証することはできませんでした。依存関係が自動で入らないのは、Ubuntu の APT に慣れてしまった筆者としては辛い世界です。

インストールの段階で clang コマンドを PATH に通しておくと、後から clang.executable の設定をしなくていいので楽です。インストールの途中で “for all users” か “for current user” を選択すると自動で PATH が通ります。ただ、もし PATH を通さなかったとしても VSCode のインストール後に User Settings へ次の設定を追加すれば問題ありません。

"clang.executable": "C:\\Program Files\\LLVM\\bin\\clang++.exe"
VSCode for windows の導入

例によって公式のセットアップ手順書 Setting up Visual Studio Code を参考にするとインストールできます。.NET Framework 4.5 が必要らしいので、Windows 7 ユーザの方は事前に導入しておきます。筆者は Windows 10 を使っているため、すでに導入されていました。

最後に Linux での作業と同じように、”vscode-clang” プラグインのインストールと Workspace Settings 内の clang.cxxflags の設定をやります。Workspace Settings をいじる前には、もちろんワークスペースを開いておく必要があります。先ほどネットワークドライブとして割り当てたホームディレクトリから、ワークスペースとなるディレクトリを探して開きます。(もし、ホームディレクトリより上位をワークスペースとする場合は、そこを共有できるように Samba の設定変更が必要です。)

Linux での作業と少し違うのは Workspace Settings の設定ファイルが Linux と共有されている 点です。ワークスペースの中の .vscode/settings.json は、先ほど Linux から設定したファイルと同じものなのです。したがって Workspace Settings を開くと、すでに clang.cxxflags の設定があるはずです。だから、設定はこんな感じになります。

"clang.cxxflags": [
    "-I/home/uchan/bitnos/include",
    "-IZ:/bitnos/include"
]

設定を終えたら VSCode を再起動します。再起動後、無事に補完機能が使えているはずです。やったー!

補完候補が表示されている様子

2017/06/15 by uchan

EDK II で UEFI アプリケーションを作る

この記事は UEFI アプリケーションを EDK II 上で作り、QEMU で動かすまでを解説します。動作確認は Ubuntu 16.04 でやっていますが、Linux ならどれも同じ方法で動作すると思います。

UEFI アプリを作る 3 つの方法

UEFI アプリを作るには、使う SDK によって主に 3 つの方法があります。参考記事:UEFIのSDK事情 - syuu1228’s blog

  • SDK を使わない
  • gnu-efi
  • EDK II

SDK を使わないで作る方法は ツールキットを使わずに UEFI アプリケーションの Hello World! を作る - 品川高廣(東京大学)のブログ が参考になります。Hello World アプリを作るだけなら、恐らくこの方法が最も簡単だと思います。新しく買った UEFI 搭載の組み込みボードの動作をサクッと確認したい、みたいな用途にはちょうどいいでしょう。しかし、必要となるすべての構造体を自分で定義する必要があったり、頼れるライブラリもないので、発展性は低いです。

gnu-efi は UEFI API を叩くアプリケーションを作るためのものです。使い方が結構シンプルなようで、これを用いたサンプルを多く見かけます。ただ、命名規則 や ABI が UEFI の公式とはちょっと違ったり、UEFI で規定されている API のうちよく使うものしか実装されてなかったりして、使いこなすうちに物足りなくなることもあると教えてもらいました。gnu-efi については id:tnishinaga さんの記事 gnu-efiを使ってAARCH64/ARM64のUEFIサンプルアプリを動かしてみる が参考になるでしょう。

EDK II は元々、UEFI の開発に深くかかわっている Intel が作っていた SDK です。gnu-efi が「UEFI アプリケーション」専用なのに対し、EDK II は UEFI アプリはもちろん、周辺のライブラリや、UEFI ファームウェアそのものを開発するための SDK という役割も持っており、超高機能です。高機能ゆえに UEFI アプリを作るという目的のためには複雑すぎてとっつきにくい印象があります。が、フル装備の SDK ですから、慣れておけば後々困ることもないと思います。ということで、この記事では EDK II への入門を目指します。

EDK II の入手とセットアップ

EDK II SDK は GitHub から入手します。参照:EDK II 公式サイト

$ git clone https://github.com/tianocore/edk2.git

入手できたら edk2 ディレクトリに移動し、edksetup.sh を読み込みます。

$ cd edk2
$ source edksetup.sh

source edksetup.sh BaseTools を実行するように書いてある解説ブログなどがあるのですが、現在のバージョンの EDK II では BaseTools を付けないのが正しいです(付けたとしても単に無視されます)。

edksetup.sh が読み込まれると幾つかの環境変数が設定された旨が表示されます。以降のビルド作業などは、edksetup.sh を読み込んだシェル上で行ってください。

EDK II のファイル構造

ここで EDK II のファイル構造を眺めておきましょう。この知識はアプリを作るときに役立つはずです。(なお、以下で登場する $WORKSPACE は git clone した edk2 ディレクトリへのパスを指します。)

$WORKSPACE/
  Conf/
    target.txt      ビルド設定
    tools_def.txt   ツールチェーンの設定
  Build/            ビルド成果物が出力されるディレクトリ
  MdePkg/           EDK の中心的ライブラリのパッケージディレクトリ
  ...Pkg/           その他のパッケージディレクトリ
  edksetup.sh       環境変数設定用スクリプト

Conf ディレクトリにある target.txt は build コマンドで何がビルドされるかを決定するファイルです。詳しい説明は後述します。

Build ディレクトリは build コマンドによるビルド成果物が出力されるディレクトリです。例えば AppPkg をビルドすると Build/AppPkg/DEBUG_GCC5/X64/Hello.efi のような場所に目的の EFI アプリが出力されます。

Conf と Build を除くと、その他のディレクトリは「パッケージ」ごとに分かれています。多くのパッケージは別のパッケージの機能を利用して実装されており、パッケージ間で依存関係を持っています。ただし MdePkg だけは他のパッケージに依存しておらず、その他のパッケージから利用される基本的なライブラリとして構成されています。

次に、AppPkg の構造を見てみます。

AppPkg/
  AppPkg.dec        パッケージ宣言(declaration)ファイル
  AppPkg.dsc        パッケージ記述(description)ファイル
  Applications/
    Hello/          Hello モジュールディレクトリ
      Hello.c
      Hello.inf     モジュール定義ファイル

パッケージとして構成するのに必要なのはパッケージ宣言ファイルとパッケージ記述ファイルです。パッケージ宣言ファイルは、パッケージ名を定義したり、パッケージ内のソースコードから利用する定数を定義したりするのに使います。パッケージ記述ファイルは、出力ディレクトリ名やサポートされるアーキテクチャ、サポートされるビルドターゲットなど、ビルドに関する設定を含みます。これら 2 つのファイルについて、詳しくは後述します。

Applications ディレクトリは UEFI アプリケーションを格納するためのディレクトリです。名前は慣習的なもので、AppPkg.dsc の Components セクションのパスを修正すれば別の名前のディレクトリでも問題ないはずです。

Hello.inf は Hello モジュールの定義ファイルです。このファイルにはモジュールの名前やモジュールの種別(ライブラリ、ドライバ、アプリ)、そのモジュールを構成するソースコード名のリストなどが書かれています。このファイルについても詳しくは後述します。

target.txt

target.txt を開くといくつかの設定項目があることが分かります。重要な設定項目を説明します。

設定項目 設定値
ACTIVE_PLATFORM ビルド対象のパッケージの .dsc ファイル。
TARGET DEBUG、RELEASE、NOOPT、UserDefined のいずれか。NOOPT は最適化をせずビルドするオプションらしい。
TARGET_ARCH どのアーキテクチャ向けのバイナリを作るか。IA32 とか X64 とか ARM とか。
TOOL_CHAIN_TAG tools_def.txt の “Supported Tool Chains” からビルドに用いるツールチェインを選ぶ。VS2015 とか GCC5 とか。

独自パッケージの作成

さて、いよいよ自作の UEFI アプリの作り方を説明します。新しいアプリを作るには主に 2 つの方法があります。1 つは AppPkg/Applications にディレクトリを増やすことで作る方法、もう 1 つは独自のパッケージを作る方法です。ここでは後者を説明します。

参考記事:技術者見習いの独り言: UEFIアプリケーション/ドライバー開発の話

独自パッケージを作るには新しいディレクトリと .dec/.dsc ファイルが必要です。また、UEFI アプリを構成するモジュールを作る必要があり、.inf ファイルが必要です。既存のパッケージやモジュールのファイルを参考に書いていただければいいのですが、慣れないと難しいのでそれぞれの書き方を説明します。

.dec : パッケージ宣言ファイル

パッケージ名とその GUID を設定するのが主目的のファイルです。ほぼ最小のファイルは次のようになります。

[Defines]
  DEC_SPECIFICATION              = 0x00010005
  PACKAGE_NAME                   = MyPkg
  PACKAGE_GUID                   = d6a78c81-0d7f-4b46-af1d-e6ed44e5ffaa
  PACKAGE_VERSION                = 0.1

PACKAGE_GUIDuuidgen コマンドで生成したものに置き換えてください。DEC_SPECIFICATION は多分、.dec ファイルのフォーマットのバージョン番号だろうと思います。他のパッケージの設定値と同じにしておけば問題ないと思います。

PACKAGE_NAMEPACKAGE_VERSION はご自由にどうぞ。

.dsc : パッケージ記述ファイル

主にパッケージのビルド設定を書くファイルです。ほぼ最小のファイルは次のようになります。

[Defines]
  PLATFORM_NAME                  = MyPkg
  PLATFORM_GUID                  = 75dd174f-907b-4f8f-8afa-f0029d98ed5a
  PLATFORM_VERSION               = 0.1
  DSC_SPECIFICATION              = 0x00010005
  OUTPUT_DIRECTORY               = Build/MyPkg$(ARCH)
  SUPPORTED_ARCHITECTURES        = IA32|X64
  BUILD_TARGETS                  = DEBUG|RELEASE|NOOPT

  DEFINE DEBUG_ENABLE_OUTPUT     = FALSE

[LibraryClasses]
  # Entry point
  UefiApplicationEntryPoint|MdePkg/Library/UefiApplicationEntryPoint/UefiApplicationEntryPoint.inf

  # Common Libraries
  BaseLib|MdePkg/Library/BaseLib/BaseLib.inf
  BaseMemoryLib|MdePkg/Library/BaseMemoryLib/BaseMemoryLib.inf
  PcdLib|MdePkg/Library/BasePcdLibNull/BasePcdLibNull.inf
  UefiBootServicesTableLib|MdePkg/Library/UefiBootServicesTableLib/UefiBootServicesTableLib.inf
  !if $(DEBUG_ENABLE_OUTPUT)
    DebugLib|MdePkg/Library/UefiDebugLibConOut/UefiDebugLibConOut.inf
    DebugPrintErrorLevelLib|MdePkg/Library/BaseDebugPrintErrorLevelLib/BaseDebugPrintErrorLevelLib.inf
  !else   ## DEBUG_ENABLE_OUTPUT
    DebugLib|MdePkg/Library/BaseDebugLibNull/BaseDebugLibNull.inf
  !endif  ## DEBUG_ENABLE_OUTPUT

  UefiLib|MdePkg/Library/UefiLib/UefiLib.inf
  PrintLib|MdePkg/Library/BasePrintLib/BasePrintLib.inf
  MemoryAllocationLib|MdePkg/Library/UefiMemoryAllocationLib/UefiMemoryAllocationLib.inf
  UefiRuntimeServicesTableLib|MdePkg/Library/UefiRuntimeServicesTableLib/UefiRuntimeServicesTableLib.inf
  DevicePathLib|MdePkg/Library/UefiDevicePathLib/UefiDevicePathLib.inf

[Components]
  MyPkg/Hello/Hello.inf

最小といいつつ結構大きいですね。各部分について説明していきます。

Defines セクション

Defines セクションはビルドの全体的な設定です。

たくさん出てくる「プラットフォーム」とは、どうやら「パッケージ」とほぼ同じ意味らしいです。取り敢えず筆者はパッケージ=プラットフォームという認識で居ますが、今のところ困っていません。

PLATFORM_GUID は例のごとく uuidgen コマンドで生成した値を設定してください。DSC_SPECIFICATION は他の .dsc ファイルを参考に。PLATFORM_NAMEPLATFORM_VERSION はご自由に。

OUTPUT_DIRECTORY はどこに成果物を書き出すかを指定します。$WORKSPACE を基準としたパスを書きます。

SUPPORTED_ARCHITECTURESBUILD_TARGETS はそれぞれ、このパッケージがサポートするアーキテクチャとビルドターゲットの選択肢を列挙します。この選択肢から選んだ値を target.txt の TARGET_ARCHTARGET に設定することになります。

LibraryClasses セクション

LibraryClasses セクションでは、ライブラリ名と実際に使用するライブラリ実体のマッピングを定義します。同じような機能を提供するライブラリが複数存在する場合、ここのマッピングを切り替えることでどちらのライブラリを使用するかを選択できます。例えば

DebugLib|MdePkg/Library/UefiDebugLibConOut/UefiDebugLibConOut.inf

であれば、DebugLib という名前で MdePkg/Library/UefiDebugLibConOut/UefiDebugLibConOut.inf というライブラリを指定しています。パッケージ内のモジュール(今回の例で言えば Hello モジュール)が DebugLib という名前でライブラリを参照したとき、実際には UefiDebugLibConOut.inf ライブラリが使用される、というわけです。

上記の代わりに次のように指定したとします。

DebugLib|MdePkg/Library/BaseDebugLibNull/BaseDebugLibNull.inf

すると、モジュールは相変わらず DebugLib を参照していても、その実体は BaseDebugLibNull.inf に切り替わります。

例の .dsc ファイルでは、このマッピングの仕組みと条件式を組み合わせて使っています。DEBUG_ENABLE_OUTPUT が真の場合、DebugLib は本来のデバッグライブラリを指し、偽の場合は空のライブラリに切り替わります。DEBUG_ENABLE_OUTPUT 自体は Defines セクションで定義された定数です。

LibraryClasses セクションには、そのパッケージ内のモジュールが必要とするライブラリを、依存関係も含めてすべて列挙する必要があります。そのため、Hello World でさえ多くのライブラリの列挙が必要になってしまっています。慣れましょう。

Components セクション

ここは単にパッケージに含まれるモジュール(ライブラリ、ドライバ、アプリ)の .inf ファイルを列挙します。列挙したものがビルドされる、という理解で良いと思います。

.inf : モジュール定義ファイル

個々のモジュールの名前、モジュールの種別、構成するソースコードなどを指定するファイルです。Hello World モジュールの例を示します。

[Defines]
  INF_VERSION                    = 0x00010006
  BASE_NAME                      = Hello
  FILE_GUID                      = 430e7e71-c5af-408d-aa22-795ee53ae750
  MODULE_TYPE                    = UEFI_APPLICATION
  VERSION_STRING                 = 0.1
  ENTRY_POINT                    = UefiMain

[Sources]
  Hello.c

[Packages]
  MdePkg/MdePkg.dec

[LibraryClasses]
  UefiLib
  UefiApplicationEntryPoint

Defines セクションはまあ今まで見てきたのから類推できると思います。BASE_NAME はモジュール名を指定します。FILE_GUID は例にもれず uuiegen で生成します。

MODULE_TYPE はモジュールの種別を設定します。edk2 のリポジトリを眺めてみると、取り得る値として UEFI_APPLICATION, DXE_DRIVER, UEFI_DRIVER, BASE, PEIM などがあることが分かります。それぞれどんな意味なのか筆者はよく分かっていませんが、UEFI アプリを作りたい場合は UEFI_APPLICATION にしておけばいいはずです。

ENTRY_POINT にはメイン関数となる関数名を設定します。ここに設定した名前の関数がアプリケーションの開始点となります。基本的にどんな名前でもいいのですが、EfiMain だけは使えないようです。それ以外の名前にしてください。ここではよくサンプルで見かける UefiMain という名前にしてみました。

Souces セクションにはこのモジュールを構成するソースコードを列挙します。

Packages セクションでは、このモジュールが依存するパッケージを列挙します。ここに列挙されたパッケージからヘッダファイルが検索されてモジュールのコンパイルに利用されます。

LibraryClasses セクションでは、このモジュールが依存するライブラリを列挙します。Packages セクションと役割は若干似ていますが、LibraryClasses セクションの情報はリンクするライブラリを特定するのに使われます。

ここでは UefiLibUefiApplicationEntryPoint をリンクするよう指示しています。前者は Print 関数を利用するために必要なライブラリです。後者はスタンドアロンの UEFI アプリを構成するのに必要なライブラリです。

スタンドアロンの UEFI アプリは UEFI Shell が起動する前に起動することができます。OS のブートローダーを作る場合などはスタンドアロンとして構成します。ところで UefiApplicationEntryPoint と似たようなライブラリに ShellCEntryLib があり、例えば AppPkg/Applications/Hello/Hello.inf で使用されています。UefiApplicationEntryPoint の代わりにこのライブラリをリンクすると、作成したアプリは UEFI Shell から起動するタイプのアプリとなります。この 2 つのライブラリは同名の関数を提供しているので、どちらかしかリンクすることはできません。

Hello.c

おっと、Hello World を作るのに最後の大事なピースを紹介し忘れるところでした。ソースコードです。

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

EFI_STATUS
EFIAPI
UefiMain (
    IN EFI_HANDLE ImageHandle,
    IN EFI_SYSTEM_TABLE *SystemTable
    )
{
  Print(L"Hello EDK II.\n");
  while (1);
  return 0;
}

スタンドアロンの UEFI アプリのエントリポイントは一般的な C 言語プログラムと違い、EFI_HANDLEEFI_SYSTEM_TABLE * を引数として受け取ります。EDK II に含まれる CryptoPkg/Application/Cryptest/Cryptest.c はスタンドアロンのアプリのサンプルソースであり、エントリポイントの引数の型と戻り値の型は同じですね。

Print 関数を使うには Library/Uefilib.h のインクルードと UefiLib のリンクが必要です。品川先生のブログを読むと、Print 関数を使わないで文字列を表示する方法が紹介されています。実験してみると面白いかもしれません。

ビルド

さて、ここまできたら target.txt に設定してビルドすることができます。Ubuntu 16.04 であれば、次のような設定値にすれば良いでしょう。

設定項目 設定値
ACTIVE_PLATFORM MyPkg
TARGET RELEASE
TARGET_ARCH X64
TOOL_CHAIN_TAG GCC5

この設定が完了したら、edksetup.sh を読み込んだシェル上で build コマンドを実行します。

$ build
Build environment: Linux-4.4.0-66-generic-x86_64-with-Ubuntu-16.04-xenial
Build start time: 08:24:03, Jun.14 2017

WORKSPACE        = /home/uchan/workspace/github.com/tianocore/edk2
ECP_SOURCE       = /home/uchan/workspace/github.com/tianocore/edk2/EdkCompatibilityPkg
EDK_SOURCE       = /home/uchan/workspace/github.com/tianocore/edk2/EdkCompatibilityPkg
EFI_SOURCE       = /home/uchan/workspace/github.com/tianocore/edk2/EdkCompatibilityPkg
EDK_TOOLS_PATH   = /home/uchan/workspace/github.com/tianocore/edk2/BaseTools
CONF_PATH        = /home/uchan/workspace/github.com/tianocore/edk2/Conf


Architecture(s)  = X64
Build target     = RELEASE
Toolchain        = GCC5

Active Platform          = /home/uchan/workspace/github.com/tianocore/edk2/MyPkg/MyPkg.dsc

...中略...

- Done -
Build end time: 08:24:05, Jun.14 2017
Build total time: 00:00:03

最後が Done となればビルド完了です。失敗時は Failed となります。

テスト実行

ビルド成果物は Build/MyPkgX64/RELEASE_GCC5/X64/Hello.efi にあるはずです。これを QEMU で起動させる方法をご紹介します。

ファームウェア

QEMU で UEFI アプリを動作させるには、OVMF と呼ばれる UEFI ファームウェアを入手する必要があります。幸いなことに EDK II は OVMF を含んでいるので、我々はすぐにビルドすることができます。

OVMF をビルドするには、target.txt で ACTIVE_PLATFORMOvmfPkg/OvmfPkgX64.dsc を指定します。それで build コマンドを実行するだけです。ビルドが終わると Build/OvmfX64/RELEASE_GCC5/FV/OVMF.fd に目的の OVMF ファームウェアが生成されているはずです。

Build/OvmfX64/RELEASE_GCC5/FV ディレクトリには OVMF.fd の他に OVMF_CODE.fd と OVMF_VARS.fd があります。この 3 つのファイルについて説明します。

UEFI の設定画面を見たことがあると思いますが、いろいろな設定項目がありますよね。それらの設定値はマザーボードの不揮発性メモリ(NVRAM)に保存されますが、その保存領域と UEFI のプログラム部分を一緒くたにしたファイルが OVMF.fd です。分離したものが OVMF_CODE.fd と OVMF_VARS.fd となります。

OVMF_VARS.fd が分離されていることで、UEFI_CODE.fd は共通化しつつ、OVMF_VARS.fd を複数用意してデバッグ対象別に UEFI の設定を変えるなどということが可能です。OVMF_VARS.fd は OVMF_CODE.fd に比べてとても小さいので、ディスク容量の節約になりますね。(と言っても OVMF_CODE.fd は 2MB 程度なので、それほど大きいわけでもないのですけどね)

UEFI に詳しい人によると、分離された形式のほうが新しい形式なので、OVMF.fd という古いものを使うよりオススメということでした。こだわりがなければ従っておきましょう(笑)

ディスクイメージ

さて、これでファームウェアの準備はできました。次は自作した UEFI アプリを含む、FAT でフォーマットされたディスクイメージを作成します。といっても、実際にディスクイメージを作るのはちょっと面倒なので、ここでは QEMU の便利な機能を使います。それは、あるディレクトリ以下を仮想的にディスクとして扱えるようにする機能です。

ディスクのルートディレクトリとして振る舞うディレクトリを用意します。名前はなんでもいいですが、ここでは image とします。そして、自作の UEFI アプリを自動起動させるためには、/EFI/BOOT/BOOTX64.efi としてその UEFI アプリを配置する必要があります。具体的なコマンドを示します。

$ make -p image/EFI/BOOT
$ cp Build/MyPkgX64/RELEASE_GCC5/X64/Hello.efi image/EFI/BOOT/BOOTX64.efi

これで、QEMU に与えるディスク(として振る舞うディレクトリ)の準備ができました。次に OVMF_VARS.fd をコピーしておきます。なぜなら、UEFI の設定変更で書き換わってしまうからです。

$ cp Build/OvmfX64/RELEASE_GCC5/FV/OVMF_VARS.fd ./

ここまでできたら QEMU を起動させてみましょう。

$ qemu-system-x86_64 \
-drive if=pflash,format=raw,readonly,file=Build/OvmfX64/RELEASE_GCC5/FV/OVMF_CODE.fd \
-drive if=pflash,format=raw,file=./OVMF_VARS.fd \
-drive if=ide,file=fat:image,index=0,media=disk

drive 指定はちょっと複雑ですね。詳しくは man qemu-system-x86_64 としてマニュアルを読んでみてください。取り得る値など詳しく書いてあります。簡単に説明するとこんな感じです。

  • if: インターフェースを指定します。
    • pflash は「パラレルフラッシュ」のことです。要は基板上に実装されたファームウェアを入れるフラッシュメモリのことです。
    • ide は IDE インターフェースと言って、2010 年ごろまで使われていた古い規格です。ハードディスクや CD-ROM をパラレルケーブルで接続します。
    • 他には scsi や floppy なども指定できるみたいです。
  • format: ディスクイメージのフォーマットを指定します。
    • raw は単純なディスクイメージであることを示します。ディスクの中身がそのままファイルになったものです。
    • 他には qcow2 などのフォーマットもあります。大容量のディスクイメージ用途などによく使われます。
    • QEMU はディスクイメージからフォーマットを推測する機能があるので、raw 以外の場合は指定を省略することが多い印象です。
    • オプションはカンマ区切りで複数指定できます。OVMF_CODE.fdは書き換わることはないので読み取り専用としています。
  • file: ディスクイメージのファイル名を指定します。
    • 基本的にはファイルへのパスを指定します。
    • fat: オプションを指定すると、あるディレクトリ以下を FAT フォーマットのディスクであるかのように扱うことができます。
    • fat:rw: というように読み書き可能オプションを加えると、指定したディレクトリ以下に書き込みができるようになります。
    • fat:image であれば、image ディレクトリの内容を FAT ディスクとして使うという意味になります。
  • index: ディスクをつなぐポート番号を指定します。
    • if=ide のディスクの場合、index 0, 1, 2, 3 はマスターとスレーブ、プライマリとセカンダリの違いになるのだと思います。(この辺の用語は聞かなくなって久しいので、若い人は知らないかもしれません)
    • if=floppy の場合、index 0 は A ドライブ、1 は B ドライブとなります。
  • media: disk か cdrom を指定します。

さて、上記のコマンドを実行すると QEMU が立ち上がります。画面に “TianoCore” というロゴが表示され、しばらく待つと Print 関数に指定したメッセージが表示されるはずです。これでめでたく EDK II による UEFI アプリの作成と QEMU による実験のやり方が分かりました。おめでとうございます!

UEFI Shell

UEFI を実験する場合に UEFI Shell を使いたくなることがあります。UEFI Shell とは UEFI に組み込まれているシェルで、基本的には ShellCEntryLib をリンクした UEFI アプリを起動するためのものです。また、接続されたディスクのファイルを確認したり、システムのメモリマップを取得したり、パーティションプログラムを実行したりなど、いろいろな機能が備わっています。

QEMU を起動させて何もしないでいると、自動で BOOTX64.efi が起動してしまいます。UEFI Shell を使用するためには、QEMU 起動直後に F2 を連打し、ブートメニューを表示させ、”Boot Manager” メニューを選び、”EFI Internal Shell” を選びます。

UEFI Shell の使い方については UEFI シェル - ArchWiki が詳しいです。

その他

osdev-jp とは

osdev-jp は 2016 年 3 月 30 日の「サイボウズ・ラボユース成果報告会」に偶然集まった OS 自作好きの 3 人 Liva, uchan, hikalium によって創設された、OS 開発者のためのコミュニティです。OS 開発に有用な情報を収集し公開しています。また、OS を自作している人たちのためのコミュニティであることも目指しています。

成果物は本サイト “osdev-jp core” および GitHub 上で記事やサンプルソースの形で公開されています。

記事のライセンス

クリエイティブ・コモンズ・ライセンス
本サイトの記事および osdev-jp の Wiki の記事は クリエイティブ・コモンズ 表示 - 継承 4.0 国際 ライセンスの下に提供されています。

サンプルソースについては、それぞれが個別に定めるライセンスに従って利用してください。

貢献方法

osdev-jp に貢献したいという意欲と技術がある方は osdev-jp のメンバーとなり、OS 開発に関する情報をまとめる作業に加わっていただくことができます。また、メンバーにならずとも、誤字修正の報告をするなどの方法で貢献することもできます。

メンバーには「オープンコミュニティメンバー」と「コアメンバー」の 2 種類があります。

  • オープンコミュニティメンバー:誰でもなることができます。osdev-jp の Wiki に記事やメモを載せたり、GitHub のリポジトリを利用できます。
  • コアメンバー:審査を経てなることができます。コアメンバーになると、本サイト “osdev-jp core” に記事を投稿できます。

メンバーの種類、メンバーになる方法、メンバーができることについて、詳しくは osdev-jp コミュニティガイドライン をご覧ください。