2014年11月29日土曜日

[OCaml]XML Parser PXPを使ってみた(Bufferモジュールを利用)

前記事の、E_char_dataイベントで文字列を ^ で連結するやり方は効率が悪すぎた。

ブログ更新をtweetしていたら、@camloebaさんに Bufferがあることを教えていただいた。
いつもありがとうございます!

ということで、Bufferを使うように修正してみた。

修正後のプログラム

修正したのはparse関数だけ。
let parse element_func_list =
  let tags = Stack.create () in
  let element_func () =
    let item = List.find element_func_list ~f:(fun (element_list, _) ->
        Stack.to_list tags = element_list
      ) in
    match item with
      Some (_, func) -> Some func
    | None -> None in
  let in_content = ref false in
  let content = Buffer.create 1024 in
  let apply_func = ref (fun _ -> ()) in
  Pxp_ev_parser.process_entity config entry entmng (fun ev ->
      (* let event_string = Pxp_event.string_of_event ev in *)
      match ev with
        Pxp_types.E_start_tag (name, _, _, _) -> begin
          Stack.push tags name;
          (* print_endline @@ List.to_string (Stack.to_list tags) ~f:ident; *)
          match element_func () with
            Some func' -> begin
              apply_func := func';
              in_content := true
            end
          | _ -> ()
        end
      | Pxp_types.E_end_tag (name, _) -> begin
          ignore @@ Stack.pop_exn tags;
          if !in_content then
            begin
              !apply_func @@ Buffer.contents content;
              Buffer.clear content;
              in_content := false;
            end;
        end
      | Pxp_types.E_char_data str -> begin
          if !in_content then
            Buffer.add_string content str
        end
      | _ -> ())
このような修正は、動的な型の言語の場合、必要な修正箇所を見逃しやすいが、 OCamlのような静的な型の言語の場合は、コンパイルエラーで検出できるので非常にやりやすい。
Emacs+Tuaregでコード書いており、保存と同時にエラーがある行の色が変わるので、修正箇所は一目瞭然。

実行結果

satoshi@xubuntu:~/workspace/ocaml-try/pxp$ time ./parse_wiki > /dev/null

real    0m0.093s
user    0m0.081s
sys     0m0.011s
修正前は、
satoshi@xubuntu:~/workspace/ocaml-try/pxp$ time ./parse_wiki > /dev/null

real    0m3.361s
user    0m3.168s
sys     0m0.180s
およそ35倍速くなった!
これなら全く問題なし。
計測していないが、メモリの使用量も、かなり減ったはず。

なお、^による文字列連結に関しては、 Real World OCaml Chapter 3. Lists and Patternsの Performance of String.concat and ^ にも書かれていた。
^ を使うたびに文字列を生成するので、少しずつ文字列を連結して、大きな文字列を作る場合は、非常にパフォーマンスが悪い。

[OCaml]XML Parser PXPを使ってみた

OCamlのXML ParserであるPXPを使ってみた。

処理したXMLは日本語版Wikipediaの文書データ。
この文書データはここからダウンロードできる。

この文書データは、複数のpage要素からなり、 title要素にタイトル、 text要素に内容が書かれている。
今回は、このタイトルと内容を抜き出して、出力する処理を書いてみた。

PXPのインストール

PXPはopamでインストールできる。
$ opam install pxp

PXPについて

treeモードとeventモードがある。
treeモードでは、DOM treeを構築できるようだ。

やりたいことは 大きなサイズの日本語版Wikipediaのデータをパースすること。
DOMを構築する必要はないので、eventモードを使う。

XML data as stream of events を参考にコードを書いてみる。

eventモードにはPush parsingとPull pasingの二種類のパース方法がある。
今回はPush parsingを利用した。

前準備

ここからWikipediaのデータをダウンロードする。
テストが簡単になるように、10ページ分だけ切り出したデータを作っておく。
$ grep -m 10 -n -F "</page>" jawiki-20141122-pages-articles.xml | cut -d: -f1 | tail -n 1 | xargs -I @ head -n @ jawiki-20141122-pages-articles.xml > jawiki_10.xml
$ echo "</mediawiki>" >> jawiki_10.xml


サンプルプログラム


内容を取り出したい要素と、その内容を取り出したときに呼ばれる関数を、 処理関数parseに渡す方式で実装した。

let element_func_list = [
  (["title"; "page"; "mediawiki"], (fun content -> ..snip..));
  (["text"; "revision"; "page"; "mediawiki"], (fun content -> ..snip..))]

let () = parse element_func_list

parse_wiki.ml


open Core.Std

