20日目は1文字を表示するAPIを作りました。これでOSに配備したアプリケーションからOSの機能を呼び出すことができます。
API
APIを作ります。プログラムからOSの機能を呼ぶ部分です。コマンドラインに書いたコマンドからOSの機能を呼び出します。
C言語で作ったput_charをアプリケーション(OSにコピーした実行ファイル)から呼びます
/** * コンソールに指定した文字を1文字書きます * @param cons 描画先のコンソール * @param chr 描画する文字コード * @param move 制御用フラグ 0:カーソル位置を進めない */ void cons_putchar(struct CONSOLE *cons, int chr, char move)
MOV AL,'h' CALL 2*8:0xbe8 ;
- ALに文字を入れてセグメント2(OS領域)の0xbe8を呼ぶとconsoleにALに入れた文字がでる
そんなシンプルなAPIになります。
引数を扱う専用の処理を呼び出す
アセンブリからC言語の関数を引数付きで呼べないらしいのでcons_putcharを直接呼ばず、引数を扱う専用の処理(_asm_cons_putchar)を一旦呼びます。
ALに文字コードを入れた状態で、OSのセグメント0xbe8
にロードされる_asm_cons_putchar
を呼びます。
C言語で定義した関数に引数を渡すにはスタックに引数となる値を積む必要があります。それがC言語ではできないそうです。*1
MOV AL,'h' CALL 2*8:0xbe8 ;
引数専用の処理でスタックに引数をつむ
_asm_cons_putchar: PUSH 1 ; move:moveフラグ AND EAX,0xff ; AHやEAXの上位を0にして、EAXに文字コードが入った状態にする。 PUSH EAX ; chr:表示対象の文字コード PUSH DWORD [0x0fec] ; cons:コンソールの値 CALL _cons_putchar ADD ESP,12 ; スタックに積んだデータを捨てる RETF
スタックに積んでいる順がpopした際にcons_putcharの引数として適当な順番になっていることが確認できます
※push命令を順序を変えずに抜粋。 PUSH 1 ; move:moveフラグ PUSH EAX ; chr:表示対象の文字コード PUSH DWORD [0x0fec] ; cons:コンソールの値
void cons_putchar(struct CONSOLE *cons, int chr, char move)
_asm_cons_putchar
の以下の部分は何をやっているのかよくわかりません。抜いても動作は変わらず、正しく文字表示ができました。
AND EAX,0xff ; AHやEAXの上位を0にして、EAXに文字コードが入った状態にする。
consの渡し方だけ意味不明なので別途解説します
void console_task(struct SHEET *sheet, unsigned int memtotal) { //抜粋 struct CONSOLE cons; cons.sht = sheet; cons.cur_x = 8; cons.cur_y = 28; cons.cur_c = -1; *((int *) 0x0fec) = (int) &cons;
書き込み対象のコンソールの情報consを0x0fec
に書き込んでいます。ここをアセンブリで参照してスタックに積んでいます。
PUSH DWORD [0x0fec] ; cons:コンソールの値
今のままだと、consの番地がOSとアプリケーションに決めうちになっています。
23bitレジスタ,16bitレジスタ,8bitレジスタ復習(EAX,AL等)
一応補足ですが、EAX,AX,AL,AHは全てEAXの一部分です。
EAX:32bitレジスタ AX:16bitレジスタ AL: 8bitレジスタ AH: 8bitレジスタ
ALはEAXのどこの部分か
格レジスタがEAXのどの部分に当たるかを図にしました。
EAX:■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ AX :□□□□□□□□□□□□□□□□■■■■■■■■■■■■■■■■ AL :□□□□□□□□□□□□□□□□□□□□□□□□■■■■■■■■ AH :□□□□□□□□□□□□□□□□■■■■■■■■□□□□□□□□
IDT経由で割り込みとしてシステムコールを呼び出す
MOV AL,'h' CALL 2*8:0xbe8 ;
この文字出力APIは番地が決めうちになってしまっています。OSの開発が進むにつれこの番地が変わった場合、 新しい番地に書き直す必要があります。このAPIを読んでいる箇所を全て書き直す必要があります。
これは問題なので関数をIDTに登録し、割り込み命令扱いとし、INT命令で呼び出すように変更します。
[BITS 32] MOV AL,'h' INT 0x40 MOV AL,'e' INT 0x40 MOV AL,'l' INT 0x40 MOV AL,'l' INT 0x40 MOV AL,'o' INT 0x40 RETF
APIを変更し、INT 0x40で呼び出せるようにしました。上記でhelloと表示されます。
また本書ではIDTテーブルに限りがあることから、INT 0x40
の処理にswitch文的な機能を持たせてedxで処理を切り替えるように作ってあります。
プログラムのメモリのハードコードを割り込みテーブルを使って解決するというのは、ちょっと強引な気がしました。本来の使い方とはかけ離れていると思うのですが、一般的なやり方なのでしょうか。
追記:Linuxでは0x80で呼び出すそうです。
http://kzlog.picoaccel.com/post-104/
アプリケーションからOSの機能を呼び出すのは、ソフトウェア割り込みが一般的なんですかね。そういえばLinuxもint 0x80だし。その他、コールゲートというのがあるのは知っているけれど、他にはあるのかな。
感想
この本はなんかライブ感?がある感じで(毎回挨拶からスタートしてる)これもしかして草稿は30日で書き上げられたのかな?とふと思いました。