HN 日本語サマリー

← 一覧へ戻る
プログラミング

移植性のないトランスピュータCコンパイラを移植可能にする

Making portable my unportable transputer C compiler (nanochess.org)

58 pointsby nanochess8 コメント

要約

著者は1998年当時のトランスピュータ用Cコンパイラを現代の64ビットマシンでコンパイルしようとしたが、ポインタと整数型の混在、プロトタイプ宣言の欠如、Small-Cからの引き継ぎ問題など、多くの移植性の問題に直面した。これらの問題を解決するため、コードの構造を改善し、型定義の扱いを変更するなどの設計変更を行った。

全文翻訳

移植性のないトランスピュータ用Cコンパイラを移植可能にする

by Oscar Toledo G. 2026年9月21日

[画像: 1998年頃のトランスピュータ用Cコンパイラが入ったフロッピーディスク]

数週間前、トランスピュータ用の最新Cコンパイラが入ったフロッピーディスクを見つけました。これはDelorieのG++またはDJGPP(1998年頃)でコンパイルできるように強化されたものでした。もちろん、コンパイル時には多くの警告が出ましたが、32ビットのIntel 80486プロセッサ上でコンパイルされる32ビットコンパイラだったので、問題なく動作しました。

これは、AMD Am29000プロセッサ用のコード生成に移行する前の中間ステップであり、このバージョンは、Lexical Analyzer、ANSI C構文、および完全なプリプロセッサを備えるために、遺残していたSmall-Cの構造を取り除いてさらに改良されました。

懐古的な演習として、1998年版のトランスピュータ用Cコンパイラを最新の64ビットコンピュータ、Macbook Air M1でコンパイルしようとしましたが、多くの困難に直面しました。例えば:

  • コードは整数とポインタを相互に(両方とも32ビット)使用していました。
  • FILE *型は使用されておらず、代わりにintが使用されていました。
  • コンパイラ関数にはプロトタイプがなく、clangはポインタが64ビット、整数が32ビットであるため、正当な理由で警告を発しました。

これらの移植性の問題の一部はSmall-Cから引き継がれていました。structキーワードがなかったため、必要なすべての構造体はバイトプール(char)で作成され、ワードは2バイト(8080プロセッサ用)に分割されていました。これを32ビットプラットフォーム用に拡張した際、これらのワードは4バイトになりました。さらに悪いことに、ポインタはint型に変換されていましたが、64ビットポインタを32ビット整数に変換することは不可能になりました。設計変更が必要です!

[画像: ポインタが配列に2バイトとして保存されているSmall-Cコードの断片]
Small-Cコードの断片。ポインタが配列に2バイトとして保存されています。

最初のステップ

古いプログラムが最新の64ビットシステムでコンパイルできなくなるのは、これが初めてではありません。しかし、このコンパイラをエミュレーションでのみ動作する好奇心の対象として放置するのではなく、最新のラップトップで動作するようにしましょう。ネタバレ:簡単ではありませんでした。

私は自分に目標を設定しました。コンパイラを最新の64ビットプラットフォームでコンパイルできる程度にのみ変更し、それでもトランスピュータ用に自身をコンパイルできるようにすることです。これは、私のコンパイラに実装されているものだけを使用できることを意味します。

コンパイラは複数のソースファイルに分割されており、すべてcc.cという単一のドライバファイルから呼び出されます。

私のDJGPPポートは2つの異なるファイルを作成しました:cc.cとcc2.cです。前者はDJGPPでコンパイルされることを意図しており、後者はトランスピュータ上で私のCコンパイラで直接コンパイルされることを意図していました。

変数entrada、salida、entrada2をこれらのメインファイルに移動することから始めました。最新マシン用のファイルをFILE *を使用するように変更し、Kernighan&Ritchie関数プロトタイプの追加も開始しました。K&Rプロトタイプは、正しい戻り型を示すためだけに使用されます。例えば、unsigned char *expresion();です。

