Yabu.log

ITなどの雑記

30日OS自作本28日目1ファイルAPI編

28日目の内容です。 - ファイルAPIの作成 - hariboteOSを日本語対応させます。

OS自体の開発はここでおしまい。なのです。😭 記事が長くなるので前半後半で分けます。本記事ではとりあえずpart1としてファイルAPIについて書きます。

ファイルAPI

ファイルAPIの具体的なイメージなどよくわからず、また本書掲載のコードが5つ用意したうち2つしか使っていなかったので、以下2点に挑戦して見ました。

  • APIの実装コードにコメントをつけて理解する
  • 全てのファイルAPIを使うアプリを作る

では、ファイル操作のためのAPIを新たにOSに作ります。

struct TASK {
  //・・・無関係メンバ略

  //新規追加
  struct FILEHANDLE *fhandle;
    int *fat;
};
struct FILEHANDLE {
    char *buf; //メモリにロードしたファイル内容への参照
    int size;  //ファイルサイズ
    int pos;   //シークヘッダの位置(読み込み、書き込みに利用)
};
edx api
21 ファイルオープン
22 ファイルクローズ
23 ファイルシーク
24 ファイルサイズの取得
25 ファイルの読み込み

コード解説

コードは大体、console.cより抜粋。(定義などは適当に.hファイルから拝借)

ファイルオープン

//定義メモ
#define ADR_DISKIMG        0x00100000 //フロッピーの内容がロードされるメモリの位置
struct FILEINFO *file_search(char *name, struct FILEINFO *finfo, int max);
void file_loadfile(int clustno, int size, char *buf, int *fat, char *img);

・・・省略・・・

//API引数
// EDX = 21
// EBX = ファイル名
//
//API戻り値
// EAX = ファイルハンドル(失敗の場合0)

} else if (edx == 21) {
  //空きのファイルバッファを探す
  for (i = 0; i < 8; i++) {
    //ここのiの最大値が8なのがよくわからない。
    //一つのタスクが1度に扱えるファイル数が8個までという制限の意味?
    if (task->fhandle[i].buf == 0) {
      break;
    }
  }

  fh = &task->fhandle[i];
  reg[7] = 0; //失敗時の戻り値で初期化

  if (i < 8) {

    //file_search関数の引数について整理します。
    //1.edx=文字列のアドレス、そこにタスクのベースアドレスを加算してリニアアドレスを算出
    //2.ADR_DISKIMG + 0x002600で、フロッピーの内容がコピーされた場所の内、ルートディレクトリの開始位置ということになります。
    //3.224 = ルートディレクト領域の大きさ(IPLで定義)
    finfo = file_search(
      (char *) ebx + ds_base,
      (struct FILEINFO *) (ADR_DISKIMG + 0x002600),
      224
    );

    //file_search()の戻り値=0はファイルが見つからなかった時

    if (finfo != 0) {

      //最終的にreg[7]はapiを読んだ時の戻り値(EAX)になります。
      //そこにこれから作成するファイルハンドラを代入します。
      reg[7] = (int) fh;

      //ファイルハンドラ初期化
      fh->buf = (char *) memman_alloc_4k(memman, finfo->size);
      fh->size = finfo->size;
      fh->pos = 0;

      //ファイルハンドラのbufの位置にデータ読み込み
      //void file_loadfile(int clustno, int size, char *buf, int *fat, char *img)の引数整理
      //引数1:finfo->clustno,そのファイルがどのクラスタ内にあるか、
      //引数2:finfo->size,そのファイルのサイズ
      //引数3:fh->buf読み込んだファイルを格納するアドレス
      //引数4:task->fat fatの全データが格納されている
      //引数5:0x003e00,フロッピーからロードしたファイル実態が格納されているアドレス

      file_loadfile(
        finfo->clustno,
        finfo->size,
        fh->buf,
        task->fat,
        (char *) (ADR_DISKIMG + 0x003e00));
    }
  }

ファイル名でヒットしたfatを元に、メモリ上にブート時に配置されたファイルをコピーしています。

ファイルクローズ

//API引数
// EDX = 22
// EAX = ファイルハンドル

} else if (edx == 22) {
  fh = (struct FILEHANDLE *) eax;

  memman_free_4k(memman, (int) fh->buf, fh->size);
  fh->buf = 0;

コピーしていたファイル内容を破棄します。(メモリの解放) buf=0のものはクローズ扱いです。 逆にコマンドの処理が完了時にbuf=0になっていないものはOSが勝手に解放します。

OSによる自動解放

start_app(0x1b, 0 * 8 + 4, esp, 1 * 8 + 4, &(task->tss.esp0));

・・・省略・・・

for (i = 0; i < 8; i++) {    /* クローズしてないファイルをクローズ */
  if (task->fhandle[i].buf != 0) {
    memman_free_4k(memman, (int) task->fhandle[i].buf, task->fhandle[i].size);
    task->fhandle[i].buf = 0;
  }
}

コードの重複がありますね〜30日終わったらうまく抽象化したいですね〜

ファイルシーク

//API引数
// EDX = 23
// EAX = ファイルハンドルのアドレス
// ECX = シークモード
//  0:シークの原点はファイルの先頭
//  1:シークの原点は現在のアクセス位置
//  2:シークの原点はファイルの終端
// EBX = シーク量

} else if (edx == 23) {
  fh = (struct FILEHANDLE *) eax;
  if (ecx == 0) {
    //ECX = 0:シークの原点はファイルの先頭
    //ファイル先頭位置から指定サイズ分(ebx)シーク
    fh->pos = ebx;
  } else if (ecx == 1) {
    //ECX = 1:シークの原点は現在のアクセス位置    
    //現在のposから指定サイズ分(ebx)シーク
    fh->pos += ebx;
  } else if (ecx == 2) {
    //2:シークの原点はファイルの終端
    //ファイル末尾(fh->size)から指定サイズ分(ebx)シーク
    fh->pos = fh->size + ebx;
  }
  if (fh->pos < 0) {
    //posが0以下?なにこれ?オーバーフロー対策?
    fh->pos = 0;
  }
  if (fh->pos > fh->size) {
    //ファイルサイズを超えるようなシークを無効化している?
    //ecx==2の時のシーク意味ないかこれ?
    fh->pos = fh->size;
  }

ファイルサイズの取得

特に解説することがない。

//API引数
// EAX = ファイルハンドル
// ECX = ファイルサイズ取得モード
//  0:普通のファイルサイズ
//  1:現在の読み込み位置はファイル先頭から何バイト目か
//  2:ファイル終端から見た現在位置までのバイト数
//
//API戻り値
// EAX = ファイルサイズ

} else if (edx == 24) {
  fh = (struct FILEHANDLE *) eax;
  if (ecx == 0) {
    reg[7] = fh->size;
  } else if (ecx == 1) {
    reg[7] = fh->pos;
  } else if (ecx == 2) {
    reg[7] = fh->pos - fh->size;
  }

ファイルの読み込み

特に難しくないでしょう。

//API引数
// EDX = 25
// EAX = ファイルハンドル
// EBX = バッファの番地
// ECX = 最大読み込みバイト数
//
//API戻り値
// EAX = 今回読み込めたバイト数

} else if (edx == 25) {
  fh = (struct FILEHANDLE *) eax;
  for (i = 0; i < ecx; i++) {
    if (fh->pos == fh->size) {
      break;
    }
    *((char *) ebx + ds_base + i) = fh->buf[fh->pos];
    fh->pos++;
  }
  reg[7] = i;
}
  • 現在のhariboteOSにはファイル書き込みの機能がないのでwriteは作らない
  • そういえばhariboteOSにファイルシステムではないらしい。
  • オートクローズの機構がある。
    • コマンド実行終了後、開いたままのファイルがあればOSがクローズする

