少ないリソースを酷使する

低レイヤーとレトロPC(PC98,MSX)が好きな情報学生

farポインタを理解する(目指せガンダムエンジニア)

この記事は岩手県立大学 Advent Calendar 2020の4日目です.(遅刻)

今日,私のTwitterのタイムラインで「ガンダムのプログラムがC言語で書かれている」という話題で盛り上がっていました.


その中で,「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以上のメモリアクセスを可能とします.

f:id:T-takeda:20201206210848p:plain

アセンブラで書くなら

アセンブラなら

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で埋められます!
f:id:T-takeda:20201206221944p:plain

まとめ

farポインタとっても便利.本の中では何度かみたことあるがPC-98でプログラミングする以外であんまり使ったことはない. 大体このレベルだとアセンブラで書くので出会う機会がなかった気もする. これがわかればガンダムのプログラムのメンテナンスもバッチリですね!

参考

↓私よりわかりやすい解説記事 mcommit.hatenadiary.com

・初めて読む8086 www.amazon.co.jp

・30日でできる! OS自作入門 www.amazon.co.jp