楽しい部分から始めましょう

#includeディレクティブは、ソースファイル内の処理の現在の状態を保存します。それは次のように行われます:

    incl[nivel_incl++] = entrada;
    incl[nivel_incl++] = funcion_actual;
    incl[nivel_incl++] = comienzo_funcion;
    incl[nivel_incl++] = linea_actual;
    incl[nivel_incl++] = dentro_funcion;

ここでinclは整数配列です。最初の行は現在のファイル用、次の行は関数定義へのポインタ、残りの3つは整数です。大きな問題が見えますか? 64ビットマシンには64ビットポインタがありますが、intはまだ32ビットです。

また、コンパイラのソースコード全体はスペイン語のコメントと変数名を使用しています。

私はコードをstructの用語で書き直しました:

struct {
    FILE *entrada;
    unsigned char *funcion_actual;
    int comienzo_funcion;
    int linea_actual;
    int dentro_funcion;
} incl[MAX_INCL];

そして、コードの新しいバージョンは移植可能で、かなりクリーンになりました:

    incl[nivel_incl].entrada = entrada;
    incl[nivel_incl].funcion_actual = funcion_actual;
    incl[nivel_incl].comienzo_funcion = comienzo_funcion;
    incl[nivel_incl].linea_actual = linea_actual;
    incl[nivel_incl].dentro_funcion = dentro_funcion;
    nivel_incl++;

再設計が進むにつれて、さらに多くのプロトタイプ関数を作成しました。ちょうど「楽勝だ」と思っていたところでした...

それほど簡単ではありません

最初の注意喚起は、式処理サブルーチンから来ました:

/*
** Analiza una expresión, y genera el codigo.
*/
unsigned char *expresion()
{
  struct nodo *origen;
  unsigned char *tipo;

  origen = ultimo_nodo;
  tipo = almacena_expresion(SI);
  evalua_arbol(NO);
  libera_arbol(ultimo_nodo);
  ultimo_nodo = origen;
  return tipo;
}

この関数はC式のコンパイルを行い、型へのポインタを返します。しかし、almacena_expresion関数は次のように動作します:

/*
** Analiza una expresión y la mantiene en memoria.
*/
int almacena_expresion(operador_coma)
  int operador_coma;
{
  int info[1], izq;

  if (operador_coma) {
    if (nivel0(info))
      carga_valor(info);
  } else {
    if (nivel1(info))
      carga_valor(info);
  }
  return info[0];
}

型ポインタがint配列に保存されています。ccexpr.cファイルを完全に書き直して、int info[1]をunsigned char *infoに変更し始めました。

expresion_constanteでは、int origenをstruct nodo *origenに置き換える必要がありました。私のコンパイラは、ポインタを整数に、またはその逆に代入する際に警告を発しなかったようです。

すぐに、型処理にも独自のポインタ問題があることがわかりました。例えば、型コピーのサブルーチンがあり、それは次のようになっています:

/*
** Copia un tipo en la siguiente posición disponible.
*/
copia_tipo(tipo)
  unsigned char *tipo;
{
  int a;
  while (*tipo >= APUNTADOR) {
    if (*tipo == MATRIZ) {
      guarda_tipo(*tipo++);
      guarda_tipo(*tipo++);
      guarda_tipo(*tipo++);
      guarda_tipo(*tipo++);
      guarda_tipo(*tipo++);
    } else
      guarda_tipo(*tipo++);
  }
  if (*tipo == STRUCT) {
    guarda_tipo(*tipo++);
    guarda_tipo(*tipo++);
    guarda_tipo(*tipo++);
    guarda_tipo(*tipo++);
  }
  guarda_tipo(*tipo++);
}

このサブルーチンはC型の説明をコピーします。これは、int a, b, c;のように複数の変数の型を設定する際に役立ちます。guarda_tipo関数は単にバイトを型プールに保存します。