ファイル操作APIの利用

apiのライブラリに登録されているこれらのメソッドを読んで見ます。 ちなみに本記事で解説したAPIアセンブリに定義されている以下の関数経由で読んでいます。

int api_fopen(char *fname);
void api_fclose(int fhandle);
void api_fseek(int fhandle, int offset, int mode);
int api_fsize(int fhandle, int mode);
int api_fread(char *buf, int maxsize, int fhandle);

サンプルとしてabc.txtというファイルを作ったのでこれで実験します。

$ cat txt/abc.txt
ABCDEFGHIJKLMNOPQRSTUVWXYZ

こちらをhariboteOSのディレクトリに投入します。

f:id:yuyubu:20180628225014p:plain

本書掲載のプログラムでは

  • ファイルシーク
  • ファイルクローズ
  • ファイルサイズ

が使われていなかったので、使って見ました。

#include "apilib.h"

void HariMain(void)
{
    int fh;

    //ファイルオープン
    fh = api_fopen("abc.txt");
    char s[20];

    //ファイルサイズを出力
    int size = api_fsize(fh,0);
    sprintf(s,"size:%d\n",size);
    api_putstr0(s);

    //ファイルシーク(読み込み位置を10文字進める)
    api_fseek(fh,10,0);

    if (fh != 0) {
        char c; //読み取り文字格納
        for (;;) {
            //ファイルリード(1文字づつ)
            if (api_fread(&c, 1, fh) == 0) {
                break;
            }
            api_putchar(c);
        }
    }
    //ファイルクローズ
    api_fclose(fh);
    api_end();
}

このアプリの実行結果は以下のようになりました。アプリ名はtypeiplです。(本書のipl.nasを表示するアプリのガワをそのまま流用しています。)

f:id:yuyubu:20180628225428p:plain

  • ファイルサイズは27バイトです。ローマ字は26文字ですが、ヌル文字を含んでいる?のでしょうか
  • ファイルシークにより11字目から出力が始まります。10文字目がなので次のKから始まっていることが確認できます。
  • クローズしなくてもOSがファイルクローズしますが、一応明示的にAPIを使って見ました。

