Yabu.log

ITなどの雑記

30日OS自作本20日目

20日目は1文字を表示するAPIを作りました。これでOSに配備したアプリケーションからOSの機能を呼び出すことができます。

f:id:yuyubu:20180613053734p:plain

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日で書き上げられたのかな?とふと思いました。

*1:あくまで本書に書いてあることの私の理解です。もしかしたら最近のアセンブリは出来るようになっている気もする。

30日OS自作本19日目

19章の内容です。typeコマンドでOSにコピーしたファイルの読み込みと、ファイルとしてコピーした機械語の実行を行いました。

f:id:yuyubu:20180612052308p:plain

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しておしまい。

感想

  • コンソールにスクロール機能が付いていないので大きいファイルをtypeコマンドで表示した場合、先頭の方のデータがわからない。
  • typeコマンドで断片化したファイルの表示ができたら面白いと思う
    • ファイルを書き込んでも必ず断片化されるわけではないので、用意が大変そう。バイナリを直接いじるのが良さそう?
  • FAT12デフラグするプログラムも書くイメージが湧いた

    • ディスクのフォーマットに依存する
  • doxygen(c言語javadoc的なやつ)的なのが付いていないから、数週間ぶりに関数などを見ると使い方を忘れる。

30日OS自作本18日目

細かいコンソールの修正といくつかのコマンド(mem,cls,dir)を作る内容でした。

動画

www.youtube.com

文字列を比較する

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コマンドのソースを読む内容の本があったのを思い出しました。

lsを読まずにプログラマを名乗るな!

lsを読まずにプログラマを名乗るな!

いつか読もうと思っています。

感想

コマンドを作っているというより、コンソールを改造しているような感じです。実装もconsole_taskの中にベタ書きになっているので。でも多分今後の章で切り出す気がします。

そういえば普通のコンソール(bash,zsh,cmd)でも実行ファイルとして切り出されておらず、コンソールの機能として実装されているコマンドがあったりするのかしら?

30日でできる! OS自作入門

30日でできる! OS自作入門

30日OS自作本17日目

17日目の内容です。

www.youtube.com

ソースの動作具合

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が表示されない」と表記しましたが、他のものより非常に遅いだけで表示自体はできていたようです。

www.youtube.com

マルチタスクの詳細設定

ある程度汎用化されているものの現段階でマルチタスクは 全てのタスクを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命令やっとるだけかな(待機中)。 超高速で移動して待機ってなんか勿体無い。 各移動先から手裏剣投げれると面白いかな。 でもマルチタスクの本質として各移動先で別の処理ができる、というのが重要だから、 移動先からいろいろ(手裏剣以外にも鎌とかマキビシとか)ぶん投げれれば楽しいかな。

30日でできる! OS自作入門

30日でできる! OS自作入門

*1:お店の行列の割り込みとかにイメージが近いけど、割り込みという概念はすでに使っているので紛らわしい

Kotlinでラムダ式のインライン展開を逆コンパイルで検証

Java読書会で読んだKotlin in Actionの復習です。 最初はQiitaに投稿するつもりで書いたのですが、 ただ逆コンパイルしたソースを貼り付けてるだけなのでブログに書きます。

インライン関数とそのメリットについて

Kotlinではラムダ式は無名クラスにコンパイルされる

  • ラムダは無名クラスにコンパイルされます。
  • 呼び出しのたびに新しいオブジェクトが生成されます
  • 実行時のオーバーヘッドとなる

ただし、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のパフォーマンスに関する知識がないため、特にこの部分で説明できることがない
    • インライン展開の方がパフォーマンス的には有利らしい。

Kotlinイン・アクション

Kotlinイン・アクション

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命令を使いましょう。
      

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

www.youtube.com

感想

CPUがマルチコアだとどうなる?とか、Javaでマルチスレッドなプログラミングを行った時にCPUレベルではどのように動く?などまだまだイメージできないことは多いですが、

この辺の違いを意識していこう。

30日でできる! OS自作入門

30日でできる! OS自作入門

*1:マウスの割り込み操作は多分受け付けているが描画処理が実行されていない。