panda's tech note

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

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

今日はPXEブートでカーネルを読み込みます。少々複雑なので,C言語で書きたいところですが,C言語から16ビットコードを生成できるコンパイラが限られているため,今回はアセンブリで書きます。PXEのAPI自体は32ビットモードも対応しているため,そちらを使う方法もありますが,今回は16ビットモードからPXE APIを呼び出します。

昨日,PXEの管理構造体にはPXENV+と!PXEがあると説明しましたが,今回は64ビットCPU時代のOSを書いているので,!PXEのみをサポートすることにします。PXENV+の管理構造体はバージョンを確認するために使います。!PXEはバージョン2.1以降でサポートされています。

PXENV+の管理構造体は以下のようになっています。今回はsignatureversionのみを使います。

struct pxenv_plus {
    // Signature = "PXENV+"
    uint8_t signature[6];
    // Version: Major in MSB, Minor in LSB
    uint16_t version;
    // Length of the data structure
    uint8_t length;
    // Checksum of the data structure
    uint8_t checksum;
    // Pointer to the real mode PXE API entry point
    uint32_t rm_entry;
    // Offset to the protected mode PXE API entry point
    uint32_t pm_offset;

    uint16_t pm_selector;
    uint16_t stack_seg;
    uint16_t stack_size;
    uint16_t bc_code_seg;
    uint16_t bc_code_size;
    uint16_t bc_data_seg;
    uint16_t bc_data_size;

    uint16_t undi_data_seg;
    uint16_t undi_data_size;
    uint16_t undi_code_seg;
    uint16_t undi_code_size;

    // Pointer to the !PXE structure
    uint32_t pxe_ptr;
};

!PXEの管理構造体は以下の通りです。このデータの正当性確認のために,signatureを確認し,length分のチェックサムを確認します。また,今回は16ビットモード(リアルモード)でのAPIを使うため,rm_entryを参照します。

struct pxe {
    // Signature = "!PXE"
    uint8_t signature[4];
    // Length of the data structure
    uint8_t length;
    // Checksum of the data structure
    uint8_t checksum;
    // Revision
    uint8_t revision;
    // Reserved
    uint8_t reserved;
    // Pointer to UNDI ROM ID structure
    uint32_t undi_rom_id;
    // Pointer to BC ROM ID structure
    uint32_t bc_rom_id;
    // Pointer to the real mode PXE API entry point
    uint32_t rm_entry;
    // Pointer to the protected mode PXE API entry point
    uint32_t pm_entry;
    //...
}

PXE APIの呼び出し

PXE APIは,void function(uint16_t opcode, uint16_t offset, uint16_t segment);のように第一引数にオペコード,第二・第三引数には入出力パラメタを保持するメモリ領域へのポインタ(オフセットおよびセグメント)を指定します。

!PXEでのPXE APIの呼び出しは cdecl呼び出し規則 に従います。cdecl呼び出し規則は,関数の引数を右から左にスタックに積む形式なので,segmentoffsetopcodeの順にスタックに積んでPXE APIへのロングコール (lcall)を呼びます。一方,PXENV+でのPXE APIの呼び出しは,レジスタにより行います。呼び出し規則は,%bx: opcode%di: offset%es: segmentです。この両方の呼び出し規則に対応するために,以下のようなルーチンを用意しました。引数は,PXENV+の呼び出し規則と同じものを使います。ラベルrm_entryには!PXE構造体のrm_entryが入っています。

pxeapi:
    pushw	%es
    pushw	%di
    pushw	%bx
    lcall	*(rm_entry)
    popw	%bx
    popw	%di
    popw	%es
    ret

!PXE構造体の読み込み

!PXE構造体へのポインタは %ss:(%sp+4) に保存されています。以下のコードは,このポインタの先の構造体を読み込み,signatureおよびlength分のチェックサムを確認しています。また,rm_entryにリアルモード用のPXE API関数へのポインタを保存します。

    /* Get the segment:offset of the !PXE structure from %ss:(%sp+4) */
    movw	%ss,%ax
    movw	%ax,%es
    movw	%sp,%bx
    addw	$4,%bx
    movl	%es:(%bx),%eax
    movw	%ax,(pxe_off)
    shrl	$16,%eax
    movw	%ax,(pxe_seg)

    /* (snip) */

    /* Parse the !PXE structure */
    movw	(pxe_seg),%ax
    movw	%ax,%es
    movw	(pxe_off),%bp
    /* Check the signature */
    movl	%es:(%bp),%eax
    cmpl	$PXE_SIGNATURE,%eax
    jne	error_pxe
    /* Check the checksum */
    movw	%bp,%si
    xorw	%cx,%cx
    movb	%es:4(%bp),%cl	/* Length */
    call	checksum
    testb	%al,%al
    jnz	error_pxe
    /* Get the rm_entry from the structure */
    movl	%es:16(%bp),%eax
    movl	%eax,(rm_entry)