まぁざっとこんな感じです。

感想

サンプルコード中にマジックナンバーなどよくわからない値が多数存在しています。 このアドレスの破片っぽいものは何だったっけ?と自分のブログを検索してみると以外と過去にメモった情報が見つかって助かることがあります。 今回は一番このブログに助けられた章になりました。 皆様も最終日が近付くと、初日辺りの細かいアドレスなど忘れてしまうので、メモなど取りながら進めることをお勧めします。

電子書籍の中をアドレスや変数名で検索できるとさらに良いのですが。

プログラマのためのSQL 読書会(21)に参加

26章の初めから26.7まで読みました。

ビューがメインテーマです。

  • ビューの更新を知る人は少ない。

    • 情報技術者試験でよく出る
    • https://www.ap-siken.com/kakomon/23_aki/q30.html
      • このサイトに更新可能ビューの条件があるが一部違っている。本書の更新可能ビューの条件を引用します。
        • 1.1つだけのテーブルから作られていること
        • 2.GROUP BY句を使っていないこと
        • 3.HAVING句を使っていないこと
        • 4.集約関数を使っていないこと
        • 5.計算列を使っていないこと
        • 6.UNION,INTERSECT,EXCEPTを使っていないこと
        • 7.SELECT DISTINCT句を使っていないこと
        • 8.ビューに含まれていない規定テーブルの全ての列はNULLを許可するかデフォルト値が指定されていること。*1
  • IBMではViewではなく「視点」というらしい。

  • 確かにIBMの汎用機周りは謎翻訳が多い

    • そういえばindicator*2のことを標識とよく言っている
  • クエリーのリライトプラグインというものがある

    • 特定の効率の悪いクエリを書き換える機構が存在するらしい
  • マルチテナントシステムだとテナントごとにビューを切ったりする。

  • ビューの権限

  • アクセス権限の設定はビューだけではない?

    • ビューだけではなく規定テーブルへのアクセス権限の設定も必要。
      • そういう作業を最近した人がいる
      • 詳しい人曰く
        • sql securityの値
          • definer
          • invocar
      • を適切に設定することで迂回できるようです。
  • oracleにはscottというデモ用のアカウントがいる

    Oracle Databaseに付属するdemobld.sqlOracle Database 10g以降ではutlsampl.sql)を実行すると「EMP」「DEPT」というふたつのテーブルと「SCOTT/TIGER」というスキーマよりなる伝統的なデモ環境が構築される。「SCOTT」とはオラクルの前身であるSDLに在籍していたBruce Scottを指し、「Tiger」は彼の愛猫の名前に由来する。Scottは優秀な開発者であり最初期のSQL*Plusも彼の手によるものとされている。Scottはすでにオラクルを後にしているが、この伝統は変わる様子がない。

  • 1対他の結合を行うクエリやスキーマをスター型のクエリ,スタースキーマというらしい。

    • ER図がスター型になるので
  • MyISAM

    • ストレージエンジン。以前はISAMと呼ばれるものだった。
      • ISAM -> C,COBOLなどの汎用機の人が詳しいかも。
    • MySQLが使い始めてMyISAMというものを改良して使っている。
  • MyISAMは古い。最近はinnoDBを使っている

  • mysqlのmerge table

  • ビューの循環参照(実験してみる)

    • ALTER VIEWで循環参照になるビューを作ればどうなるか、という話題になった。
    • tbl1←v1←v2という参照関係のテーブルビューを作る
--定義
create table tbl1(id int);
insert into tbl1 values(1);
create view v1 as select id from tbl1;
create view v2 as select * from v1;

--確認
select * from tbl1;
select * from v1;
select * from v2;

/*もちろん結果は全て

+------+
| id   |
+------+
|    1 |
+------+

になる。
*/
  • ここでv1をalter viewで変更して v1 <-> v2という循環参照を作る
    • が、この循環参照を作るalter文は実行できなかった。
alter view v1 as select * from v2;
--ERROR 1146 (42S02): Table 'test.v1' doesn't exist

--もちろん構文は間違っていない。v2をtbl1にすれば実行できる。
--alter view v1 as select * from tbl1;
--結果:Query OK, 0 rows affected (0.00 sec)
  • mysql "循環参照"でググっても関連しそうな情報はなかった。

  • SQL Serverにはindex viewがある!

  • 誤植

    • P.466のSQL,'Sales'のSが小文字sになっている
▼P.466

SELECT *
  FROM Departments
 WHERE (dept_name = 'sales') --ココ!'sales'ではなく'Sales'!!
   AND (city_name = 'New York');
  • 多分原著から間違っているのでは?(未確認ですが)とのこと。

次は26.7から!

プログラマのためのSQL 第4版

プログラマのためのSQL 第4版

*1:これは規定テーブルに行を挿入する際、値を一位に決めるためである

