こんにちは.だいみょーじんです.
この記事は,自作OS Advent Calendar 2021 25日目の記事です.
対象読者:OS自作をやっていたり,興味がある人.
背景
OS自作のみならず,ソフトウェア開発全般において最も難しく,かつ面倒なのは「Hello, World!」するまでの開発環境構築だと思います.
PCを買い替えたりすると,また同様の環境を構築する手間がかかるし,PCを買い替える頃には以前のPCで環境構築したときの手順なんて頭から吹っ飛んでしまっているわけで,またいろいろ調べる羽目になるのです.
その点Dockerはとても便利です.
一度Dockerfileに環境構築の手順をプログラムとして記述してしまえば,Dockerさえあればいつでもどこでも同一の開発環境を再現できるのです.
この記事では,私がDockerを使ってOS開発環境を構築した以下の手順を紹介します.
- とりあえずDockerfileを書こう
- Makefileで自動化しよう
- GitHub用の秘密鍵を配置して
git pushできるようにしよう - Docker上でQEMUを動かそう
- GDBでデバッグしよう
とりあえずDockerfileを書こう
以下のように開発環境の構築手順を記述します.
(GitHub上のソースファイル)
FROM alpine
RUN apk update
# gcc, git, ld, make, etc.
RUN apk add --no-cache alpine-sdk
# git setting
RUN git config --global pull.rebase false
# debugger
RUN apk add --no-cache gdb
# emulator
RUN apk add --no-cache qemu-system-i386
# ssh
RUN apk add --no-cache openssh
RUN mkdir ~/.ssh
# editor
RUN apk add --no-cache vim
# set time zone UTC+9 (Japan)
RUN apk add --no-cache tzdata
RUN cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime
RUN apk del tzdata
# clone the repository
WORKDIR ~
RUN git clone https://github.com/TaiseiIto/hariboslinux.git
# make the OS image file
WORKDIR hariboslinux
RUN make
# gdb setting
RUN echo add-auto-load-safe-path `pwd`/gdb/.gdbinit > ~/.gdbinit
# gdb real mode disassemble
RUN wget https://raw.githubusercontent.com/qemu/qemu/master/gdb-xml/i386-32bit.xml -P gdb
# VNC port
EXPOSE 5900
上から順番に説明します.
FROM alpineは今回構築する開発環境の元となるDockerイメージとしてAlpine Linuxを指定しています.
(Alpine Linuxが軽量でよいという話を聞いたのでこれを選択したわけですが,どうやらFROM scratchとすると本当にまっさらな状態から環境を構築できることを知ったので,いずれFROM scratchに移行するかもです.)
それに続いてRUNコマンドが続いています.
これらはFROMコマンドで生成されたAlpine Linux上でコマンドを実行し,環境を構築していくコマンドです.
apkはAlpine Linuxのパッケージ管理システムです.
Debian系のaptやRedHat系のyumに相当するものですね.
RUN apk updateでこれからインストールするパッケージの参照先を更新します.
RUN apk add --no-cache alpine-sdkでコンパイラのgcc,バージョン管理システムのgit,リンカのld,ビルド自動化のmakeなどをまとめてインストールします.
不要なキャッシュを残さないようにして軽量化するため,--no-cacheオプションを指定しています.
RUN git config --global pull.rebase falseはgit pullの動作に関する設定で,単純にローカルのHEADの先にリモートのHEADがある場合はローカルのHEADをリモートのHEADまで移動させ,両HEADがそれ以前のコミットから分岐している場合はmergeコミットを生成するようにしています.
以下,デバッガのGDB,エミュレータのQEMU,git pushの通信で使用するSSH,エディタのvimをインストールします.
その後,RUN apk add --no-cache tzdataで開発環境の時計を日本標準時に設定するため一時的にタイムゾーンデータベースtzdataをインストールし,RUN cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtimeで日本標準時に設定したらRUN apk del tzdataで用済みになったtzdataを削除します.
続いてWORKDIR ~でホームディレクトリに移動し,RUN git clone https://github.com/TaiseiIto/hariboslinux.gitで自作OSのリポジトリを配置します.
WORKDIR hariboslinuxで配置したリポジトリに移動し,RUN makeで自作OSのビルドまで行います.
残りの3行については後の項目で説明するので今は飛ばします.
Makefileで自動化しよう
面倒なDockerコマンドたちをまとめています.
- Dockerfileを実行してDockerイメージを生成する
docker-build - Dockerコンテナのシェルに入る
docker-login - Dockerコンテナを削除する
docker-remove-container - Dockerイメージを削除する
docker-remove-image - DockerイメージからDockerコンテナを生成して実行する
docker-run - Dockerコンテナを起動する
docker-start - Dockerコンテナを終了する
docker-stop - Dockerコンテナ上で生成された自作OSのイメージファイルをホストにダウンロードする
download-image
# docker
DOCKER = docker
DOCKER_IMAGE_NAME = hariboslinux
DOCKER_IMAGE_TAG = latest
DOCKER_CONTAINER_NAME = hariboslinux
DOCKER_CONTAINER_SHELL = /bin/sh
DOCKER_VNC_PORT_OPTION = -p $(VNC_PORT):$(VNC_PORT)
docker-build:
$(DOCKER) build --no-cache -t $(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG) .
docker-login:
$(DOCKER) attach $(DOCKER_CONTAINER_NAME)
docker-remove-container:
$(DOCKER) rm $(DOCKER_CONTAINER_NAME)
docker-remove-image:
$(DOCKER) rmi $(DOCKER_IMAGE_NAME)
docker-run:
$(DOCKER) run --name $(DOCKER_CONTAINER_NAME) $(DOCKER_VNC_PORT_OPTION) -i -t $(DOCKER_IMAGE_NAME)
docker-start:
$(DOCKER) start $(DOCKER_CONTAINER_NAME)
docker-stop:
$(DOCKER) stop $(DOCKER_CONTAINER_NAME)
download-image:
$(DOCKER) cp $(DOCKER_CONTAINER_NAME):/~/hariboslinux/$(IMAGE_FILE) .
GitHub用の秘密鍵を配置してgit pushできるようにしよう
GitHubへのpushをSSHで行う場合,あなたがGitHubに登録した公開鍵に対応する秘密鍵をコンテナに配置する必要があります.
注意事項!!!
当たり前ですが,秘密鍵の扱いには十分に注意してください.
間違っても秘密鍵をパブリックリポジトリに公開しないこと!
間違っても秘密鍵を含むDockerイメージをDocker Hubに公開しないこと!
とにかく秘密鍵は絶対に公開しないこと!
この記事を読んで問題が発生した場合私は一切の責任を負いません.
私は,予めコンテナ上の決められた場所(/~/hariboslinux/ssh/github)に秘密鍵を配置し,以下のシェルスクリプトを実行することでgit pushできるようにしてあります.
# !/bin/sh
# This program gives the push access right.
# Only the developer can execute it.
# Put the private key "/~/hariboslinux/ssh/github" before the execution.
currentdir=$(pwd)
cd $(dirname $0)
echo -n "Your GitHub email:"
read email
echo -n "Your GitHub name:"
read name
git config --global user.email $email
git config --global user.name $name
git remote set-url origin git@github.com:TaiseiIto/hariboslinux.git
cat ../ssh/config >> ~/.ssh/config
chmod 600 ../ssh/github
cd $currentdir
まずGitHubアカウントのメールアドレスとユーザ名を入力させ,git configコマンドでアカウント情報を設定します.
次に,git remote set-url origin git@github.com:TaiseiIto/hariboslinux.gitでGitHubとの通信プロトコルをHTTPSからSSHに変更します.
次に,cat ../ssh/config >> ~/.ssh/configでGitHubとの通信で使用する秘密鍵の場所をSSHの設定ファイルに追記します.
(GitHub上のソースファイル)
Host github github.com
HostName github.com
IdentityFile /~/hariboslinux/ssh/github
User git
最後に,chmod 600 ../ssh/githubで秘密鍵に対する権限を設定すれば,Dockerコンテナ上のローカルリポジトリからSSHでgit pushできるようになります.
Docker上でQEMUを動かそう
DockerfileでQEMUをインストールしたので,QEMU自体は普通に動きます.
問題は,X Window SystemなどのGUI環境が入っていないDocker環境でどうやってQEMUのGUIを表示するかです.
方法はいろいろあると思いますが,私はリモートデスクトップソフトのVNC(Virtual Network Computing)を使用しました.
以下に構成図を示します.
まず,操作者はVNCクライアントを通して自作OSを操作することになります.
操作者によるマウス操作,キーボード操作のDockerコンテナへの送信およびDockerコンテナからの自作OSの出力画面の受信は,ホストOS上の5900番ポートを通してVNCクライアントとDockerコンテナがRFBプロトコルで通信することで実現しています.
DockerfileにEXPOSE 5900と記述して5900番ポートをホストOSに公開し,docker runコマンドでDockerコンテナを生成する時にオプション-pでDockerコンテナの5900番ポートとホストOSの5900番ポートを結びつけます.
こうしてDockerコンテナが自身の5900番ポートをホストOSの5900番ポートにリダイレクトすることにより,あたかもVNCクライアントとDockerコンテナ上のQEMUが直接通信しているように動作します.
# VNC port
EXPOSE 5900
VNC_PORT = 5900
# docker
DOCKER = docker
DOCKER_IMAGE_NAME = hariboslinux
DOCKER_IMAGE_TAG = latest
DOCKER_CONTAINER_NAME = hariboslinux
DOCKER_CONTAINER_SHELL = /bin/sh
DOCKER_VNC_PORT_OPTION = -p $(VNC_PORT):$(VNC_PORT)
docker-run:
$(DOCKER) run --name $(DOCKER_CONTAINER_NAME) $(DOCKER_VNC_PORT_OPTION) -i -t $(DOCKER_IMAGE_NAME)
QEMUは,RFB通信で受信したマウス操作,キーボード操作を自作OSに対するマウス割り込み,キーボード割り込みに変換します.
自作OSはVRAMに書き込むことで画面を出力し,QEMUがその画面をRFB通信でDockerコンテナを通してVNCクライアントに送信することで,画面表示を実現します.
QEMU起動時にオプション-vnc :0で任意のIPアドレスとのRFB通信を受け入れるようにします.
また,VNCとは無関係ですがQEMU起動時にオプション-serial stdioで自作OSのRS-232通信とDockerコンテナの標準入出力を結びつけ,さらにそれをパイプラインでteeコマンドに流すことで通信内容をserialout.txtというファイルに保存しています.
あとはQEMUを終了するmake stopコマンドも作りました.
# emulator
# emulate intel 386 processor
EMULATOR = qemu-system-i386
EMULATOR_BOOT_OPTION = -boot order=a
# debug by gdb through tcp:2159
EMULATOR_DEBUG_OPTION = -S -gdb tcp::$(DEBUG_PORT)
# a raw image of the floppy disk
EMULATOR_DRIVE_OPTION = -drive file=$(IMAGE_FILE),format=raw,if=floppy
# memory size assigned for the emulator
EMULATOR_MEMORY_OPTION = -m 4G
# serial console
EMULATOR_SERIAL_OPTION = -serial stdio
EMULATOR_SERIAL_OUT = serialout.txt
# VESA VBE 2.0
EMULATOR_VIDEO_OPTION = -vga std
# virtual network computing for all ip address
EMULATOR_VNC_OPTION = -vnc :0
# run the OS on QEMU
run: $(IMAGE_FILE) stop
$(EMULATOR) $(EMULATOR_BOOT_OPTION) $(EMULATOR_DRIVE_OPTION) $(EMULATOR_MEMORY_OPTION) $(EMULATOR_SERIAL_OPTION) $(EMULATOR_VIDEO_OPTION) $(EMULATOR_VNC_OPTION) | tee $(EMULATOR_SERIAL_OUT) &
# stop QEMU
stop:
for i in $$(ps ax | grep $(EMULATOR) | grep -v grep | awk '{print $$1}'); do kill $$i; done
GDBでデバッグしよう
Dockerとはあまり関係ないかもですが,一応開発環境の重要な部分として自作OSをGDBでデバッグする方法も紹介しておきます.
GDBとQEMUが2159番ポート(たぶん他のポートでもいいんだろうけど2159番ポートがそれっぽかったのでこのポートにしている)で通信することでQEMU上で動作する自作OSをデバッグできます.
QEMU側の設定
QEMU起動時にオプション-gdbを渡すことで,処理を進めずに指定したポートでGDBを待機する状態で起動してくれます.
以下のMakefileでは,コマンドmake debugで,QEMUをGDB待機状態で起動したのち,GDBを起動しています.
DEBUG_PORT = 2159
# emulator
# emulate intel 386 processor
EMULATOR = qemu-system-i386
EMULATOR_BOOT_OPTION = -boot order=a
# debug by gdb through tcp:2159
EMULATOR_DEBUG_OPTION = -S -gdb tcp::$(DEBUG_PORT)
# a raw image of the floppy disk
EMULATOR_DRIVE_OPTION = -drive file=$(IMAGE_FILE),format=raw,if=floppy
# memory size assigned for the emulator
EMULATOR_MEMORY_OPTION = -m 4G
# serial console
EMULATOR_SERIAL_OPTION = -serial stdio
EMULATOR_SERIAL_OUT = serialout.txt
# VESA VBE 2.0
EMULATOR_VIDEO_OPTION = -vga std
# virtual network computing for all ip address
EMULATOR_VNC_OPTION = -vnc :0
# debug the operating system onQEMU by gdb
debug: $(IMAGE_FILE) stop
($(EMULATOR) $(EMULATOR_BOOT_OPTION) $(EMULATOR_DRIVE_OPTION) $(EMULATOR_MEMORY_OPTION) $(EMULATOR_SERIAL_OPTION) $(EMULATOR_VIDEO_OPTION) $(EMULATOR_VNC_OPTION) $(EMULATOR_DEBUG_OPTION) &) && \
make -C gdb
DEBUGGER = gdb
all:
$(DEBUGGER)
GDB側の設定
DockerfileでGDBの設定ファイルをホームディレクトリ直下に配置しています.
# gdb setting
RUN echo add-auto-load-safe-path `pwd`/gdb/.gdbinit > ~/.gdbinit
# tcp port
target remote localhost:2159
# real mode
# set tdesc filename target.xml
# go to entry point of kernel.bin
break *0x7c00
continue
delete 1
.gdbinitにはGDB起動直後に自動実行したいことを記述します.
まず,target remote localhost:2159で,GDBとQEMUを2159番ポートで接続します.
次のset tdesc filename target.xmlは自作OSがリアルモードで動作している部分をデバッグするときに用いる設定ですが,プロテクトモード移行後をデバッグする際には不要なのでコメントアウトしています.
リアルモードデバッグに関しては以前書いた記事を参照してください.
その後,break *0x7c00で0x7c00にブレークポイントを配置し,continueでQEMUを動作させ,0x7c00に到達したところで一時停止します.
最後にdelete 1で0x7c00のブレークポイントを削除します.
.gdbinitにこれを記述することでGDB起動時にマスターブートレコード実行直前まで自動的に進めてくれるので,あとは頑張ってデバッグするだけです.
まとめ
- Dockerを使うことで,環境構築が簡単になった.
- Makefileで面倒なDockerコマンドを簡略化した.
- Dockerコンテナに自分のGitHub用の秘密鍵を配置して
git pushできるようにした. - VNCを使ってDockerコンテナ上のQEMUで動く自作OSを操作できるようにした.
- GDBでデバッグできるようにした.
