文字を表示する関数を作ってみよう

文字の表示には,VRAM と呼ばれるメモリ領域に ASCII コードを書き込むだけでよく,カーソルの移動には,表示した文字数をビデオコントローラーに教えるだけでよいことが前回までに分った。
これらを踏まえ文字列を表示する関数を作ってみることにしよう。

mem.h

メモリ操作には,C の標準関数で定義されているような,メモリを操作する関数があれば便利だろう。そこで,mem.h という新しいファイルに,連続したメモリをコピーする memcopy(),2バイト(画面上で一文字)単位で連続したメモリに値をセットする memsetw() を用意してみた:

#ifndef MEM_H
#define MEM_H
static __inline__ void memsetw(void *dest, unsigned short val, unsigned int count) {
  int d0, d1;
  __asm__ __volatile__(
	   "cld		\n\t"
	   "rep stosd	\n\t"
	   : "=&D" (d0), "=&c" (d1), "=&a" (val)
	   : "0" ((long) dest), "1" (count), "2" (val)
	   : "memory");
}

static __inline__ void *memcpy(void *dest, const void *src, unsigned int n) {
  int d0, d1, d2;
  __asm__ __volatile__(
	   "cld		\n\t"
	   "rep movsd	\n\t"
	   "test %4, 2	\n\t"
	   "je 1f	\n\t"
	   "movsw	\n"
	   "1:		\t"
	   "test %4, 1	\n\t"
	   "je 2f	\n\t"
	   "movsb	\n"
	   "2:		\t"
	   : "=&c" (d0), "=&D" (d1), "=&S" (d2)
	   : "0" (n/4), "q" (n), "1" ((long) dest), "2" ((long) src)
	   : "memory");
  return dest;
}
#endif MEM_H

static inline の中でインラインアセンブラを使用したので,少々キチガイじみた書き方になっていたりする。
しかしアセンブラ本体だけに注目すれば,これらは連続したメモリ領域を操作する基本的な命令で,すぐ理解できる。
インラインアセンブラの複雑な使い方はあとで説明する。…と思う。

i389.h

I/Oポートを操作する関数などハードに依存しそうな関数はひとつのファイルにまとめることにしよう――ここでは i386.h というファイルを作成した。

#ifndef I386_H
#define I386_H

static __inline__ void outb(unsigned short port, unsigned char val) {
  __asm__ __volatile__ ("out dx, al" : : "d" (port), "a" (val));
}

static __inline__ unsigned char inb(unsigned short port) {
  register unsigned char val;
  __asm__ __volatile__ ("in al, dx" : "=a" (val) : "d" (port));
  return val;
}

static __inline__ void hlt() {
  __asm__ __volatile__ ("hlt");
}

#endif

hlt() はアセンブラ命令 hlt を実行する関数で,次の割り込みがあるまで CPU を停止する。(一般的にタイマー割り込みが一定間隔で実行されるので,これは一定の間 CPU を停止させる)
例えば,WindowsNT系のOSでプログラムをほとんど立ち上げていないとき,プロセスマネージャーで各プロセスのCPUの使用状況を確認すると「System Idle Process」というプロセスがCPUを殆ど占領していることが分る。「System Idle Process」は hlt をループするプログラムで,CPUパワーを節約する役割りを果している。

video.c

画面を操作する関数は video.c にまとめた:

#ifndef VIDEO_H
#define VIDEO_H

#define BLANK ((0x0f)<<8) | 0x20

void init_video(void);
void k_putchar(char);
void k_scroll_up(void);
void k_clrscr(void);
void puts(const char *);
#endif
#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 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 k_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;
  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();
}

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

void puts(const char *str) {
  while(*str)
    k_putchar(*str++);
}

VRAMを操作する簡単な関数がまとめてあり。カーソルを動かす関数や,画面をスクロールする関数,そして文字列を表示する puts() などが含まれる。

kernel.c

今回作った関数をもとに,次のようなプログラムを書いた:

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

void main() {
  int i;
  init_video();

  puts("11111\n");
  puts("22222\n");
  for(i = 0; i<23; i++) {
    puts("\n");
  }
  puts("33333");

  while(1)
    hlt();
}

これは,「11111」「22222」を表示したあと,23回改行をして,「33333」を表示している。画面が正しくスクロールされていることを確認しよう。

compile

ipl.binの作成:

gcc -ffreestanding -fno-common -fno-builtin -fomit-frame-pointer -O2 -c-ffreestanding -fno-common -fno-builtin -fomit-frame-pointer -O2 -c -o ipl.o ipl.S
ld -nostdlib -Ttext=0x7c00 --oformat binary -o ipl.bin ipl.o

kernel.binの作成:

gcc -ffreestanding -fno-common -fno-builtin -fomit-frame-pointer -O2 -c -masm=intel -o kernel.o kernel.c
gcc -ffreestanding -fno-common -fno-builtin -fomit-frame-pointer -O2 -c -masm=intel -o video.o video.c
gcc -ffreestanding -fno-common -fno-builtin -fomit-frame-pointer -O2 -c -o head.o head.S
ld -nostdlib -Ttext=0x4000 --oformat binary -o kernel.bin head.o kernel.o video.o

fd.imgの作成:

mformat -f 1440  -C -B ipl.bin -i fd.img ::
mcopy kernel.bin -i fd.img ::

QEMUで実行:

qemu -boot a -fda fd.img -m 8m

画面