*2:システムグローバルなフラグ集,CPUのレジスタで言う所のEFLAGSのようなもの

30日OS自作本27日目

27日目の記事になります。というよりほとんどLDTの解説になります。

www.youtube.com

LDT

  • OSのセグメントにアプリケーションからアクセスすることはできない。(21,22日目のセキュリティ対応)
  • ただしアプリは別のアプリのセグメントにアクセスできてしまう。(コード例:crack7)
  • CPUはこの問題に対処するためにLDT(Local Descriptor Table)という仕組みを持っている
  • LDTを使うことでタスクローカルなセグメントを作ることができる。

LDTのアドレス指定

タスク切り替えは以下のtask state segmentをGDTに登録してタスク切り替えのたびにレジスタ等に値をロードします。

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;
};

このTSSのldtrにLDTの開始アドレスを登録するとタスク切り替え時にLDTの場所をCPUが指定することになります。

hariboteOSでmtask.cでLDTを設定しています。

#define MAX_TASKS       1000    /* 最大タスク数 */
#define TASK_GDT0       3       /* TSSをGDTの何番から割り当てるのか */

//こいつらは権限。一応書いていますがとりあえず今は関係ないので無視でOK
#define AR_TSS32       0x0089
#define AR_LDT         0x0082

set_segmdesc(gdt + TASK_GDT0 + i            , 103, (int) &taskctl->tasks0[i].tss, AR_TSS32); /*タスクのGDTの設定*/
set_segmdesc(gdt + TASK_GDT0 + MAX_TASKS + i, 15 , (int) taskctl->tasks0[i].ldt , AR_LDT  ); /*LDTの設定*/

LDTを使う前後の比較

アプリのセグメントの設定

LDTを使う前のcmd_app()ではGDT上のタスクのTSSの配置位置(task->sel)から相対的にコードセグメント、データセグメントを計算しています。

set_segmdesc(gdt + task->sel / 8 + 1000, finfo->size - 1, (int) p, AR_CODE32_ER + 0x60); //コードセグメント
set_segmdesc(gdt + task->sel / 8 + 2000, segsiz - 1,      (int) q, AR_DATA32_RW + 0x60); //データセグメント

セグメントをセットするコードに関して整理します。

  • gdt + task->sel / 8 + 1000

これはコードセグメントですが、各項は以下の意味を持っています。

意味
gdt GDTのベースアドレス
task->sel/8 GDTを参照する時のセレクタ
1000 GDTの1000~2000をアプリケーションのコードセグメントとするための加算アドレス

task->selをなぜ8で割るかというと、GDTのセレクタ値は8の倍数で指定されているからです。

index sel(dec) sel(binary)
0 0 0b00000
1 8 0b01000
2 16 0b10000
3 24 0b11000
4 32 0b20000

これはGDT場では8飛びでデータが配置されているという意味ではなく、あくまでGDTを指定するためのインデックスの仕様です。 詳しくは後述するのでそちらを参照してください。

このようにLDT導入前ではTSSの配置indexからアプリケーションのコードセグメント、データセグメントを相対的に求めてGDT上に確保しています。

例:TSSがGDTの5の位置のアプリの場合,

  • index = 5
  • セレクタ値(sel) = 5*8
  • コードセグメント = sel/8 + 1000 = 1005
  • データセグメント = sel/8 + 2000 = 2005

LDT導入前のGDTのマッピングは以下のようになります。

index content
1003 task_a用(アプリはないので使っていない)
1004 idle用(アプリはないので使っていない)
1005 1個目に確保されるアプリのコードセグメント
1006 2個目に確保されるアプリのコードセグメント
... 省略
2005 1個目に確保されるアプリのデータセグメント
2006 2個目に確保されるアプリのデータセグメント

次にLDTを使う場合です。

set_segmdesc(task->ldt + 0, finfo->size - 1, (int) p, AR_CODE32_ER + 0x60);
set_segmdesc(task->ldt + 1, segsiz - 1,      (int) q, AR_DATA32_RW + 0x60);

アプリケーションのセグメントはLDTを使ってこのように確保していきます。 セレクタ値を計算で使って相対的に決めている前バージョンよりコードはわかりやすくなっています。

LDTは結構上の方に書いていますが、gdt + TASK_GDT0 + MAX_TASKS + iの位置に配置されます。

例:TSSがGDTの3の位置のアプリの場合,

  • index = 5
  • セレクタ値(sel) = 5 * 8
  • ldtの位置 = TASK_GDT0 + MAX_TASKS = 1005
  • コードセグメント = ldtの0番目の位置
  • データセグメント = ldtの1番目の位置

LDT導入後のGDT内容は以下のようになります。。

index content
1003 taska用(アプリはないので使っていない)
1004 idle用(アプリはないので使っていない)
1005 1個目に確保されるアプリのLDTへの参照
1006 2個目に確保されるプリのLDTへの参照
... 省略

