使我的transputer C编译器可移植
摘要
Oscar Toledo G. 详细阐述了将他的老式transputer C编译器从32位系统移植到现代64位系统所面临的挑战和必要修改,特别是指针和类型兼容性问题。
暂无内容
查看缓存全文
缓存时间: 2026/09/24 04:02
# 移植我的不可移植 transputer C 编译器
来源:https://nanochess.org/transputer_c_compiler.html
*作者:Oscar Toledo G\. 2026年9月21日*
装有我为 transputer 开发的 C 编译器(约1998年)的软盘
几周前,我找到了一张软盘,里面存放着我为 transputer 开发的最新版 C 编译器。该编译器经过增强,可使用 Delorie 的 G\+\+ 或 DJGPP 进行编译(约1998年)。编译过程中确实产生了大量警告,但由于它是运行在 32 位 Intel 80486 处理器上的 32 位编译器,因此工作完全正常。这是将其转换为生成适用于 AMD Am29000 处理器代码的中间步骤 (https://nanochess.org/am29000_c_compiler_web_browser.html),而这个版本又经过进一步改进,去除了残留的 Small-C 结构,拥有了规范的词汇分析器、ANSI C 语法和完整的预处理器。作为一次怀旧尝试,我尝试在现代 64 位计算机(Macbook Air M1)上编译1998年版本的 transputer C 编译器,结果遇到了诸多困难。例如:
- 代码中整数与指针可以互换使用(两者当时都是 32 位)
- 未使用 `FILE *` 类型,而是使用 `int`
- 编译器函数没有原型声明,因此 *clang* 基于正确理由发出警告,因为指针现在是 64 位,而整数是 32 位。
其中一些可移植性问题源自 Small-C。由于 Small-C 没有 `struct` 关键字,所有必需的结构体都在字节池(`char`)中创建,字被分成两个字节(为 8080 处理器设计),当我将其扩展到 32 位平台时,这些字变成了 4 字节。更糟糕的是,指针被转换为 `int` 类型。但现在,将 64 位指针转换为 32 位整数已经不可能了。需要进行设计更改!Small-C 代码片段,其中指针作为两个字节存储在数组中。Small-C 代码片段,其中值作为两个字节存储在数组中。
## 初步尝试
旧程序无法在现代 64 位系统上编译已不是第一次了。然而,与其让这个编译器只能在模拟环境下作为新奇事物运行,不如让它在你的现代笔记本电脑上工作。剧透:这并不容易。我为自己设定了一个目标:希望仅对编译器进行足够修改,使其能在现代 64 位平台上编译,同时仍能为 transputer 编译自身。这意味着我只能使用我的编译器已实现的特性。
编译器分为多个源文件,所有文件都从一个名为 *cc.c* 的主驱动文件调用。我的 DJGPP 移植版本创建了两个不同的文件:*cc.c* 和 *cc2.c*。第一个用于 DJGPP 编译,第二个用于直接在 transputer 上用我的 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` 是一个整数数组。第一行用于当前文件,下一行是指向函数定义的指针,其他三行都是整数。看出大问题了吗?64 位机器的指针是 64 位,而 `int` 仍然是 32 位。此外,所有编译器源代码仍然使用西班牙语注释和变量名。我用结构体重写了代码:
```
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++;
```
随着重新设计的进行,我添加了更多函数原型。正当我以为这很简单时……
## 并非如此容易
第一个需要关注的地方来自表达式处理子程序:
```
/*
** 解析表达式并生成代码。
*/
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` 函数如下:
```
/*
** 解析表达式并将其保存在内存中。
*/
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_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`)包含数组长度(32 位,存储为4字节),结构体类型(`STRUCT`)指向结构体定义。32 位指针转换为整数并保存为4字节。这在现代 64 位架构上再次完全不可移植。最简单的方法是扩展到八次 `guarda_tipo` 函数调用(64 位转为8字节)。但幸运的是我找到了另一种方法。
此外,还有一个问题是结构体也在同一个字节池中分配,就像这样(接下来是尴尬的代码):
```
/*
** 一个新结构体。
*/
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;
}
```
然后结构体的每个成员也被分配到类型池中。此外,我追踪到三组宏,每组定义了一个伪结构体:
```
/* 结构体定义 */
#define EST_QUE_ES 0 /* char, 标识是结构体还是枚举标签 */
#define EST_ES_UNION 1 /* char, 标识是联合体还是结构体 */
#define EST_TAM 2 /* int, 结构体/联合体总大小 */
#define EST_LISTA 6 /* char*, 成员列表 */
#define EST_SIG 10 /* char*, 下一个标签 */
#define EST_NOMBRE 14 /* char[], 标签名称 */
/* 成员定义 */
#define MIE_TIPO 0 /* char*, 成员类型 */
#define MIE_POSICION 4 /* int, 在结构体中的位置 */
#define MIE_SIG 8 /* char*, 下一个成员 */
#define MIE_NOMBRE 12 /* 成员名称 */
/* 枚举器定义 */
#define ENUM_VALOR 0 /* int, 枚举器的值 */
#define ENUM_SIG 4 /* char*, 下一个枚举器 */
#define ENUM_NOMBRE 8 /* char[], 枚举器名称 */
```
这里面嵌入了五个指针。我可以尝试将其转换为结构体,但我有个更好的主意。我的 Am29000 C 编译器 (https://nanochess.org/am29000_c_compiler_web_browser.html) 就是这个编译器的演进版本,我已经用优雅的结构体替换了这些丑陋的定义。因此,我可以进行反向移植,也就是将更新的代码适配到旧代码中。反向移植后的代码看起来很棒,更重要的是,它是可移植的:
```
struct rotulo { /* 结构体定义 */
struct rotulo *sig; /* 下一个标签 */
int tam; /* 结构体总大小 */
struct miembro *lista; /* 成员列表 */
char que_es; /* 标识是结构体还是枚举标签 */
char es_union; /* 标识是联合体还是结构体 */
char nombre[1]; /* 名称 */
};
struct miembro { /* 结构体成员定义 */
struct miembro *sig; /* 下一个成员 */
int posicion; /* 在结构体中的位置 */
unsigned char *tipo; /* 声明的类型 */
char nombre[1]; /* 名称 */
};
struct enumerador { /* 枚举器定义 */
struct enumerador *sig; /* 下一个枚举器 */
int valor; /* 枚举器的值 */
char nombre[1]; /* 名称 */
};
```
同时,我也将所有错误消息从西班牙语翻译成了英语,因为我的源代码文件仍然使用 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` 的时代):
```
/*
** "break" 语句
*/
void s_break() {
/* 检查是否有未关闭的循环 */
if (ultimo_bucle == NULL) {
error("No loops open");
return; /* 没有 */
}
desp_pila(ultimo_bucle[B_PILA]); /* 有,调整栈 */
salto(ultimo_bucle[B_FIN]); /* 跳转到退出标签 */
}
```
新版本为:
```
void s_break() {
/* 检查是否有未关闭的循环 */
if (ultimo_bucle == NULL) {
error("No loops open");
return; /* 没有 */
}
desp_pila(ultimo_bucle->pila); /* 有,调整栈 */
salto(ultimo_bucle->fin); /* 跳转到退出标签 */
}
```
## 25个错误走向成功
经过所有这些更改、大量的时间和咖啡!我终于将指针和整数使用错误减少到了25个。这些错误可分为以下几类:
- 保存/读取结构体数据的指针(3处)
- 读取变量、参数或函数的类型(5处)
- 使用表达式节点指针保存整数(17处)
我并非特别自豪这段代码,但让我们看看我所做的一个例子:
```
if (nodo_temp->oper == N_RESULTA) {
/* 返回结构体的函数 */
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`,但完美是“能用就行”的敌人,因此我只是添加了字段来处理额外数据。如果是在过去,我不会这样做,因为内存空间很重要;可能我会用联合体解决,但表达式树中多几个字段不会占用太多内存。
添加到 `struct nodo` 的字段有:`struct nodo *tri` 和 `int extra_val`。我不得不进行一些字段调整,并注意到优化的机会,但为了避免引入更多错误,我没有进行优化。
错误减少到8个。其中几个是因为有指向结构体类型的指针,因此我用结构体索引(从线性列表开头计数)替换了它们。
现在减少到5个访问全局和局部变量定义的错误:
```
/* 定义名称格式 */
#define NOMBRE 0
#define IDENT 17
#define CLASE 18
#define NIVEL 19
#define TIPO 20
#define POSICION 24
```
又是一个硬编码的结构体。`TIPO` 指向类型定义,这在64位上仍然不可移植。让我们再次为此创建一个结构体:
```
#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`(一个无限制输入函数,曾被 Richard Morris 创建的1988蠕虫利用 (https://en.wikipedia.org/wiki/Morris_worm))的消息,但我可以接受。
## 但它能工作吗?
我开始用编译器编译它自身,结果它生成了错误,因为我意外地在一些函数中插入了 ANSI C 调用序列,所以我不得不将它们改回 K&R 语法。我还发现变量处理中有一些小错误(调整遗留的问题),最终我成功完整地编译了编译器自身。
任何有编译器经验的人都知道这还不够。输出可能完全有问题。我必须在我的 transputer 操作系统 (https://nanochess.org/transputer_operating_system.html) 中测试它。
运行了我的 transputer 汇编器 *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.
```
它生成了一个大小为 35997 字节的可执行文件 *cc2*。作为参考,我之前的编译器是 38176 字节。它更短是因为使用结构体避免了冗长的字节访问代码。
那么,它能在我的 transputer 操作系统中工作吗? \*badum\-tss\*
在测试之前,我想起了我在我 transputer 操作系统 (https://nanochess.org/transputer_operating_system.html) 的光线追踪器中30年后发现的浮点临时值补丁,于是也把它加了上去。我还用同一个目录(包含 DJGPP 版本,该目录幸运地也包含生成的汇编文件)中的 HOLA.C 和 PRUEBA.C 进行了两个小测试。我用新编译器编译了它们,成功了!生成的汇编代码完全相同。
## 在我的操作系统中尝试
我构建了包含编译器所有文件的软盘映像。目标是让它能编译自身。我预计它仍然能装入 128 KB 的内存。我的构建软盘命令行是:
```
./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.pto`,输入 cc2.e 文件,并设置 8192 字节的栈,加上零字节的额外数据,这创建了 cc2.p 可执行文件。然后我运行了它。屏幕上显示乱码……
相似文章
关于C扩展、可移植性和替代编译器
本文讨论了编写可移植C代码的实际挑战,这些挑战源于对非标准编译器扩展和glibc条件头文件的依赖,并通过构建C编译器的示例进行说明。
我是如何开发Am29000的C编译器和网络浏览器的
Oscar Toledo G分享了他于1998年至1999年间为Am29000自制电脑开发C编译器和网络浏览器的经历,讨论了技术挑战和当时的技术背景。
[P] 读了太多架构手册后,我构建了一个可移植的GPU ISA [P]
一个名为WAVE的可移植GPU ISA,将内核编译为通用二进制文件,并翻译成特定厂商的后端(Metal、PTX、HIP、SYCL),已在多个GPU上验证结果。
将我的C游戏移植到WASM,这是我遇到的所有Bug
一位开发者分享了将C游戏移植到WebAssembly的经验,详细介绍了因32位与64位差异遇到的Bug,并提供了调试技巧。
发掘我为Am29000自制计算机用机器码编写的1996年窗口操作系统
Oscar Toledo G. 讲述了他于1996-1997年间为自制Am29000计算机用机器码开发窗口操作系统的经历,以及近期重新发现并记录这一系统的历程。