panda's tech note

令和アドベントカレンダー: advos

2018年12月のアドベントカレンダーでadvosというOSを作りましたが,平成の最後にadvosでやり残した実装をやろうと思います。(某氏より,「令和に向けたアドベントカレンダーだから平成アドベントカレンダーではなく,令和アドベントカレンダーだと指摘を受けたので,訂正します。」)

Extra Day 1: PXEブート(その1)

今日は,ネットワークからシステムを起動するPXE (Pre-Boot Execution Environment)ブートを取り扱います。ネットワークブートやディスクレスブートとも呼ばれます。

PXEブートの仕組み

PXEブートは,まず通常のDHCPによるアドレス設定を行います。具体的には,BIOSがDHCP Discoverメッセージを送信し,DHCPサーバはDHCP Offerによりアドレスを割り当てます。次に,BIOSはこのアドレスを送信元としてDHCP RequestをDHCPサーバに送付することで,割り当てられたアドレスを使用することを通知します。DHCPサーバは,その確認応答としてDHCP Ackをクライアントに返します。

DHCPによりアドレスの設定が終わると,BIOSはDHCPサーバにOption 66 (Boot Server)およびOption 67 (Bootstrap File)のDHCP Requestを送信します。DHCPサーバはこれらのオプションにTFTPサーバおよびファイル名を含むDHCP Ackを返します。PXEブートでは,このTFTPサーバから指定されたファイル名のファイルをTFTPで取得し,メインメモリに読み込み,実行します。このファイルは仕様上は0x0000:0x7c00に読み込まれることになっていますが,今回は特にこのアドレスを想定せずに実装しています。

また,メインメモリには,実行されるファイル以外にPXEブートの管理構造体(TFTPサーバ情報等)が保存されており,起動後のレジスタ %es:%bx (PXENV+)または%ss:(%sp+4) (!PXE) で場所を取得できます。PXEバージョン1.5以降では!PXEが使われますが,PXENV+も後方互換性のため使われています。さらに,%ss:%spから下位アドレス方向に最低1.5 KiBはスタックとして利用できることが保証されています。

PXEブートの設計

PXEブートでは,MBRの512バイト制限とは大きく異なり,最大32 KiBまでのファイルを読み込むことができます。そのため,MBRのように2段階のブートローダを実装せずに,このファイルをブートローダとして使い,これからカーネルやモジュールファイルを読み込,実行します。

PXEブート環境の構築と実行

いろいろとTFTPサーバやDHCPサーバを別のコンテナで実行しようと試みましたが,コンテナ間をブリッジで繋ぐことができなかったので,同一コンテナ内にDHCPサーバとTFTPサーバを入れ,QEMUを実行する環境を構築しました。

PXEブート環境のDockerfileは以下のように書きました。DHCPサーバとしてisc-dhcp-server,TFTPサーバとしてtftpd-hpaを使うため,これらのパッケージをインストールしています。また,ブリッジを作成し,そのブリッジに対してアドレス設定をCLIコマンドで設定するため,iproute2bridge-utilsをインストールします。また,TFTPサーバで公開するディレクトリとして/tftpbootを作成し,パーミッションを読み書き可能なように0777に設定します。さらに,コンパイルしたpxebootファイル(後述)をpxeadvosという名前でこのディレクトリに設置します。

FROM ubuntu:18.04

MAINTAINER Hirochika Asai <panda@jar.jp>

## Install build-essentials and qemu
RUN apt-get update
RUN apt-get install -y --no-install-recommends build-essential qemu-system isc-dhcp-server iproute2 bridge-utils tftpd-hpa

## Copy source to the workdir
COPY src /usr/src
WORKDIR /usr/src
RUN make
RUN mkdir -p /tftpboot
RUN chmod 0777 /tftpboot
RUN cp boot/pxeboot /tftpboot/pxeadvos

## For qemu+bridge
RUN mkdir /etc/qemu
RUN echo "allow all" > /etc/qemu/bridge.conf

## Run the OS with qemu
CMD ["/pxe-cmd.sh"]

