使我的transputer C编译器可移植

Hacker News Top 工具

摘要

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扩展、可移植性和替代编译器

Lobsters Hottest

本文讨论了编写可移植C代码的实际挑战,这些挑战源于对非标准编译器扩展和glibc条件头文件的依赖,并通过构建C编译器的示例进行说明。