ちなみにLDTです。これはタスクごとに作成されます。

index content
0 アプリのコードセグメント
1 アプリのデータセグメント

アプリケーションの起動

void start_app(int eip, int cs, int esp, int ds, int *tss_esp0);
  • LDT導入前:start_app(0x1b, task->sel + 1000 * 8, esp, task->sel + 2000 * 8, &(task->tss.esp0));
  • LDT導入後:start_app(0x1b, 0 * 8 + 4 , esp, 1 * 8 + 4 , &(task->tss.esp0));

LDT導入後はどのアプリケーションもセレクタ値は

  • cs:0 * 8 + 4
  • ds:1 * 8 + 4

となります。導入前と比べてどうでしょうか。導入前はセレクタ値を加工してタスクごとに指定するセグメントが変わるようになっていますが、LDT導入後は固定値になっています。 これは衝突しないのか?と疑問に思うかもしれませんが、このセグメントレジスタの指す先はGDTではなくタスクごとに用意されるLDTなので衝突することはありません。

なぜGDTやLDTのセレクタ値は8倍したり4を加算したりするのか

そもそもなぜ8をかけているかというと、この本には解説されていませんが、

8倍の理由はセグメント番号の下位3bitは別の意味があって、ここでは0にしておかないと行けないからです。

p135(day7)より

一旦ここでセグメントの下位3bitを踏まえたセレクタ値について説明します。

セレクタ値の構造は以下の様になっています。

31       23       15       7        0
+--------+--------+--------+--------+
|             index            |T|RP|
+--------+--------+--------+--------+
図中の略称 正式名称 長さ(bit) 意味
index 13 DTの位置を示すindex
RP RPL:Requested Privilege Level 2 必要権限
T table indicator 1 0=GDT 1=LDT

具体例を書き起こしてみます。

GDTを指す場合:sel = index * 8

index sel(dec) sel(binary)
0 0 0b00000
1 8 0b01000
2 16 0b10000
3 24 0b11000
4 32 0b20000

LDTを指す場合:sel = index * 8 + 4

index sel(dec) sel(binary)
0 4 0b00100
1 12 0b01100
2 20 0b10100
3 28 0b11100
4 36 0b20100
  • 8を掛けることで下位3bitが全て0になる。
  • 8を掛けた後に4を足すことでtable indicatorの位置が1になる
    • 下位2bitは0のまま

表に起こすことで上記の3点の理解が深まると思います。

RPLの役目について

RPLは0が最強で3が最弱です。アプリケーションは通常3,OSは0のレベルで動作します。 この権限の設定はGDTにもあり、GDTのアクセス権の6,7bit目に当たります。(day6で解説しています)RPLの値がGDTの権限属性に満たない場合は一般保護例外が発生します。

アプリケーションからOSのセグメントに不正にアクセスすることはできませんが、 代理でOSに処理をさせる様な挙動、例えばコールゲート経由でOSに不正なアドレスを渡すと、OSの権限で動いてしまうので不正なアドレス操作をする恐れがあります。

RPLはこれを防ぐためにあるようです。

はじめての486の10章セキュリティによると、 RPLの設定はハードウェアが自動で行うのではなく、OSが責任をもつようです。

ところで、要求者特権レベルtのチェックは486が行うのですが、設定の作業はオペレーティングシステムが行わなければなりません。

ちなみに現時点でhariboteOSにはこのRPLの設定機構はありません

LDTは現在使われていない?

LDTは、ハードウェアに夜タスク切り替え機構のために提供されたが、OSメーカーが採用しなかった。現在のプログラムは仮想メモリによって隔離されており、LDTは使われていない。

低レベルプログラミングP52より

30日OS自作本は結構古い本なので、こういう最近の低レイヤー情報はありがたい。

低レベルプログラミング

低レベルプログラミング

参考リンク

私が知りたいことが全て書いてあった。 http://softwaretechnique.jp/OS_Development/kernel_loader2.html

毎度お世話になってる486本

感想

LDTは新要素なので486本を読み直したりググりまくって時間をかけたがあまり難しい概念ではない様に思います。ただ最近使われていないというのはちょと驚き。

OS自作本,486本をそろそろ読み終えるが、次に読みたい本の候補が決まってきた

  • Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識
  • はじめてのOSコードリーディング ~UNIX V6で学ぶカーネルのしくみ
  • 低レベルプログラミング
  • 詳解Linuxカーネル

うーん1冊消化するたびに積ん読が何冊も増えるような?w 本棚が爆発するんじゃないでしょうか。

Effective Javaも読みたいし、Deep Learningもかじりたい(超欲張り)。 秋に情報安全支援士受けるんでネットワークやセキュリティの総復習もしたい。

  • Effective java 3rd
  • ゼロから作るDeep Learning(続編もきましたね!)
  • コンピューターネットワーク
  • ソースコードで体感するネットワークの仕組み
  • 体系的に学ぶ 安全なWebアプリケーションの作り方 第2版
  • ルーター自作でわかるパケットの流れ

