Yabu.log

ITなどの雑記

デスクトップ周りのケーブルは全て巻き取り式 or 短いものに買い替えた

絡まった配線を解く作業は時間の無駄だけでなく、精神を蝕むストレスの原因になっているとおもう。 そんな作業はもうやりたくないのでテーブルの上の配線類は全て短いケーブルか巻き取り式のものに変えた。

  • USB type-C

これめちゃくちゃ便利です。Type-Cの端子がついたノートPC持ってる人は全員買うべきだと思います。 Macのデフォでついているゴワゴワした長いType-Cケーブルはもう絶対に持ち歩きたくありません

  • Lightningケーブル
  • mini usb

短いLightnignケーブルはAmazonのブランドで売られているものの10cmを選択すれば買えます ちなみに無線充電も試しましたが、不要だと感じたので使っていません。

  • オーディオ系

airpodsを持っているので基本的にBTが使えるデバイスではそちらを利用。 どうしてもオーディオジャックしかついておらず使えないもの、例えば写真右のFMラジオなど は右の巻き取り式のイヤホンを使っている

これはやりすぎな感じがありますが、徹底してやるつもりだったので例外は設けずやりました。 持ち歩く機会はありませんが、基本的にかなり短い状態で使っているので掃除するとき等、他のケーブルに絡まることが少なくなりました。*1

これでディスクトップ周りの線で長いものは

  • ディスプレイの電源
  • タップ

のみになりました。 これで絡まった線を解く作業に精神を疲弊させることは今後ありません。万歳。

先頭で紹介しているType-Cのケーブルはマジでオススメです。

*1:というよりもう他に絡まるケーブルがないけど

C言語ポインタ勉強会に参加

オンラインで開催。

f:id:yuyubu:20180811224641p:plain

atnd.org

少人数制で東工大の先生(uchan_nos)がポインタについてわかりやすく教えてくれるという贅沢な1.5Hでした

typedefでやると関数ポインタがわかりやすくなる

typedef int add_type(int,int);
int add(int a, int b);
add_type* fp = add;

typedef int* int_ptr_type;
int_ptr_type arr[10];

関数ポインタがわかりにくい、という人は、ひとまずこれでポインタ型を説明すると、 関数ポインタの演算子の優先度のために無理やり挿入された()に煩わされずに済むと思います。*1

実例

#include<stdio.h>

typedef int add_type(int,int);

int add(int a, int b);

int main(){

  //typedefバージョン。わかりやすい
  add_type* fp = add;
  int num = fp(1,3);
  printf("%d\n",num);//4が出力される

  //普通の関数ポインタバージョン
  int (*fp2)(int,int) = add;
  int num2 = fp2(2,2);
  printf("%d\n",num2);//4が出力される

  //普通の関数呼び出し
  int num3 = add(3,1);
  printf("%d\n",num3);//4が出力される

}

int add(int a,int b){
  return a+b;
}

メモリレイアウト

メモリには

  • .text:実行可能,読み取り可能,変更不可能
  • .data:実行不可能,読み取り可能,変更可能
  • .rodata:実行不可能,読み取り可能,変更不可能

な3領域がある

質問:char配列とcharポインタを文字列リテラルで初期化した時の違いについて

Char型文字配列を文字リテラルで初期化した時と Char型アドレスを文字リテラルで初期化した時の違いですが、

前者は値の変更ができるが、後者は値の変更ができないように思えますが、 これは前者だとヒープ、後者だと静的領域に確保していて変更できないということでしょうか

char test1[] = "test\0";
char *test2 = "test\0";

test1[1] = 't';
puts(test1); 
test2[1] = 't';//エラー
puts(test2);

質問回答

char配列を文字列リテラルで初期化した場合は、スタックに確保される(基本的に関数内のローカル変数はスタックで初期化される)

しかし、char配列を文字列リテラルで初期化した場合は、どこにデータが置かれるかわからないので*2、環境によっては変更不可能なrodataに置かれたり、.dataに置かれたして、変更可能か不可能かは不安定。だからコンパイルエラーにならず実行時エラーになる。

  • ちなみに私の環境だとbus errorというものが出る。
  • busエラーとは何か?
    • busエラーはCの未定義動作でよくでてくるエラー
  • ポイント:関数の中でローカル変数を宣言するとスタック変数になる。
  • ヒープは関係ない。mallocした時しか使われない

用語

初めて聞くものがいくつかありました。 一般的ではないアカデミックな世界の用語?と最初は思いましたが、ググったら普通にでてくるので一般的なものだと思います*3

  • ダブルポインタ:ポインタのポインタのこと。凡例:**pp
  • 参照外し(dereference):ポインタ変数の参照先の値を取ってくること。凡例:int* p = &num,int x = *p

ダブルポインタの逆アセンブル結果の解説

講義で使われた擬似コードどほぼ同じコードを書いて逆アセンブルしたところ、講師のuchanさんに1行ずつ解説していただけました!

#include<stdio.h>

int main(){
  int x = 42;
  int *p = &x;  //ポインタ
  int **pp = &p; //ダブルポインタ
  printf("%d\n",**pp); //42が出力される
}

アセンブルしたニーモニックにコメントを振る形で掲載します。

int main(){
push   rbp
mov    rbp,rsp
;準備。説明略


sub    rsp,0x20
;32バイトを引いて、変数が収まる範囲を準備してあげる
;本来なら8バイト変数2つ(`p,pp`)と4バイト変数一つ(`x`)の合計20バイト分で十分だが、
;アライメントの関係で16倍の数値で確保している

lea    rdi,[rip+0x57]        # 100000fa6 <_main+0x66>
lea    rax,[rbp-0x10];raxにはpのアドレスが
lea    rcx,[rbp-0x4];rcxにはxのアドレスが入ってる

;※leaはメモリから読みだすという操作をしない。のでrbp-0x4のアドレスがレジスタそのまま入る
;lea命令はコンパイラがよく使う
;余談:lea命令を使うと複雑な命令が1命令でできたりする。(掛け算とか)


  ;int x = 42;
mov    DWORD PTR [rbp-0x4],0x2a
;rbpはスタックのベースポインタ
;アドレスは低い方に成長していく。
;スタックの最上位に4バイトのデータ(int型)であるxを入れる。

  ;int *p = &x;
mov    QWORD PTR [rbp-0x10],rcx
;pのアドレス(rbp-0x10)の先に、xのアドレスを代入する
;注:アドレスのサイズは8バイト

  ;int **pp = &p;
mov    QWORD PTR [rbp-0x18],rax
;スタックの一番最下層に,`**pp`を確保しそこにrax(pのアドレスを格納する)

  ;printf("%d\n",**pp);
mov    rax,QWORD PTR [rbp-0x18] ;pのアドレスがraxに入る,  **(pp)
mov    rax,QWORD PTR [rax]      ;xのアドレスがraxが入る   *(*pp)
mov    esi,DWORD PTR [rax]      ;4バイト読みだしてesi    (**p)
;末尾の()付きの部分が実行されたイメージ。

mov    al,0x0
call   100000f84 <_main+0x44>
xor    esi,esi
}