let config = let open Pxp_types in
  {default_config with encoding = `Enc_utf8}
               
let source = Pxp_types.from_file "/home/satoshi/Documents/jawiki/jawiki_10.xml"

let entmng = Pxp_ev_parser.create_entity_manager config source

let entry = `Entry_document []

let element_func_list = [
  (["title"; "page"; "mediawiki"],
   (fun content ->
      print_endline @@ sprintf "Title: %s" content));
  (["text"; "revision"; "page"; "mediawiki"],
   (fun content ->
      let max_length = 40 in
      let content' = 
        if String.length content > max_length then
          (String.slice content 0 max_length) ^ "..."
        else
          content in
      print_endline @@ sprintf "Text: %s" content'))]

let parse element_func_list =
  let tags = Stack.create () in
  let element_func () =
    let item = List.find element_func_list ~f:(fun (element_list, _) ->
        Stack.to_list tags = element_list
      ) in
    match item with
      Some (_, func) -> Some func
    | None -> None in
  let in_content = ref false in
  let content = ref "" in
  let apply_func = ref (fun _ -> ()) in
  Pxp_ev_parser.process_entity config entry entmng (fun ev ->
      (* let event_string = Pxp_event.string_of_event ev in *)
      match ev with
        Pxp_types.E_start_tag (name, _, _, _) -> begin
          Stack.push tags name;
          (* print_endline @@ List.to_string (Stack.to_list tags) ~f:ident; *)
          match element_func () with
            Some func' -> begin
              apply_func := func';
              in_content := true
            end
          | _ -> ()
        end
      | Pxp_types.E_end_tag (name, _) -> begin
          ignore @@ Stack.pop_exn tags;
          if !in_content then
            begin
              !apply_func !content;
              content := "";
              in_content := false;
            end;
        end
      | Pxp_types.E_char_data str -> begin
          if !in_content then
            content := !content ^ str
        end
      | _ -> ())

let () =
  parse element_func_list

実行結果

satoshi@xubuntu:~/workspace/ocaml-try/pxp$ ./parse_wiki
Title: Wikipedia:アップロードログ 2004年4月
Text: <ul><li>14:46 2004年4月30日 [[利用..
Title: Wikipedia:削除記録/過去ログ 2002年12月
Text: Below is a list of the most recent delet...
Title: アンパサンド
Text: {{記号文字|&amp;}}
[[Image:Trebuchet...
Title: Wikipedia:Sandbox
Text: #REDIRECT [[Wikipedia:サンドボック...
..snip.. 

エンコーディングの指定

<?xml 〜?>ディレクティブでUTF-8を指定してみたが、駄目だった。
configでエンコーディングを指定したら、正しく処理できた。
let config = let open Pxp_types in
  {default_config with encoding = `Enc_utf8}
自動でエンコーディングを認識させる方法はあるのかな?

文字列の連結処理

page要素10個分のxmlファイルのサイズは
-rw-rw-r-- 1 satoshi satoshi  533408 11月 28 23:56 jawiki_10.xml
たった500kb弱なのに、想定よりも処理に時間がかかっている。
satoshi@xubuntu:~/workspace/ocaml-try/pxp$ time ./parse_wiki > /dev/null

real    0m3.361s
user    0m3.168s
sys     0m0.180s
ちなみに実行環境はWindows8.1@Core i3のVirtualBox上のXubuntu14.04で、OCaml4.01.0を使っている。
重い環境だと思うが、メモリ不足にもなっていないのに、3秒以上もかかるのは何故だろう。
E_char_dataのイベントで文字を ^ で連結している処理が駄目なのかな?
      | Pxp_types.E_char_data str -> begin
          if !in_content then
            content := !content ^ str  ← ◆ここを () に変更
        end
文字列連結部分を () にしてみるとどうなるかな?
satoshi@xubuntu:~/workspace/ocaml-try/pxp$ time ./parse_wiki > /dev/null

real    0m0.087s
user    0m0.086s
sys     0m0.000s
おおよそ40倍早くなった。これが原因かー。
JavaのStringBuilderに相当するものは、OCamlでは何になるんだろう。

参考

2014年10月16日木曜日

[OS作成]30日でできる!OS自作入門 5日目 (3)

extern char hankaku_font[16 * 256];
で各文字のイメージを取得できるようになったので、
  • 一文字を描画する関数 draw_char 
  • 文字列を描画する関数 draw_string 
を作成し、画面に文字列を描画できるようにした。
void Main(void) {
..snip..
  draw_string(bootInfo->vram, bootInfo->scrnx, 8, 8, "ABC 123", COLOR_WHITE);
  draw_string(bootInfo->vram, bootInfo->scrnx, 31, 31, "Tiny OS.", COLOR_BLACK);
  draw_string(bootInfo->vram, bootInfo->scrnx, 32, 32, "Tiny OS.", COLOR_WHITE);
..snip..
}

void draw_char(char *vram, int width,  int x, int y, char c, char color) {
  int i, j;
  char img;
  char *vram_aux;
  char *font;
  font = hankaku_font + c * 16;
  vram += y * width + x;
  for (i = 0; i < 16; i++) {
    img = font[i];
    for (j = 0; j < 8; j++) {
      if ((img & 0x80) != 0)
    vram[j] = color;
      img <<= 1;
    }
    vram += width;
  }
}

void draw_string(char *vram, int width, int x, int y, char *string, char color) {
  char c;
  while ((c = *(string++)) != 0) {
    draw_char(vram, width, x, y, c, color);
    x += 8;
  }
}


次は変数の値をsprintfで表示できるようにするのだけど、sprintfを作るのは面倒なので、Haribote OSのものをそのまま使うことにしよう。

2014年10月13日月曜日

[OS作成]30日でできる!OS自作入門 5日目 (2)

フォントはfont.sで定義するようにした。
フォントイメージを定義したhankaku.txtからfont.sを生成するプログラムを作成した。

hankaku.txt OSASKの半角フォントを定義したファイルで、以下のような形式となっている。
char 0x61
........
........
........
........
........
.***....
....*...
.....*..
..****..
.*...*..
*....*..
*....*..
*...**..
.***.**.
........
........

font.s では、フォントの最初の文字(文字コード0x00)のアドレスをhankaku_fontとしてtinyos.cから参照できるようにする。
半角フォント1文字は16バイトからなり、256文字分定義する。

font.s

.file "font.s"

.global hankaku_font

 .data

hankaku_font:
 // char 0x00
 .byte 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00
 // char 0x01
 .byte 0x00,0x00,0x38,0x44,0x82,0xaa,0xaa,0x82,0x82,0xaa,0x92,0x44,0x38,0x00,0x00,0x00
..snip..
font.sを生成するプログラムはOCamlで作成した。

makefont.ml

open Core.Std

let file_name = "hankaku.txt"

let is_skip_line line =
  let line' = String.strip line in
  line' = "" || String.get line 0 = '#'

let is_char_line line =
  try
    String.slice line 0 4 = "char"
  with
    _ -> false

let pattern_to_int p =
  String.fold p ~init:0 ~f:(fun acc c ->
      (acc lsl 1) + (if c = '*' then 1 else 0))

type parse_state = Init | Read_header | Read_data

let () =
  let lines = In_channel.read_lines file_name in
  let state = ref Init in
  List.iter lines ~f:(fun line ->
      if not (is_skip_line line) then
        if is_char_line line then begin
          if !state = Read_data then
            print_newline ();
          state := Read_header;
          print_endline ("\t// " ^ line);
          print_string "\t.byte "
        end
        else begin
          if !state = Read_data then
            print_string ",";
          state := Read_data;
          let img = pattern_to_int line in
          print_string ("0x" ^ (Printf.sprintf "%02x" img));
        end
    );
  print_newline ()
OMakeでfont.sを作成できるようにした。
$ omake font.s

ソースコードはgithubにコミットした。
https://github.com/takeisa/tinyos/tree/master/makefont

OCamlは、このような用途のプログラムも簡単に作成できる。
OCamlでの開発では、Emacs + Tuareg + merlin を使っているが、 コード補完が効くし、REPLとしてutopが利用できるのでとても便利。

OCamlを使ってみたい人は、ここを参考に環境構築すれば良いですよ。

2014年10月11日土曜日

[OS作成]30日でできる!OS自作入門 5日目 (1)

筆者の独自ツールをそのまま使えば良かったんじゃないかと思いつつも、 Linux(Ubuntu14.04)の標準ツールだけで、OSを作成中。

静的なデータ領域の参照アドレスが正しくなるように、こちらのリンカスクリプトを参考にして、 リンカスクリプトでbootpackのヘッダ部分を定義する方式に変更した。

tinyos_lnk.ls

OUTPUT_FORMAT("binary")

ENTRY("Main")

SECTIONS {
 .head 0x0 : {
  LONG(128 * 1024)      /* 0 : stack+.data+heap の大きさ(4KBの倍数) */
  LONG(0x54696e79)      /* 4 : シグネチャ "Tiny" */
  LONG(0)               /* 8 : mmarea の大きさ(4KBの倍数) */
  LONG(0x310000)        /* 12 : スタック初期値&.data転送先 */
  LONG(SIZEOF(.data))   /* 16 : .dataサイズ */
  LONG(LOADADDR(.data)) /* 20 : .dataの初期値列のファイル位置 */
  LONG(0xE9000000)      /* 24 : 0xE9000000 */
  LONG(Main - 0x20)     /* 28 : エントリアドレス - 0x20 */
  LONG(24 * 1024)       /* 32 : heap領域(malloc領域)開始アドレス */
 }

 .text : { *(.text) }
 .data 0x310000 : AT ( ADDR(.text) + SIZEOF(.text) ) {
       *(.data)
       *(.rodata*)
       *(.bss)
 }
 .eh_frame : { *(.eh_frame) }
}

静的変数が正しく参照できなかったので、きっとdataセクションを0x310000にすれば良いのだろうなと思っていたのだけど、 単純に .data 0x310000 と書くと、作成されるファイルのサイズが大きくなってしまい、FDイメージが作成できなかった。
ATでtextセクションの後ろにくるように指定でき、イメージを小さくできた。

このリンカスクリプトにより、次の静的変数は正しく参照できるようになった。
  • パレットの定義変数 static unsigned char table_rgb[16 * 3] 
  • フォントA static char font_A[16]

文字Aが表示できたので、今日はここまで。

2014年10月5日日曜日

[OS作成]30日でできる!OS自作入門 4日目

やっと4日目だ。
4日目はパレットを設定し、デスクトップらしき画像を描画する。

 最初、全てのパレット設定が黒になってしまった。
table_rgbが指し示すアドレスが正しくないようだ。
こちらのブログ と同様にstaticを外したら、色が設定された。
tinyos.cのリスティングファイルを確認すると、staticを付けるとパレットの定義は、 dataセクションに格納されていた。
dataセクションのアドレスは boot.sで
 # +20 : .dataの初期値列がファイルのどこにあるか
 .int 0x10c8
と定義しているが、 リンカスクリプトでdataセクションのアドレスを設定し、 そのアドレスをboot.sにも設定すれば良いのだろうか。

qemuで実行した時のスクリーンショット。書籍と同じだ。


ソースが大きくなってきたのでgithubで公開する。こちらを参照。

アセンブラでC言語から呼べる関数を作るときの注意点

自由に使って良いレジスタ EAX,ECX,EDX だけ。
他のレジスタは値を変更してはならない。
戻り値はEAXレジスタに設定する。

オブジェクトファイルの逆アセンブル

objdumpコマンドを使う。
$ objdump -d func.o

func.o:     ファイル形式 elf32-i386

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

00000000 :
   0: f4                    hlt    
   1: c3                    ret    

00000002 :
   2: 8b 4c 24 04           mov    0x4(%esp),%ecx
   6: 8a 44 24 08           mov    0x8(%esp),%al
   a: 88 01                 mov    %al,(%ecx)
   c: c3                    ret

GASでのEFLAGSレジスタのPUSH/POP

NASMでは、pushfd, popfd と書くが、GASではpushfl, popflと書く。

参考

2014年10月4日土曜日

[OS作成]30日でできる!OS自作入門 3日目 (8)

C言語から呼べる、アセンブラで作成した関数のリンクもうまくできたようだ。
やっと3日目が終了。長かった...

func.sに定義したio_hltで、hltを実行関数で、tinyos.cのMain関数から呼び出される。

最初、エラーもなく、コンパイル・リンクができたので、実行してみると、どうも挙動が変。
生成されたバイナリを見てみると、hltのコードが出力されていなかった。
原因がなかなか分からず、試行錯誤していたが、
.section .text
と書かなければならないところを、
.section text
と書いていたことが原因であった。
つまらないところで、手間取ってしまった。
でも、.section text と書いた場合、何を意味することになるのだろう。

ここまでのソースを書いておこう。
コードが多くなってきたので、今後はgithubを使おう。

Makefile

image_file=tinyos.img

image_file: ipl.bin tinyos.sys
 mformat -f 1440 -B ipl.bin -C -i ${image_file} ::
 mcopy tinyos.sys -i ${image_file} ::

ipl.bin: ipl.s ipl_lnk.ls
 gcc -nostdlib -o $@ -Tipl_lnk.ls ipl.s
 gcc -Tipl_lnk.ls -c -g -Wa,-a,-ad ipl.s > ipl.lst

tinyos.sys: boot.bin func.o tinyos.c
 gcc -m32 -S -c -nostdlib -Wl,--oformat=binary *.c
 gcc -m32 -c -g -Wa,-a,-ad tinyos.s > tinyos.lst
 gcc -m32 -c -nostdlib -Wl,--oformat=binary *.c -o tinyos.o
 ld -static -m elf_i386 -o tinyos.bin -e Main --oformat=binary tinyos.o func.o
 cat boot.bin tinyos.bin > $@

boot.bin: boot.s boot_lnk.ls
 gcc boot.s -nostdlib -Tboot_lnk.ls -o boot.bin
 gcc -Tboot_lnk.ls -c -g -Wa,-a,-ad boot.s > boot.lst

func.o: func.s
 as --32 -a -ad func.s > func.lst
 as --32 func.s -o func.o

img:
 make image_file

run:
 qemu-system-i386 -m 32 -localtime -vga std -fda ${image_file}

debug:
 qemu-system-i386 -m 32 -localtime -vga std -fda ${image_file} \
  -gdb tcp::10000 \
  -S

clean:
 rm tinyos.sys boot.bin
 rm *.o

boot.s

.set BOTPAK,  0x00280000 # bootpackのロード先
.set DSKCAC,  0x00100000 # ディスクキャッシュの場所
.set DSKCAC0, 0x00008000 # ディスクキャッシュの場所(リアルモード)

// BOOT_INFO
.set CYLS, 0x0ff0 # シリンダ数
.set LEDS,0x0ff1 # LEDの状態
.set VMODE, 0x0ff2 # ビデオモード
.set SCRNX, 0x0ff4 # 解像度X
.set SCRNY, 0x0ff6 # 解像度Y
.set VRAM, 0x0ff8 # VRAMの開始アドレス

.text
.code16

 // ビデオモードを変更する
 movb $0x00, %ah # VGA Graphics 320x200x8bit
 movb $0x13, %al
 int $0x10

 // 画面の状態を記録する
 movb $8, (VMODE)
 movw $320, (SCRNX)
 movw $200, (SCRNY)
 movl $0x000a0000, (VRAM)

 // LEDの状態を記録する
 movb $0x02, %ah
 int $0x16
 movb %al, (LEDS)

 // PICが割り込みを受け付けないようにする
 movb $0xff, %al
 outb %al, $0x21
 nop   # outは連続して使用しない
 outb %al, $0xa1
 cli   # CPUでも割り込み禁止

 // A20互換モードを無効にして1MB以上のアドレスにアクセスできるようにする
 call waitkbdout
 movb $0xd1, %al
 outb %al, $0x64
 call waitkbdout
 movb $0xdf, %al # A20を有効にする
 outb %al, $0x60
 call waitkbdout

 // プロテクトモードに移行する
.arch i486
 lgdt (GDTR0)
 movl %cr0, %eax
 andl $0x7fffffff, %eax # ページング禁止
 orl $0x00000001, %eax # プロテクトモード移行
 movl %eax, %cr0
 jmp pipelineflush
pipelineflush:
 movw $1*8, %ax
 movw %ax, %ds
 movw %ax, %es
 movw %ax, %fs
 movw %ax, %gs
 movw %ax, %ss

 // bootpackを転送する
 movl $bootpack, %esi # 転送元
 movl $BOTPAK, %edi # 転送先
 movl $512*1024/4, %ecx # 4で割っているのは4バイト単位で処理するため
 call memcpy

 // ディスクイメージを本来の位置へ転送する

 // ブートセクタ
 movl $0x7c00, %esi
 movl $DSKCAC, %edi
 movl $512/4, %ecx
 call memcpy
 
 // 残り
 movl $DSKCAC0+512, %esi
 movl $DSKCAC+512, %edi
 movl $0, %ecx
 movb (CYLS), %cl   # 読み込んだシリンダ数
 imull $512*18*2/4, %ecx # 1シリンダあたりのバイト数/4を掛ける
 sub $512/4, %ecx      # IPL分を引く
 call memcpy

 // bootpackを起動する
 movl $BOTPAK, %ebx
 movl 16(%ebx), %ecx
 addl $3, %ecx
 shrl $2, %ecx
 jz skip  # 転送するものがない
 movl 20(%ebx), %esi  # .dataのアドレス
 addl %ebx, %esi
 movl 12(%ebx), %edi # .data転送先
 call memcpy
skip: 
 movl 12(%ebx), %esp  # スタック初期値
 ljmpl $2*8, $0x0000001b
 
waitkbdout:
 inb $0x64, %al
 andb $0x02, %al
# inb $0x60, %al # 元のソースにはないコード
 jnz waitkbdout
 ret

memcpy:
 movl (%esi), %eax
 addl $4, %esi
 movl %eax, (%edi)
 addl $4, %edi
 subl $1, %ecx
 jnz memcpy
 ret

.align 16
 
GDT0:
 // GDTの構成
 // short limit_low, base_low
 // char base_mid, access_right
 // char limit_high, base_high
 
 // null selector
 .skip 8, 0x00
 // base=0x00000000 limit=0xcfffff access_right=0x92
 .word 0xffff, 0x0000, 0x9200, 0x00cf
 // base=0x00280000 limit=0x47ffff access_right=0x9a
 .word 0xffff, 0x0000, 0x9a28, 0x0047
 .word 0x0000
 
GDTR0:
 .word 8 * 3 - 1 # GDTのサイズ?
 .int GDT0

.align 16
bootpack:
 # + 0 : stack+.data+heap の大きさ(4KBの倍数)
 .int 0x00
 # + 4 : シグネチャ "Hari"
 .ascii "Tiny"
 # + 8 : mmarea の大きさ(4KBの倍数)
 .int 0x00
 # +12 : スタック初期値&.data転送先
 .int 0x00310000
 # +16 : .dataのサイズ
 .int 0x11a8
 # +20 : .dataの初期値列がファイルのどこにあるか
 .int 0x10c8
 # +24 +28 のセットで 1bからの命令が E9 XXXXXXXX (JMP)になり、C言語のエントリポイントにJMPするようだ
 # +24 : 0xe9000000
 .int  0xe9000000
 # +28 : エントリアドレス-0x20
 .int 0x00
 # +32 : heap領域(malloc領域)開始アドレス
 .int 0x00

func.s

.file "func.s"
.section .text
.global io_hlt
 
io_hlt:
 hlt
 ret

tinyos.s

void io_hlt(void);

void Main(void) {
 fin:
  //__asm__("hlt\n\t");
  io_hlt();
  goto fin;
}

2014年10月2日木曜日

[OS作成]30日でできる!OS自作入門 3日目 (8)

[OS作成]30日でできる!OS自作入門 3日目 (7) では、

0x00280000番地からメモリをダンプして bootpackの内容になっていることを確認後、 ステップ実行して、0x1bにジャンプし、 0番地からのメモリをダンプしてみたが、 bootpackの内容にはなっていなかった。
ljmplで0x1bにジャンプしても、0x00280000番地が0番地に割り当てられていないようである。

いろいろ調べてみたが、原因が分からず、今だC言語で書いた関数の呼び出しができない。
と書いたが、書籍の推奨環境であるWindowsでディスクイメージ(haribote.img)を作成し、Debian Wheezy 32bitのqemu+gdbで動作を確認してみたところ、64bit環境と挙動は同じになった。

qemu上で実行するコードを、gdbで正しくデバッグするには何か設定が必要なのかな...

正しく動作していると仮定して、動作が確認できるところまで進むことにしよう。

2014年9月30日火曜日

[OS作成]30日でできる!OS自作入門 3日目 (7)

32bitモードへの移行と、C言語で書いたOS本体の実行を実装する。

3日目では、32bitモードへの移行に関する詳細な解説は無く、5日目以降になっている。
C言語とアセンブリ言語で作成したオブジェクトファイルの扱いにおいては、 筆者独自のツールを使っており、標準ツールに置き換えるには、どのようにすれば良いのか、今ひとつ分からない。

他の人のブログを参考にして、乗り切ることにしよう。

コードを読んでいて、どうして1MB以上のメモリにアクセスするようにするために、キーボード関連のコードが出てくるのかと思っていたら、ここに 書いてあった。
---引用---
これに対する互換性のために、IBMのエンジニアはA20アドレスライン(8086はA0からA19までのアドレスラインしか持たない)にキーボードコントローラを通して信号を送り、A20互換モードを有効化/無効化できるようなメカニズムを提供した。なぜキーボードコントローラなのかと不思議に思うだろう。答えは未使用のピンがあったからである。

きっと有名な仕様なんだろうな。こういう話は面白い。

Ubuntu14.04の64bit版を使っているので、gccやldはデフォルトで64bit版のコードに対応するため、 gccやldに32bit用のコードを生成するオプションを付ける必要があった。
こちらのブログでは、hrbファイルのヘッダファイルの固定値を直接レジスタに代入していたが、なるべくオリジナルのコードのままにしたかったので、bootpack以降に、ヘッダに相当するデータを定義して、それを参照するようにした。

以下にMakefile, boot.s, tinyos.c のソースを示す。

Makefile

image_file=tinyos.img

ipl.bin: ipl.s ipl_lnk.ls
 gcc -nostdlib -o $@ -Tipl_lnk.ls ipl.s
 gcc -Tipl_lnk.ls -c -g -Wa,-a,-ad ipl.s > ipl.lst

tinyos.sys: boot.s boot_lnk.ls
 gcc boot.s -nostdlib -o $@ -Tboot_lnk.ls -o boot.bin
# gcc boot.s -m32 -nostdlib -o $@ -Tboot_lnk.ls -o boot.bin
 gcc -Tboot_lnk.ls -c -g -Wa,-a,-ad boot.s > boot.lst
 gcc *.c -m32 -c -nostdlib -Wl,--oformat=binary -o tinyos.o
# gcc *.c -m32 -S -c -nostdlib -Wl,--oformat=binary -o tinyos.o
# gcc -c -g -Wa,-a,-ad tinyos.s > tinyos.lst
 ld -m elf_i386 -o tinyos.bin -e Main --oformat=binary tinyos.o
 cat boot.bin tinyos.bin > $@

image_file: ipl.bin tinyos.sys
 mformat -f 1440 -B ipl.bin -C -i ${image_file} ::
 mcopy tinyos.sys -i ${image_file} ::

img:
 make image_file

run:
 qemu-system-i386 -m 32 -localtime -vga std -fda ${image_file}

debug:
 qemu-system-i386 -m 32 -localtime -vga std -fda ${image_file} \
  -gdb tcp::10000 \
  -S

clean:
 rm tinyos.sys

boot.s

.set BOTPAK,  0x00280000 # bootpackのロード先
.set DSKCAC,  0x00100000 # ディスクキャッシュの場所
.set DSKCAC0, 0x00008000 # ディスクキャッシュの場所(リアルモード)

// BOOT_INFO
.set CYLS, 0x0ff0 # シリンダ数
.set LEDS,0x0ff1 # LEDの状態
.set VMODE, 0x0ff2 # ビデオモード
.set SCRNX, 0x0ff4 # 解像度X
.set SCRNY, 0x0ff6 # 解像度Y
.set VRAM, 0x0ff8 # VRAMの開始アドレス

.text
.code16

 // ビデオモードを変更する
 movb $0x00, %ah # VGA Graphics 320x200x8bit
 movb $0x13, %al
 int $0x10

 // 画面の状態を記録する
 movb $8, (VMODE)
 movw $320, (SCRNX)
 movw $200, (SCRNY)
 movl $0x000a0000, (VRAM)

 // LEDの状態を記録する
 movb $0x02, %ah
 int $0x16
 movb %al, (LEDS)

 // PICが割り込みを受け付けないようにする
 movb $0xff, %al
 outb %al, $0x21
 nop   # outは連続して使用しない
 outb %al, $0xa1
 cli   # CPUでも割り込み禁止

 // A20互換モードを無効にして1MB以上のアドレスにアクセスできるようにする
 call waitkbdout
 movb $0xd1, %al
 outb %al, $0x64
 call waitkbdout
 movb $0xdf, %al # A20を有効にする
 outb %al, $0x60
 call waitkbdout

 // プロテクトモードに移行する
.arch i486
 lgdt (GDTR0)
 movl %cr0, %eax
 andl $0x7fffffff, %eax # ページング禁止
 orl $0x00000001, %eax # プロテクトモード移行
 movl %eax, %cr0
 jmp pipelineflush
pipelineflush:
 movw $1*8, %ax
 movw %ax, %ds
 movw %ax, %es
 movw %ax, %fs
 movw %ax, %gs
 movw %ax, %ss

 // bootpackを転送する
 movl $bootpack, %esi # 転送元
 movl $BOTPAK, %edi # 転送先
 movl $512*1024/4, %ecx # 4で割っているのは4バイト単位で処理するため
 call memcpy

 // ディスクイメージを本来の位置へ転送する

 // ブートセクタ
 movl $0x7c00, %esi
 movl $DSKCAC, %edi
 movl $512/4, %ecx
 call memcpy
 
 // 残り
 movl $DSKCAC0+512, %esi
 movl $DSKCAC+512, %edi
 movl $0, %ecx
 movb (CYLS), %cl   # 読み込んだシリンダ数
 imull $512*18*2/4, %ecx # 1シリンダあたりのバイト数/4を掛ける
 sub $512/4, %ecx      # IPL分を引く
 call memcpy

 // bootpackを起動する
 movl $BOTPAK, %ebx
 movl 16(%ebx), %ecx
 addl $3, %ecx
 shrl $2, %ecx
 jz skip  # 転送するものがない
 movl 20(%ebx), %esi  # .dataのアドレス
 addl %ebx, %esi
 movl 12(%ebx), %edi # .data転送先
 call memcpy
skip: 
 movl 12(%ebx), %esp  # スタック初期値
 ljmpl $2*8, $0x0000001b
 
waitkbdout:
 inb $0x64, %al
 andb $0x02, %al
 inb $0x60, %al # 元のソースにはないコード
 jnz waitkbdout
 ret

memcpy:
 movl (%esi), %eax
 addl $4, %esi
 movl %eax, (%edi)
 addl $4, %edi
 subl $1, %ecx
 jnz memcpy
 ret

.align 16
 
GDT0:
 // GDTの構成
 // short limit_low, base_low
 // char base_mid, access_right
 // char limit_high, base_high
 
 // null selector
 .skip 8, 0x00
 // base=0x00000000 limit=0xcfffff access_right=0x92
 .word 0xffff, 0x0000, 0x9200, 0x00cf
 // base=0x00280000 limit=0x47ffff access_right=0x9a
 .word 0xffff, 0x0000, 0x9a28, 0x0047
 .word 0x0000
 
GDTR0:
 .word 8 * 3 - 1 # GDTのサイズ?
 .int GDT0

.align 16
bootpack:
 # + 0 : stack+.data+heap の大きさ(4KBの倍数)
 .int 0x00
 # + 4 : シグネチャ "Hari"
 .ascii "Tiny"
 # + 8 : mmarea の大きさ(4KBの倍数)
 .int 0x00
 # +12 : スタック初期値&.data転送先
 .int 0x00310000
 # +16 : .dataのサイズ
 .int 0x11a8
 # +20 : .dataの初期値列がファイルのどこにあるか
 .int 0x10c8
 # +24 +28 のセットで 1bからの命令が E9 XXXXXXXX (JMP)になり、C言語のエントリポイントにJMPするようだ
 # +24 : 0xe9000000
 .int  0xe9000000
 # +28 : エントリアドレス-0x20
 .int 0x00
 # +32 : heap領域(malloc領域)開始アドレス
 .int 0x00

tinyos.c

void Main(void) {
 fin:
  __asm__("hlt\n\t");
  goto fin;
}

gdbを使い、正しく動いているのか確認してみた。
bootpackへのジャンプ
 ljmpl $2*8, $0x0000001b
の実行前に 0x00280000番地からメモリをダンプして bootpackの内容になっていることを確認後、 ステップ実行して、0x1bにジャンプし、 0番地からのメモリをダンプしてみたが、 bootpackの内容にはなっていなかった。
ljmplで0x1bにジャンプしても、0x00280000番地が0番地に割り当てられていないようである。

いろいろ調べてみたが、原因が分からず、今だC言語で書いた関数の呼び出しができない。

Ubuntuの64bit環境だと、いろいろと手間取ることが多いので、Debianの32bit環境で試してみよう。

参考

2014年9月22日月曜日

[OS作成]30日でできる!OS自作入門 3日目 (6)

やっとOS本体のコードを実行できた。
前回まで動かなかった原因は、1セクタ分しかメモリに読み込んでおらず、 OS本体が読み込まれていなかったことが原因だった。 こんなコードを書いていた。
 .set MAX_SECTOR, 18 # 最大セクタ数
..略..
 add $1, %cl
 cmp MAX_SECTOR, %cl ←◆ここ
 jbe readloop # セクタは 1 〜 MAX_SECTOR
これでMAX_SECTOR(18)とCLレジスタを比較しているつもりだったが、 これは間違いだった。 即値と比較するには、以下のコードのように識別子の頭に$を付けなければならなかった。 ($を付けないと、間接アドレシングになるのかな?)
 .set MAX_SECTOR, 18 # 最大セクタ数
..略..
 add $1, %cl
 cmp $MAX_SECTOR, %cl ←◆$を付ける
 jbe readloop # セクタは 1 〜 MAX_SECTOR
同様にして、以下の識別子を参照している箇所は、即値となるように$を付けた。
 .set MAX_RETRY, 5 # 再読み込み最大回数
 .set MAX_SECTOR, 18 # 最大セクタ数
 .set MAX_HEAD, 2 # 最大ヘッド数
 .set MAX_CYLINDER, 10 # 最大シリンダ数
イメージを作成して、qemuで動作確認すると、画面は黒くなり、ビデオモードの変更を確認できた。
ここまでのコードは以下の通り。

ipl.s

/*
 ipl.s
*/
 .text 
 .code16
 jmp entry
 .byte 0x90
 .ascii "TINY_IPL" # ブートセクタの名前
 .word 512  # 1セクタのバイト数
 .byte 1  # クラスタの数
 .word 1  # FAT開始セクタ
 .byte 2  # FATの個数
 .word 224  # ルートディレクトリ領域のエントリ数
 .word 2880  # ドライブのセクタ数
 .byte   0xF0  # メディアタイプ
 .word 9  # FAT領域のセクタ数
 .word 18  # 1トラックのセクタ数
 .word 2  # ヘッド数
 .int 0  # ?
 .int 2880  # ドライブのセクタ数
 .byte 0, 0, 0x29 # ?
 .int 0xFFFFFFFF # ボリュームシリアル番号
 .ascii "TINY-OS    " # ディスクの名前
 .ascii "FAT12   " # フォーマットの名前
 .space 18

// プログラム
 .set MAX_RETRY, 5 # 再読み込み最大回数
 .set MAX_SECTOR, 18 # 最大セクタ数
 .set MAX_HEAD, 2 # 最大ヘッド数
 .set MAX_CYLINDER, 10 # 最大シリンダ数

entry:
 movw $0, %ax
 movw %ax, %ss
 movw $0x7C00, %sp
 movw %ax, %ds

 // 2セクタから1セクタ分読み込む
 movw $0x0820, %ax
 movw %ax, %es
 movb $0, %ch  # シリンダ0
 movb $0, %dh  # ヘッド0
 movb $2, %cl  # セクタ2

readloop: 
 movw $0, %si  # 失敗回数

retry:
 movb $0x02, %ah # Read sector(s) into memory
 movb $1, %al  # 1セクタ読み込む
 movw $0, %bx  # ES:BX Data buffer(0x8200に読み込む)
 movb $0x00, %dl # Aドライブ
 int $0x13  # BIOS interrupt call
 jnc next  # 読み込みOK でnextへ

 add $1, %si
 cmp $MAX_RETRY, %si
 jae error  # SI >= MAX_RETRY でerrorへ

 movb $0x00, %ah # Reset disk system
 movb $0x00, %dl # Aドライブ
 int $0x13
 jmp retry

next:
 movw %es, %ax # ES = ES + 0x20(512バイト)
 add $0x20, %ax
 movw %ax, %es
 // セクタ
 add $1, %cl
 cmp $MAX_SECTOR, %cl
 jbe readloop # セクタは 1 〜 MAX_SECTOR
 movb $1, %cl
 // ヘッド
 add $1, %dh
 cmp $MAX_HEAD, %dh
 jb readloop # ヘッドは 0 〜 MAX_HEAD - 1
 movb $0, %dh
 // シリンダ
 add $1, %ch
 cmp $MAX_CYLINDER, %ch
 jb readloop # シリンダは 0 〜 MAX_CYLINDER - 1
 jmp 0xC200  # 0x8000 + 0x4200 = 0xC200
 
error: 
 movw $msg, %si
putloop:
 movb (%si), %al
 add $1, %si
 cmp $0, %al
 je fin  # メッセージの後ろの0x00で終了する
 movb $0x0E, %ah # Write Character in TTY Mode
 movw $15, %bx # カラーコード
 int $0x10  # BIOS interrupt call
 jmp putloop
fin:
 hlt
 jmp fin

// メッセージ 
msg: 
 .string "\n\nload error\n"

 .org 0x1FE
 .byte 0x55, 0xAA # 55AAでブートセクタ

2014年9月21日日曜日

[OS作成]30日でできる!OS自作入門 3日目 (5)

まだまだ3日目だ。

今度は、OS本体起動時に、ビデオモードを設定してみる。

tinyos.s

.text
.code16 
 movb $0x00, %ah
 movb $0x13, %al
 int $0x10
fin: 
 hlt
 jmp fin
正しく実行できれば、真っ暗な画面が表示されるはず。
だが、試してみると動かない。

原因としては、次ののどちらか。
jmp 0xC200 で 0xC200にジャンプしていない。
FDからOS本体のイメージを正しく読んでいない。

前日に、jmp 0xC200 で実行したところ、正しく動作せず、
jmp *0xC200 にしたら、見掛け上は動いているようだったので
安心していたが、このコードでは、
0xC200に書いてあるアドレスにジャンプする命令だったようだ。

ソースとリンカスクリプトを再チェック、ビルド方法の確認をしてみたが、今のところお手上げ。
仕様がないので、qemu上のコードをGDBでデバッグすることにした。

Makefile に デバッグ用のターゲットを追加する。

Makefile

debug:
 qemu-system-i386 -m 32 -localtime -vga std -fda ${image_file} \
  -gdb tcp::10000 \
  -S
GDBは、起動時に実行するスクリプトファイルを指定できる。
今後の開発で、多分、実行したいコマンドが増えると思うので、作成しておく。

gdb.scr

target remote localhost:10000
make debug で qemuを起動した後に、gdbを-xオプションでスクリプトファイルを指定して起動する。
$ gdb -x gdb.scr
ブレークポイントを0x7c00に設定する。
(gdb) b *0x7c00
Breakpoint 5 at 0x7c00
ブレークポイントまで実行する。
(gdb) c
Continuing.

Breakpoint 5, 0x00007c00 in ?? ()
止まった。 ジャンプする直前で止めたいので、lstファイルを確認すると、該当箇所は0x00acなので、 0x7cacにブレークポイントを設定し、continueする。
  80                            // シリンダ
  81 00a3 80C501                add     $1, %ch
  82 00a6 3A2E0A00              cmp     MAX_CYLINDER, %ch
  83 00aa 72B9                  jb      readloop        # シリンダは 0 〜 MAX_CYLINDER - 1
  84 00ac E90000                jmp     0xC200          # 0x8000 + 0x4200 = 0xC200  ← ◆ここ
(gdb) b *0x7cac
Breakpoint 2 at 0x7cac
(gdb) c
Continuing.

Breakpoint 2, 0x00007cac in ?? ()
レジスタ値を確認する。
(gdb) i all-registers 
eax            0x840 2112
ecx            0x101 257
edx            0x0 0
ebx            0x0 0
esp            0x7c00 0x7c00
ebp            0x0 0x0
esi            0x0 0
edi            0x0 0
eip            0x7cac 0x7cac
eflags         0x202 [ IF ]
cs             0x0 0
ss             0x0 0
ds             0x0 0
es             0x840 2112 ← ◆これ
fs             0x0 0
gs             0x0 0
あれ?ESレジスタが1回分しか0x20が加算されておらず、0x840になっている。 停止させたアドレスが間違っているのかと思って、continueしたけど
(gdb) c
Continuing.
このまま止まらなかった。 イメージ読み込みがうまく動いていないのか。
ソースをチェックすることにしよう...

参考

[OS作成]30日でできる!OS自作入門 3日目 (4)

いよいよOS本体を起動する処理の作成だ。

IPLとOS本体を含むイメージファイルを作成するには、筆者作成のedimg.exeを使うと簡単であるが、ここ  や ここ  を参考にすると、UbuntuやDebianのmtoolsパッケージに含まれるmformatコマンドを使ってFDイメージを作成しているので、同様の手順で作成することにした。

mformatコマンドを使うとイメージファイルを次のコマンドで作成できる。
$ mformat -f 1440 -B ブートセクタファイル -C -i イメージファイル名 ::
ipl.binはブートセクタのファイルをBオプションで指定できる。
なお、man mformatを見ても、iオプションの説明は無かった。
OS本体をイメージファイルにコピーするには、
$ mcopy OS本体 -i イメージファイル名 ::
とする。

それぞれのコマンドの後ろにtarget directoryとして :: としているが、イメージファイルの場合は、これを指定するようだ。

Makefileは以下のようになった。

Makefile

image_file=tinyos.img

ipl.bin: ipl.s lnk.ls
    gcc -nostdlib -o $@ -Tlnk.ls ipl.s
    gcc -Tlnk.ls -c -g -Wa,-a,-ad ipl.s > ipl.lst

tinyos.sys: tinyos.s lnk.ls
    gcc -nostdlib -o $@ -Tlnk.ls tinyos.s
    gcc -Tlnk.ls -c -g -Wa,-a,-ad tinyos.s > tinyos.lst

image_file: ipl.bin tinyos.sys
    mformat -f 1440 -B ipl.bin -C -i ${image_file} ::
    mcopy tinyos.sys -i ${image_file} ::

img:
    make image_file

run:
    qemu-system-i386 -m 32 -localtime -vga std -fda ${image_file}

書籍通りに、まずは簡単なコードで確認する。

tinyos.s

fin:   
    hlt
    jmp fin

make img でイメージを作成してみる。

なお、tinyos.sのリスティングファイル(アセンブル結果)は以下の通り。

tinyos.lst

   1                  fin:   
   2 0000 F4               hlt
   3 0001 EBFD             jmp fin

書籍の通りに、0x2600にファイル情報がある。
00002600: 5449 4e59 4f53 2020 5359 5320 1800 e9b5  TINYOS  SYS ....
00002610: 3445 3445 0000 e9b5 3445 0200 0300 0000  4E4E....4E......
0x4200からOS本体のコードがある。
00004200: f4eb fd00 0000 0000 0000 0000 0000 0000  ................
イメージを0x8000から読み込ませているので、
イメージの0x4200からのコードを実行するには、
0x8000 + 0x4200 = 0xC200 になる。

ipl.s を修正して、FDの読み込み処理後に
    jmp    0xC200        # 0x8000 + 0x4200 = 0xC200
を追加した。

IPL用のリンカスクリプトとは別に、OS本体用のリンカスクリプトも必要だ。

tinyos_lnk.ls

OUTPUT_FORMAT("binary");

SECTIONS {
     . = 0xC200;
}
Makefileを、これに合わせて修正する。
image_file=tinyos.img

ipl.bin: ipl.s lnk.ls
 gcc -nostdlib -o $@ -Tipl_lnk.ls ipl.s
 gcc -Tipl_lnk.ls -c -g -Wa,-a,-ad ipl.s > ipl.lst

tinyos.sys: tinyos.s lnk.ls
 gcc -nostdlib -o $@ -Ttinyos_lnk.ls tinyos.s
 gcc -Ttinyos_lnk.ls -c -g -Wa,-a,-ad tinyos.s > tinyos.lst

image_file: ipl.bin tinyos.sys
 mformat -f 1440 -B ipl.bin -C -i ${image_file} ::
 mcopy tinyos.sys -i ${image_file} ::

img:
 make image_file

run:
 qemu-system-i386 -m 32 -localtime -vga std -fda ${image_file}

だんだん汚なくなってきた。そのうち綺麗に書き直そう。

make imgしてイメージを作成後、qemuで試してみる。
が、正しく動かない。load errorと出てしまう。
JMPがすっとばされているのか?
リスティングファイルを確認する。

ipl.lst

  83 00ac E90000                jmp     0xC200          # 0x8000 + 0x4200 = 0xC200
どこにもC200が無い。これでは駄目だ。

いろいろ調べて、試行錯誤してみると、

ipl.s

    jmp    *0xC200        # 0x8000 + 0x4200 = 0xC200

ipl.lst

  83 00ac FF2600C2              jmp     *0xC200         # 0x8000 + 0x4200 = 0xC200
うーむ。*0xC200と書くのが正しいのか。
(実は、最初は指定アドレスは即値なので$0xC200と書いて怒られた。)
きちんとGASのリファレンスを読んだほうが良さそうだな。

これで試してみると、一応見かけ上は動いているみたい。
今日はここまで。

9/21 追記
この後、ビデオモードを変更する処理を書いてみたが、どうも上のコードでは、0xC200へjmpしていないようだ。
0xC200へJMPさせるには、どう書けば良いのか?うーむ。

参考


2014年9月20日土曜日

[OS作成]30日でできる!OS自作入門 3日目 (3)

課題は、18セクタ分を読み込む処理を書き,
その後に、それをベースに、10シリンダ分を読み込む処理を書くこと。
/*
 ipl.s
*/
 .code16
 jmp entry
 .byte 0x90
 .ascii "TINY_IPL" # ブートセクタの名前
 .word 512  # 1セクタのバイト数
 .byte 1  # クラスタの数
 .word 1  # FAT開始セクタ
 .byte 2  # FATの個数
 .word 224  # ルートディレクトリ領域のエントリ数
 .word 2880  # ドライブのセクタ数
 .byte   0xF0  # メディアタイプ
 .word 9  # FAT領域のセクタ数
 .word 18  # 1トラックのセクタ数
 .word 2  # ヘッド数
 .int 0  # ?
 .int 2880  # ドライブのセクタ数
 .byte 0, 0, 0x29 # ?
 .int 0xFFFFFFFF # ボリュームシリアル番号
 .ascii "TINY-OS    " # ディスクの名前
 .ascii "FAT12   " # フォーマットの名前
 .space 18

// プログラム
 .set MAX_RETRY, 5 # 再読み込み最大回数
 .set MAX_SECTOR, 18 # 最大セクタ数
 .set MAX_HEAD, 2 # 最大ヘッド数
 .set MAX_CYLINDER, 10 # 最大シリンダ数

entry:
 movw $0, %ax
 movw %ax, %ss
 movw $0x7C00, %sp
 movw %ax, %ds

 // 2セクタから1セクタ分読み込む
 movw $0x0820, %ax
 movw %ax, %es
 movb $0, %ch  # シリンダ0
 movb $0, %dh  # ヘッド0
 movb $2, %cl  # セクタ2

readloop: 
 movw $0, %si  # 失敗回数

retry:
 movb $0x02, %ah # Read sector(s) into memory
 movb $1, %al  # 1セクタ読み込む
 movw $0, %bx  # ES:BX Data buffer(0x8200に読み込む)
 movb $0x00, %dl # Aドライブ
 int $0x13  # BIOS interrupt call
 jnc next  # 読み込みOK でnextへ

 add $1, %si
 cmp MAX_RETRY, %si
 jae error  # SI >= MAX_RETRY でerrorへ

 movb $0x00, %ah # Reset disk system
 movb $0x00, %dl # Aドライブ
 int $0x13
 jmp retry

next:
 movw %es, %ax # ES = ES + 0x20(512バイト)
 add $0x20, %ax
 movw %ax, %es
 // セクタ
 add $1, %cl
 cmp MAX_SECTOR, %cl
 jbe readloop # セクタは 1 〜 MAX_SECTOR
 movb $1, %cl
 // ヘッド
 add $1, %dh
 cmp MAX_HEAD, %dh
 jb readloop # ヘッドは 0 〜 MAX_HEAD - 1
 movb $0, %dh
 // シリンダ
 add $1, %ch
 cmp MAX_CYLINDER, %ch
 jb readloop # シリンダは 0 〜 MAX_CYLINDER - 1

fin:
 hlt
 jmp fin

error: 
 movw $msg, %si
putloop:
 movb (%si), %al
 add $1, %si
 cmp $0, %al
 je fin  # メッセージの後ろの0x00で終了する
 movb $0x0E, %ah # Write Character in TTY Mode
 movw $15, %bx # カラーコード
 int $0x10  # BIOS interrupt call
 jmp putloop

// メッセージ 
msg: 
 .string "\n\nload error\n"

 .org 0x1FE
 .byte 0x55, 0xAA # 55AAでブートセクタ
プログラムにバグが無ければ、FDから読み込んだ (18 * 2 * 10 - 1) * 512 バイト分のデータが0x8200以降に格納されているはず。

FDを作って実機で動かせば、FDのデータを読み込んで様子が分かるのだろうけど、今のパソコンにFDDは付いていない。
qemuだけだと、正しく動いているのか、暴走しているのか良く分からない。
gdbでデバッグできるようだけど、まだ試していない。

今日はここまで。次はOS本体を書き始めるようだ。

2014年9月17日水曜日

[OS作成]30日でできる!OS自作入門 3日目 (2)

ファイル読み込みに失敗したら、最大5回リトライする処理。
読み込み失敗時は、ディスクシステムのリセットをするのがポイント。

最大リトライ回数の定義に、.setディレクティブを使ってみた。

変更部分は以下の通り。
// プログラム
 .set MAX_RETRY, 5 # 再読み込み最大回数

entry:
 movw $0, %ax
 movw %ax, %ss
 movw $0x7C00, %sp
 movw %ax, %ds

 // 2セクタから1セクタ分読み込む
 movw $0x0820, %ax
 movw %ax, %es
 movb $0, %ch  # シリンダ0
 movb $0, %dh  # ヘッド0
 movb $2, %cl  # セクタ2

 movw $0, %si  # 失敗回数

retry:
 movb $0x02, %ah # Read sector(s) into memory
 movb $1, %al  # 1セクタ読み込む
 movw $0, %bx  # ES:BX Data buffer(0x8200に読み込む)
 movb $0x00, %dl # Aドライブ
 int $0x13  # BIOS interrupt call
 jnc fin  # 読み込みOK でfinへ

 add $1, %si
 cmp MAX_RETRY, %si
 jae error  # SI >= MAX_RETRY でerrorへ

 movb $0x00, %ah # Reset disk system
 movb $0x00, %dl # Aドライブ
 int $0x13
 jmp retry

fin:
 hlt
 jmp fin

一応、うまく動作しているようだ。

2014年9月16日火曜日

[OS作成]30日でできる!OS自作入門 3日目 (1)

次の課題は、ブートセクタの次のセクタを読み込む処理。

シリンダ、ヘッド、セクタを指定して int 0x13 を呼び出す。
エラー発生時はCF=1となるので、その場合はエラーメッセージを表示する。

今日からイメージファイル名をtinyos.imgにした。
リンカスクリプトは変更なし。

Makefile

image_file=tinyos.img

ipl.bin: ipl.s lnk.ls
 gcc -nostdlib -o $@ -Tlnk.ls ipl.s
 gcc -Tlnk.ls -c -g -Wa,-a,-ad ipl.s > ipl.lst

zero.img:
# 1474560 - 512 = 1474048
 dd if=/dev/zero of=zero.img ibs=512 count=2879

image_file: ipl.bin zero.img
 cat ipl.bin zero.img > ${image_file}

img:
 make image_file

run:
 qemu-system-i386 -m 32 -localtime -vga std -fda ${image_file}

ipl.s

/*
 ipl.s
*/
 .code16
 jmp entry
 .byte 0x90
 .ascii "TINY_IPL" # ブートセクタの名前
 .word 512  # 1セクタのバイト数
 .byte 1  # クラスタの数
 .word 1  # FAT開始セクタ
 .byte 2  # FATの個数
 .word 224  # ルートディレクトリ領域のエントリ数
 .word 2880  # ドライブのセクタ数
 .byte   0xF0  # メディアタイプ
 .word 9  # FAT領域のセクタ数
 .word 18  # 1トラックのセクタ数
 .word 2  # ヘッド数
 .int 0  # ?
 .int 2880  # ドライブのセクタ数
 .byte 0, 0, 0x29 # ?
 .int 0xFFFFFFFF # ボリュームシリアル番号
 .ascii "TINY-OS    " # ディスクの名前
 .ascii "FAT12   " # フォーマットの名前
 .space 18

// プログラム
entry:
 movw $0, %ax
 movw %ax, %ss
 movw $0x7C00, %sp
 movw %ax, %ds

 // 2セクタから1セクタ分読み込む
 movw $0x0820, %ax
 movw %ax, %es
 movb $0, %ch  # シリンダ0
 movb $0, %dh  # ヘッド0
 movb $2, %cl  # セクタ2

 movb $0x02, %ah # ディスク読み込み
 movb $1, %al  # 1セクタ読み込む
 movw $0, %bx  # ES:BX Data buffer(0x8200に読み込む)
 movb $0x00, %dl # Aドライブ
 int $0x13  # BIOS interrupt call
 jc error

fin:
 hlt
 jmp fin

error: 
 movw $msg, %si
putloop:
 movb (%si), %al
 add $1, %si
 cmp $0, %al
 je fin  # メッセージの後ろの0x00で終了する
 movb $0x0E, %ah # Write Character in TTY Mode
 movw $15, %bx # カラーコード
 int $0x10  # BIOS interrupt call
 jmp putloop

// メッセージ 
msg: 
 .string "\n\nload error\n"

 .org 0x1FE
 .byte 0x55, 0xAA # 55AAでブートセクタ

$ make img
$ make run

で実行したが、エラーメッセージは出ていない。
指定したセクタを正しく読めているようであるが、本当にそうなのかは、画面からは分からない。

今日はここまで。

2014年9月15日月曜日

[OS作成]30日でできる!OS自作入門 2日目 (2)

書籍では、BIOSの解説ページ
http://community.osdev.info/?(AT)BIOS
が紹介されているが、SPAM業者?か何かに書き換えられているようで、
変なリンクがたくさん表示されおり、肝心の情報は何もない。

うーむ。どこを見れば良いかな?
探してみた。

このページが良いかな?
Interrupt Jump Table

知りたい割り込み番号をクリックすると詳細を表示できる。
例えば、今回、使っている int $0x10 は、
http://www.ctyme.com/intr/int-10.htm
であり、AX=$0x0Eについては、
http://www.ctyme.com/intr/rb-0106.htm
が、詳細情報である。

書籍と同様に、開発しやすくなるように、Makefileを作ることにしよう。

疑問点が一つある。
ブートセクタ以降に、以下のコードを書いていたが、これは何だろう。
以下のように編集して、ブートセクタ以降を全て0x00にしたイメージを使い、qemuで動作確認してみたが、問題なく動いた。
 .org 0x1FE
 .byte 0x55, 0xAA # 55AAでブートセクタ

// ブートセクタ以降
 
// .byte 0xF0, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00
 .byte 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
 .space 4600
// .byte 0xF0, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00
 .byte 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
 .space 1469432
とりあえず気にせずに、このまま進めよう。

作成したMakefileは以下の通り。
image_file=helloos.img

ipl.bin: ipl.s lnk.ls
 gcc -nostdlib -o $@ -Tlnk.ls ipl.s
 gcc -Tlnk.ls -c -g -Wa,-a,-ad ipl.s > ipl.lst

zero.img:
# 1474560 - 512 = 1474048
 dd if=/dev/zero of=zero.img ibs=512 count=2879

image_file: ipl.bin zero.img
 cat ipl.bin zero.img > ${image_file}

img:
 make image_file

run:
 qemu-system-i386 -m 32 -localtime -vga std -fda ${image_file}

リスティングファイル(アセンブル結果)を出力するには、gccの-Wa,-aオプションを渡して、asコマンドのリスティング出力を有効にする。

書籍では、独自コマンドを使って、イメージを作成していたが、
catコマンドで
(1)gccで作成した512バイトのブートセクタイメージ
(2)1.44Mバイト-512バイト(ブートセクタ分)のサイズを0で埋めたファイル
を連結して作成している。


これで、次のターゲットを利用できるようになった。

イメージファイルを作成する。
$ make img

作成したイメージをqemuを起動する。
$ make run

参考


[OS作成]30日でできる!OS自作入門 2日目 (1)

今度はプログラム部分を分かり易く置き換えていく。

helloos3.s

/*
 helloos3.s
*/
 .code16
 jmp entry
 .byte 0x90
 .ascii "HELLOIPL" # ブートセクタの名前
 .word 512  # 1セクタのバイト数
 .byte 1  # クラスタの数
 .word 1  # FAT開始セクタ
 .byte 2  # FATの個数
 .word 224  # ルートディレクトリ領域のエントリ数
 .word 2880  # ドライブのセクタ数
 .byte   0xF0  # メディアタイプ
 .word 9  # FAT領域のセクタ数
 .word 18  # 1トラックのセクタ数
 .word 2  # ヘッド数
 .int 0  # ?
 .int 2880  # ドライブのセクタ数
 .byte 0, 0, 0x29 # ?
 .int 0xFFFFFFFF # ボリュームシリアル番号
 .ascii "HELLO-OS   " # ディスクの名前
 .ascii "FAT12   " # フォーマットの名前
 .space 18

// プログラム
entry:
 movw $0, %ax
 movw %ax, %ss
 movw $0x7C00, %sp
 movw %ax, %ds
 movw %ax, %es

 movw $msg, %si
putloop:
 movb (%si), %al
 add $1, %si
 cmp $0, %al
 je fin  # メッセージの後ろの0x00で終了する
 movb $0x0E, %ah # Write Character in TTY Mode
 movw $15, %bx # カラーコード
 int $0x10  # BIOS interrupt call
 jmp putloop
fin:
 hlt
 jmp fin

// メッセージ 
msg: 
 .string "\n\nhello, world\n"

 .org 0x1FE
 .byte 0x55, 0xAA # 55AAでブートセクタ

// ブートセクタ以降
 
 .byte 0xf0, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00
 .space 4600
 .byte 0xF0, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00
 .space 1469432
起動時はリアルモードなので、.code16で、16bitコードを生成するように指定する。

ELFヘッダをスキップしたイメージを作るには、リンカスクリプトを使えば良いことが分かった。
NASMのORGに相当するものはGASには無いので、ORGで指定するアドレスはリンカスクリプトで設定する。

lnk.ls

OUTPUT_FORMAT("binary");

IPL_BASE = 0x7C00;

SECTIONS {
  . = IPL_BASE;
}
今までは、asコマンドとddコマンドでイメージを作成していたが、これからは、gccコマンドでイメージを作成できる。
$ gcc -nostdlib -o helloos3.img -Tlnk.ls helloos3.s
今まで通り、qemuで確認してOK。
$ qemu-system-i386 -m 32 -localtime -vga std -fda helloos3.img
ここを参考にすると、リンカスクリプトを
OUTPUT_FORMAT("binary");
IPLBASE = 0x7c00;

SECTIONS {
        . = IPLBASE;
        .text        : {*(.text)}
        .data        : {*(.data)}
        . = IPLBASE + 510;
        .sign        : {SHORT(0xaa55)}
}
のように書くと 0x1FEからの0xAA,0x55も定義できるようだけど、試していない。
今のところ、.text, .dataの設定は不要だった。
そのうち必要になるのかな?

Z80のアセンブラには慣れているせいか、GASの標準であるAT&T構文ではsourceとdestinationが逆なのは、ちょっと気持ち悪く感じる。

参考


2014年9月14日日曜日

[OS作成]30日でできる!OS自作入門 1日目 (3)

書籍と同様に、ディレクティブを使って、もう少し分かりやすく書き直した。
/*
 helloos2.s
*/
 .byte 0xEB, 0x4E, 0x90
 .ascii "HELLOIPL" # ブートセクタの名前
 .word 512  # 1セクタのバイト数
 .byte 1  # クラスタの数
 .word 1  # FAT開始セクタ
 .byte 2  # FATの個数
 .word 224  # ルートディレクトリ領域のエントリ数
 .word 2880  # ドライブのセクタ数
 .byte   0xF0  # メディアタイプ
 .word 9  # FAT領域のセクタ数
 .word 18  # 1トラックのセクタ数
 .word 2  # ヘッド数
 .int 0  # ?
 .int 2880  # ドライブのセクタ数
 .byte 0, 0, 0x29 # ?
 .int 0xFFFFFFFF # ボリュームシリアル番号
 .ascii "HELLO-OS   " # ディスクの名前
 .ascii "FAT12   " # フォーマットの名前
 .space 18

// プログラム

 .byte 0xB8, 0x00, 0x00, 0x8E, 0xD0, 0xBC, 0x00, 0x7C
 .byte 0x8E, 0xD8, 0x8E, 0xC0, 0xBE, 0x74, 0x7C, 0x8A
 .byte 0x04, 0x83, 0xC6, 0x01, 0x3C, 0x00, 0x74, 0x09
 .byte 0xB4, 0x0E, 0xBB, 0x0F, 0x00, 0xCD, 0x10, 0xEB
 .byte 0xEE, 0xF4, 0xEB, 0xFD

// メッセージ 

 .string "\n\nhello, world\n"

 .org 0x1FE
 .byte 0x55, 0xAA # 55AAでブートセクタ

// ブートセクタ以降
 
 .byte 0xf0, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00
 .space 4600
 .byte 0xF0, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00
 .space 1469432
動作確認。
$ as --32 -o helloos2 helloos2.s
$ dd if=helloos2 of=helloos2.img skip=52 ibs=1 count=1474560
$ qemu-system-i386 -m 32 -localtime -vga std -fda helloos2.img

GASを使っていて、分かったこと

・コメントは3種類の書き方がある。
// コメント
/* コメント */
# コメント
・.stringディレクティブを使うと文字列の最後に0x00を付ける。
・.string,.asciiの文字列にはエスケープシーケンスが使える。

その他、分かったこと

・最初のセクタの最後の2バイトが0x55,0xAAの場合に、ブートセクタと判断し、プログラムを実行する。

ふーん。そういうふうに起動ディスクか判断していたのね。
ハードディスクの場合も同様かな?

やっと一日目が完了。先は長いな...

2014年9月13日土曜日

[OS作成]30日でできる!OS自作入門 1日目 (2)

数日経ったが、まだ1日目である。むぅぅぅ...

「30日でできる! OS自作入門」のサポートページにはLinux用のツールが公開されているが
筆者の独自ツールを使っているようだった。
あまり独自ツールは使いたくなかったので、いろいろと検索してみると、GASとGCCでも開発できるようだったので、
こちらを試してみた。
OS自作入門では、NASKというNASMベースのアセンブラを使っているが、GASとは構文が全く違うので、
書籍のコードを、いちいちGAS用に変換しなければならず、少し面倒だ。

NASM Intel構文
GAS AT&T構文

GASでは .intel_syntax ディレクティブを使うと、Intel構文にできるようであるが、とりあえずデフォルトのAT&T構文で開発を進めよう。
と言うことで、作成したのがこれ。
helloos1.s
.byte 0xEB, 0x4E, 0x90, 0x48, 0x45, 0x4C, 0x4C, 0x4F
.byte 0x49, 0x50, 0x4C, 0x00, 0x02, 0x01, 0x01, 0x00
.byte 0x02, 0xE0, 0x00, 0x40, 0x0B, 0xF0, 0x09, 0x00
.byte 0x12, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00
.byte 0x40, 0x0B, 0x00, 0x00, 0x00, 0x00, 0x29, 0xFF
.byte 0xFF, 0xFF, 0xFF, 0x48, 0x45, 0x4C, 0x4C, 0x4F
.byte 0x2D, 0x4F, 0x53, 0x20, 0x20, 0x20, 0x46, 0x41
.byte 0x54, 0x31, 0x32, 0x20, 0x20, 0x20, 0x00, 0x00

.space 0x10

.byte 0xB8, 0x00, 0x00, 0x8E, 0xD0, 0xBC, 0x00, 0x7C
.byte 0x8E, 0xD8, 0x8E, 0xC0, 0xBE, 0x74, 0x7C, 0x8A
.byte 0x04, 0x83, 0xC6, 0x01, 0x3C, 0x00, 0x74, 0x09
.byte 0xB4, 0x0E, 0xBB, 0x0F, 0x00, 0xCD, 0x10, 0xEB
.byte 0xEE, 0xF4, 0xEB, 0xFD, 0x0A, 0x0A, 0x68, 0x65
.byte 0x6C, 0x6C, 0x6F, 0x2C, 0x20, 0x77, 0x6F, 0x72
.byte 0x6C, 0x64, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00

.space 368

.byte 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0xAA
.byte 0xf0, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00

.space 4600

.byte 0xF0, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00

.space 1469432
参考にしたBlogでは .skipディレクティブを使っていたが、GASのマニュアルを参照すると、

.skip
.skip is recognized on the 680x0 platform as a synonym for .space.
.space
Syntax: .space size[, fill]
This directive emits size bytes, each of value fill. Both size and fill are absolute expressions. If the comma and fill are omitted, fill is assumed to be zero.

ということなので、RESBに対応するディレクティブは.skipではなく、.spaceを使用した。

$ as --32 -o helloos1 helloos1.s

--32 を付けて 32ビットコードを生成するようにした。

30日でできる!OS自作入門を Linux & GAS で行う (1日目) を参考に、同様にして、ELFヘッダのサイズを調べる。
$ readelf -h helloos1
ELF ヘッダ:
  マジック:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 
  クラス:                            ELF32
  データ:                            2 の補数、リトルエンディアン
  バージョン:                        1 (current)
  OS/ABI:                            UNIX - System V
  ABI バージョン:                    0
  型:                                REL (再配置可能ファイル)
  マシン:                            Intel 80386
  バージョン:                        0x1
  エントリポイントアドレス:               0x0
  プログラムの開始ヘッダ:          0 (バイト)
  セクションヘッダ始点:          1474664 (バイト)
  フラグ:                            0x0
  このヘッダのサイズ:                52 (バイト)  ← ◆これ
  プログラムヘッダサイズ:            0 (バイト)
  プログラムヘッダ数:                0
  セクションヘッダ:                  40 (バイト)
  セクションヘッダサイズ:            7
  セクションヘッダ文字列表索引:      4

ヘッダのサイズが52バイトなので、ddコマンドで52バイトスキップしてイメージを作成する。
$ dd if=helloos1 of=helloos1.img skip=52 ibs=1 count=1474560
1474560+0 レコード入力
2880+0 レコード出力
1474560 バイト (1.5 MB) コピーされました、 7.76736 秒、 190 kB/秒
最初に作った helloos0.imgと比較する。
$ cmp helloos0.img helloos1.img

qemuで動作確認。
$ qemu-system-i386 -m 32 -localtime -vga std -fda helloos1.img
違いが無いので、当然動く。問題無し。

GASやGCCでの開発方法をBlogで公開してくださった方々に感謝。

参考


2014年9月11日木曜日

[OS作成]30日でできる!OS自作入門 1日目 (1)

書籍ではWindowsを使っているが、
VirtualBox上のUbuntu14.04で開発することにした。

最初はバイナリエディタを使って、FDイメージを作成し、このディスクを使ってマシンを起動すると、
hello, world!
を表示できるようにするのが課題。

VirutalBoxでもqemuは動くのだろうか。
まずはqemuをインストールだ。

$ sudo apt-get install qemu

コンソールベースのバイナリエディタは何か良いのないかなと探してみたら、
Emacsのhexl-modeが、それらしい。
さっそく試してみる。

$ echo "hoge" > hoge.txt

emacs から M-x hexl-find-file で hoge.txt を開く。



標準で、こんな機能があったのか。
もっと早く知りたかった...

FDの容量の1.44MBまで00を埋めるのは面倒なので、ddコマンドで作ってしまおう。

$ dd ibs=1024 count=1440 if=/dev/zero of=helloos0.img
1440+0 レコード入力
2880+0 レコード出力
1474560 バイト (1.5 MB) コピーされました、 0.0289785 秒、 50.9 MB/秒

Emacsでhelloos.imgを開いて、hexl-modeで編集する。
このhexl-modeは1バイトをhexで入力するには、いちいちC-M-xで入力しないと駄目なのね...
面倒くさいと思いつつ、書籍の通りに入力。
ダンプリストを打ち込むなんて何年ぶりだろう。



qemuで起動してみる。

$ qemu-system-i386 -m 32 -localtime -vga std -fda helloos0.img



動いた!
hello, worldが表示された。

参考

バイナリファイルを編集するには

2014年9月10日水曜日

[OS作成]30日でできる!OS自作入門 0日目

30日でできる!OS自作入門という書籍がある。
発売当初は気になっていたのだけど、すっかり存在を忘れていた。
先日、マイナビ Booksを見ていたら、電子書籍化されていることが分かり、
また、キャンペーンらしく、少し安くなっていたので、購入してみた。

書籍紹介の内容を引用すると
プログラミングの基礎からはじめて、30日後にはウィンドウシステムを有する32bitマルチタスクOSをフルスクラッチで作り上げるという入門書。
一からOSを作ろうという内容。

章立ては Chapter0(0日目) から Chapter31(31日目) まである。
まずは、最初の0日目からスタート。

OSとは何?
何を作っていくかの説明。

文体はくだけた感じで、気軽に読める。
最初はアセンブラを使っていくようだ。
アセンブラを使うのは久しぶりだ。

投げ出さずに最後まで行けるかな?
mrubyTinySchemeを動くようにすると楽しそうだ。

参考

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

2014年7月26日土曜日

[OCaml]Unicodeライブラリ ucorelibを使ってみた

Camomile http://camomile.sourceforge.net/ の作者が作成した Unicodeライブラリ ucorelibを試してみた。

シグネチャを見れば、だいたい何をしているのか分かるのだけど、
検索してもサンプルコードが見当らなかったので、書いておこう。

なお、ucorelibのシグネチャはここ

Unicode文字、文字列を操作するモジュールとして、次の2つのモジュールがある。
UCharUnicode文字を操作する。
TextUnicode文字列を操作する。

他にも便利そうなモジュールがあるが、使っていない。
詳細はシグネチャを参照。

以下はutopでの実行例である。
良く使いそうな関数だけを選択した。

UCoreLibを使えるように、utopから以下を実行済み。
#require "ucorelib" ;;
open UCoreLib ;;

UCharモジュール


of_int文字コードからucharを作成する。
code文字コードを取得する。
of_charcharからucharを作成する。
eq同じ文字か比較する。
compare文字を比較する。

文字コードからucharを作成する。
utop[30]> let u_a = UChar.of_int 0x3042 (* あ *) ;;
val u_a : uchar = <abstr>

文字コードを取得する。
utop[31]> UChar.code u_a ;;
- : int = 12354

charからucharを作成する。
utop[32]> UChar.of_char 'a' ;;
- : uchar = <abstr>

同じ文字か比較する。
utop[33]> UChar.eq u_a (UChar.of_int 0x3042) ;;
- : bool = true

文字を比較する。
utop[34]> let u_i = UChar.of_int 0x3044 (* い *) ;; 
val u_i : uchar = <abstr>
utop[35]> UChar.compare u_a (* あ *)  u_i (* い *) ;;
- : int = -2

Textモジュール


of_stringstringから文字列を作成する。
to_string文字列からstringに変換する。
length文字列の長さを取得する。
eq文字列が同じか判定する。
compare文字列を比較する。
get文字列から指定位置の文字を取得する。
append文字列を連結する。
append_char文字列に文字を連結する。
sub部分文字列を取得する。
uchar文字を文字列に変換する。
empty空文字列。
fold畳み込み処理。
iterイテレート処理。

stringから文字列を作成する。
utop[36]> let t1 = Text.of_string "あいうえお" ;;
val t1 : text = <abstr>

文字列からstringに変換する。
utop[53]> Text.to_string t1;;
- : string = "あいうえお"

文字列の長さを取得する。
utop[37]> Text.length t1 ;;
- : int = 5

文字列を比較する。
utop[42]> let t2 = Text.of_string "あいうええ" ;;
val t2 : text = <abstr>
utop[43]> Text.compare t1 (* あいうえお *) t2 (* あいうええ *) ;;
- : int = 2

文字列から指定位置の文字を取得する。
utop[46]> let uc_1_2 = Text.get t1 (* あいうえお *) 2 ;;
val uc_1_2 : uchar = <abstr>
utop[49]> printf "%02X" (UChar.code uc_1_2) ;;
3046- : unit = () ← ◆「う」

文字列を連結する。
utop[54]> Text.to_string @@ Text.append t1 t2 ;;
- : string = "あいうえおあいうええ"

文字列に文字を連結する。
utop[57]> Text.to_string @@ Text.append_char u_i (* い *) t1 ;;
- : string = "あいうえおい"

関数名は append_uchar の方が良く、また、引数の順番も、最初が text 次に uchar の方が自然だと思うが、現状は、このようになっている。

部分文字列を取得する。
utop[58]> Text.to_string @@ Text.sub t1 (* あいうえお *) 1 3;;
- : string = "いうえ"

文字を文字列に変換する。
utop[59]> Text.to_string @@ Text.of_uchar uc_1_2 (* う *) ;;
- : string = "う"

畳み込み処理。
各文字の後ろに'|'を挿入した文字列を取得する例。
utop[63]> Text.fold (fun a c -> a ^ (Text.to_string (Text.of_uchar c)) ^ "|") "" t1 (* あいうえお *) ;;
- : string = "あ|い|う|え|お|"

イテレート処理。
utop[66]> Text.iter (fun c -> (printf "%s\n" (Text.to_string (Text.of_uchar c)))) t1 (* あいうえお *) ;;
あ
い
う
え
お
- : unit = ()

OCamlが標準でUnicodeに対応してくれると良いのだけど。

参考


2014年6月15日日曜日

[Ubuntu][Debian]awesome window managerでスクリーンセーバとDPMS機能を無効にする

どのようにすれば良いのか、なかなか分からなかったので記録を残しておこう。
環境はUbuntu 14.04のawesome window manager。
多分Debian Wheezyでも同様なはず。

GnomeやUnityを使えば、画面をブラックアウトするまでの時間や
ディスプレイの電源をオフにするまでの時間は管理ツールで設定できるが、
awesomeで、画面のブラックアウトと電源OFFを無効にしたかったのだけど、管理ツールが無いので、方法が分からなかった。
いろいろと検索すると、gnome-power-managerやxfce4-power-managerを使う方法が見つかったのだけど、情報が古いみたいで、その通りには設定できなかった。

どうしたものかと、さらに調べてみると、xsetコマンドを使えば良いことが分かった。

スクリーンセーバを無効にする。
$ xset s off
ディスプレイの電源を切れないようにする。
$ xset -dpms

awesome起動時に、このコマンドを実行するには、~/.config/awesome/rc.lua に以下の設定を追加すれば良い。
awful.util.spawn_with_shell("xset s off")
awful.util.spawn_with_shell("xset -dpms")

2014年5月31日土曜日

[OCaml]Core_benchでベンチマーク


Core_benchを使ってみたので、コードを残しておこう。
ベンチマークを取ったのは、マージソートをする関数。
Core_benchは
$ opam install core_bench
でインストールできる。

open Core.Std
open Core_bench.Std

let make_random_list seed n len =
  Random.init seed;
  let rec make_list len lst =
    if len = 0 then lst
    else make_list (len - 1) ((Random.int n) :: lst) in
  make_list len []

let rec merge xs1 xs2 =
  match (xs1, xs2) with
    ([], _) -> xs2
  | (_, []) -> xs1
  | (hd1 :: tl1, hd2 :: tl2) ->
    if hd1 < hd2
    then hd1 :: (merge tl1 xs2)
    else hd2 :: (merge xs1 tl2)

let rec merge_sort xs =
  match xs with
    [] -> []
  | [_] -> xs
  | [a; b] ->
    if a <= b then xs
    else [b; a]
  | _ ->
    let divide_list xs = List.split_n xs ((List.length xs) / 2) in
    let (xs1, xs2) = divide_list xs in
    merge (merge_sort xs1) (merge_sort xs2)

let run_bench tests =
  Bench.bench
    tests

let bench () =
  let lst1 = make_random_list 0 100 1000 in
  [ Bench.Test.create ~name:"merge_sort1" (fun () ->
       ignore (merge_sort lst1))
  ]
  |> run_bench

Bench.bench関数の仕様が変わったようで、Real World OCamlに書かれていたコードは動かなかった。
この関数に渡すパラメタで、ベンチマークのカスタマイズできるようだけど、試していない。
utopによる実行結果は、以下の通り。
表形式で実行結果が表示された。

utop[1]> #require "core_bench";;
utop[7]> bench ();;
Estimated testing time 10s (1 benchmarks x 10s). Change using -quota SECS.
┌─────────────┬──────────┬─────────┬──────────┬──────────┬───────────┐
│ Name        │ Time/Run │ mWd/Run │ mjWd/Run │ Prom/Run │   mGC/Run │
├─────────────┼──────────┼─────────┼──────────┼──────────┼───────────┤
│ merge_sort1 │   1.08ms │ 57.29kw │  919.07w │  919.07w │ 218.65e-3 │
└─────────────┴──────────┴─────────┴──────────┴──────────┴───────────┘
- : unit = ()
utop[8]>

ふと思ったのだけど、merge_sort関数をマルチスレッドで動かす、お手軽な方法はあるのかな?

参考


2014年5月21日水曜日

[OCaml]Yahoo financeから株式情報を取得する

HTTPクライアントでコンテンツを取得する方法、正規表現モジュールを使って文字列を抽出する方法が分かったので、
サンプルプログラムとして、Yahoo financeから株式情報を取得する処理を書いてみた。

open Core.Std

(* 指定した株式コード、市場の株式情報をYahoo financeから取得する。 *)
let get_yahoo_stock_info code market =
  let module C = Http_client.Convenience in
  let url = Printf.sprintf "http://stocks.finance.yahoo.co.jp/stocks/detail/?code=%s.%s" code market in
  C.http_get url

let extract pattern str =
  let module P = Pcre in
  let rex = P.regexp pattern in
  (P.extract ~rex:rex str).(1)

(* コンテンツを解析して、市場、会社名、業種、取引単位の組を取得する。 *)
let parse_stock_info content =
  let module P = Pcre in
  let market = extract "<span class=\"stockMainTabName\">(.*?)</span>" content in
  let industory =
    let industory' = extract "yjSb\">(.*?)</dd>" content in
    if String.slice industory' 0 2 = "<a"
    then extract "<a[^>]+>(.*?)<" industory'
    else industory' in
  let company_name = extract "<th class=\"symbol\"><h1>(.*?)</h1></th>" content in
  let unit = extract "<dl class=\"tseDtlDelay\"><dd class=\"ymuiEditLink mar0\"><strong>(.*?)</strong>株</dd>" content in
    (market, company_name, industory, unit)

utopで確認。   

utop[22]> let content = get_yahoo_stock_info "6753" "t" in parse_stock_info content;;                                                                                    
- : string * string * string * string =                                                                                                                                  
("東証1部", "シャープ(株)", "電気機器", "1,000")

結構、スッキリ、簡単に書ける。コードも読みやすい。
ただし、正規表現リテラルがないようなので、\は\\と書かなければならないのが、まどろっこしい。

※2014/5/21現在は、上記の正規表現で株式情報を取得できるが、Yahoo financeのコンテンツの構成が変更されると、取得できなくなる可能性がある。

2014年5月18日日曜日

[OCaml]Coreでusleep

sleepシステムコールでは1秒未満で待つことができないので、usleepを使おうと思ったが、OCamlのUnix標準モジュールではサポートされていなかった。
ここを見るとselectシステムコールを使う例があったので、試してみたら、構文エラーになった。
何で?と思って調べたら、自分の環境では常にCoreを使うようにしていた。
open Core.StdしているとUnix.selectはCore版になるのね...
Coreを使っている場合は以下のようにusleep関数を定義すればOK。

open Core.Std

let usleep time =
  ignore @@ Unix.select ~read:[] ~write:[] ~except:[] ~timeout:(`After time) ()


utopで確認。

utop[14]> let print_now () = print_endline @@ Time.to_string @@ Time.now ();;
val print_now : unit -> unit = <fun>
utop[18]> print_now (); usleep 0.25; print_now ();;
2014-05-18 19:04:40.408135+09:00
2014-05-18 19:04:40.659611+09:00 ←◆0.25sec待機
- : unit = ()

Coreで再定義する前の関数を参照する方法は無いのかな?

参考


2014年5月17日土曜日

[OCaml]PCRE-OCamlのサンプルコード

OCamlの正規表現ライブラリはいくつかあるようだけど、PCRE-OCamlがお手軽そうだったので、使ってみた。
メモしておこう。

PCRE-OCamlはopamでインストールできる。
$ opam install pcre-ocaml

以下、utopでの例。

utop[0]> #require "pcre";;
utop[1]> open Pcre;;

execはsubstringsを返す。

utop[16]> let s = exec ~rex:(regexp "aaa(.*)bbb") "aaaほげbbbほげccc";;
val s : substrings = <abstr>

get_substringsでstring arrayを取得できる。

utop[17]> get_substrings s;;
- : string array = [|"aaaほげbbb"; "ほげ"|]
utop[18]> (get_substrings s).(0);;
- : string = "aaaほげbbb"

マッチしないときは例外が出る。

utop[42]> let s = exec ~rex:(regexp "ほげげ") "aaaほげbbb";;
Exception: Not_found.

exec_allはsubstrings arrayを返す。

utop[36]> let sa = exec_all ~rex:(regexp "aaa(.*?)aaa") "aaaほげ1aaa aaaほげ2aaa";;
val sa : substrings array = [|<abstr>; <abstr>|]
utop[37]> get_substrings sa.(0);;
- : string array = [|"aaaほげ1aaa"; "ほげ1"|]
utop[38]> get_substrings sa.(1);;
- : string array = [|"aaaほげ2aaa"; "ほげ2"|]

マッチした文字列の配列を返すextract, extract_all関数もある。(こちらの関数の方が扱いが簡単。)

utop[48]> extract ~rex:(regexp "aaa(.*?)aaa") "aaaほげ1aaa aaaほげ2aaa";;
- : string array = [|"aaaほげ1aaa"; "ほげ1"|]

utop[49]> extract_all ~rex:(regexp "aaa(.*?)aaa") "aaaほげ1aaa aaaほげ2aaa";;
- : string array array =
[|[|"aaaほげ1aaa"; "ほげ1"|]; [|"aaaほげ2aaa"; "ほげ2"|]|]

UTF-8で日本語を使うときには、フラグ指定で、
regexp ~flags:[`UTF8] "aaa(.*?)aaa"
としておいた方が良いかな?

参考


2014年5月16日金曜日

[OCaml][Emacs]mlファイルとmliファイルを交互に切り替えるelisp

Emacsのtuareg-modeでOCamlのコードを書いている。
編集中はmliファイルまたはmlファイルを頻繁に切り替えているのだけど、switch-buffer(C-x b)で指定するのが面倒になったので、簡単に切り替えることができるelispを作成した。
tuaregには対応するコマンドは、きっとあるだろうなと思い、tuareg-* を探してみたが、見当たらなかった。残念。

キーバインディングは、空いていた C-c , に割り当てた。

(defun switch-file-ext (file-name ext1 ext2)
  (let ((file-ext (file-name-extension file-name)))
    (unless (member file-ext (list ext1 ext2))
      (error "unmatch file extension"))
    (message file-ext)
    (concat (file-name-directory file-name)
            (file-name-base file-name)
            "."
            (if (string= file-ext ext1) ext2 ext1))))

(defun switch-to-ml-or-mli ()
  (interactive)                                                                      (find-file (switch-file-ext (buffer-file-name) "ml" "mli")))
 
(define-key tuareg-mode-map (kbd "C-c ,") 'switch-to-ml-or-mli)

これで、mliファイルまたはmlファイルを編集中に、C-c , をキー入力すると、
例えば、hoge.mli を編集している場合は hoge.ml に、
逆に、hoge.ml の場合は、 hoge.mli に切り替えできるようになった。

こんなふうに手軽に機能拡張できるEmacsは素晴しいね。

2014/5/17 追記
Twitterで、nomaddoさん星のキャミバ様に教えていただいたが、上記の関数を作らなくても、tuareg-find-alternate-fileがあることが判明。
デフォルトでは、C-c C-aに割り当てられていた。
 /(^o^)\ナンテコッタイ

2014年5月14日水曜日

[OCaml]Ocamlnetのhttp_clientでコンテンツを取得する

OCamlで簡単に使えるHTTPクライアントを探していたのだけど、
Ocamlnetライブラリのhttp_client.Convenienceモジュールを使うと、
とても簡単に、コンテンツを取得できた。

Ocamlnetのインストール

Ocamlnetはopamでインストールできる。
$ opam install ocamlnet
現時点のバージョンは3.7.3だった。

Http_client.Convenienceモジュール

Http_client.ConvenienceモジュールにはGETリクエストでコンテンツを取得する関数がある。
詳細はリファレンスマニュアルを参照。

val http_get : string -> string
  Does a "GET" request with the given URL and returns the message body
val http_get_message : string -> Http_client.http_call
  Does a "GET" request with the given URL and returns the message

サンプルコード

utopで試してみた。

utop[0]> #require "netclient";;
utop[1]> Http_client.Convenience.http_get "http://ほげほげ.com/";;
- : string =
"<html><body><h1>It works!</h1>
<p>This is the default web page for this server.</p>
<p>The web server software is running but no content has been added, yet.</p>
</body></html>
"
utop[2]> Http_client.Convenience.http_get_message "http://ほげほげ.com/";;
- : Http_client.http_call = <obj>
utop[4]> let c = Http_client.Convenience.http_get_message "http://ほげほげ.com/";;
val c : Http_client.http_call = <obj>
utop[6]> c#status;;
- : Http_client.status = `Successful
utop[7]> c#response_body;;
- : Netmime.mime_body = <obj>
utop[8]> c#response_body#value;;
- : string =
"<html><body><h1>It works!</h1>
<p>This is the default web page for this server.</p>
<p>The web server software is running but no content has been added, yet.</p>
</body></html>
"
utop[9]>

すごく簡単。

OCamlnetは、HTTPクライアント以外にも、FTP、POP、SMTP等、ネットワークに関連する様々なモジュールを提供している。
リファレンスは分かりやすく、結構便利だ。

参考


2014年5月11日日曜日

[Debian][Java]Debian Wheezy(64bit版)にJava8をインストール

Debian Wheezy(64bit版)の環境を作成中。
Java8をインストールしたのでメモを残しておく。

JDKをhttp://www.oracle.com/techntwork/java/javase/downloads/jdk8-downloads-2133151.htmlからダウンロードする。
2014/5/11時点の最新バージョンは jdk1.8.0_05。

/tmpにダウンロードした。

以下はrootで作業する。
$ mkdir /opt/java-oracle
$ tar -zxf /tmp/jdk-8u5-linux-x64.tar.gz -C /opt/java-oracle
$ export JHome=/opt/java-oracle/jdk1.8.0_05
$ update-alternatives --install /usr/bin/java java $JHome/bin/java 20000
$ update-alternatives --install /usr/bin/javac javac $JHome/bin/javac 20000
インストール結果を確認する
$ update-alternatives --config java
$ java -version

hello,world!で確認

==Hello.java==
public class Hello {
    public static void main(String[] args) {
    System.out.println("hello, world!\n");
    }
}

satoshi@lucy:~/workspace/java$ javac Hello.java
satoshi@lucy:~/workspace/java$ ll
合計 12
-rw-r--r-- 1 satoshi satoshi 418  5月 11 20:54 Hello.class
-rw-r--r-- 1 satoshi satoshi 114  5月 11 20:54 Hello.java
satoshi@lucy:~/workspace/java$ java Hello
hello, world!

とりあえずコンパイル、実行はできた。
Clojureは動くかな?
後で試してみよう。

2014年4月6日日曜日

[OCaml][Common Lisp][Ruby][SICP]両替の組み合わせを数えるプログラムの実行速度

SICPにあった、両替の組み合わせを数えるプログラムを、Common Lisp、OCaml、Rubyで書いて、実行速度を比べてみた。

各言語のコード

Common Lisp

(defun change-count (amount)
  (change-count-aux amount 5))

(defun change-count-aux (amount kind)
  (cond
    ((= amount 0) 1)
    ((< amount 0) 0)
    ((= kind 0) 0)
    (t (+ (change-count-aux amount (1- kind))
      (change-count-aux (- amount (first-denomination kind)) kind)))))

(defvar *money* #(1 5 10 25 50))

(defun first-denomination (kind-of-coins)
  (elt *money* (1- kind-of-coins)))


OCaml

open Core.Std

let money = List.to_array [1; 5; 10; 25; 50]

let change_count amount =
  let first_denomination kind = money.(kind - 1) in
  let rec count amount kind =
    match (amount, kind) with
    | (0, _) -> 1
    | (amount', _) when amount' < 0 -> 0
    | (_, 0) -> 0
    | (_, _) -> (count amount (kind - 1))
                + (count (amount - (first_denomination kind)) kind)
  in
  count amount (Array.length money)
   
let () =
  print_endline "*start";
  flush stdout;
  printf "Change count 1000=%d\n" (change_count 1000)


Ruby

#!/usr/bin/env ruby

Money = [1,5,10,25,50]

def first_denomination(kind)
  Money[kind - 1]
end

def count(amount, kind)
  if amount == 0
    1
  elsif amount < 0
    0
  elsif kind == 0
    0
  else
    count(amount, kind - 1) + count((amount - first_denomination(kind)), kind)
  end
end

def change_count(amount)
  count(amount, 5)
end

print "*start\n"
printf("Change count 1000=%d\n", change_count(1000))


実行時間の比較

10ドルを1,5,10,25,50セントで両替する場合の組み合わせ数を求める時間を測定した。

Common Lispは Clozure CL 1.9を使用し、REPL上でtimeマクロで測定した。
OCaml は 4.01を使用。ocamlbuildでnative指定でコンパイルし、timeコマンドで測定した。
Rubyは2.0.0-p353を使用。timeコマンドで測定した。

環境はWindow8のVirtualBox上のDebian Wheezy。
CPUは Intel Core-i5 4200U 1.6GHz。
VirtualBoxには2CPUを割り当て。

Common Lisp

CL-USER> (time (change-count 1000))
(CHANGE-COUNT 1000)
took 4,475 milliseconds (4.475 seconds) to run.
During that period, and with 2 available CPU cores,
     4,752 milliseconds (4.752 seconds) were spent in user mode
         0 milliseconds (0.000 seconds) were spent in system mode
801451

OCaml

satoshi@debian:~/workspace/sicp/ch1$ time ./Ch1.native
*start
Change count 1000=801451
./Ch1.native  2.54s user 0.01s system 99% cpu 2.553 total

Ruby

satoshi@debian:~/workspace/sicp/ch1$ time ruby ch1.rb
*start
Change count 1000=801451
ruby ch1.rb  42.54s user 0.06s system 99% cpu 42.630 total


結果

Common Lisp(CCL1.9): 4.752sec
OCaml4.01: 2.54sec
Ruby: 42.54sec

何の根拠もなく、Common Lispの方がOCamlより速いのだろうと思っていたが、結果は逆で、OCamlの方が1.8倍ほど速かった。
圧倒的にRubyは遅かった。VM上でコードを実行するので、もう少し速いと思っていた。Ruby2.1だともう少し速いのかな?

その他

この規模のプログラムでは、各言語での読み易さには、あまり違いがない。OCamlのmatch 〜 with構文は、少し読み易いかなという程度。
Common LispやRubyと異なり、厳格な型チェックをするOCamlのコンパイルが通った後の、プログラム実行時の安心感は格別だ。

2014年3月29日土曜日

[OCaml]リバーシゲームを作ってみた

OCamlの勉強がてら、リバーシ(オセロ)を作ってみた。


コンピュータの思考アルゴリズムは minimaxアルゴリズムを使用した4手先読み。
盤の評価関数は、四隅と縦横の列にディスクを置けると高評価という単純なもの。
でも、適当に手を指していたら、コンピュータに一回負けた...

いろいろ試行錯誤しながら書いた汚ないコードだけど、公開しておこう。

https://github.com/takeisa/ocaml-reversi

モジュールは以下の通り。

reversi.mlリバーシ本体
board.ml
cell.ml盤の升目の定義
computer.mlコンピュータの思考ロジック
disk.mlディスクの定義
pos.ml座標

2014年3月16日日曜日

[OCaml]FFIでhello.world!

OCamlからFFI(Foreign Function Interface)を使い、Cで書いた hello, world! を出力する関数を呼び出す。

準備

ctypesパッケージが必要

opamでインストールできる。
$ opam install ctypes

libffiが必要

$ dpkg --list | grep libffi
ii  libffi-dev:i38 3.0.10-3     i386         Foreign Function Interface library
ii  libffi5:i386   3.0.10-3     i386         Foreign Function Interface library

私のシステム(Debian Wheezy)にはもうインストールされていた。

ソース

hello.h

void print_hello(void);

hello.c

#include <stdio.h>
#include "hello.h"

void print_hello(void) {
  printf("hello, world!\n");
}


hello.ml

open Ctypes
open Foreign

let print_hello =
  foreign "print_hello" (void @-> returning void)


hello.mli

val print_hello : unit -> unit

main.ml

open Hello

let () =
  print_hello ();


コンパイル

共有ライブラリの作成

$ gcc -c -fPIC -Wall hello.c
$ gcc -shared -o libhello.so hello.o


OCamlソースのコンパイル

$ ocamlfind ocamlopt -linkpkg hello.mli
$ ocamlfind ocamlopt -linkpkg -package ctypes,ctypes.foreign hello.ml
$ ocamlfind ocamlopt -cclib libhello.so -linkpkg -g -thread -package ctypes.foreign -package core -o main hello.cmx


実行

$ LD_LIBRARY_PATH=. ./main
hello, world!

動いた!

まとめ

簡単そうだったけど、試行錯誤したので、メモを残しておこう。

静的ライブラリは使えない(使えなかった)

最初は静的ライブラリが使えると思っていた。
いろいろ試していたのだけど、
Fatal error: exception Dl.DL_error("〜/main.native: undefined symbol: print_hello")
というエラーメッセージを見て、動的ライブラリでないと駄目だということに気が付いた。

リンカへのライブラリ指定

hello.cを置いたディレクトリの下に、ocamlソース用のディレクトリを作成して、corebuildコマンドでビルドしていた。
$ corebuild -pkg ctypes.foreign -lflags -cclib,-L../..,-cclib,libhello.so main.native
実行すると、
$ ./main.native
main.native: error while loading shared libraries: ../../libhello.so: cannot open shared object file: No such file or directory
となってしまい、../../libhello.soを参照してしまう。
カレントディレクトリにlibhello.soを置いて、corebuildを実行してみたが、余計なファイルがあると、以下のようなエラーが発生し、ビルドできない。
$ corebuild -pkg ctypes.foreign -lflags -cclib,libhello.so main.native
SANITIZE: a total of 1 file that should probably not be in your source tree
  has been found. A script shell file
  "/home/satoshi/workspace/real_world_ocaml/ffi/ocaml/_build/sanitize.sh" is
  being created. Check this script and run it to remove unwanted files or use
  other options (such as defining hygiene exceptions or using the -no-hygiene
  option).
IMPORTANT: I cannot work with leftover compiled files.
ERROR: Leftover object files:
  File libhello.so in . has suffix .so
Exiting due to hygiene violations.
あきらめて、ビルドにはocamlfindコマンドを使うことにした。

シグネチャの作成

corebuildを使って、以下のようなコマンドを実行すると、mlファイルからシグネチャを作ることができる。
$ corebuild -pkg ctypes.foreign hello.inferred.mli
モジュールの公開メソッドが多いとき、自分で一から書くよりは楽ちん。

参考

Real World OCamlがオンラインで公開されている。

2014年3月8日土曜日

[OCaml]omake -P で FAM not enabled

環境はDebian wheezy。
omakeの自動ビルドを試そうと、omake -Pしてみた。

~/workspace/ocaml/reversi$ omake -P  
*** omake: reading OMakefiles
*** omake: finished reading OMakefiles (0.00 sec)
Fatal error: exception Invalid_argument("FAM not enabled")

エラーとなった。
omakeをインストールする際に、libfam-devをインストールしていなかったためかと思い、
sudo apt-get install libfam-dev してから、
再度、opam reinstall omake とした。

再度、試してみる。
~/workspace/ocaml/reversi$ omake -P
*** omake: reading OMakefiles
*** omake: finished reading OMakefiles (0.01 sec)
*** omake: 0/1 targets are up to date
*** omake: failed (0.10 sec, 0/0 scans, 0/0 rules, 0/13 digests)
*** omake error:
   Failure: om_notify_open: (null)     

また、エラーになった。  
famデーモンを起動していないと出るらしい。
famなんかインストールしたっけ?

/etc/init.d$ ls *fam*
zsh: no matches found: *fam*

あれ?? famがない...
famを入れれば良いのかな?

~/workspace/ocaml/reversi$ sudo apt-get install fam
パッケージリストを読み込んでいます... 完了
依存関係ツリーを作成しています               
状態情報を読み取っています... 完了
以下のパッケージが新たにインストールされます:
  fam
アップグレード: 0 個、新規インストール: 1 個、削除: 0 個、保留: 1 個。
74.8 kB のアーカイブを取得する必要があります。
この操作後に追加で 270 kB のディスク容量が消費されます。
取得:1 http://ftp.jp.debian.org/debian/ wheezy/main fam i386 2.7.0-17 [74.8 kB]
74.8 kB を 0秒 で取得しました (124 kB/s)
以前に未選択のパッケージ fam を選択しています。
(データベースを読み込んでいます ... 現在 110180 個のファイルとディレクトリがインストールされています。)
(.../archives/fam_2.7.0-17_i386.deb から) fam を展開しています...
man-db のトリガを処理しています ...
fam (2.7.0-17) を設定しています ...
Starting file alteration monitor: FAM.

どうやらFAMが有効になったらしい。
omake -P を試してみる。

~/workspace/ocaml/reversi$ omake -P
*** omake: reading OMakefiles
*** omake: finished reading OMakefiles (0.00 sec)
*** omake: done (0.02 sec, 0/2 scans, 0/7 rules, 0/46 digests)
*** omake: polling for filesystem changes

お。動いた!
試しに、ファイルを編集してみる。

*** omake: file reversi_client.ml changed ←◆これを編集
*** omake: rebuilding
*** omake: done (0.25 sec, 1/3 scans, 2/11 rules, 6/93 digests)
*** omake: polling for filesystem changes

リビルドされた。これは便利。

参考

2014年3月5日水曜日

[OCaml]cursesライブラリの wide character サポートを有効にする

OCaml にも、Cursesライブラリがある(OCaml Curses)。
このライブラリは opam でインストールできるが、現バージョンのcurses-1.0.3では、wide characterサポートが有効になっておらず、日本語は文字化けして正しく表示できなかった。
opamでのインストール時に実行するconfigureに、--enable-widec を付けてビルドできれば良いのだけど、具体的にどのようにすればできるのか不明だった。
何とかできないかなーと、tweetしていたところ、
星のキャミバ様さんに、ローカルリポジトリを作る方法で、簡単に自分用パッケージを作成できることを教えていただいた。

想像していたよりも、かなり簡単にパッケージを作成することができ、wide characterサポートを有効にしたOCaml cursesライブラリをインストールすることできた。
星のキャミバ様さん、ありがとうございます!

以下、手順。

ローカルリポジトリにするディレクトリ(~/opam_myrepo とした)を作成する。
~$ mkdir ~/opam_myrepo

そのリポジトリを登録する。
~$ opam repo add myrepo ~/opam_myrepo
myrepo     Synchronizing with /home/satoshi/opam_myrepo
[WARNING] "/home/satoshi/opam_myrepo" doesn't contain a "packages" nor a "compilers" directory.
Is it really the directory of your repo ? ("opam remote remove myrepo" to revert)
Updating ~/.opam/repo/compiler-index ...
Updating ~/.opam/compilers/ ...
Updating ~/.opam/repo/package-index ...
Updating ~/.opam/packages/ ...
Updating the cache of metadata (~/.opam/state.cache) ...

登録できたか確認する。
~$ opam repo
  10 [local]     myrepo     /home/satoshi/opam_myrepo
   0 [http]     default     https://opam.ocaml.org

opam_myrepoディレクトリ配下に、packages/curses.1.0.3/を作り、その中に、descr, opam, url ファイルを作る。
curses.1.0.3をインストール済みだったので、~/.opam/packages/curses/curses.1.0.3 からコピーした。

~$ cd opam_myrepo
~/opam_myrepo$ mkdir -p packages/curses.1.0.3
~/opam_myrepo$ cd packages/curses.1.0.3
~/opam_myrepo/packages/curses.1.0.3$ cp ~/.opam/packages/curses/curses.1.0.3/* ./

この後、cursesは再インストールするので、削除する。(reinstallするなら、削除しなくても良いかも)
$ opam remove curses

コピーしたopamを編集して、
--enable-widecを追加する。
〜略〜
build: [
  ["./configure" "--enable-widec"] ←◆これ
  [make "byte"]
  [make "opt"]
  [make "install"]
]
〜略〜

リポジトリをアップデートする。
~/opam_myrepo/packages/curses.1.0.3$ opam update
myrepo     Synchronizing with /home/satoshi/opam_myrepo
default    Downloading https://opam.ocaml.org/urls.txt
default    Downloading https://opam.ocaml.org/index.tar.gz
Updating ~/.opam/repo/compiler-index ...
Updating ~/.opam/compilers/ ...
Updating ~/.opam/repo/package-index ...
Updating ~/.opam/packages/ ...
Updating the cache of metadata (~/.opam/state.cache) ...
0 to install | 11 to reinstall | 17 to upgrade | 0 to downgrade | 0 to remove
You can now run 'opam upgrade' to upgrade your system.

curses をインストールする。
~/opam_myrepo/packages/curses.1.0.3$ opam install curses
The following actions will be performed:
 - install curses.1.0.3
1 to install | 0 to reinstall | 0 to upgrade | 0 to downgrade | 0 to remove

=-=-= Installing curses.1.0.3 =-=-=
curses.1.0.3 ocaml-curses-1.0.3.ogunden1.tar.gz is in the local cache, using it.
Building curses.1.0.3:
  ./configure --enable-widec  ←◆ここ。できたー!!
  make byte
  make opt
  make install
Installing curses.1.0.3.

wide character サポートが有効になったか調べる。
$ echo -e '#require "curses";;\nlet s = if Curses.Curses_config.wide_ncurses then "wide char ok!" else "no wide char support." in print_endline s;;' | ocaml
〜略〜
/home/satoshi/.opam/system/lib/herelib/pa_herelib.cma: loaded
# /home/satoshi/.opam/system/lib/curses: added to search path
/home/satoshi/.opam/system/lib/curses/curses.cma: loaded
# wide char ok! ←◆wide characterが有効になった
- : unit = ()

サンプルプログラム

open Core.Std

let () =
  let module C = Curses in
  let main_window = C.initscr () in
  let err = C.mvwaddstr main_window 10 2 "hello, world!" in
  let err = C.mvwaddstr main_window 11 2 "こんにちは、世界!" in
  let err = C.refresh () in
  Unix.sleep 5;
  C.endwin ()

文字化けせずに、「こんにちは、世界!」を表示できた。
 

参考


2014年3月2日日曜日

[OCaml]Life gameを作ってみた

ここ数日、プログラミングHaskell を読んでいる。
この本には、対話プログラムの例題としてライフゲームが載っているのだけど、
OCamlでは、どんなになるかなーと思い、書いてみた。

昨年の年末から年始にかけて、プログラミングの基礎 (Computer Science Library) を読んで、OCamlで基本的な処理は書けるようになったのだけど、それから2ヶ月の間、全く使っていなかったら、いろいろと忘れていた。やっぱり、言語は使っていないと忘れてしまって駄目だなぁ。

コードは こちら(https://github.com/takeisa/ocaml-lifegame.git) から。
Coreライブラリを使っている。
opamでcoreをインストールして、
$ corebuild life_game.native
とすれば実行ファイルができる。

実行例

途中で止めた時のスクリーンショット。
「*」が生きているセル。
ライフゲームを知らない人は、何だか良く分からない実行結果ですね。


OCamlでコードを書いて思ったこと

・Haskellにある (.) ($) は便利だな。自分で定義すれば良いのだけど、OCamlにも欲しい。
・Screenモジュールで 座標を表わす pos_t型を以下のように定義し、
type pos_t = {x: int; y: int}
シグネチャでは、
type pos_t
として、中身を非公開にした。
しかし、程度のプログラムでは、int * intのタプルそのままで座標を管理する方がコードは簡単になったかもしれない。
・コードの編集時には、Emacs tuareg-modeはとても便利。
でも、ソースをモジュール分割した場合、そのモジュールを認識させるためには、いちいちコンパイルしなければならず、面倒だった。何か良い方法があるのかな?
・OCamlでは簡単に副作用があるコードをどこでも気軽に書けるので、便利と言えば便利なのだけど、気を付けないと、純粋なコードと副作用があるコードが混在しやすそう。
・OCamlはHaskellみたいに、関数の直前に型定義を書けないのかな?シグネチャを分けて書くの面倒だなぁ。
・HaskellよりOCamlの方がコードは若干は冗長になった。これは書き方が悪いか...
・厳密な型チェックは、型に関連するバグが出ないので、やっぱり便利。
・あたりまえだけど、この程度のプログラムではHaskellもOCamlも、あまり変わらない。
・文字出力後、画面を更新する際は、flush stdout を忘れずに。
・HaskellもOCamlもコードを書いていて楽しいので、他にも書いてみよう。
・ライブラリを把握していないので、実装に時間がかかるし、車輪の再発明をしがち。
Cheat sheetは便利。

参考