30日OS自作本20日目
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日で書き上げられたのかな?とふと思いました。
30日OS自作本19日目
19章の内容です。typeコマンドでOSにコピーしたファイルの読み込みと、ファイルとしてコピーした機械語の実行を行いました。
FAT12の復習
1セクタ=512バイト
sector | offset | size(Byte) | content |
---|---|---|---|
1 | 0x0000 | 0x0200 | boot sector |
9 | 0x0200 | 0x1200 | fat area |
9 | 0x1400 | 0x1200 | fat area(sub) |
14 | 0x2600 | 0x1c00 | root directory |
2849 | 0x4200 | 0x164200 | file area |
合計2882セクタ
FAT上でどのように物理ファイルを扱うか
MakeFile内で以下のようにOSに直接ファイルを書き込んでいます。
copy from:haribote.sys to:@: \ copy from:ipl10.nas to:@: \ copy from:make.bat to:@: \
NNNNNNNNNNNNNNNNEEEEEETTRRRRRRRRRRRRRRRRRRRRTTTTDDDDCCCCSSSSSSSS 48415249424F54455359530000000000000000000000549CCA4C0200486E0000 49504C31302020204E41530000000000000000000000927B924C3A00950B0000 4D414B45202020204241540000000000000000000000927B924C40002E000000
本書の流れに剃って バイナリダンプしたファイルをバイナリエディタ上で直接探します。
$ hexdump -C haribote.sys 00000000 b8 00 90 8e c0 bf 00 00 b8 00 4f cd 10 3d 4f 00 |..........O..=O.| 00000010 75 52 26 8b 45 04 3d 00 02 72 49 b9 05 01 b8 01 |uR&.E.=..rI.....| 00000020 4f cd 10 3d 4f 00 75 3c 26 80 7d 19 08 75 35 26 |O..=O.u<&.}..u5&| ・・・(略)
先頭の16バイトの内容b800908ec0bf0000b8004fcd103d4f00
でimgファイル内をググってみます
0x41FF
が開始のようです。
同じようにして各ファイルの内容が配備されているアドレスを表に起こして見ました。
file name | clustno | ofset |
---|---|---|
haribote.sys | 0x0002 | 41FF |
ipl10.nas | 0x003A | B1FF |
make.bat | 0x0040 | BDFF |
本書によるとディスクイメージ内のアドレス = clustno * 512 + 0x003e00
で決まるようです。
例えばharibote.sysの場合、0x0002
* 512 + 0x03e00
= 0x0400
+ 0x03e00
= 0x4200
となるのですが、なんか私の調査結果だとさらに-1されてるように見えるのですが?????
typeコマンドは本書に掲載されているものをそのままでなぜか動きました。
初アプリ
アプリというか、静的な機械語の実行ファイルをOSに保持させてそれをプログラムから呼ぶだけ。 19章ではhlt命令を実行するだけのプログラムを実行した。
$ cat hlt.nas [BITS 32] fin: HLT JMP fin
上記をアセンブルした下記をhariboteOS内にファイルとしてコピして呼ぶ。
$ hexdump -C hlt.hrb 00000000 f4 eb fd |...| 00000003
手順としては見つかったファイル内容をセグメントに登録してそこにfar jumpしておしまい。
感想
30日OS自作本18日目
細かいコンソールの修正といくつかのコマンド(mem,cls,dir)を作る内容でした。
動画
文字列を比較する
string.hで定義されているstrcmpを使用します。
arr[0] == "t" && arr[0] == "s" && arr[2] == "t"
これが
strcmp(arr,"tst")==0
こんなにシンプルにかけます。
#include<stdio.h> #include<string.h> int main(void){ char arr[3] = "tst"; if(strcmp(arr,"tst")==0){ printf("true"); } return 0; }
文字列の取り扱い、Cは普段使っているもの(Java,JS,VB)と結構違うので苦労します。
コマンド
- mem:今まで画面に出してたmemory freeみたいな情報をコンソールにだすだけ
- cls:コンソールに書かれている内容を削除するだけ。
- dir:linuxでいうlsコマンド
dir
dirコマンドに利用するファイルはhariboteOSの中で作成するのではなく、windowsからコピーしてきます。 ですので、本書の内容ではファイルフォーマットはWindowsのものをそのまま拝借するようになっています。
struct FILEINFO { unsigned char name[8]; //8 unsigned char ext[3]; //3 unsigned char type; //1 char reserve[10]; //10 unsigned short time; //2 unsigned short date; //2 unsigned short clustno; //2 unsigned int size; //4 };
FILEINFO(見やすさのために書いてます) NNNNNNNNNNNNNNNNEEEEEETTRRRRRRRRRRRRRRRRRRRRTTTTDDDDCCCCSSSSSSSS FILE(私の実行結果) 48415249424F544553595300000000000000000000007A16C84C0200706C0000 49504C31302020204E41530000000000000000000000927B924C3900950B0000 4D414B45202020204241540000000000000000000000927B924C3F002E000000
あれ?これファイル内容は入ってなくないか?と思いましたが、19日目の内容を見るとこのclustnoに入っている数字の先にファイル内容が入っているようです。
N文字ごとに改行する置換
下記の正規表現で16進数で書かれたテキストを32バイトごとに改行します
置換前 (.{64}) 置換後 $1\r\n
バイナリエディタからコピペで持ってきた時点ではもちろん一行の長い16進数でしかなかったので 上記で置換しました。
余談
昔、結合したセルが一行になるようなエクセルのシートに研修の感想を書かされたことがあります。 これ右端に行った時の改行を自分でやらないといけないんですよね(それじゃ紙に書いてるのと一緒じゃねーか) 固定幅フォント何文字で改行されるのか調べてsakura editorで同じような置換をしてそのまま貼り付けて提出しま した。
コマンド名の変更
dirコマンドなんてWindowsみたいで嫌いだ、Linuxみたいにlsコマンドの方がいいよーという人もいるかもしれません。そういう人は、もちろんlsコマンドにしてくれていいですよ。どこを直したらいいかなんて、ここまで読んできた皆さんには言わなくてもわかりますよね?
とのことですので他のも全部変えて見ました
- mem:free
- cls:clear
- dir:ls
lsコマンド作った、というとなんかすごいことをやったように思えますが、今の所、リードしたデータをWindowのファイルフォーマットに合わせてデータを分解して画面に出してるだけですね〜 そういえばlsコマンドのソースを読む内容の本があったのを思い出しました。
- 作者: 藤原克則
- 出版社/メーカー: 秀和システム
- 発売日: 2017/09/01
- メディア: Kindle版
- この商品を含むブログを見る
いつか読もうと思っています。
感想
コマンドを作っているというより、コンソールを改造しているような感じです。実装もconsole_taskの中にベタ書きになっているので。でも多分今後の章で切り出す気がします。
そういえば普通のコンソール(bash,zsh,cmd)でも実行ファイルとして切り出されておらず、コンソールの機能として実装されているコマンドがあったりするのかしら?
- 作者: 川合秀実
- 出版社/メーカー: 毎日コミュニケーションズ
- 発売日: 2006/03/01
- メディア: 単行本
- 購入: 36人 クリック: 735回
- この商品を含むブログ (299件) を見る
30日OS自作本17日目
17日目の内容です。
ソースの動作具合
13章目以降初めて全てのソースが動作しました。16日目は全滅だったので非常に嬉しいです。ただしGUIは若干もたついており遅延があるような気がします。タイミングがシビアなアクションゲームなどはできませんが、普通に使っている分には問題ない感じです。
ウインドウを作る
- 1.枠を作る
- 2.別のタスクとして動きを作る(テキストの入力カーソルの点滅を別タスクに)
- 3.タブキーで切り替えできるようにする
- 4.文字入力できるようにする(FIFOを全てのタスクから参照できるようにする。)
- 5.記号入力できるようにする
- 6.小文字入力できるようにする。
こんな感じでwindowを作っていきます
キー配列
hariboteOSではOSがキーボードコントローラから読み取ったキーコードと文字のペアをOS内にハードコードで保持しています。
static char keytable0[0x80] = { 0, 0, '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '^', 0, 0, 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '@', '[', 0, 0, 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ';', ':', 0, 0, ']', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', ',', '.', '/', 0, '*', 0, ' ', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, '7', '8', '9', '-', '4', '5', '6', '+', '1', '2', '3', '0', '.', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x5c, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x5c, 0, 0 }; static char keytable1[0x80] = { 0, 0, '!', 0x22, '#', '$', '%', '&', 0x27, '(', ')', '‾', '=', '‾', 0, 0, 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '`', '{', 0, 0, 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', '+', '*', 0, 0, '}', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', '<', '>', '?', 0, '*', 0, ' ', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, '7', '8', '9', '-', '4', '5', '6', '+', '1', '2', '3', '0', '.', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, '_', 0, 0, 0, 0, 0, 0, 0, 0, 0, '|', 0, 0 };
これはおそらくwindowsのJIS配列のものなので、US配列を使っているとキーコードに対応する文字が微妙に違っています。
うーんこれは悩んだ人もいると思うのですが?ググっても同様の問題を抱えている人が見つかりません。 一応見える範囲で直して見ました。
static char keytable0[0x80] = { 0, 0, '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=', 0, 0, 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '[', ']', 0, 0, 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ';', 0x27, '`', 0, 0x5c, 'Z', 'X', 'C', 'V', 'B', 'N', 'M', ',', '.', '/', 0, '*', 0, ' ', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, '7', '8', '9', '-', '4', '5', '6', '+', '1', '2', '3', '0', '.', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x5c, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x5c, 0, 0 }; static char keytable1[0x80] = { 0, 0, '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '_', '+', 0, 0, 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '{', '}', 0, 0, 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ':', 0x22, 0x7e, 0, '|', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', '<', '>', '?', 0, '*', 0, ' ', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, '7', '8', '9', '-', '4', '5', '6', '+', '1', '2', '3', '0', '.', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, '_', 0, 0, 0, 0, 0, 0, 0, 0, 0, '|', 0, 0 };
~
の文字コード(0x7e)は調べました。(jisの人たちはキーボードから入力できないんですね〜)
$ echo \~ |od -h 0000000 0a7e 0000002
矢印キー入力で文字が入力されるのがちょっとヤダな~
キー | キーコード | 数値 |
---|---|---|
← | 4B | 4 |
↑ | 48 | 8 |
→ | 4D | 6 |
↓ | 50 | 2 |
これ思ったんですけど、テンキーの上下左右をカーソルに割り当ててるだけですねw
687 456 123
これはMacBook Pro特有のマッピング?他のハードだと矢印キーはどんなキーコードになるんだろう?
キーコードを文字に対応させるプログラムかなんかがあって、そこが差し替え可能になっていると便利ですね。 多分ドライバとかってそのためにあるんだろうけど。
シングルクオートのフォントが変?
若干右に傾いてるような・・・シングルクォートってまっすぐだと思うのですが。。。
hankaku.txt内の0x27の位置にあるデータ
char 0x27 .....*.. ....*... ...*.... ........ ........ ........ ........ ........ ........ ........ ........ ........ ........ ........ ........ ........
と思ったけどiPhoneのキーボード見てみるとガッツリ傾いていますね。
シングルクォーと以外にこんな記号あったような気がするが、なんかの勘違いかな。うーん思い出せない。
SHIFT2つ同時押し+asdf
本章によると左右のSHIFTを押しながらa,s,d,fのキーを押すと、文字入力ができないようです。 私の環境では再現できませんでした。
面白いことを発見しました。左シフト+右シフト+「A」が入力できないのです。
ちなみにasdf以外はSHIFT両押しでも入力できるようです(zとか)
でも、左シフト+右シフト+「Z」は入力できるんです。
他lock系のキー
capslockのキーボードランプ点滅はできませんでした。(これキーボードじゃなくてOSが制御してたんですね。) 点滅できるといえばできますが、多分これホスト側のOSで点滅させているだけで、qemuを介しての動作ではないと思います。 やはり実機で試してみることが大事ですね。
また、私が使ってるMacBook Proにはnumlockとscrollrockのキーがありません。
感想
GUIのラグ
ラグで思い出しましたが、過去に特殊な事情でリモート接続を多重に繰り返すような環境で作業していたことがあります。 詳しいことは聞いていませんが多分WANかなんかで遠隔地を複数回経由したようなラグがありました。 本章の微妙なラグでそんなことを思い出しました。
30日OS自作本16日目
16日目の内容です。 タスク管理の機構を強化するような内容です。
タスク管理
15日目でハードコードされた部分を無くします。タスクも構造体の配列で管理するようにします。
#define MAX_TASKS 1000 /* 最大タスク数 */ //前回から引き続き登場task state segment struct TSS32 { int backlink, esp0, ss0, esp1, ss1, esp2, ss2, cr3; int eip, eflags, eax, ecx, edx, ebx, esp, ebp, esi, edi; int es, cs, ss, ds, fs, gs; int ldtr, iomap; }; //タスク struct TASK { int sel; /* selはGDTの番号のこと */ int flags; //0:未使用 1:使用中 2:動作中 struct TSS32 tss; //task state segmentへの参照 }; //タスク制御 struct TASKCTL { int running; /* 動作しているタスクの数 */ int now; /* 現在動作しているタスクがどれだか分かるようにするための変数 */ struct TASK *tasks[MAX_TASKS];//タスクの参照の配列 struct TASK tasks0[MAX_TASKS];//タスクの配列 };
細かいですが、flagsは何で複数形なんだろう?変数の型が配列とかならわかるけど、気になる
スリープ
- 実行する必要のないタスクはマルチタスクの対象としたくない
- 例:マウス描画処理はマウス割り込み発生時以外は動かす必要がない。
- マルチタスクの対象から外す -> スリープさせる
- マルチタスク制御用の構造体(taskctl)のマルチタスク対象一覧(tasks[])から一時的に外す
- これをスリープという
- task_sleep()関数で指定したタスクのflgを1にする
動きが不安定なソース
自分の環境だとやはり13日目以降、安定しないソースがいくつかあるようです。 本章は全てのソースが何らかの動作不良を抱えていました。
ソース | 不具合内容 |
---|---|
harib13a | 動きがガタガタ |
harib13b | 動きがガタガタ |
harib13c | 表示が非常に遅い |
harib13d | 表示が非常に遅い |
harib13e | 動きがガタガタ |
追記:harib13c,dは「画面の3/4が表示されない」と表記しましたが、他のものより非常に遅いだけで表示自体はできていたようです。
マルチタスクの詳細設定
ある程度汎用化されているものの現段階でマルチタスクは 全てのタスクを0.002秒ごとに実行しているだけになります。
priority、levelという概念を導入し、マルチタスクを更に高機能化します
一つのタスクにかかる時間を調整する
一律に0.001秒ごとに切り替え、ではなく、taskAは0.1秒実行、taskBは0.01秒実行というふうな タスクごとの割り当て時間を可変的に設定できると便利です。こちらをTASKにpriorityという変数を導入し、 タスクきりかえのタイマーを常に0.002秒ではなく、任意の時間を設定します。
struct TASK { int sel, flags; /* selはGDTの番号のこと */ int level, priority; struct TSS32 tss; };
導入まえ
timer_settime(task_timer, 2);
導入後
timer_settime(task_timer, task->priority);
タスクの優先度をLEVELで制御する
スリープ機能の導入により、マウスの描画(taskA)はマウス割り込み発生時にのみ実行するようになっています。
まちタスクがこんな感じになっている時,
- task1(実行ずみ)
- task2(実行中)←runnning
- task3(未実行)
task2が実行中、task3が未実行の周期でマウス割り込みが発生すると
- task1(実行ずみ)
- task2(実行中)←runnning
- task3(未実行)
- taskA(未実行スリープから復帰)
のようになり、マウス描画(taskA)はtask2のマルチタスク終了後、task3の実行時間を更に待つことになります。 task3が一瞬で終わるようにpriorityが設定されていればいいのですが、こちらに長い実行時間が割り当てられている場合、マウス描画がもたつくことになります。
そこでLEVELという概念を導入し、task3の実行前にtaskAを先に実行させるような仕組みを作ります。*1
- task1(実行ずみ)
- task2(実行中)←runnning
- taskA(未実行スリープから復帰) !!!!ここに並ばせる
- task3(未実行)
struct TSS32 {//変更なしなので略。 }; struct TASK { int sel, flags; /* selはGDTの番号のこと */ int level, priority; struct TSS32 tss; }; struct TASKLEVEL { int running; /* 動作しているタスクの数 */ int now; /* 現在動作しているタスクがどれだか分かるようにするための変数 */ struct TASK *tasks[MAX_TASKS_LV]; }; struct TASKCTL { int now_lv; /* 現在動作中のレベル */ char lv_change; /* 次回タスクスイッチのときに、レベルも変えたほうがいいかどうか */ struct TASKLEVEL level[MAX_TASKLEVELS]; struct TASK tasks0[MAX_TASKS]; };
命名の文句ばっかりで申し訳ないですが、個人的にLEVELの構造体の名前にpriority(優先度)という単語を採用する方がしっくりきますね。。。
感想
15日目に書かれている内容ですが、マルチタスクを影分身と例えているところは言い得て妙。 影分身の移動先がマルチタスク対象の実行命令(far JMP)。 まぁ影分身ってたくさんいるって誤魔化してるだけだから全員HLT命令やっとるだけかな(待機中)。 超高速で移動して待機ってなんか勿体無い。 各移動先から手裏剣投げれると面白いかな。 でもマルチタスクの本質として各移動先で別の処理ができる、というのが重要だから、 移動先からいろいろ(手裏剣以外にも鎌とかマキビシとか)ぶん投げれれば楽しいかな。
- 作者: 川合秀実
- 出版社/メーカー: 毎日コミュニケーションズ
- 発売日: 2006/03/01
- メディア: 単行本
- 購入: 36人 クリック: 735回
- この商品を含むブログ (299件) を見る
*1:お店の行列の割り込みとかにイメージが近いけど、割り込みという概念はすでに使っているので紛らわしい
Kotlinでラムダ式のインライン展開を逆コンパイルで検証
Java読書会で読んだKotlin in Actionの復習です。 最初はQiitaに投稿するつもりで書いたのですが、 ただ逆コンパイルしたソースを貼り付けてるだけなのでブログに書きます。
インライン関数とそのメリットについて
- ラムダは無名クラスにコンパイルされます。
- 呼び出しのたびに新しいオブジェクトが生成されます
- 実行時のオーバーヘッドとなる
ただし、inline修飾子がある場合はラムダ式がインライン展開され匿名クラスが作られない。 実行ファイルが大きくなる、などのデメリットもあるようだが、呼び出しコストが下がるというメリットがある。
- Kolinの標準ライブラリの関数はほとんどinlineになっている
コンパイル対象のKotlinのソース
fun main(args: Array<String>) { doTenTimes { println("hell world") } } inline fun doTenTimes(function:()->Unit){ for(i in 1 .. 10){ function() } }
このdoTenTimes()のinline修飾子の有無を変えて調査しました。
コンパイル結果
inline 修飾子がない場合
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov. // Jad home page: http://www.kpdus.com/jad.html // Decompiler options: packimports(3) annotate // Source File Name: hello.kt import java.io.PrintStream; import kotlin.Unit; import kotlin.jvm.functions.Function0; import kotlin.jvm.internal.Intrinsics; import kotlin.jvm.internal.Lambda; public final class HelloKt { public static final void main(String args[]) { Intrinsics.checkParameterIsNotNull(args, "args"); // 0 0:aload_0 // 1 1:ldc1 #9 <String "args"> // 2 3:invokestatic #15 <Method void Intrinsics.checkParameterIsNotNull(Object, String)> static final class main._cls1 extends Lambda implements Function0 { public volatile Object invoke() { invoke(); // 0 0:aload_0 // 1 1:invokevirtual #12 <Method void invoke()> return Unit.INSTANCE; // 2 4:getstatic #18 <Field Unit Unit.INSTANCE> // 3 7:areturn } public final void invoke() { String s = "hell world"; // 0 0:ldc1 #20 <String "hell world"> // 1 2:astore_1 System.out.println(s); // 2 3:getstatic #26 <Field PrintStream System.out> // 3 6:aload_1 // 4 7:invokevirtual #32 <Method void PrintStream.println(Object)> // 5 10:return } public static final main._cls1 INSTANCE = new main._cls1(); static { // 0 0:new #2 <Class HelloKt$main$1> // 1 3:dup // 2 4:invokespecial #60 <Method void HelloKt$main$1()> // 3 7:putstatic #62 <Field HelloKt$main$1 INSTANCE> //* 4 10:return } } doTenTimes((Function0)main._cls1.INSTANCE); // 3 6:getstatic #21 <Field HelloKt$main$1 HelloKt$main$1.INSTANCE> // 4 9:checkcast #23 <Class Function0> // 5 12:invokestatic #27 <Method void doTenTimes(Function0)> // 6 15:return } public static final void doTenTimes(Function0 function) { Intrinsics.checkParameterIsNotNull(function, "function"); // 0 0:aload_0 // 1 1:ldc1 #30 <String "function"> // 2 3:invokestatic #15 <Method void Intrinsics.checkParameterIsNotNull(Object, String)> int i = 1; // 3 6:iconst_1 // 4 7:istore_1 for(byte byte0 = 10; i <= byte0; i++) //* 5 8:bipush 10 //* 6 10:istore_2 //* 7 11:iload_1 //* 8 12:iload_2 //* 9 13:icmpgt 29 function.invoke(); // 10 16:aload_0 // 11 17:invokeinterface #34 <Method Object Function0.invoke()> // 12 22:pop // 13 23:iinc 1 1 //* 14 26:goto 11 // 15 29:return } }
inline 展開ありの場合
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov. // Jad home page: http://www.kpdus.com/jad.html // Decompiler options: packimports(3) annotate // Source File Name: hello.kt import java.io.PrintStream; import kotlin.jvm.functions.Function0; import kotlin.jvm.internal.Intrinsics; public final class HelloKt { public static final void main(String args[]) { Intrinsics.checkParameterIsNotNull(args, "args"); // 0 0:aload_0 // 1 1:ldc1 #9 <String "args"> // 2 3:invokestatic #15 <Method void Intrinsics.checkParameterIsNotNull(Object, String)> //* 3 6:nop int i$iv = 1; // 4 7:iconst_1 // 5 8:istore_1 for(byte byte0 = 10; i$iv <= byte0; i$iv++) //* 6 9:bipush 10 //* 7 11:istore_2 //* 8 12:iload_1 //* 9 13:iload_2 //* 10 14:icmpgt 35 //* 11 17:nop { String s = "hell world"; // 12 18:ldc1 #17 <String "hell world"> // 13 20:astore_3 System.out.println(s); // 14 21:getstatic #23 <Field PrintStream System.out> // 15 24:aload_3 // 16 25:invokevirtual #29 <Method void PrintStream.println(Object)> } // 17 28:nop // 18 29:iinc 1 1 //* 19 32:goto 12 // 20 35:nop // 21 36:return } public static final void doTenTimes(Function0 function) { Intrinsics.checkParameterIsNotNull(function, "function"); // 0 0:aload_0 // 1 1:ldc1 #38 <String "function"> // 2 3:invokestatic #15 <Method void Intrinsics.checkParameterIsNotNull(Object, String)> int i = 1; // 3 6:iconst_1 // 4 7:istore_2 for(byte byte0 = 10; i <= byte0; i++) //* 5 8:bipush 10 //* 6 10:istore_3 //* 7 11:iload_2 //* 8 12:iload_3 //* 9 13:icmpgt 29 function.invoke(); // 10 16:aload_0 // 11 17:invokeinterface #44 <Method Object Function0.invoke()> // 12 22:pop // 13 23:iinc 2 1 //* 14 26:goto 11 // 15 29:return } }
- inline修飾子をつけることでコンパイル後のバイトコードがかなり違うことがわかる。
- 匿名クラスを引数にした関数呼び出しのコストなどはJavaのパフォーマンスに関する知識がないため、特にこの部分で説明できることがない
- インライン展開の方がパフォーマンス的には有利らしい。
- 作者: Dmitry Jemerov,Svetlana Isakova,長澤太郎,藤原聖,山本純平,yy_yank
- 出版社/メーカー: マイナビ出版
- 発売日: 2017/10/31
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (1件) を見る
30日OS自作本15日目
15日目はマルチタスク(タスクスイッチ)をやりました。
マルチタスクp290~310
- CPUは複数のタスクを同時に実行することができない。タスクをそれぞれ少しずつ実施すること実現できる。
- そのための仕組みがタスクスイッチ
- TR(task register)に現在のタスク番号を格納する
これを短い期間で繰り返している。
タスクスイッチ
保存するレジスタ等の内容
TSS(task status segment)という種類のセグメントを利用します。 int型のメンバが26個ありますので、保存するレジスタ等の情報は全てで26*4=104バイトの領域が必要です。
struct TSS32 { int backlink, esp0, ss0, esp1, ss1, esp2, ss2, cr3; int eip, eflags, eax, ecx, edx, ebx, esp, ebp, esi, edi; int es, cs, ss, ds, fs, gs; int ldtr, iomap; };
こちらをGDTに登録し、タスクスイッチのたびにロード、セーブという具合です。
- 1段目
int backlink, esp0, ss0, esp1, ss1, esp2, ss2, cr3;
はレジスタの情報ではありません
JMP命令とEIPレジスタ
- 次の命令の実行場所はEIPレジスタに保持して管理されている
- JMP命令というのは実施
MOV EIP,<JUMP先アドレス>
と同じ意味- ただしアセンブラでEIPに直接代入できない。
- だから素直にJMP命令を使いましょう。
- ただしアセンブラでEIPに直接代入できない。
JUMP命令のnearモードとfarモード
- 命令の読み込み位置はcsとeipによって決まる
- リアルモード時はcs*16+eipで表現。
- csはセグメントのベースアドレス、eipはオフセットアドレスの意味になる。
- プロテクトモード時のcsの挙動の理解が曖昧。
実行イメージ
taskA実行中にtaskBに切り替える例で説明されています。 1.TSSをtaskA,taskBで用意
struct TSS32 tss_a, tss_b;
2.tss_bのeipにタスクとなるプログラムを設定
(関数ポインタは初見?な気がするが特に説明なし?)
void task_b_main(void) { for (;;) { io_hlt(); }//HLT命令。何もしない。 }
tss_b.eip = (int) &task_b_main;
3.GDTにTSSを登録設定
set_segmdesc(gdt + 3, 103, (int) &tss_a, AR_TSS32); set_segmdesc(gdt + 4, 103, (int) &tss_b, AR_TSS32);
4.10秒のカウント表示時に同時にタスクスイッチ命令
} else if (i == 10) { /* 10秒タイマ */ putfonts8_asc_sht(sht_back, 0, 64, COL8_FFFFFF, COL8_008484, "10[sec]", 7); taskswitch4(); //コレ!!!!!
5.アセンブリに定義したタスクスイッチ関数を実行。 ここでfarモードでジャンプ。
_taskswitch4: ; void taskswitch4(void); JMP 4*8:0 RET
コレで3.で登録したGDT+4(task_b)の位置にJMP。この時、ジャンプ先のセグメントが実行可能セグメントではなく、 TSSなのでCPUはEIPやCSを書き換えずにタスクスイッチ命令だと解釈し、TSSで指定したタスクに変わるらしい。
※本章では以後決め打ちになっているタスク名やアドレスは関数の引数にして汎用的なタスクスイッチ処理に作り変えられる。
動画
上記のプログラムを動かした動画。10秒経過の次の瞬間にHLT命令が実行されマウス操作が受け付けられなくなっている。*1
感想
CPUがマルチコアだとどうなる?とか、Javaでマルチスレッドなプログラミングを行った時にCPUレベルではどのように動く?などまだまだイメージできないことは多いですが、
- マルチコア
- マルチタスク
- マルチスレッド
この辺の違いを意識していこう。
- 作者: 川合秀実
- 出版社/メーカー: 毎日コミュニケーションズ
- 発売日: 2006/03/01
- メディア: 単行本
- 購入: 36人 クリック: 735回
- この商品を含むブログ (299件) を見る
*1:マウスの割り込み操作は多分受け付けているが描画処理が実行されていない。