一応逆アセンブル結果の全体を貼っておきます

念のため。僕が作ったデタラメな擬似コードじゃないよ〜という意味で。

$ gobjdump -d -S -M intel ./a.out

./a.out:     ファイル形式 mach-o-x86-64


セクション .text の逆アセンブル:

0000000100000f40 <_main>:
#include<stdio.h>

int main(){
   100000f40:   55                      push   rbp
   100000f41:   48 89 e5                mov    rbp,rsp
   100000f44:   48 83 ec 20             sub    rsp,0x20
   100000f48:   48 8d 3d 57 00 00 00    lea    rdi,[rip+0x57]        # 100000fa6 <_main+0x66>
   100000f4f:   48 8d 45 f0             lea    rax,[rbp-0x10]
   100000f53:   48 8d 4d fc             lea    rcx,[rbp-0x4]
  int x = 42;
   100000f57:   c7 45 fc 2a 00 00 00    mov    DWORD PTR [rbp-0x4],0x2a
  int *p = &x;
   100000f5e:   48 89 4d f0             mov    QWORD PTR [rbp-0x10],rcx
  int **pp = &p;
   100000f62:   48 89 45 e8             mov    QWORD PTR [rbp-0x18],rax
  printf("%d\n",**pp);
   100000f66:   48 8b 45 e8             mov    rax,QWORD PTR [rbp-0x18]
   100000f6a:   48 8b 00                mov    rax,QWORD PTR [rax]
   100000f6d:   8b 30                   mov    esi,DWORD PTR [rax]
   100000f6f:   b0 00                   mov    al,0x0
   100000f71:   e8 0e 00 00 00          call   100000f84 <_main+0x44>
   100000f76:   31 f6                   xor    esi,esi
}
   100000f78:   89 45 e4                mov    DWORD PTR [rbp-0x1c],eax
   100000f7b:   89 f0                   mov    eax,esi
   100000f7d:   48 83 c4 20             add    rsp,0x20
   100000f81:   5d                      pop    rbp
   100000f82:   c3                      ret    

セクション __TEXT.__stubs の逆アセンブル:

0000000100000f84 <__TEXT.__stubs>:
   100000f84:   ff 25 86 00 00 00       jmp    QWORD PTR [rip+0x86]        # 100001010 <_main+0xd0>

セクション __TEXT.__stub_helper の逆アセンブル:

0000000100000f8c <__TEXT.__stub_helper>:
   100000f8c:   4c 8d 1d 75 00 00 00    lea    r11,[rip+0x75]        # 100001008 <_main+0xc8>
   100000f93:   41 53                   push   r11
   100000f95:   ff 25 65 00 00 00       jmp    QWORD PTR [rip+0x65]        # 100001000 <_main+0xc0>
   100000f9b:   90                      nop
   100000f9c:   68 00 00 00 00          push   0x0
   100000fa1:   e9 e6 ff ff ff          jmp    100000f8c <_main+0x4c>

セクション __TEXT.__unwind_info の逆アセンブル:

0000000100000fac <__TEXT.__unwind_info>:
   100000fac:   01 00                   add    DWORD PTR [rax],eax
   100000fae:   00 00                   add    BYTE PTR [rax],al
   100000fb0:   1c 00                   sbb    al,0x0
   100000fb2:   00 00                   add    BYTE PTR [rax],al
   100000fb4:   00 00                   add    BYTE PTR [rax],al
   100000fb6:   00 00                   add    BYTE PTR [rax],al
   100000fb8:   1c 00                   sbb    al,0x0
   100000fba:   00 00                   add    BYTE PTR [rax],al
   100000fbc:   00 00                   add    BYTE PTR [rax],al
   100000fbe:   00 00                   add    BYTE PTR [rax],al
   100000fc0:   1c 00                   sbb    al,0x0
   100000fc2:   00 00                   add    BYTE PTR [rax],al
   100000fc4:   02 00                   add    al,BYTE PTR [rax]
   100000fc6:   00 00                   add    BYTE PTR [rax],al
   100000fc8:   40 0f 00 00             rex sldt WORD PTR [rax]
   100000fcc:   34 00                   xor    al,0x0
   100000fce:   00 00                   add    BYTE PTR [rax],al
   100000fd0:   34 00                   xor    al,0x0
   100000fd2:   00 00                   add    BYTE PTR [rax],al
   100000fd4:   84 0f                   test   BYTE PTR [rdi],cl
   100000fd6:   00 00                   add    BYTE PTR [rax],al
   100000fd8:   00 00                   add    BYTE PTR [rax],al
   100000fda:   00 00                   add    BYTE PTR [rax],al
   100000fdc:   34 00                   xor    al,0x0
   100000fde:   00 00                   add    BYTE PTR [rax],al
   100000fe0:   03 00                   add    eax,DWORD PTR [rax]
   100000fe2:   00 00                   add    BYTE PTR [rax],al
   100000fe4:   0c 00                   or     al,0x0
   100000fe6:   01 00                   add    DWORD PTR [rax],eax
   100000fe8:   10 00                   adc    BYTE PTR [rax],al
   100000fea:   01 00                   add    DWORD PTR [rax],eax
   100000fec:   00 00                   add    BYTE PTR [rax],al
   100000fee:   00 00                   add    BYTE PTR [rax],al
   100000ff0:   00 00                   add    BYTE PTR [rax],al
   100000ff2:   00 01                   add    BYTE PTR [rcx],al

