前回までで無事にロングモードに入り、C言語の関数を呼び出すことが できました。
今回はついに文字を表示させます。
文字の表示
VGAにテキストを出力する仕組みは以前の通りです。 今回はテキスト表示に関する機能を使いやすいようにまとめて 最終的に printk() による出力を目指します。
カーネル用のCライブラリ
OS開発用のコードでは当然標準のCライブラリを使うことは できませんので、必要なら自分で用意する必要があります。
mem〜〜やstr〜〜系の関数はとくにあると便利です。 今回実装する printk も strchr() が必要です。
標準関数については このあたり を検索すると基本的な関数に ついては実装例も掲載されているのでありがたくコピー参考にさせて 頂きます。
標準関数はいろいろ追加するとmakefileが混雑してきますので klib というフォルダにまとめておいて静的ライブラリ( libXXX.a)に まとめてあとからカーネル本体とリンクするようにします。
今回は printk() の実装が最終目標なので この記事 のprintfの実装もこの klib に移植します。 移植と言っても関数名を少し変更した程度です。
これらの実装は配布ソースコードの src/klib 以下をご参照ください。
テキストVGA
早速VGA用の簡易ドライバ(?) を書きます。
その前に、 文字の表示については前回のように然るべきメモリ上にテキストデータを 押しこむだけでいいのですが、もうひとつ重要な機能が必要です。
テキストを出力するので テキストカーソル の表示が必要です。
テキストカーソルの制御はCPUの I/Oポートを介して行いますので、 先にI/OポートにC言語からアクセスできるようにしておきます。
I/Oポートへのアクセスはアセンブリの in/out命令を実行すればいいので インラインアセンブラを使用してCから呼び出せるようにします。
uint8_t inb(uint16_t port)
{
uint8_t data;
asm volatile("inb %1,%0" : "=a" (data) : "d" (port));
return data;
}
uint16_t inw(uint16_t port)
{
uint16_t data;
asm volatile("inw %1,%0" : "=a" (data) : "d" (port));
return data;
}
uint32_t inl(uint16_t port)
{
uint32_t data;
asm volatile("inl %1,%0" : "=a" (data) : "d" (port));
return data;
}
void outb(uint16_t port, uint8_t data)
{
asm volatile("outb %0,%1" :: "a" (data), "d" (port));
}
void outw(uint16_t port, uint16_t data)
{
asm volatile("outw %0,%1" :: "a" (data), "d" (port));
}
void outl(uint16_t port, uint32_t data)
{
asm volatile("outl %0,%1" :: "a" (data), "d" (port));
}
本当は関数自体をインライン関数にするともう少し高速になりますが、 今回は分かりやすさを優先して普通に portio.c として実装しました。
これで準備完了です。
本体の実装に入ります。
vga_text.c
使用する変数は以下の通りです。
static const size_t COLS = 80;
static const size_t ROWS = 25;
static uint16_t *vga_buf;
static size_t pos;
static uint8_t attr;
static const uint8_t def_attr = VGA_DEFAULT_ATTR;
VGA_DEFAULT_ATTR は黒背景白文字(0x0F)を指定します。
vga_text.hにて
#define VGA_DEFAULT_ATTR 0x0F /* text = white , back = black */
#define VGA_ATTR(text, back) (uint8_t)( (text) | ((back) << 8) )
このように定義してあります。
vga_text_init()
void vga_text_init()
{
vga_buf = (uint16_t *) VGA_TEXT_MEM;
outb(CRTC_ADDR, CRTC_CURSOR_H); pos = (inb(CRTC_DATA) << 8);
outb(CRTC_ADDR, CRTC_CURSOR_L); pos |= (inb(CRTC_DATA) << 0);
vga_text_screen_clear_screen();
attr = def_attr;
}
vga_bufは
#define VGA_TEXT_MEM 0xFFFFFFFF800B8000 で
初期化します。 次にカーソル位置を取得してposに記録しています。 これはBIOSなどがテキストを表示していた場合テキストカーソルの位置が 先頭でない場合があるためです。 最後にテキストの属性をデフォルトの黒背景白文字にして準備完了です。
カーソルの操作は、一度に1バイトしか転送できない関係で2回にわけて 上位/下位 1バイト ずつアクセスします。先ほど用意した in/out関数を早速使い CRTコントローラのアドレスにアクセスします。
まずはカーソルポジション上位一バイト分を要求すると 直後のアドレスにアドレスにデータが入るので取得できます。 同様に下位1バイトも要求しています。
なお CRTC関連のアドレスは CRTC_ADDR = 0x3D4 CRTC_DATA = 0x3D5 要求用のレジスタ値は CRTC_CURSOR_H = 0x0E CRTC_CURSOR_L = 0x0F です。
続いて画面に一文字出力する関数を用意します。
void vga_text_putch(char ch)
{
switch(ch){
case '\b':if( pos > 0 ) vga_buf[--pos] = (((def_attr)<<8)|0x20); break;
case '\n': pos += COLS; /* break */
case '\r': pos -= (pos%COLS); break;
default: vga_buf[pos++] = (((attr)<<8) | ch ); break;
}
if(pos >= COLS*ROWS){
size_t i,j;
for(i = 0,j = COLS; j < COLS*ROWS; i++,j++) vga_buf[i] = vga_buf[j];
while( i < COLS*ROWS ) vga_buf[i++] = (((def_attr)<<8)|' ');
pos -= COLS;
}
}
基本的には vga_buf[pos]に (((attr)<<8) | ch ) を代入するだけです。 switchの defaultにかかれているのがそれです。
基本はこれだけですが、 改行やリターンなどの特殊値の場合は 然るべき位置にposを設定します。改行は一行降りるだけではなく カーソルを行の先頭に持っていくので break せずに \r 用の 処理も一緒にするようにします。
また、 画面がいっぱいになったらスクロールするようにするために、 ポジションが最大表示領域(COLS*ROWS)を超えた場合 バッファのデータを一行分上にコピーして、最終行をクリアしています。
i=0, j=COLS から おなじ速度で値が大きくなるので次々と 一行前(COLS分だけ前)にコピーされているのがわかるかと思います。
これで画面に文字が表示できます。 文字を表示したあとは、カーソルの位置を変更する必要があるので、この機能も まとめておきます。
vga_text_update_cursor
void vga_text_updata_cursor()
{
outb(CRTC_ADDR, CRTC_CURSOR_H);
outb(CRTC_DATA, pos >> 8 );
outb(CRTC_ADDR, CRTC_CURSOR_L);
outb(CRTC_DATA, pos & 0xFF );
}
基本的にやっていることは initでのデータ取得時と変わりません。 in() でデータを取得する代わりに out() でカーソルの位置情報を 上下1バイトづつ書き込むだけです。
これでカーソルも表示可能になりました。
以上でテキストの文字を出力するための機能は完成です。 必要であれば、 attr の値を変更する関数なども追加しておきます。
void vga_text_set_attr(uint8_t new_attr){ attr = new_attr; }
uint8_t vga_text_get_attr(){ return attr; }
これだけですが・・・
printk()
一文字出力が可能になったのでこの vprintfの移植が可能になりました。 早速自作カーネルに組み込んでみます。 本体部分はすでに klib にコピー済みなので"ガワ"だけ作れば 完了です。
文字を出力してカーソルを更新する kputc() を用意して klibの __vprintf を呼び出せばOKです。 たったこれだけで printk() が使えるようになりました。
static void kputc(int c){
vga_text_putch(c);
vga_text_updata_cursor();
}
void vprintk(const char *fmt, va_list ap){ __vprintf(kputc, fmt, ap); }
void printk(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
vprintk(fmt, ap);
va_end(ap);
}
応用例として以下のような関数も作ってみました。
void warning(const char *warning_msg, ...)
{
uint8_t store_attr = vga_text_get_attr();
vga_text_set_attr( VGA_ATTR( YELLOW, BLACK ) );
printk("[!] kernel warning!: ");
va_list ap;
va_start(ap, warning_msg);
vprintk(warning_msg, ap);
va_end(ap);
printk("\n");
vga_text_set_attr( store_attr );
}
vga_text_set_attr() で 黒背景黄色文字 で警告を表示できます。
さっそく使ってみる
というわけで念願の printk ができたので早速使ってみます。
void kmain(){
bss_init();
vga_text_init();
vga_text_set_attr( VGA_ATTR( GREEN, BLACK ) );
printk("Welcome to myOS!\n");
vga_text_set_attr( VGA_DEFAULT_ATTR );
vga_text_write_bar();
warning("warning test");
printk("See you!\n");
printk("[hlt]");
}
実行結果は上の通りです。 無事にテキストが表示できました!
今回は以上です。 printkが使えるようになったので一気に色々な機能の実装がはかどります。 次回は multiboot info構造体からGRUBが調べてくれたデータを取得して 画面に表示してみます。
今回製作したソースコードはこちら