DHCPキャッシュ情報の取得

起動時に使用したDHCPの情報は,bootph構造体の形式で"Cached Information"として保存されています。このCached InformationはPXE APIを通じて取得できます。PXE APIのオペコードは0x0071です。

この入出力バッファは以下のような構造です。

struct {
    uint16_t status;            // 結果
    uint16_t packet_type;       // 2 (=DHCP Ackの内容)
    uint16_t buffer_size;       // 0
    uint16_t buffer_offset;     // 0 (BC data領域を返す)
    uint16_t buffer_segment;    // 0 (BC data領域を返す)
    uint16_t buffer_limit;      // 0
};

この結果,buffer_segmentおよびbuffer_offsetで示される領域からbootph構造体が取得できます。このうち,SIPはDHCP Ackに含まれるnext server(すなわちTFTPサーバ),GIPにDHCP Relay AgentのIPアドレスが含まれます。この2つの情報は,TFTP Openに必要な情報です。

具体的なコードは src/boot/pxeboot.S を参照してください。

TFTPサーバからのファイルの読み込み

TFTPサーバからカーネルを読み込みます。TFTPサーバからファイルを読み込むには,TFTPサーバとの接続を確立 (Open) し,ファイルを読み込み (Read),接続を閉じます(Close)。

Open

Open命令のオペコードは0x0020です。また,Open命令の入出力バッファは以下の通りです。

struct {
    uint16_t status;            // 結果
    uint32_t SIP;               // TFTPサーバのIPアドレス
    uint32_t GIP;               // Relay AgentのIPアドレス
    uint8_t filename[128];      // ファイル名
    uint16_t port;              // ポート番号
    uint16_t packet_size;       // パケットサイズ
};

SIPおよびGIPは上述したDHCPキャッシュ情報から取得します。ポート番号は69,パケットサイズは512を設定します。

Read

Read命令のオペコードは0x0022です。TFTPサーバへの接続をOpen命令により確立した後は,Read命令によりそのファイルからデータを読み取ります。Read命令の入出力バッファは以下の通りです。

struct {
    uint16_t status;            // 結果(出力)
    uint16_t packet_number;     // パケット番号(出力)
    uint16_t buffer_size;       // 読み込んだバッファサイズ(出力)
    uint16_t buffer_offset;     // 読み込み先のバッファ(オフセット)
    uint16_t buffer_segment;    // 読み込み先のバッファ(セグメント)
};

確立した接続から1パケットを読み込み,パケットの内容をbuffer_offsetおよびbuffer_segmentで指定したメモリ領域に書き込みます。書き込んだパケットのサイズをbuffer_size,パケットのシーケンス番号がpacket_numberに入ります。このシーケンス番号によりパケットロスを検出できます。また,buffer_sizeがパケットサイズよりも小さい場合,ファイルの最後のパケットであることを検出できます。

Close

Close命令のオペコードは0x0021です。以下のように入出力バッファはstatusのみを持ちます。

struct {
    uint16_t status;            // 結果(出力)
};

実装

Open/Close/Read命令を組み合わせて,TFTPサーバからファイルをメモリに読み込むルーチンを以下のように実装しました。

/* Load the content of a file specified by a null-terminated string starting
 * from %ds:(%si) to (%edi). */