*1:いずれどこかで読み解く必要はありますが。

*2:.textエリアには置かれないと思うが

*3:私はC言語は入門書を一冊読んだだけなので語彙が少ない。

C言語で美咲フォントを扱う1

とりあえずfontx2ファイルを読み込んでヘッダだけ表示してみた。

https://github.com/yuyabu/MisakiFont-sandbox

>./font
hdader size is 22
filesignature is FONTX
fontname is MISAKI
width is 8
hight is 8
char code flg is 1
blocks is 5c
start_code is 4081
end_code is 7e81

一応MakeFileも作ってる。 警告マックスのオプションつけてるだけだけど。。。

font:font.c font.h
    gcc -W -Wall -o font font.c

まずはヘッダを定義している構造体。こういうファイルフォーマットのヘッダって多分誰かがすでに作っているけど、自分で書いて見た

 typedef struct{
   char filesignature[6];
   char fontname[8];
   char font_w;
   char font_h;
   char char_code_flg;
   char blocks;
   char start_code[2];
   char end_code[2];
 } fontx2header;

情報を出力している箇所のみコードで出してみる

//この書き方はできないみたい。filesignatureの開始アドレスからnull文字までが全部出てしまう。
//printf("filesignature is %s",header.filesignature);

char temp[100];//ポインタで用意すると色々トラブル発生

 strncpy(temp,header.filesignature,5);

 printf("filesignature is %s\n" ,temp                );

 strncpy(temp,header.fontname,7);
 
 printf("fontname is %s\n"      ,temp                );
 printf("width is %x\n"         ,header.font_w       );
 printf("hight is %x\n"         ,header.font_h       );
 printf("char code flg is %x\n" ,header.char_code_flg);
 printf("blocks is %x\n"        ,header.blocks       );

 //配列をそのまま16進数で表示する方法がわからないのでかなり頭の悪いやり方でやってる
 printf("start_code is %x%x\n"   ,(unsigned char)header.start_code[0] ,(unsigned char)header.start_code[1]);
 printf("end_code is %02x%02x\n" ,(unsigned char)header.end_code[0]   ,(unsigned char)header.end_code[1]  );

char配列をprintfで16進数表示する方法がわからないので かなりアドホックなコードを書いた。

 //配列をそのまま16進数で表示する方法がわからないのでかなり頭の悪いやり方でやってる
 printf("start_code is %x%x\n"   ,(unsigned char)header.start_code[0] ,(unsigned char)header.start_code[1]);
 printf("end_code is %02x%02x\n" ,(unsigned char)header.end_code[0]   ,(unsigned char)header.end_code[1]  );

文字列を表示のためだけにバッファする変数はchar配列にすると色々面倒が起きずにすむ。

char temp[100];//ポインタで用意すると色々トラブル発生

ポインタにしたり、ポインタにして文字で初期化したものをtempに使って 変更を加えたりするとbus errorがでる。

多分静的変数領域に確保されて 実行時に変更できないという意味だと思う。

配列だとheapにできるので実行時に変更できる、のだと思う。

C言語ちょっと勉強してから空いてたり、色々他にも学んでいるせいで(Goとか) 文法など間違いまくるが、ググり直せばすぐかけるようになるし、 エラー見て大体トラブルシュートできるようになった。 少なくとも総当たりでいじってデバッグみたいなことはなくなった。

やっぱりちゃんと勉強しておいてよかったC言語

次回はコンソールに美咲フォントを出力して見たい

情報源

fontx2の情報 http://elm-chan.org/docs/dosv/fontx.html https://qiita.com/meketen/items/a66c6953b3cff380d9b8

C言語でファイル読み書き http://wisdom.sakura.ne.jp/programming/c/c43.html http://simd.jugem.jp/?eid=50

第13回osdev-jpもくもく会に参加してLTしました

非常に有意義な1日でした。

LT

https://speakerdeck.com/yuyabu/gong-kai-yong

スパコンのOSを書いてる人、RustでOSを作っている人(2人も!)が登壇したりなどで、 色々と格差を感じるものがありましたがなんとかここ半月ほど調べていたことを20分くらいで話すことが出来ました。

ここ1週間くらいは日課のランニングができなかったり、3ヶ月ほど毎日書いていたブログが一切かけないくらいデバッグ/調査の日々でした。 前日は4時くらいまで起きて発表内容をまとめていたので、当日はちょっと疲労困憊でろれつが回っていませんでした。(解放されてちょっとホッとしている)

初めてのLTで不慣れなこともあり、上級者の人にとってはあまり面白いものではなかったと思うし、初心者の人には何を言っているか意味不明な発表になってしまっていたと思います。当日は会場の入り口で迷ったり、初めての参加で雰囲気に慣れなかったりなどでとにかく余裕がなかった。もうちょっと聞く側のことを考えるべきだと思います。

私は発表の中でデバッグを始めるのは簡単、と言いましたが、何点かハマりポイントがありますので、後日解説するブログを書きたいと思います。

解消された疑問

CPUに超絶詳しそうな人が目の前にいたので日頃の疑問を聞いて見ました。

CRレジスタのPEフラグが立った状態でセグメントレジスタ(CS)が0を指している状態はどういう意味か?

GDTの0番目のエントリはnullセレクタを格納するように決まっています。

Because the first entry of the GDT is not used by the processor, a selector that has an index of zero and a table indicator of zero (i.e., a selector that points to the first entry of the GDT), can be used as a null selector. The processor does not cause an exception when a segment register (other than CS or SS) is loaded with a null selector.

これはhariboteOSもその通りになっています

GDT0:
        RESB    8               ; ヌルセレクタ ※RESBはオペランドで指定したバイト数分0で埋める命令
        DW      0xffff,0x0000,0x9200,0x00cf ; 読み書き可能セグメント32bit
        DW      0xffff,0x0000,0x9a28,0x0047 ; 実行可能セグメント32bit(bootpack用)

ですが、null selectorをセグメントレジスタで指定した場合は一般保護例外が発生します。

が、以下にhariboteOSの2段目のIPLでプロテクトモードに切り替えている箇所を張りますが、CR0レジスタのPEフラグを立てた後(7行目)、CSレジスタ以外のセグメントレジスタを8にしてCSレジスタが0のまま OS本体をコピーする処理(bootpackの転送のコメント以下)が動作しています。

