printfを作ってみる

前回までに文字を表示する関数を作ったが,簡単のためこの関数は変数を表示できなかった。動的な変数の値が確認できれば,開発を効率よく進められるだろうから,そろそろprintf関数を作っておこう。

video.c

今回の video.c は次のようになる:

#include "i386.h"
#include "mem.h"
#include "video.h"

unsigned short  crt_port;       /* video base port addr*/
unsigned char   crt_width;      /* video max coulmns */
unsigned char   crt_height;     /* video max lines */
unsigned short  cur_pos;        /* cursor position */
unsigned short  *vram;          /* video ram addr */

void init_video() {
  crt_port   = 0x3D4;
  crt_width  = 80;
  crt_height = 25;
  vram       = (unsigned short *)0xb8000;
  k_clrscr();
}

void k_set_cur() {
  outb(crt_port, 15);
  outb(crt_port + 1, cur_pos);
  outb(crt_port, 14);
  outb(crt_port + 1, cur_pos >> 8);
}

void k_scroll_up() {
  // scroll up
  memcpy( (void *)(vram),
          ((void *)(vram))+crt_width*2,
          crt_width*(crt_height-1)*2 );
  // clean last line
  memsetw( (void *)(vram) + crt_width*2*(crt_height-1),
           BLANK,
           crt_width);
  cur_pos -= crt_width;
  k_set_cur();
}

void k_clrscr() {
  memsetw( (void *)(vram),
           BLANK,
           crt_height * crt_width);
  cur_pos = 0;
  k_set_cur();
}

void putchar(char c) {
  switch (c) {
  case '\r':
    cur_pos -= cur_pos % crt_width;
    break;
  case '\n':
    cur_pos += (crt_width - (cur_pos % crt_width));
    break;
  case '\b':
    if(cur_pos > 0)
      (vram)[--(cur_pos)] = ((0x0f)<<8) | 0x20;
    break;
  case '\t':
    cur_pos += 8;
    break;
  default:
    (vram)[(cur_pos)++] = ((0x0f)<<8) | c;
  }

  // check if cursor position at the bottom of the screen
  if(cur_pos >= crt_width*crt_height)
    k_scroll_up();

  k_set_cur();
}

char * uint_to_str(char *buf, unsigned src, int base) {
  char *p = buf;
  char *p1, *p2;

  do {
    *p++ = "0123456789ABCDEF"[src%base];
  } while(src /= base);

  // Terminate BUF
  *p = 0;

  // Reverse BUF
  for(p1=buf, p2=p-1; p1 < p2; p1++, p2--) {
    char tmp = *p1;
    *p1 = *p2;
    *p2 = tmp;
  }

  return buf;
}

void printf(char * fmt, ...) {
  char **arg = (char **) &fmt;
  char c;
  arg++;

  while((c = *fmt++) != 0) {
    if(c != '%')
      putchar(c);
    else {
      char buf[64];
      char *p;
      c = *fmt++;
      switch(c) {
      case 'd':
        // if value is minus, put '-' in the head
        if( 0 > *((int *) arg) ) {
          putchar('-');
          *((int *) arg) *= -1;
        }
        p = uint_to_str(buf, *((unsigned *) arg++), 10);
        goto print_s;
      case 'x':
        p = uint_to_str(buf, *((unsigned *) arg++), 16);
        goto print_s;
      case 's':
        p = *arg++;
      print_s:
        printf(p);
        break;
      default:
        putchar(c);
      }
    }
  }
}

コード解説

今回追加した関数は uint_to_str と printf である。

一つ目の関数 uint_to_str は数字を文字列に変換する。このコードで分かりにくいのは,次のコードだろう:

 *p++ = "0123456789ABCDEF"[src%base];

これは,数字から文字への変換を行っている。Cでは文字列は配列として扱われるので,文字列リテラルに添え字をつけて,その数字を文字列に変換することができる。もう一つの変換方法として,文字リテラル'0'を数字に足してその数字を文字へ変換する方法があるが,今回は16進数へも一貫して変換できる前者を採用した。

二つ目の関数 printf は標準Cライブラリの printf に似たような動作をする関数である。始めに目に付くのは,次のコードだろう:

void printf(char * fmt, ...)

Cは可変長引数をサポートしていて,'...'がその宣言である。このように定義された関数には一つ目の引数 char * fmt を渡してさえいれば(少なくとも一つの引数は必ず渡す),残りの引数は何個でも渡すことが出来るし,また無くても構わない。どうしてCは可変長引数をサポートできるのか,また可変長引数として渡した名前のない引数をどうやって受け取るのかは,Cの引数とは何かが分かれば,容易に理解できる。
引数とは,関数を call する前にスタックに push した値のことである。つまり,Cではスタックの許す限り引数を渡せるし,引数の数は固定されない。Cでは関数呼び出しの際に,変数の型によらず引数を 4byte の値として(複数の引数があれば後ろから順番に)スタックにつみ,関数を呼び出す。そして,呼び出された関数はスタックから引数を取り出し自身の処理を行う。
今まで,我々はパラメータを用いて,このスタックに積まれている引数を利用出来たので,スタックを意識することは無かった。しかし,次で説明する可変長引数の参照のためそれを知っていなければならなかった。
以上より渡された複数の引数はこの順番通りにメモリに並んでいるわけであるから,可変長引数の値を取り出すには,次のようにして,始めの引数のアドレスを持ったポインタ変数を用意してインクリメントしながら順番に取り出せばいい:

void printf(char * fmt, ...) {
  //最初の引数が char * だったので char ** にしたが,もちろん void * でも構わないし分かりやすいかもしれない
  char **arg = (char **) &fmt; 
  arg++; //インクリメントすれば二番目の引数をさすようになる
  ...
        //二番目の引数を取り出し,次の引数を指すようインクリメントする
        p = uint_to_str(buf, *((unsigned *) arg++), 10); 
  ...
        p = uint_to_str(buf, *((unsigned *) arg++), 16);
  ...
        p = *arg++;
  ...
}

可変長引数で注意しなければならないことは,呼び出された関数は渡された引数の個数を知らないということだ。そのため,必ず受け取る引数には,渡された引数の数が分かるような値を渡す必要がある。例えば,この printf 関数では一番目の引数である文字列に含まれる%の数が,可変長引数の数を知る手がかりになっている。