うーむ時間は有限なので効率的に生きたいですね。 平日の時間投入はこれ以上は難しそうなので休日頑張るしかないな。

30日OS自作本24日以降のXボタンの影がおかしい

OS自作本24日目以降、Xボタンの色がおかしいことに気がつきました。

f:id:yuyubu:20180624004128p:plain
ボタンの影の部分が濃い灰色ではなく濃い青色になっている

f:id:yuyubu:20180626182115p:plain
参考:23日目終了時点でのXボタン
24日目(harib21e)に追加したウインドウの非アクティブ・アクティブを切り替えるchange_wtitle8ですが

  • 非アクティブ時のウインドウのバーの濃い灰色(COL8_848484)をアクティブ時に濃い青色(COL8_000084)に上書き
  • 非アクティブ時のウインドウとXボタンの影が同じ色(濃い灰色=COL8_848484)
  • この処理は色でしか判断しておらずXボタンの影が青く塗られてしまう。

という問題がありXボタンの影が24日目以降変です。

f:id:yuyubu:20180624003957p:plain

void change_wtitle8(struct SHEET *sht, char act)
{
    int x, y, xsize = sht->bxsize;
    char c, tc_new, tbc_new, tc_old, tbc_old, *buf = sht->buf;
    if (act != 0) {
        tc_new  = COL8_FFFFFF;
        tbc_new = COL8_000084;
        tc_old  = COL8_C6C6C6;
        tbc_old = COL8_848484;
    } else {
        tc_new  = COL8_C6C6C6;
        tbc_new = COL8_848484;
        tc_old  = COL8_FFFFFF;
        tbc_old = COL8_000084;
    }
    for (y = 3; y <= 20; y++) {
        for (x = 3; x <= xsize - 4; x++) {
            c = buf[y * xsize + x];
            if (c == tc_old && x <= xsize - 22) {
                c = tc_new;
            } else if (c == tbc_old) {
                c = tbc_new;
            }
            buf[y * xsize + x] = c;
        }
    }
    sheet_refresh(sht, 3, 3, xsize, 21);
    return;
}

解決策をいくつか考えて見ました。

  • 1.SHEET構造体にtitleという属性を持たせてchange_wtitle8の代わりにtilte引数付きでmake_window8を呼ぶ
  • 2.232色の中からxボタンの影として濃い灰色COL8_000084と似た色を選ぶ
  • 3.233色目に濃い灰色をCOL8_000084と同じ色を登録してxボタンの影にし、else if(c == tbc_old)この文の処理対象とさせない

下に行くほど付け焼き刃対応になります。工数がかかりますが、本来は一番上の選択肢を取るべきでしょう。 メモリ破壊とかそういう危険なバグではないので、とりあえず一番下で直して見ました。

  • graphic.cのinit_palette()の末尾に追加
static unsigned char table_2[3] = {
    0x84, 0x84, 0x84 /* 15:暗い灰色 */
};
set_palette(233, 233, table_2);
  • window.cのmake_wtitle8()の一部を変更