; プロテクトモード移行

        LGDT    [GDTR0]         ; 暫定GDTを設定
        MOV     EAX,CR0
        AND     EAX,0x7fffffff  ; bit31を0にする(ページング禁止のため)
        OR      EAX,0x00000001  ; bit0を1にする(プロテクトモード移行のため)
        MOV     CR0,EAX
        JMP     pipelineflush
pipelineflush:
        MOV     AX,1*8          ;  読み書き可能セグメント32bit
        MOV     DS,AX
        MOV     ES,AX
        MOV     FS,AX
        MOV     GS,AX
        MOV     SS,AX

; bootpackの転送

        MOV     ESI,bootpack    ; 転送元
        MOV     EDI,BOTPAK      ; 転送先
        MOV     ECX,512*1024/4
        CALL    memcpy

; ついでにディスクデータも本来の位置へ転送

; まずはブートセクタから

        MOV     ESI,0x7c00      ; 転送元
        MOV     EDI,DSKCAC      ; 転送先
        MOV     ECX,512/4
        CALL    memcpy

; 残り全部

        MOV     ESI,DSKCAC0+512 ; 転送元
        MOV     EDI,DSKCAC+512  ; 転送先
        MOV     ECX,0
        MOV     CL,BYTE [CYLS]
        IMUL    ECX,512*18*2/4  ; シリンダ数からバイト数/4に変換
        SUB     ECX,512/4       ; IPLの分だけ差し引く
        CALL    memcpy

; asmheadでしなければいけないことは全部し終わったので、
;   あとはbootpackに任せる

; bootpackの起動

        MOV     EBX,BOTPAK
        MOV     ECX,[EBX+16]
        ADD     ECX,3           ; ECX += 3;
        SHR     ECX,2           ; ECX /= 4;
        JZ      skip            ; 転送するべきものがない
        MOV     ESI,[EBX+20]    ; 転送元
        ADD     ESI,EBX
        MOV     EDI,[EBX+12]    ; 転送先
        CALL    memcpy
skip:
        MOV     ESP,[EBX+12]    ; スタック初期値
        JMP     DWORD 2*8:0x0000001b

この状態の時CSレジスタは0つまりnull selectorを指しているのではないか?なぜ一般保護例外が上がらずに動いていると疑問を持っていましたが、CPUがリアルモードからプロテクトモードに変わるタイミングはPEフラグに1を入れた後ではなく、そのあとにJMP命令(多分far jump)をしたタイミングだったのです。

CPUのドキュメントにもPEフラグを立てた後はJUMP命令を実行せよと書かれています

Immediately after setting the PE flag, the initialization code must flush the processor's instruction prefetch queue by executing a JMP instruction.

出典:INTEL 80386 PROGRAMMER'S REFERENCE MANUAL 1986/10.3 Switching to Protected Mode*1

だとするとPEビットONからfar JUMPまでの間、変な状態でメモリ書き込みを行っているような気がしますね。

Haribote OSをCやnasmの情報付きでデバッグすることはできるか?

スライドを見ればわかる通り、デバッグはシンボル情報なしで機械語をステップ実行する形で行いました。 一般的にデバッグは普通機械語そのものではなく、コンパイル前のプログラムに対してステップ実行すると思いますが、 デバッグ情報付きでOSをコンパイルする方法がわからなかったので、*2相談してみたところできなさそうです。

ただし、著者オリジナルのツールを一切使わずにhariboteOSを作り上げた場合はその限りではないかもしれません。*3

もくもく会の感想

もくもく会ですが、強い人多すぎ!みたいな感想多いですが、 教育に関わっている人や学生の人、社会人で趣味でやってる人、本業の人など 参加者の層のボリュームが厚いというのが私の印象です。

私もこの会は3回ほど前から参加してみたいな〜と思いつつ 勝手にOS自作本読破が義務教育と思い込んで尻込みしていたのですが、 割と現在進められている方が参加されてかなり有意義なサポートを受けられている様子も見受けられるので、尻込みしている方もどんどん参加すればいいんじゃないかなと思います。

普段はこのレイヤーのトークができる人が周りにいないので雑談するだけでも楽しかったです。

あとサイボウズさんの食堂?的なスペースかっこいいですね。

もくもく

あまり作業出来ませんでしたが、パタヘネの機械語の学習環境を作ろうとしていました。

現在パタヘネを読んでいるのですが、MIPS向けのマシン語を学ぶ章に突入しました。 C言語MIPSマシン語コンパイルしたり、 バイナリを逆アセンブルする課題などあるのですが、 どれも紙面上の話なので実際のコンパイラ、逆アセンブルを動かして見たいと思い、 検証環境を調査しました。

自分のやりたいこととしてはクロスコンパイラ、というものを使って動作しているCPUとは別ターゲットのCPU(MIPS)のアセンブルを作る、とうことが該当しそうです

ほんとは生成したバイトコードをシュミレータか何かで動かせるといいのですが。

gcc -march=mips32 sample.c -o sample

みたいな感じで動かせるかなと思ったけど、うまくいきませんでした。 自分のmacOSgccは多分Xcodeコマンドラインツールで入れたものなので、対応していないのかもしれまんせん。

$ gcc --version
Configured with: --prefix=/Applications/Xcode.app/Contents/Developer/usr --with-gxx-include-dir=/usr/include/c++/4.2.1
Apple LLVM version 9.1.0 (clang-902.0.39.2)
Target: x86_64-apple-darwin17.7.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin

この辺試せばいけそう?な気がする

http://www.theairportwiki.com/index.php?title=Building_a_cross_compile_of_GCC_for_MIPS_on_OS_X

vagrantを使ってlinuxでやる

https://www.youtube.com/watch?v=AVTRTR589bk&t=305s&frags=pl%2Cwn

vagrantを入れる作戦で進めてみます。 (そういえばxv6の改造の発表をやってた人もvagrant使ってたような?)

Web版のmipsお試しコンパイラ

こういうのもある。ちょっとした検証にはいいかもしれないけど、 リンカとか使ったアドレスを置換する挙動とかも見てみたいし力不足感はある。

http://reliant.colab.duke.edu/c2mips/

教育用の組み込みOSの話

教育用の組み込みOSの話をしている人の発表が刺さりました。