しかし、配列型(MATRIZ)には配列長(4バイトとして保存された32ビット)が含まれ、構造体型(STRUCT)は構造体定義を指します。32ビットポインタが整数に変換され、4バイトとして保存されていました。これもまた、最新の64ビットアーキテクチャには全く移植性がありません。

最も簡単な方法は、8回のguarda_tipo関数呼び出し(64ビットを8バイトに拡張)にすることです。しかし幸いなことに、別の方法(以下に説明)を見つけました。

また、構造体が同じバイトプールに割り当てられているという問題もあります。以下のような具合です(恥ずかしいコードが以下にあります):

/*
** Una nueva estructura.
*/
unsigned char *nueva_estructura(nombre)
  unsigned char *nombre;
{
  unsigned char *ap;
  int conteo;

  if(ultima_estruct != NULL)
    escribe_entero(ultima_estruct + EST_SIG, sig_tipo);
  ultima_estruct = sig_tipo;
  if(lista_estruct == NULL)
    lista_estruct = sig_tipo;
  conteo = 0;
  while(conteo++ < EST_NOMBRE)
    guarda_tipo(0);
  while(*nombre)
    guarda_tipo(*nombre++);
  guarda_tipo(0);
  return ultima_estruct;
}

そして、構造体の各メンバーも型プールに割り当てられます。

さらに、これらは3つのマクロのグループに追跡され、各グループが疑似構造体を定義していました:

/* Definiciones de estructura */

#define EST_QUE_ES    0   /* char, indica si es un rótulo de struct o enum */
#define EST_ES_UNION  1   /* char, indica si es una unión o una estructura */
#define EST_TAM       2   /* int, tamaño total de la estructura/unión */
#define EST_LISTA     6   /* char*, lista de miembros */
#define EST_SIG      10   /* char*, siguiente rótulo */
#define EST_NOMBRE   14   /* char[], rótulo */

/* Definiciones de miembros */

#define MIE_TIPO      0   /* char*, tipo del miembro */
#define MIE_POSICION  4   /* int, posición dentro de la estructura */
#define MIE_SIG       8   /* char*, siguiente miembro */
#define MIE_NOMBRE   12   /* nombre del miembro */

/* Definiciones de enumeradores */

#define ENUM_VALOR    0   /* int, valor del enumerador */
#define ENUM_SIG      4   /* char*, siguiente enumerador */
#define ENUM_NOMBRE   8   /* char[], nombre del enumerador */

これらには5つのポインタが埋め込まれています。これを構造体に変換することもできましたが、もっと良い考えがありました。Am29000 Cコンパイラはこのコンパイラの進化形であり、すでにこれらの醜い定義をエレガントな構造体に置き換えていました。そのため、バックポートすることができました。つまり、より新しいコードを取り込み、古いコードに適合させたのです。

バックポートされたコードは非常に見栄えが良く、さらに移植性があります:

struct rotulo {           /* DEFINICIÓN DE ESTRUCTURA */
  struct rotulo *sig;     /* Siguiente rótulo */
  int tam;                /* Tamaño total de la estructura */
  struct miembro *lista;  /* Lista de miembros */
  char que_es;            /* Indica si es un rótulo de struct o enum */
  char es_union;          /* Indica si es una unión o una estructura */
  char nombre[1];         /* Nombre */
};

struct miembro {          /* DEFINICIÓN DE MIEMBRO DE ESTRUCTURA */
  struct miembro *sig;    /* Siguiente miembro */
  int posicion;           /* Posición dentro de la estructura */
  unsigned char *tipo;    /* Tipo declarado */
  char nombre[1];         /* Nombre */
};

struct enumerador {       /* DEFINICIÓN DE ENUMERADOR */
  struct enumerador *sig; /* Siguiente enumerador */
  int valor;              /* Valor del enumerador */
  char nombre[1];         /* Nombre */
};

