令和アドベントカレンダー: 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+の管理構造体は以下のようになっています。今回はsignature
とversion
のみを使います。
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呼び出し規則は,関数の引数を右から左にスタックに積む形式なので,segment
,offset
,opcode
の順にスタックに積んで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ブートでカーネルを起動しました。明日は,システムコールを実装しようと思います