30日でできる!OS自作入門を Linux & GAS で行う (3日目)

さて、めっきり更新が遅くなってしまいましたが、3日目です。

ポイントは

  1. いかにして32ビットプロテクトモードへ突入するか
  2. いかにしてCのソースが吐いたバイナリにjmpするか

です。

いかにして32ビットプロテクトモードへ突入するか

32ビットプロテクトモードに飛び、32ビットのコードを動作させるには、ちょっとした準備が必要です。

  1. 割り込みを禁止しておく
  2. A20信号線をONにする。
  3. グローバルディスクリプタテーブルを準備し、CPUに認識させておく

これを行うコードを抜粋すると、以下のようなコードになります:

    # stop interrupts from PIC
    movb    $0xff,%al
    outb    %al,$0x21
    nop
    outb    %al,$0xa1

    # stop to accept interrupt
    cli

    # enable A20 signal line
    call    waitkbdout
    movb    $0xd1,%al
    outb    %al,$0x64
    call    waitkbdout
    movb    $0xdf,%al
    outb    %al,$0x60
    call    waitkbdout

    # load discripter table
    lgdtl    (gdtr0)
    ...
    ...
    ...
    .align   8
gdt0:
    # null selector.
    .skip   8,0x00
    # 1st GDT.
    .word   0xffff,0x0000,0x9200,0x00cf
    # 2nd GDT.
    .word   0xffff,0x0000,0x9a28,0x0047
    # last GDT.
    .word   0x0000
gdtr0:
    .word   8*3-1 # Size of Global Descriptor Table
    .int    gdt0  # where is the entry of the gdt?

まず、割り込みの禁止です。PICからCPUへの割り込みを通らないようにし、その上でcli命令を発行してCPUサイドでも割り込みの受付を禁止しています。
次に、キーボードコントローラを叩いてA20信号線をアサートしています。詳細はOS自作本をご覧ください。
最後に、lgdtl命令を使って、グローバルディスクリプタテーブルを読み込んでいます。()がついているので、特定の番地から読み込んでいることが分かります。テーブルの本体は、gdtr0ラベル以下にあります。グローバルディスクリプタテーブルの先頭2バイトはディスクリプタテーブルの大きさで、次の32ビットがテーブル本体のアドレスとなります。

さて、前準備が終わったら、念願のプロテクトモードに突入です!具体的には、以下のような手順を踏む必要があります:

  1. コントロールレジスタ0(cr0)の0ビット目を立てる
  2. パイプラインをフラッシュするために、jmpする
  3. ディスクリプタテーブルで設定したセグメントと整合性をとるために、データセグメントに値を入れる。

これをコードにして表すと、以下のようになります:

    andl    $0x7fffffff,%eax
    movl    %cr0,%eax
    orl     $0x01,%eax
    movl    %eax,%cr0
    jmp     pipelineflash
pipelineflash:
    movw    $8,%ax
    movw    %ax,%ds
    movw    %ax,%es
    movw    %ax,%fs
    movw    %ax,%gs
    movw    %ax,%ss
いかにしてC言語のソースファイルから吐き出されたバイナリにジャンプするか

これでプロテクトモードには移行しましたが、実はまだ32bitのコードを読めるようにはなっていません。どうしたら32ビット用のコードを実行できるのでしょうか?実は、あらかじめ準備しておいたグローバルディスクリプタテーブルに用意されているセグメントにジャンプすると、コードセグメントが切り替わり、本格的に32ビットモードに突入することができるのです。コードセグメントを切り替えつつジャンプする、セグメント間ジャンプをGASで記述するには、jmp命令を使用します。ただし、普通のjmp命令(セグメント内のjmp命令)とは文法が異なるので注意です。具体的には、以下のような記述をします:

jmp     飛んだ先で使用するコードセグメント,番地

詳しくは、はじめて読む486―32ビットコンピュータをやさしく語るあたりを読むと詳細に書いてありますので、ぜひそちらを参照してみてください。当然ですが、ジャンプした先には32ビットの実行可能なバイナリがあるひつようがあります。これは、セグメント間ジャンプする前に予め読み込んでおいてください。例としては、以下のようなコードになります:

# Copy bootpack.c code to 0x00280000
    movl    $bootpack,%esi
    movl    $botpak,%edi
    movl    $512*1024/4,%ecx
    call    memcpy

そんなわけで、本日のソースコードを公開します。3日目ともなると結構な分量なので、こちらから落としてください。それと、誤りなどありましたらご指摘頂けますと、大変うれしいです。そんな感じで今日は終了〜。