同時に、すべてのエラーメッセージをスペイン語から英語に翻訳しました。ソースコードファイルはまだWindowsコードページを使用しており、clangは不正な文字エンコーディングについて繰り返し警告を発していたからです。

私のコンパイラは元々標準Cライブラリを持っていなかったため、isxdigit、strlen、strcpy、strcatなどの関数は各プログラムで複製されていました。これらを私のcc2.cドライバプログラムに配置し、cc.cは標準ライブラリstrings.hとctype.hを含めるようにしました。

式ノードがintからstruct nodo *に変更されたことによる、以前のバージョンからのいくつかのエラーも解決しました。例えばこのようになっています:

  int izq;

  izq = ultimo_nodo;

ここでint izqはstruct nodo *izqであるべきでした。

構造体の代わりに配列が使用されていた他のいくつかのケース(これもstructが利用できなかった時代からのものです):

/*
** Sentencia "break"
*/
void s_break()
{
  /* Ve si hay un bucle abierto */
  if (ultimo_bucle == NULL) {
    error("No loops open");
    return;                         /* No */
  }
  desp_pila(ultimo_bucle[B_PILA]);  /* Si, arregla la pila */
  salto(ultimo_bucle[B_FIN]);       /* Salta a la etiqueta de salida */
}

そして新しいバージョン:

void s_break()
{
  /* Ve si hay un bucle abierto */
  if (ultimo_bucle == NULL) {
    error("No loops open");
    return;                         /* No */
  }
  desp_pila(ultimo_bucle->pila);    /* Si, arregla la pila */
  salto(ultimo_bucle->fin);         /* Salta a la etiqueta de salida */
}

25のエラーから成功まで

これらの変更、多くの時間、そしてコーヒーの後!ポインタと整数を使用する25個のエラーにまで絞り込むことができました。これらは次のように分類できます:

  • 構造体データのポインタの保存/読み取り(3件)
  • 変数、引数、または関数の型の読み取り(5件)
  • 式ノードポインタを使用して整数を保存する(17件)