以下の2行は,QEMUの仮想NICをアタッチするブリッジのACL設定ファイルですが,今回はすべてを許可するallow allで設定します。

RUN mkdir /etc/qemu
RUN echo "allow all" > /etc/qemu/bridge.conf

以下のコマンドで,/pxe-cmd.shを実行します。このファイルはdocker-compose.ymlでボリュームとしてマウントするように指定しています。ブリッジの作成などはDockerfile内のRUN命令では作成できなかったため,CMD命令でこのシェルスクリプトを指定し,スクリプト内で作成するようにしました。

CMD ["/pxe-cmd.sh"]

pxe-cmd.shの中身は以下の通りです。ブリッジインターフェイス br100 を作成,設定し,DHCPサーバおよびTFTPサーバを起動します。そして,最後にQEMUを実行します。

#!/bin/sh

## Create a new bridge interface and add an IPv4 address
brctl addbr br100
ip a add 192.168.253.1/24 dev br100
ip link set br100 up

## Run dhcp server
touch /var/lib/dhcp/dhcpd.leases
dhcpd -4 -pf /run/dhcpd.pid -cf /etc/dhcp/dhcpd.conf br100

## Run tftp server
in.tftpd --lsten --secure /tftpboot

qemu-system-x86_64 -m 1024 -smp cores=4,threads=1,sockets=2 \
        -numa node,nodeid=0,cpus=0-3 \
        -numa node,nodeid=1,cpus=4-7 \
        -option-rom /usr/share/qemu/pxe-e1000.rom \
        -net bridge,br=br100 -net nic,macaddr=52:54:00:12:34:56 \
        -display curses

PXEブートファイル

上述したpxebootファイルはsrc/boot/pxeboot.Sファイルをコンパイルしたものです。ビルドルールはsrc/boot/Makefilepxebootを参照してください。

まず,テストのためにPXEブートの管理データ構造などは無視し,MBRのときと同様にメッセージを表示するだけのコードを書きます。以下のファイルは,Welcome to advosと表示するための src/boot/pxeboot.S です。Day 2のsrc/boot/mbr.Sとほとんど同じです。エントリポイントはpxebootにしました(src/boot/pxe.ldで指定)。

#define VGA_TEXT_COLOR_80x25    0x03

        .globl  pxeboot

        .text
        .code16

/* Entry point for the PXE boot
 *  %cs:%ip is 0x0000:0x7c000
 *  %es:%bx points to the PXENV+ structure
 *  %ss:%sp points to a valid stack (at least 1.5 KiB available)
 *  %ss:(%sp+4) points to the !PXE structure, if available
 */
pxeboot:
        cld             /* Clear direction flag (inc di/si for str ops) */
        cli

        /* Setup the stack (below $start = 0x7c00) */
        xorw    %ax,%ax
        movw    %ax,%ss
        movw    $0x7c00,%sp
        /* Reset data segment registers */
        movw    %ax,%ds
        movw    %ax,%es

        sti

        /* Set the video mode to 16-bit color text mode */
        movb    $VGA_TEXT_COLOR_80x25,%al
        movb    $0x00,%ah
        int     $0x10

        movw    $msg_welcome,%ax
        movw    %ax,%si
        call    putstr

halt:
        hlt
        jmp     halt

/* Display a null-terminated string
 * Parameters: %ds:%(si)
 * Return value: %ax
 * Preserved Registers:
 */
putstr:
putstr.load:
        lodsb           /* Load %ds:(%si) to %al, then incl %si */
        testb   %al,%al
        jnz     putstr.putc
        ret
putstr.putc:
        call    putc
        jmp     putstr
putc:
        pushw   %bx
        movw    $0x7,%bx
        movb    $0xe,%ah
        int     $0x10
        popw    %bx
        ret

        /* Data segment */
        .data

msg_welcome:
        .asciz  "Welcome to advos"
$ docker-compose build
$ docker-compose run qemu-pxe

を実行すると,

Welcome to advos      

と表示されると思います。

今日のまとめと明日の予定

今日はPXEブートを実装するための環境構築と簡単なコードを実装しました。明日はPXEブートからカーネル読み込み,実行しようと思います。