load_tftp_file:
    pushw	%bx
    pushw	%cx
    pushl	%edx
    pushw	%es
    pushw	%fs
    pushw	%bp
    pushw	%si
    pushl	%edi

    /* Allocate the stack for the command buffer */
    subw	$PXEAPI_GENERIC_BUFFER_SIZE,%sp
    movw	%sp,%bp
    movw	%ss,%bx
    movw	%bx,%es
    /* Reset the data structure first */
    movw	%bp,%di
    movw	$PXEAPI_GENERIC_BUFFER_SIZE,%cx
    xorb	%al,%al
    rep	stosb
    /* Set the cached info to build a bootph structure */
    movw	(buffer_size),%cx
    movw	(buffer_off),%bx
    movw	(buffer_seg),%dx

    /* Prepare the input buffer to open a file */
    movw	%dx,%fs
    movl	%fs:CACHED_INFO_SIP(%bx),%eax
    movl	%eax,%es:PXEAPI_TFTP_OPEN_SIP(%bp)	/* Server */
    movl	%fs:CACHED_INFO_GIP(%bx),%eax
    movl	%eax,%es:PXEAPI_TFTP_OPEN_GIP(%bp)	/* Relay agen */
    movw	$TFTP_PORT,%ax
    xchgb	%al,%ah
    movw	%ax,%es:PXEAPI_TFTP_OPEN_PORT(%bp)	/* TFTP port (69) */
    movw	$TFTP_PACKETSIZE,%ax
    movw	%ax,%es:PXEAPI_TFTP_OPEN_PACKETSIZE(%bp)
    /* Copy the null-terminated string */
    leaw	PXEAPI_TFTP_OPEN_FILENAME(%bp),%di
1:
    movb	(%si),%al
    testb	%al,%al
    jz	2f
    movb	%al,%es:(%di)
    incw	%si
    incw	%di
    jmp	1b
2:

    /* Open the TFTP session */
    movw	%bp,%di
    movw	$PXEAPI_OPCODE_TFTP_OPEN,%bx
    call	pxeapi
    movw	%es:PXEAPI_TFTP_OPEN_STATUS(%bp),%ax
    testw	%ax,%ax
    jnz	load_tftp_file.error

    /* Reset the data structure first */
    movw	%bp,%di
    movw	$PXEAPI_GENERIC_BUFFER_SIZE,%cx
    xorb	%al,%al
    rep	stosb
    /* Read */
    movw	%bp,%di
    movw	$PXEAPI_OPCODE_TFTP_READ,%bx
    movl	%es:PXEAPI_GENERIC_BUFFER_SIZE(%bp),%edx	/* arg %edi */
    xorw	%cx,%cx						/* packet # */
1:
    movw	%dx,%ax
    andw	$0xf,%ax
    movw	%ax,%es:PXEAPI_TFTP_READ_BUFFER_OFF(%bp)
    movl	%edx,%eax
    shrl	$4,%eax
    movw	%ax,%es:PXEAPI_TFTP_READ_BUFFER_SEG(%bp)
    call	pxeapi
    movw	%es:PXEAPI_TFTP_READ_STATUS(%bp),%ax
    testw	%ax,%ax
    jnz	load_tftp_file.error
    /* Check the packet number */
    incw	%cx
    movw	$0xffff,%ax			/* pseudo error code */
    cmpw	%es:PXEAPI_TFTP_READ_PACKET_NUM(%bp),%cx
    jne	load_tftp_file.error
    /* Check the size */
    xorl	%eax,%eax
    movw	%es:PXEAPI_TFTP_READ_BUFFER_SIZE(%bp),%ax
    addl	%eax,%edx
    cmpl	$0x100000,%edx
    jge	load_tftp_file.error
    /* Check if it is the last packet */
    cmpw	$TFTP_PACKETSIZE,%ax
    jge	1b

    /* Reset the data structure first */
    movw	%bp,%di
    movw	$PXEAPI_GENERIC_BUFFER_SIZE,%cx
    xorb	%al,%al
    rep	stosb
    /* Close */
    movw	%bp,%di
    movw	$PXEAPI_OPCODE_TFTP_CLOSE,%bx
    call	pxeapi
    movw	%es:PXEAPI_TFTP_OPEN_STATUS(%bp),%ax
    testw	%ax,%ax
    jnz	load_tftp_file.error

load_tftp_file.success:
    xorw	%ax,%ax
load_tftp_file.error:
    addw	$PXEAPI_GENERIC_BUFFER_SIZE,%sp
    popl	%edi
    popw	%si
    popw	%bp
    popw	%fs
    popw	%es
    popl	%edx
    popw	%cx
    popw	%bx
    ret

この実装では,kernelを読み込み,またこれを実行するためにブート情報としてメモリマップを読み込んでいます。全体の実装は src/boot/pxeboot.S を参照してください。

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

今日はPXEブートでカーネルを起動しました。明日は,システムコールを実装しようと思います