if (c == '@') {
    c = COL8_000000;
} else if (c == '$') {
    //c = COL8_848484;//←消す
    c = 233;//←入れる
} else if (c == 'Q') {

結果

www.youtube.com

hariboteOSをベースに自作OSを作成されている方は一度見直してみることをお勧めします。

30日OS自作本26日目

26日目の内容になります。

GUI描画の最適化とコンソールの昨日の強化、コマンドを3つ作ります。

www.youtube.com

GUI描画の最適化

harib23a(透明色の判定)

透明色を判定しているif分が非効率なのでこれを改善します

for(/*シート分*/){
    for(/*y幅分*/){
        for(/*x座標分*/){
            if(){/*透明色用の処理*/} //←これ効率悪い!!!!!
        }

    }
}

透明色がないシートの場合でもy幅分 * x幅分 if 文をとってしまうのが無駄です。 harib23aでは透明色ありのシートとなしのシートでfor文を変えてしまうことで高速化を期待します。

for(/*シート分*/){
    if(/*透明色なしのシート*/){//この条件を追加!!!
        for(/*y幅分*/){
            for(/*x座標分*/){
                //透明色判定を省略したVRAM書き込み
            }
        }
    }else{
        for(/*y幅分*/){
            for(/*x座標分*/){
                if(){/*透明色用の処理*/}
            }
        }
    }
}

全てのシートが透明色をもつシートの場合は、通る余分なif文が一つ増えるので速度が落ちてしまいますが、 マウスぐらいしかないので 

harib23b(一度の書き込みを1Bから4Bへ)

VRAMへの書き込みは*char型のポインタを利用するので1Bずつですが、

//unsigned char *map;
map[vy * ctl -> xsize + vx] = sid;

こちらをint型(4B)にします。

p = (int *) &map[vy * ctl->xsize + vx];

//注:ループ上限のbx1はbxの役1/4になる様に設定されている
for (bx = 0; bx < bx1; bx++) {
    p[bx] = sid4;
}

これでループ回数を役1/4に減らすことができます。 かなり早くなりそうですが。。。

harib23c(sheet_refreshsubも4B書き込み対応に)

画面全体ではなく指定したシートのみを更新するsheet_refreshsub()も4B書き込みに対応させます。

harib23d

FIFOが空っぽになった時に描画するという変更を入れます。 こちらの変更で、手元でマウスを止めたがカーソルが移動し続けているという事象を回避できる様ですが、いまいち違いがわかりませんでした。

ウインドウ機能の強化

  • コマンドの追加
    • start:新規ウインドウを開く
    • exit:ウインドウを閉じる
    • ncst:マルチタスクでアプリを起動させる

ncstはbashで言う所のコマンド末尾に&をつける様な動作です。

感想

今回はちょっとしたリファクタリング中心であまり新しい技術要素が出て来なかった様に思います。特にスピードアップに興味がない方は描画の改善は飛ばしてもいい気がします。 次回はLDTを使います。これも「はじめて読む486」に書いてあって気になっていた内容なので楽しみです。

30日OS自作本25日目

25日目の内容になります。

この章でコンソールを増やします。

Beep

12日目にBeepを試そうとしていましたが、なんと本章に動くプログラムが書かれていました。 QEMUだと鳴らないようです(笑)

色数増加 16 → 232

graphic.cに16色のRGBと色番号の対応付けがしてある配列がハードコードされていますが、これを増やします。

static unsigned char table_rgb[16 * 3] = {
    0x00, 0x00, 0x00,    /*  0:黒 */
    0xff, 0x00, 0x00,    /*  1:明るい赤 */
    0x00, 0xff, 0x00,    /*  2:明るい緑 */
    ・・・(省略)・・・
    0x84, 0x84, 0x84 /* 15:暗い灰色 */
};

今回追加する色の配列を作成しているプログラムの一部を抜き出して色番号とRGBの対応を画面に出して見ました。

#include <stdio.h>
int main(void){
    unsigned char table2[216 * 3];
    int r, g, b;
    int i=0;
    for (b = 0; b < 6; b++) {
        for (g = 0; g < 6; g++) {
            for (r = 0; r < 6; r++) {
                table2[(r + g * 6 + b * 36) * 3 + 0] = r * 51;
                table2[(r + g * 6 + b * 36) * 3 + 1] = g * 51;
                table2[(r + g * 6 + b * 36) * 3 + 2] = b * 51;

                printf("%d\tr=%d\tg=%d\tb=%d\t:\t%d\t%d\t%d\n",i,r,g,b,r * 51,g * 51,b * 51);
                i++;

            }
        }
    }
}

結果

0   r=0 g=0 b=0 :   0   0   0
1   r=1 g=0 b=0 :   51  0   0
2   r=2 g=0 b=0 :   102 0   0
・・・(中略)・・・
121 r=1 g=2 b=3 :   51  102 153
・・・(中略)・・・
215 r=5 g=5 b=5 :   255 255 255

これをビデオDAコンバータのパレットに追加しています。 復習ですが色は特定のレジスタではなく、ハードウェア(ビデオDAコンバータ)に持っているんですね。 4日目に色の登録に使ったコードを再度利用します。

void set_palette(int start, int end, unsigned char *rgb)
{
    int i, eflags;
    eflags = io_load_eflags();  /* 割り込み許可フラグの値を記録する */
    io_cli();                   /* 許可フラグを0にして割り込み禁止にする */
    io_out8(0x03c8, start);
    for (i = start; i <= end; i++) {
        io_out8(0x03c9, rgb[0] / 4);
        io_out8(0x03c9, rgb[1] / 4);
        io_out8(0x03c9, rgb[2] / 4);
        rgb += 3;
    }
    io_store_eflags(eflags);    /* 割り込み許可フラグを元に戻す */
    return;
}
  • 0x3c8:パレット番号
  • 0x3c9:RGB(色のデータ)

ここにどかっと作った216色分のデータが入った配列を突っ込みます。

set_palette(0, 15, table_rgb); //今まで使っていた色のやつ

・・・(中略(table2を作る処理))・・・

set_palette(16, 231, table2);  //今回追加する配列分

f:id:yuyubu:20180623134643p:plain

36色しか出していないようでしたので、自分で改造して全色(232色)出して見ました。

void HariMain(void)
{
    char *buf;
    int win, x, y, r, g, b;
    api_initmalloc();

    int box_size  = 8;  //色のボックのサイズ(px)
    int box_amt_x = 32; //色のボックスを水平に何個置くか
    int box_amt_y = 8;  //色のボックスを縦に何個置くか


    int content_x = box_size * box_amt_x;
    int content_y = box_size * box_amt_y;


    int win_bar_height = 20;                  //バーの高さ
    int padding = 8;                          //コンテンツの余白
    int bar_space = win_bar_height + padding; //コンテンツの描画が始まる高さ


    int win_x_siz = 256 + padding *2;
    int win_y_siz = content_y + win_bar_height + padding * 2;

    buf = api_malloc(win_x_siz * win_y_siz);
    win = api_openwin(buf, win_x_siz, win_y_siz, -1, "color");

    for(y = 0; y < content_y; y++){
        for(x = 0; x < content_x; x++){
            buf[(x + padding) + (y + bar_space) * win_x_siz ] = (x / box_size) +(y / box_size) * box_amt_x;
        }
    }

    api_refreshwin(win, padding,bar_space ,
        content_x+padding,content_y+bar_space);
    api_getkey(1);
    api_end();
}

32列 * 8行=256色で出して見ました。 当然232色しか登録してないので、登録していない残りの24色はありません。

f:id:yuyubu:20180623134708p:plain

何も設定せず表示したところ、233色以降、変な色?が出たのでgraphic.cを改造して赤色で埋めて見ました。

f:id:yuyubu:20180623134721p:plain

一行目左端から16個分は昔に登録した色が出ています。

コンソールを増やす(2個にする)

コンソールを増やすというより、まず2個作ってみて問題のある箇所を探す、という感じでインクリメンタルに改修していきます。

diffツール

sdiffというコマンドでかなりわかりやすくソースの差分が見れます。 ちょっと色塗ってくれればwinmergeみたいに使えますね。

sdiffは-wオプションで表示幅を変更できるようですが、 ネストが深すぎて私のノートPCだと最高解像度でも収まりませんでした。

$ sdiff -l -w 240 -W ../harib22d/bootpack.c bootpack.c |nkf
  • 使ってるオプション
    • l:変更のない行は左ファイルのみ表示
    • -w <数字>:一行あたりの文字数
    • -W :空白の変更を無視

-lは変更有無がちょっと見にくいので抜いたほうがいいかもしれません。

他にもdiffツールを探して見たところvimdiffというものがあるようです(vimが使えるならこれが最強?)

再起動現象再現せず(harib22i)

harib22iの再起動現象が再現しませんでした。一応証拠動画(エビデンス(笑))みたいなの撮って見た。

www.youtube.com

感想

この章はさらなるGUI機能強化のためのリファクタリング + バグ回収という趣旨が強いと思います。 最初から綺麗に設計していないこともあり、ソースを弄るたびになんらかのバグが発生しますが、 あまり手がかりのない状態で的確にバグの原因を見つけて行っている印象があります。

うまくいかなくなった原因は書き足したり削除した部分のどこかにあると考えて、元に戻して見たりして探すのである。

だそうですがprint文デバッグであったり,Eclipseのデバッガのようなものがないと私には難しいです。 一般保護例外の表示を拡張しましたが、そもそもそれが出ずにOS自体がシャットダウンしてしまうこともよく起こります。

30日OS自作本24日目

24日目の内容になります。

ウインドウ操作

  • タイマー
  • ウインドウのドラッグ移動
  • Xボタンの有効化
  • アクティブなウインドウの切り替え

この辺は動画で見た方が面白いと思います。

www.youtube.com

ホストOS側からQEMUにF11キーが送れない

F11キーがQEMU側で反応するより前に、mac側のディスクトップを表示する機能が発動してしまうのでとりあえず無効化しておきました。 Mac使っている人は参考にして見てください。

タイマー終了時に発生する謎の文字

タイマーを利用しているアプリケーションを強制終了した際に、タイマーの通知がFIFOを通じてコンソールのキー入力の箇所に届いてしまう問題がありました。

f:id:yuyubu:20180622190935p:plain

このデータは何由来のものか、本書では解説されていなかったので調べて見ました。

hankaku.txtに登録されている

char 0x80
........
..***...
.*...*..
*.....*.
*.......
*.......
*.......
*.......
*.......
*.......
*.......
*.....*.
.*...*..
..***...
...*....
..*.....

これですね。(0x80=128)asciiは7bit符号化?らしいので127(0b1111111)までしか定義されていないらしいが、一部の拡張文字セット?(extend-ascii-code)というもので128にこれが登録されているものがあった。ちなみにこれはセディーユという記号らしい。

GUI雑感

この章でMain関数が非常に複雑になってしまいます。こういうのを見るとGUIモデリングの格好の対象だなと思います。

学生の頃はワークフローとか在庫管理とか面白くなさそう、と思っていましたが、 オブジェクト指向の題材としてそういうものが取り上げられていることが多く、熱心に勉強していませんでした。

が、社会人になってから気付きましたが、複雑な業務ルールをクラス化・テーブル化したりするのは意外と楽しいものです。

OSをオブジェクト指向で作ろうとするとドメイン(=システム化の対象領域)も技術的なものなので技術がやりたい人(ビジネス的な部分を考えたくない人)にとってはオブジェクト指向を学ぶ題材としてすごく向いてると思います。