この記事は岩手県立大学 Advent Calendar 2020の4日目です.(遅刻)
今日,私のTwitterのタイムラインで「ガンダムのプログラムがC言語で書かれている」という話題で盛り上がっていました.
C言語って30年以上前から現在も現役ってすごくね?
— 京 (@shatiku_master) 2020年12月5日
C書ければ流行り廃りに関係なくずっと仕事あるって事じゃん
コーティングに慣れちゃえば別に休日に勉強しなくてもご飯は食べれる🍜
ちなガンダムもC言語で動いている模様 pic.twitter.com/jnWX17LgWN
その中で,「farポインタ使ってるwww」とか「プログラムに
far
って書いてあるのはなんぞや」とかfarポインタ に関する発言がちらほらあったので,この機会にfarポインタについてまとめてみました.まとめてみましたって言うか,偶然にも最近 某Hack365でPC-98プログラミングについて解説したときに触れたばっかりでした.
farポインタって?
結論
セグメント方式でメモリアクセスする場合に使われるポインタ形式です.
有名なところだとintel8086チップなど16bitチップ,またはその互換であるリアルモードのプログラムをC言語で書こうとすると必要になってきます.
セグメント方式って?
例えば32bitマシンなら,レジスタは32bitでポインタを宣言してメモリアクセスできるのはそのレジスタで表せるアドレスの限界4GBまでですね.
では,i8086やリアルモードなど16bit環境ではどうでしょう? 使えるレジスタは16bitです.
よって単純に考えると,表現できるアドレスは64KB(65,535Byte)と言うことになりますが,実際に扱えるアドレス空間は1MBであったりします.
なんで?
16bitでどうやって64KB以上のメモリ空間を扱うのか.
そこで登場するのがセグメント方式です.
セグメント方式は例えばi8086チップなら16バイトごとにメモリを区切って,その区切り位置をセグメントベースアドレス
とします.
んで,セグメントベースアドレスから最大64KBの範囲をセグメント
とし,セグメント内の相対的な位置をオフセットアドレス
とします.
セグメントベースアドレス
,オフセットアドレス
をレジスタ2つ使ってうまいことこういい感じにすることで,64KB以上のメモリアクセスを可能とします.
アセンブラで書くなら
アセンブラなら
mov AX, 0xa000 mov ES, AX mov BX, 0x1234 mov [ES:BX], AL
と書けば(ES*16+BX)の物理アドレスにALを書き込めます.
「30日でできる! OS自作入門」の3日目 pp.52あたりに似たような説明があります.私はこの本で初めてセグメント方式を知った気がする.
C言語で書くならfarポインタ
上記のコードをC言語で書く場合,ついにfarポインタを使います.
char __far *addr; addr = (char __far *)0xa0001234L;
修飾子__far
をつけると,下位16bitをオフセット,上位16bitをセグメントとして扱ってくれるようです.
ページ最初のガンダムのプログラムではMK_FP
と言うマクロが使われてますね.
これはセグメントアドレスとオフセットアドレスからfarポインタを生成するマクロですね.
よって上記のコードを以下のように書いても同じです.
char __far *addr; addr = (char __far *)MK_FP(0xa000, 0x1234);
MK_FP
の中身は簡単なビット演算ですね.
#define MK_FP(seg, off) (((long)seg << 0x10) | (long)off)
サンプルコード
i8086, i80286が載ってるPC-9801は16bitマシンなので,このエミュレータでfarポインタを使ったプログラムを試しに書いてみましょう.
PC-98+MS-DOS環境でC言語でアプリ開発をする方法はこのブログの過去の記事で解説しています.
t-takeda.hatenablog.com
テキストVRAMに文字を書き込んでみる
PC-98にはテキストVRAMと言うメモリ空間があって,そこに文字コードを書き込むと画面に文字を表示できます.
テキストVRAMのアドレス空間は0x0A0000 - 0x0A4FFF
です.
よって,テキストVRAMの先頭アドレスをセグメント方式で表すと,
[0xa000 : 0x0000]
計算すると
(0xa000 * 16)+0x0000 = 0x0A0000
となりますね.
これを頭に入れて次のコードを書きます.
#include <stdio.h> #define MK_FP(seg, off) (((long)seg << 0x10) | (long)off) int main(void) { printf("Hello TEXT!\n"); wchar_t __far *a; a = (wchar_t __far *)MK_FP(0xa000, 0x0000); // 0xa0000000L; printf("addr = 0x%lx\n", a); a += (80 * 10); // 10行目から // 1面=80x25 for(int i=0; i<80; i++){ *a = (wchar_t)0x41; a++; } return 0; }
PC-98のテキストVRAMでは1文字2バイトで表されるのでwchar_t
を使います.
(wchar_t __far *)MK_FP(0xa000, 0x0000)
でwchar_t
型のfarポインタ
を生成します.
テキストは1画面 横80文字,縦25行表示可能となっていて,それとテキストVRAMがそのまま対応しているので,
a += (80 * 10);
で画面上10行目の位置までポインタを進めます.
んで,その後に以下のforでポインタを加算しながら文字コード0x41
(ASCIIコードで`A`を表す)を書き込むことで画面上10行目を全て同じ文字で埋めます.
for(int i=0; i<80; i++){ *a = (wchar_t)0x41; a++; }
色々説明はいいからとにかく動かしてみましょう.
以下のようにコンパイルします.
$ ia16-elf-gcc -mcmodel=medium -march=i286 -o TextVRAM.EXE TextVRAM.c
できたEXEをエミュレータ や実機で実行すると画面上10行目が綺麗にAで埋められます!
まとめ
farポインタとっても便利.本の中では何度かみたことあるがPC-98でプログラミングする以外であんまり使ったことはない. 大体このレベルだとアセンブラで書くので出会う機会がなかった気もする. これがわかればガンダムのプログラムのメンテナンスもバッチリですね!
参考
↓私よりわかりやすい解説記事 mcommit.hatenadiary.com
・初めて読む8086 www.amazon.co.jp
・30日でできる! OS自作入門 www.amazon.co.jp