結局昨今の教育はデバイスを叩くだけで終わる、と仰っていましたが、自分の大学時代を思い出すとすごく心当たりがあります。

マイコンをいじる授業でひたすらデータシートを読んでピンの配線を確認したりなど、そういうことに時間を使いすぎていたと思います。そういうものはハードが変われば無駄になる部分が多いので、もっと制御とは何かみたいな本質的な部分を納得するまで学ぶべきだったと思います。*4

私が発表でやったことも結局半分くらいはBIOSというもうすぐ廃止される規格と戯れるものだったので、 痛いほどよくわかります。

もっと視野を広げて勉強しないといけないなと思った1日でした。

*1:386の資料で申し訳ございません

*2:hariboteOSではオリジナルのCコンパイラを使っている。多分GCCを多少いじっているもの

*3:それがhariboteOSと言えるのか不明ですが

*4:もちろん新しいハードを扱う必要がきた時に、データシートをみてサクッと使い方を理解するようなスキルも必要だと思いますが。

486のセグメントデスクリプタが直感的に読めないようになっているワケ

486のセグメントデスクリプタは...

63       55       47       39       31       23       15       7        0
+--------+----+----+--------+--------+--------+--------+--------+--------+
|base_hi |ar  |l_hi|   ar   |base_mid|     base_low    |    limit_low    |
+--------+----+----+--------+--------+--------+--------+--------+--------+

こんな感じで上位2ビットを分解して各メンバに分解しないと解読できません。 なんでこんな面倒な作りになっているのだろう?と思っていましたが、本日はじめて読む486を読んでいたところ腑に落ちました。


63       55       47       39       31       23       15       7        0

286のセグメントデスクリプタ
                   +--------+--------+--------+--------+--------+--------+
                   |   ar   |           base           |    limit        |
                   +--------+--------+--------+--------+--------+--------+

         
この辺が486で追加された
<------------------>

+--------+----+----+--------+--------+--------+--------+--------+--------+
|base_hi |ar  |l_hi|   ar   |base_mid|     base_low    |    limit_low    |
+--------+----+----+--------+--------+--------+--------+--------+--------+

こんな感じで下位ビットは286と同じ形式にして互換性を持たせるためだったんですね。

objdumpによる逆アセンブルとgdbの命令形式表示の比較

02_day/helloos5で比較しています。 このOS(というかプログラム)はブートセクタの中でhello worldを画面に出力するだけのものになります。

元のソース(アセンブリ)

; hello-os
; TAB=4

        ORG     0x7c00          ; このプログラムがどこに読み込まれるのか

; 以下は標準的なFAT12フォーマットフロッピーディスクのための記述

        JMP     entry
        DB      0x90
        DB      "HELLOIPL"      ; ブートセクタの名前を自由に書いてよい(8バイト)
        DW      512             ; 1セクタの大きさ(512にしなければいけない)
        DB      1               ; クラスタの大きさ(1セクタにしなければいけない)
        DW      1               ; FATがどこから始まるか(普通は1セクタ目からにする)
        DB      2               ; FATの個数(2にしなければいけない)
        DW      224             ; ルートディレクトリ領域の大きさ(普通は224エントリにする)
        DW      2880            ; このドライブの大きさ(2880セクタにしなければいけない)
        DB      0xf0            ; メディアのタイプ(0xf0にしなければいけない)
        DW      9               ; FAT領域の長さ(9セクタにしなければいけない)
        DW      18              ; 1トラックにいくつのセクタがあるか(18にしなければいけない)
        DW      2               ; ヘッドの数(2にしなければいけない)
        DD      0               ; パーティションを使ってないのでここは必ず0
        DD      2880            ; このドライブ大きさをもう一度書く
        DB      0,0,0x29        ; よくわからないけどこの値にしておくといいらしい
        DD      0xffffffff      ; たぶんボリュームシリアル番号
        DB      "HELLO-OS   "   ; ディスクの名前(11バイト)
        DB      "FAT12   "      ; フォーマットの名前(8バイト)
        RESB    18              ; とりあえず18バイトあけておく

; プログラム本体

entry:
        MOV     AX,0            ; レジスタ初期化
        MOV     SS,AX
        MOV     SP,0x7c00
        MOV     DS,AX
        MOV     ES,AX

        MOV     SI,msg
putloop:
        MOV     AL,[SI]
        ADD     SI,1            ; SIに1を足す
        CMP     AL,0
        JE      fin
        MOV     AH,0x0e         ; 一文字表示ファンクション
        MOV     BX,15           ; カラーコード
        INT     0x10            ; ビデオBIOS呼び出し
        JMP     putloop
fin:
        HLT                     ; 何かあるまでCPUを停止させる
        JMP     fin             ; 無限ループ

msg:
        DB      0x0a, 0x0a      ; 改行を2つ
        DB      "hello, world"
        DB      0x0a            ; 改行
        DB      0

        RESB    0x7dfe-$        ; 0x7dfeまでを0x00で埋める命令

        DB      0x55, 0xaa

gobjdumpによる逆アセンブル

$ gobjdump -b binary -m i386 -M intel -D helloos.img

helloos.img:     ファイル形式 binary


セクション .data の逆アセンブル:

00000000 <.data>:
       0:   eb 4e                   jmp    0x50
       2:   90                      nop
       3:   48                      dec    eax
       4:   45                      inc    ebp
       5:   4c                      dec    esp
       6:   4c                      dec    esp
       7:   4f                      dec    edi
       8:   49                      dec    ecx
       9:   50                      push   eax
       a:   4c                      dec    esp
       b:   00 02                   add    BYTE PTR [edx],al
       d:   01 01                   add    DWORD PTR [ecx],eax
       f:   00 02                   add    BYTE PTR [edx],al
      11:   e0 00                   loopne 0x13
      13:   40                      inc    eax
      14:   0b f0                   or     esi,eax
      16:   09 00                   or     DWORD PTR [eax],eax
      18:   12 00                   adc    al,BYTE PTR [eax]
      1a:   02 00                   add    al,BYTE PTR [eax]
      1c:   00 00                   add    BYTE PTR [eax],al
      1e:   00 00                   add    BYTE PTR [eax],al
      20:   40                      inc    eax
      21:   0b 00                   or     eax,DWORD PTR [eax]
      23:   00 00                   add    BYTE PTR [eax],al
      25:   00 29                   add    BYTE PTR [ecx],ch
      27:   ff                      (bad)  
      28:   ff                      (bad)  
      29:   ff                      (bad)  
      2a:   ff 48 45                dec    DWORD PTR [eax+0x45]
      2d:   4c                      dec    esp
      2e:   4c                      dec    esp
      2f:   4f                      dec    edi
      30:   2d 4f 53 20 20          sub    eax,0x2020534f
      35:   20 46 41                and    BYTE PTR [esi+0x41],al
      38:   54                      push   esp
      39:   31 32                   xor    DWORD PTR [edx],esi
      3b:   20 20                   and    BYTE PTR [eax],ah
      3d:   20 00                   and    BYTE PTR [eax],al
    ...
      4f:   00 b8 00 00 8e d0       add    BYTE PTR [eax-0x2f720000],bh
      55:   bc 00 7c 8e d8          mov    esp,0xd88e7c00
      5a:   8e c0                   mov    es,eax
      5c:   be 74 7c 8a 04          mov    esi,0x48a7c74
      61:   83 c6 01                add    esi,0x1
      64:   3c 00                   cmp    al,0x0
      66:   74 09                   je     0x71
      68:   b4 0e                   mov    ah,0xe
      6a:   bb 0f 00 cd 10          mov    ebx,0x10cd000f
      6f:   eb ee                   jmp    0x5f
      71:   f4                      hlt    
      72:   eb fd                   jmp    0x71
      74:   0a 0a                   or     cl,BYTE PTR [edx]
      76:   68 65 6c 6c 6f          push   0x6f6c6c65
      7b:   2c 20                   sub    al,0x20
      7d:   77 6f                   ja     0xee
      7f:   72 6c                   jb     0xed
      81:   64 0a 00                or     al,BYTE PTR fs:[eax]
    ...
     1fc:   00 00                   add    BYTE PTR [eax],al
     1fe:   55                      push   ebp
     1ff:   aa                      stos   BYTE PTR es:[edi],al
     200:   f0 ff                   lock (bad)
     202:   ff 00                   inc    DWORD PTR [eax]
    ...
    1400:   f0 ff                   lock (bad)
    1402:   ff 00                   inc    DWORD PTR [eax]
    ...

結果としてわかることですが 0~0x3dはDB命令で書き込んでいるデータ(アセンブリで言う所のとりあえず18バイトあけておく以上の箇所) で、コードとして意味がある表示ができている箇所は0x50~0x72までですね

gdbのxコマンドによる命令表示

(gdb) x/38bi 0x7c00
   0x7c00:  jmp    0x7c50
   0x7c02:  nop
   0x7c03:  dec    eax
   0x7c04:  inc    ebp
   0x7c05:  dec    esp
   0x7c06:  dec    esp
   0x7c07:  dec    edi
   0x7c08:  dec    ecx
   0x7c09:  push   eax
   0x7c0a:  dec    esp
   0x7c0b:  add    BYTE PTR [edx],al
   0x7c0d:  add    DWORD PTR [ecx],eax
   0x7c0f:  add    BYTE PTR [edx],al
   0x7c11:  loopne 0x7c13
   0x7c13:  inc    eax
   0x7c14:  or     esi,eax
   0x7c16:  or     DWORD PTR [eax],eax
   0x7c18:  adc    al,BYTE PTR [eax]
   0x7c1a:  add    al,BYTE PTR [eax]
   0x7c1c:  add    BYTE PTR [eax],al
   0x7c1e:  add    BYTE PTR [eax],al
   0x7c20:  inc    eax
   0x7c21:  or     eax,DWORD PTR [eax]
   0x7c23:  add    BYTE PTR [eax],al
   0x7c25:  add    BYTE PTR [ecx],ch
   0x7c27:  (bad)  
   0x7c28:  (bad)  
   0x7c29:  (bad)  
   0x7c2a:  dec    DWORD PTR [eax+0x45]
   0x7c2d:  dec    esp
   0x7c2e:  dec    esp
   0x7c2f:  dec    edi
   0x7c30:  sub    eax,0x2020534f
   0x7c35:  and    BYTE PTR [esi+0x41],al
   0x7c38:  push   esp
   0x7c39:  xor    DWORD PTR [edx],esi
   0x7c3b:  and    BYTE PTR [eax],ah
   0x7c3d:  and    BYTE PTR [eax],al
(gdb) x/18bi 0x7c4f
   0x7c4f:  add    BYTE PTR [eax-0x2f720000],bh
   0x7c55:  mov    esp,0xd88e7c00
   0x7c5a:  mov    es,eax
   0x7c5c:  mov    esi,0x48a7c74
   0x7c61:  add    esi,0x1
   0x7c64:  cmp    al,0x0
   0x7c66:  je     0x7c71
   0x7c68:  mov    ah,0xe
   0x7c6a:  mov    ebx,0x10cd000f
   0x7c6f:  jmp    0x7c5f
   0x7c71:  hlt    
   0x7c72:  jmp    0x7c71
   0x7c74:  or     cl,BYTE PTR [edx]
   0x7c76:  push   0x6f6c6c65
   0x7c7b:  sub    al,0x20
   0x7c7d:  ja     0x7cee
   0x7c7f:  jb     0x7ced
   0x7c81:  or     al,BYTE PTR fs:[eax]

いくつか違う点がありますが、ほぼ逆アセンブルの結果と同じですね

個人的見所

0x76~0x83のオフセットはデータ

そこに書き込まれているのは"hello, world"という文字列です

76:  68 65 6c 6c 6f          push   0x6f6c6c65
7b: 2c 20                   sub    al,0x20
7d: 77 6f                   ja     0xee
7f: 72 6c                   jb     0xed
81: 64 0a 00                or     al,BYTE PTR fs:[eax]

証拠に"hello, world"という文字のasciiを表示しました

$ echo hello, world|hexdump -C
00000000  68 65 6c 6c 6f 2c 20 77  6f 72 6c 64 0a           |hello, world.|

アセンブリではDB命令で書き込んでいます

     DB      "hello, world"

すこし気に入らないのはこれらは固定長のデータとかではなく、 本当にバイナリの並びを命令として解釈しようとしていることですね。