このコードにはあまり誇りを持っていませんが、例を見てみましょう:

      if (nodo_temp->oper == N_RESULTA) {  /* Función que retorna estructura */
        pals += (req_res = nodo_temp->der);
        nodo_b = -1;
        req = NO;
      } else {

nodo_bはstruct nodo *型ですが、型キャストなしで-1が代入されています。単にnodo_tempをnodo_bに代入し、-1をチェックする代わりにnode_b->oper == N_RESULTAをチェックしました。

struct nodoを展開しない方法を一日中考えていられたかもしれませんが、完璧は「動作する」の敵です。代わりに、追加データに対応するためにフィールドを追加しました。昔ならメモリ空間が重要だったので、おそらくunionで解決したでしょうが、式ツリーのフィールドがいくつか増えても、それほどメモリは使用しません。

struct nodoに追加されたフィールドは次のとおりです:

struct nodo *tri、およびint extra_val。

フィールドの再配置を行い、最適化の機会に気づきましたが、さらなるバグの導入を避けるために最適化しませんでした。

8つのエラーまで減りました。これらのうちいくつかは、構造体型へのポインタがあるため発生しました。これらを構造体インデックス(線形リストの先頭からのカウント)に置き換えました。

グローバル変数とローカル変数の定義へのアクセスで5つのエラーまで減りました:

/* Define formato de los nombres */

#define NOMBRE       0
#define IDENT        17
#define CLASE        18
#define NIVEL        19
#define TIPO         20
#define POSICION     24

ここでもハードコーディングされた構造体です。TIPOは型定義を指しており、これも64ビットには移植性がありません。もう一度やりましょう。structを作成します:

#define NUM_GLBS     608
#define NUM_LOCS      32

struct nombres {
    unsigned char nombre[17];
    unsigned char ident;
    unsigned char clase;
    unsigned char nivel;
    unsigned char *tipo;
    int posicion;
};

struct nombres globales[NUM_GLBS];
struct nombres locales[NUM_LOCS];

古い定義を削除すると多数のエラーと警告が発生し、新しい構造体への正しいアクセスで一つずつゆっくりと置き換えました。そしてついにエラーゼロ!gets(1988年のRichard Morrisによって作成されたワームを可能にした無制限の入力関数)の使用に関する数メッセージだけでしたが、それは許容範囲でした。

しかし、それは機能するのか?

コンパイラ自身に対してコンパイルを実行し始めましたが、ANSI Cを関数定義に誤って挿入していたためエラーが発生しました。そのため、これらをK&R構文に再作業する必要がありました。

また、変数処理におけるいくつかの小さなバグ(調整の残り物)も見つかり、最終的にコンパイラ自身の完全なコンパイルを達成しました。

コンパイラ経験者なら、これが十分ではないことを知っています。出力は完全にバグだらけかもしれません。トランスピュータオペレーティングシステムでテストする必要がありました。

tasm(私のトランスピュータアセンブラ)を実行した後、NULLが未定義であり、私のオペレーティングシステムでは利用できないfputsとstdoutを呼び出していることがわかりました。

void mensaje(cad)
  unsigned char *cad;
{
  fputs("\n", stdout);
  fputs(cad, stdout);
}

もちろん、これはDJGPPポートのアーティファクトでした。動作中のコンパイラから正しいコードを取得しました。

void mensaje(cad)
  unsigned char *cad;
{
  puts("\n");
  puts(cad);
}

tasm cc2.a cc2.e stdio.lenを再度実行した後、次の成功メッセージが表示されました:

Transputer assembler v0.1. Feb/01/2025
by Oscar Toledo G. https://nanochess.org/

0 error(s) detected.
28254 line(s) assembled.

実行可能ファイルcc2が35997バイトで生成されました。参考までに、以前のコンパイラは38176バイトでした。構造体を使用することで長いバイトアクセスコードを回避するため、短くなっています。さて、私のトランスピュータオペレーティングシステムで動作するでしょうか? badum-tss

テストを行う前に、トランスピュータオペレーティングシステム用のレイトレーサーで30年後に発見した浮動小数点一時値パッチを思い出しました。それを一緒に追加しました。

また、DJGPPバージョンの生成されたアセンブリファイルも含まれていた同じディレクトリにあったHOLA.CとPRUEBA.Cで2つの小さなテストを行いました。これらを新しいコンパイラでコンパイルし、成功しました!生成されたアセンブリコードはまったく同じでした。

オペレーティングシステムで試す

コンパイラのすべてのファイルを含むフロッピーディスクイメージをビルドしました。目標は、コンパイラ自身をコンパイルできるようにすることでした。まだ128 KBのRAMに収まるはずだと予想していました。

フロッピーディスクをビルドするためのコマンドラインは次のとおりです:

./buildboot -fd -v2 .floppy.img . tree/SOM.32.bin tree/Halt.p CC.c CC2.c CCanasin.c CCexpr.c CCgencod.c CCinter.c CCvarios.c CCvars.c cc2.a cc2.e

オペレーティングシステム(run_os_v2.sh)に入ったら、c:/ejecutable.pを実行してcc2.eファイルを入力し、8192バイトのスタックとゼロバイトの追加データを設定して、cc2.p実行可能ファイルを作成しました。

次にそれを実行しました。画面にゴミが表示されました。これは色付きコマンド用のANSIエスケープシーケンスを出力するように準備されていたためですが、私のオペレーティングシステムはそれをサポートしていません。自身のソースコードを入力したところ、inicializa()をコンパイルしようとしてクラッシュしました。

デバッグが始まったばかり

これはエミュレーションなので、すぐにフロッピーディスクイメージを確認しました。トランスピュータエミュレータ上のコンパイラがmacOS実行可能ファイルとまったく同じコードを生成していることがわかりました。これは、私のトランスピュータCコンパイラがクラッシュする前にコンパイルできたコードの一部です。

[画像: 私のトランスピュータCコンパイラによって生成されたコードの断片]
私のトランスピュータCコンパイラによって生成されたコードの断片。

プログラムが炎上してクラッシュしたり、ファイルにゴミを作成したりする方が、手がかりになるので好きです。しかし、完全にスタックしたバグとは?

トランスピュータエミュレータのデバッグ出力を有効にしたところ、8.14 GBのファイルが生成されました。これはXCodeでもTextEditでも開けませんでした。Hex Fiendを使用すると、レジスタに間違った値があることがすぐにわかりました。数百万行をレビューして、どこがおかしくなったのかを見つける必要がありました。

レジスタに間違った値があるのは明らかです。なぜなら、1つ目はコードに大きな定数がなく、2つ目はオペレーティングシステムのメモリアドレスが確立されているからです。

etiqueta()関数(レジスタ使用状況でのツリーラベリング)にまで追跡できました。そこで、変数代入ノードの演算子40(N_ASIGNA)を持つ式ノードを処理した後、奇妙なことをし始めました。そして、Wptrの値(トランスピュータスタック)が低いアドレスに向かって成長し続け、それが問題でした。コンパイラコードを上書きしてクラッシュしました。

[画像: Wptr(トランスピュータスタック)がIptr(命令ポインタ)を上書きしてクラッシュしていることを示すデバッグ出力]
Wptr(トランスピュータスタック)がIptr(命令ポインタ)を上書きしてクラッシュしていることを示すデバッグ出力。

2時間後、問題がN_ASIGNAノードにあることを発見しました。その左ノードは自身を指していました。次に、コンパイラがツリーにこの結び目をどこで作ったのかを見つける必要がありました。

さらに30分後、私のmalloc関数が常に同じアドレスを返していることがわかりました!これは予期せぬオペレーティングシステム上のバグでした。メモリ空間がいっぱいになると、mallocルーチンはNULLポインタを返すのに失敗するだけでした。

トランスピュータエミュレータを修正して、さらに128 KBのRAMを追加したところ、エラーなしでCコンパイラをコンパイルできるようになりました。さらに良いことに、アセンブリファイルはバイト単位で同じものを生成しました。cc2.aとcc3.aファイルを見ることができます。ここで、cc2.aはmacOSでコンパイルされ、cc3.aはトランスピュータエミュレータでコンパイルされました。

[画像: cc2.aとcc3.aが同じファイル長で表示されているファイルリスト]
cc2.aとcc3.aが同じファイル長で表示されているファイルリスト。cc2.aはmacOSでコンパイルされ、cc3.aはトランスピュータエミュレータでコンパイルされました。

コンパイラを移植性のない状態から使用可能な状態にするのにわずか3日しかかかりませんでした。これで、どのマシンでも同じトランスピュータ実行可能ファイルをビルドできるようになったことに非常に満足しています。

リポジトリ

開発履歴が、gitリポジトリのコミットをバージョン間の変更を確認する方法として使用されると、より明確になると考えました。コミットの順序は次のとおりです:

  1. 私のトランスピュータオペレーティングシステムからのCコンパイラ。
  2. フロッピーディスクへの最初のバックアップ(私のトランスピュータリポジトリのcc0)。
  3. フロッピーディスクへの2番目のバックアップ(私のトランスピュータリポジトリのcc1)。
  4. 64ビットマシンで動作する最終的な移植コンパイラ。

変更点とバージョンを比較して確認してください。非常に啓発的です。

有料のオープンソース開発者がさらに優れたソフトウェアを作成していることを知っていましたか?コーヒーで私の創造性を燃料にしてください☕。または、この旅に参加して月額サポーターになりましょう。さらに良いコンテンツと良いカルマが得られます!Lulu.comで私の本、私のデジタルストアで電子書籍やゲームも購入できます。

関連リンク

最終更新日:2026年9月21日