0x7c76:  push   0x6f6c6c65
0x7c7b: sub    al,0x20
0x7c7d: ja     0x7cee
0x7c7f: jb     0x7ced
0x7c81: or     al,BYTE PTR fs:[eax]

DB命令はオペランドを内容をそのままメモリに書き込む内容ですが、 DB命令を多用すると、逆アセンブル時に元の命令と対応させるのが難しい実行ファイルができてしまいそうです。

JMP命令の解釈がgdbと逆アセンブラで違う

一番最初の命令ですね。

  • 逆アセ:jmp 0x50
  • GDB: jmp 0x7c50

gdbで16進数でバイナリダンプしてみましたが、逆アセンブル時と同じく0xed,0x4eです。

(gdb) x/5bx 0x7c00
0x7c00: 0xeb    0x4e    0x90    0x48    0x45

GDBではセグメントレジスタまで考慮して命令を表示しているのかと思い、qemu monitorで各レジスタを表示してみましたが、そんなことはありませんでした。(cs=0)

(qemu) info registers
EAX=0000aa55 EBX=00000000 ECX=00000000 EDX=00000000
ESI=00000000 EDI=00000000 EBP=00000000 ESP=00006ef0
EIP=00007c00 EFL=00000202 [-------] CPL=0 II=0 A20=1 SMM=0 HLT=0
ES =0000 00000000 0000ffff 00009300
CS =0000 00000000 0000ffff 00009b00
SS =0000 00000000 0000ffff 00009300
DS =0000 00000000 0000ffff 00009300
FS =0000 00000000 0000ffff 00009300
GS =0000 00000000 0000ffff 00009300
LDT=0000 00000000 0000ffff 00008200
TR =0000 00000000 0000ffff 00008b00
GDT=     00000000 00000000
IDT=     00000000 000003ff
CR0=00000010 CR2=00000000 CR3=00000000 CR4=00000000
DR0=00000000 DR1=00000000 DR2=00000000 DR3=00000000
DR6=ffff0ff0 DR7=00000400
EFER=0000000000000000
FCW=037f FSW=0000 [ST=0] FTW=00 MXCSR=00001f80
FPR0=0000000000000000 0000 FPR1=0000000000000000 0000
FPR2=0000000000000000 0000 FPR3=0000000000000000 0000
FPR4=0000000000000000 0000 FPR5=0000000000000000 0000
FPR6=0000000000000000 0000 FPR7=0000000000000000 0000
XMM00=00000000000000000000000000000000 XMM01=00000000000000000000000000000000
XMM02=00000000000000000000000000000000 XMM03=00000000000000000000000000000000
XMM04=00000000000000000000000000000000 XMM05=00000000000000000000000000000000
XMM06=00000000000000000000000000000000 XMM07=00000000000000000000000000000000

もちろんこのベースアドレス分の違いはORG命令からきているものだと思いますが、

     ORG     0x7c00          ; このプログラムがどこに読み込まれるのか

.imgのバイナリの段階でこの情報が抜け落ちていて、gdbで実行中だけ見えるというのは納得が行きません。

オペランド長の設定が不適切なため、次の命令がオペランドに不正に塗りつぶされている

0x7c6a:  mov    ebx,0x10cd000f
0x7c6f: jmp    0x7c5f

ここですが、int命令が0x7c6dから始まるint命令がmove命令のオペランドとして吸収されています

(gdb) x/2bi 0x7c6d
   0x7c6d:  int    0x10
   0x7c6f:  jmp    0x7c5f

解決策

16bit命令として解釈させる

$ gobjdump -b binary -m i8086 -M intel -D helloos.img

- m optionをi386ではなく、i8086を指定すれば良いようだ。

00000000 <.data>:
       0:   eb 4e                   jmp    0x50
       2:   90                      nop
       3:   48                      dec    ax
       4:   45                      inc    bp
       5:   4c                      dec    sp
       6:   4c                      dec    sp
       7:   4f                      dec    di
       8:   49                      dec    cx
       9:   50                      push   ax
       a:   4c                      dec    sp
       b:   00 02                   add    BYTE PTR [bp+si],al
       d:   01 01                   add    WORD PTR [bx+di],ax
       f:   00 02                   add    BYTE PTR [bp+si],al
      11:   e0 00                   loopne 0x13
      13:   40                      inc    ax
      14:   0b f0                   or     si,ax
      16:   09 00                   or     WORD PTR [bx+si],ax
      18:   12 00                   adc    al,BYTE PTR [bx+si]
      1a:   02 00                   add    al,BYTE PTR [bx+si]
      1c:   00 00                   add    BYTE PTR [bx+si],al
      1e:   00 00                   add    BYTE PTR [bx+si],al
      20:   40                      inc    ax
      21:   0b 00                   or     ax,WORD PTR [bx+si]
      23:   00 00                   add    BYTE PTR [bx+si],al
      25:   00 29                   add    BYTE PTR [bx+di],ch
      27:   ff                      (bad)  
      28:   ff                      (bad)  
      29:   ff                      (bad)  
      2a:   ff 48 45                dec    WORD PTR [bx+si+0x45]
      2d:   4c                      dec    sp
      2e:   4c                      dec    sp
      2f:   4f                      dec    di
      30:   2d 4f 53                sub    ax,0x534f
      33:   20 20                   and    BYTE PTR [bx+si],ah
      35:   20 46 41                and    BYTE PTR [bp+0x41],al
      38:   54                      push   sp
      39:   31 32                   xor    WORD PTR [bp+si],si
      3b:   20 20                   and    BYTE PTR [bx+si],ah
      3d:   20 00                   and    BYTE PTR [bx+si],al
    ...
      4f:   00 b8 00 00             add    BYTE PTR [bx+si+0x0],bh
      53:   8e d0                   mov    ss,ax
      55:   bc 00 7c                mov    sp,0x7c00
      58:   8e d8                   mov    ds,ax
      5a:   8e c0                   mov    es,ax
      5c:   be 74 7c                mov    si,0x7c74
      5f:   8a 04                   mov    al,BYTE PTR [si]
      61:   83 c6 01                add    si,0x1
      64:   3c 00                   cmp    al,0x0
      66:   74 09                   je     0x71
      68:   b4 0e                   mov    ah,0xe
      6a:   bb 0f 00                mov    bx,0xf
      6d:   cd 10                   int    0x10
      6f:   eb ee                   jmp    0x5f
      71:   f4                      hlt    
      72:   eb fd                   jmp    0x71
      74:   0a 0a                   or     cl,BYTE PTR [bp+si]
      76:   68 65 6c                push   0x6c65
      79:   6c                      ins    BYTE PTR es:[di],dx
      7a:   6f                      outs   dx,WORD PTR ds:[si]
      7b:   2c 20                   sub    al,0x20
      7d:   77 6f                   ja     0xee
      7f:   72 6c                   jb     0xed
      81:   64 0a 00                or     al,BYTE PTR fs:[bx+si]
    ...
     1fc:   00 00                   add    BYTE PTR [bx+si],al
     1fe:   55                      push   bp
     1ff:   aa                      stos   BYTE PTR es:[di],al
     200:   f0 ff                   lock (bad)
     202:   ff 00                   inc    WORD PTR [bx+si]
    ...
    1400:   f0 ff                   lock (bad)
    1402:   ff 00                   inc    WORD PTR [bx+si]
    ...

6dからの命令が正しくint 0x10と出力されている

gdbで少し探してみたが、set architecture i8086というコマンドがあるようだ

https://stackoverflow.com/questions/28811811/how-to-use-gdb-in-16-bit-mode

GDBのxコマンドのiオプションは

アセンブラシンタックスでの (もしくは、それに近い) マシン・ インストラクションを表示します。指定されたユニットサイズは無視され; 1つのインストラクションを構成するバイト数は、そのマシンで利用されている オペコードとアドレッシングモードの種類に依存します。

なので、かなりこれが解決策っぽい!

http://flex.phys.tohoku.ac.jp/texi/gdb-j/gdb-j_41.html

が、適応してみたところ、結局32bitの機械語として解釈されてしまった。

(gdb) set architecture i8086
The target architecture is assumed to be i8086
(gdb) x/18i 0x7c50
   0x7c50:  mov    eax,0xd08e0000
   0x7c55:  mov    esp,0xd88e7c00
   0x7c5a:  mov    es,eax
   0x7c5c:  mov    esi,0x48a7c74
   0x7c61:  add    esi,0x1
   0x7c64:  cmp    al,0x0
   0x7c66:  je     0x7c71
   0x7c68:  mov    ah,0xe
   0x7c6a:  mov    ebx,0x10cd000f
   0x7c6f:  jmp    0x7c5f
   0x7c71:  hlt    
   0x7c72:  jmp    0x7c71
   0x7c74:  or     cl,BYTE PTR [edx]
   0x7c76:  push   0x6f6c6c65
   0x7c7b:  sub    al,0x20
   0x7c7d:  ja     0x7cee
   0x7c7f:  jb     0x7ced
   0x7c81:  or     al,BYTE PTR fs:[eax]

コード部分(0x50~0x7f)をステップ実行してみる

(gdb) x/18bi 0x7c50
=> 0x7c50:   mov    eax,0xd08e0000
   0x7c55:  mov    esp,0xd88e7c00
   0x7c5a:  mov    es,eax
   0x7c5c:  mov    esi,0x48a7c74
   0x7c61:  add    esi,0x1
   0x7c64:  cmp    al,0x0
   0x7c66:  je     0x7c71
   0x7c68:  mov    ah,0xe
   0x7c6a:  mov    ebx,0x10cd000f
   0x7c6f:  jmp    0x7c5f
   0x7c71:  hlt    
   0x7c72:  jmp    0x7c71
   0x7c74:  or     cl,BYTE PTR [edx]
   0x7c76:  push   0x6f6c6c65
   0x7c7b:  sub    al,0x20
   0x7c7d:  ja     0x7cee
   0x7c7f:  jb     0x7ced

画面の文字表示のAPIが0x7c6dにアロケートされるとがわかったのでそこにブレイクポイントを置いてステップ実行をしています。

(gdb) b *0x7c6d
Breakpoint 3 at 0x7c6a
(gdb) continue

www.youtube.com

次回はフロッピー読み込みをデバッグしてみます。

「Linuxの仕組み」を読んだ

推測するな計測せよ!な一冊。

[試して理解]Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識

[試して理解]Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識

技術評論社epubを買った。結構定期的にアップデートされている様子。 売るつもりがないなら電子書籍の方がお得です。

gihyo.jp

「はじめての486」読了後、読む電子書籍がなくなったので読みました。*1

OS自作本を読んだ後は、 低レイヤーの歩き方を参考にするに、xv6の本かな~と思いきや

razokulover.hateblo.jp

こんな感想が目に留まったのでこちらを先に買いました。

ちなみに私のLinux力をお伝えしておくと、 趣味と大学時代にちょろっと触ったことがある程度で、 特にそこに対して時間をかけて勉強・研究したり 実務で使ったことはありません。

  • 速度や負荷計測のコマンドがかなり使われている
  • マルチタスクやキャッシュなどの速度検証

ファイルシステムはこちらにまとめました。

yuyubu.hatenablog.com

仮想ファイルシステムディバイスアクセスをラップしており、 コンソールやHDDをまるでファイルアクセスのように利用できる点が以外でした。(常識?)

とにかく図がわかりやすい!

あまりOSについて書かれた本は読めていませんが、 ハードウェアの構成図などはもちろん プロセスがタスクスイッチで処理される様子など かなりわかりやすく書かれています。

計測パートについて

  • 読書時間が電車の中だったり、
  • 手元のMacのVirtual Boxの中のubuntuのキーボードがUS配列に対応していなかったり
    • まぁそんなもんぐらいググって設定しろってはなしですが。
  • 代わりに使ったKali Linuxにコマンドがあまり入っていない

などの理由によりあまり実測できていません。

また著者はVMWareやVirtual Boxなどの仮想環境より、 実機で検証することを推奨しています。LinuxがブートできるPCを現在保持していません。

あまり手を動かして読めていませんが、ここに書かれている計測方法で utuntu以外のOSもベンチできたら楽しいんじゃないでしょうか。

図などはかなりわかりやすいですが、初心者はプログラムを 自分でコメントを付けながら精読した方がいいと思います。

個人的にディスクIOやページング、ファイルシステムを実装するときに 参考にする1冊になるだろうと考えています。

*1:電車の中では必ず電子書籍を読むことにしている