我是如何开发Am29000的C编译器和网络浏览器的
摘要
Oscar Toledo G分享了他于1998年至1999年间为Am29000自制电脑开发C编译器和网络浏览器的经历,讨论了技术挑战和当时的技术背景。
暂无内容
查看缓存全文
缓存时间: 2026/08/17 15:56
# 我如何开发Am29000的C编译器与网页浏览器
来源:https://nanochess.org/am29000_c_compiler_web_browser.html
*作者:奥斯卡·托莱多·G 2026年8月16日*
我的G11V2(Am29000自制计算机)上运行的仿真窗口化操作系统
如果你读过我之前的文章,就会知道我曾为基于Am29000处理器的自制计算机,在32位机器码中开发过窗口化操作系统(https://nanochess.org/the_am29000_computer.html)。本文将探讨我如何为这些处理器开发C编译器及网页浏览器。
时间跨度从1998年圣诞节到1999年我生日之间。那时我20岁,互联网在墨西哥如野火般蔓延,布鲁斯·威利斯刚用巨石陨石拯救了地球,新兴互联网催生了新职业(那是平面设计师的黄金时代),人们担忧千年虫会引发数字末日(连《辛普森一家》都播出了荷马忘记更新电脑的剧集),而阿诺·施瓦辛格在《末日救世》中正用子弹屠杀恶魔。
## 为我找个C编译器
1997年间,我开发了一些实用工具、打印机驱动(我有台HP DeskJet 500,设法在爱普生Stylus 600上实现彩色打印),甚至用调制解调器卡收发传真。那时人人都问你是否有传真机,以便发送广告或获取信息。我们甚至买了传真机,结果第二年再没人问传真——电子邮件时代来临了!
总之,用机器码编程如同在泥浆中深潜,若没有潜水面罩(地址笔记和文档),只会越陷越深。
约1998年6月的墨西哥电脑广告
即便青春活力充沛,我也开始厌倦了。因为每次写新函数都得精心规划内存、挪动代码腾空间,或更糟的——重定位跳转指令却因漏改一处而引入意外错误。有时我想“这代码得膨胀”,于是代码中常见5到10个NOP指令的扩展预留,还会发现错位的子程序——因为它们塞不进原位。
那时我已是网吧(墨西哥称“cybercafé”)常客。早期网吧之一位于已倒闭的Bazar Pericoapa街对面,还供应咖啡。我们听着《Ciega, sordomuda》、《Amor de papel》、《Laura no esta》和《Barbie Girl》的节奏上网冲浪。当然,当卡布奇诺和浓缩咖啡洒进键盘后,他们很快吸取教训,从此不再提供咖啡服务。
我在网上搜寻所有Am29000资料,发现了High-C 29k编译器和支持Am29000的GCC 2.8.1。无力购买High-C 29k,我只能下载GCC源码,却发现它需要至少2MB内存(考虑虚拟内存可能更多),而我的电脑仅512KB。更糟的是它依赖Flex和Bison,且需要大量底层操作系统支持(我几乎一无所有),外加汇编器和链接器。
我需要设法自举编译器,却完全不愿为一次性使用移植两个大型程序。于是转向更近的星系——我的Transputer C编译器(https://nanochess.org/bootstrapping_c_os_transputer.html)。主要问题是Am29000处理器架构差异巨大,寄存器众多。在单遍编译器中分配寄存器令我头疼——普通变量需保存在本地寄存器,但只要出现间接引用(如&a),变量就必须存入内存。
我首次尝试将Small-C编译器移植到Am29000,1997年12月的日记记录了此事,但可能因毫无实用价值而未再提及。1998年2月2日,日记中再次强调急需C编译器,于是我在80486电脑上安装DJGPP(移植到MS-DOS的GCC)辅助开发。无法使用仅128KB板载内存的Transputer。DJGPP是DJ G++的缩写,在编译器仍高价出售时,DJ Delorie创建的DOS版GNU C++编译器即开即用,造福了全球开发者。
## 在代码树中培育编译器
1998年5月6日,我终于用DJGPP成功编译了我的Transputer C编译器源码进行测试。这意味着必须将非标准输入输出函数替换为标准C库函数。日记未载后续,但我在搜寻资料时,发现一张软盘保存了Am29000 C编译器创建的全过程。
我的软盘存放着C编译器演进历程。
这张软盘包含两个增强版Transputer C编译器和我的Am29000 C编译器初版。此时Transputer编译器已在PC机上运行,与原始Transputer环境一致。树表达式通过数组保存:一个存左节点指针,一个存右节点,一个存节点值,一个存节点类型。当然,这意味着不扩展数组就无法构建复杂表达式。你能在我的transputer git仓库的cc0目录找到此编译器(https://github.com/nanochess/transputer)。
以下是表达式树数组化的代码片段(crea\_nodo函数):
```c
++ultimo_nodo;
if(ultimo_nodo == TAM_ARBOL) {
error("Expresión muy compleja");
cancela();
}
nodo_izq[ultimo_nodo] = izq;
nodo_der[ultimo_nodo] = der;
oper[ultimo_nodo] = op;
esp[ultimo_nodo] = val;
regs[ultimo_nodo] = 0;
regsf[ultimo_nodo] = 0;
```
我逐步制定计划:必须用独特方式创建Am29000代码生成器。需要将整个函数解析到内存,确定所需本地寄存器数量,检测局部变量引用,然后构建寄存器分配器。接着,我用动态内存(malloc/free)和结构体重构表达式树生成器(仍为Transputer设计,见cc1目录)。笔记显示休息时我还玩了《古墓丽影2》的演示版。
以下是节点创建代码的演变:
```c
ultimo_nodo = malloc(sizeof(struct nodo));
if (ultimo_nodo == NULL) {
error("Expresión muy compleja");
cancela();
}
/* ... */
ultimo_nodo->izq = izq;
ultimo_nodo->der = der;
ultimo_nodo->oper = op;
ultimo_nodo->esp = val;
ultimo_nodo->regs = 0;
ultimo_nodo->regsf = 0;
```
此代码比原版更清晰,且仅受可用内存限制。1998年5月13日,我终于咬牙开始改造主解析器,将所有代码保存为树状链表的中间表示。语句序列成为链表,任何嵌套语句都化为链表分支。期间我感冒了,用镭射影碟观看《丛林之书》(杰森·斯科特·李主演),康复后立即着手创建Am29000代码生成器。
整个移植耗时两周多,需为代码生成器进行大量微测试。以下是Transputer的代码生成器:
```c
/* ** 二元及部分一元运算符代码 ** */
gen_oper(oper, rev)
int oper, rev;
{
if (oper == N_NULO) return;
if (oper == N_CUENTA) emite_linea("wcnt");
else if (oper == N_OR) emite_linea("or");
else if (oper == N_XOR) emite_linea("xor");
else if (oper == N_AND) emite_linea("and");
else if (oper == N_IGUAL) {
emite_linea("diff");
emite_linea("eqc 0");
}
else if (oper == N_SUMA) emite_linea("bsub");
else if (oper == N_MUL) emite_linea("prod");
```
同功能的Am29000代码片段:
```c
/* ** 二元及部分一元运算符代码 ** */
gen_oper(oper, inmediato, reg1, reg2, constreg, control)
int oper, inmediato, reg1, reg2, constreg, control;
{
int reg;
if (oper == N_OR || oper == N_AOR) {
gen_inst1("or", inmediato, reg1, reg2, constreg);
}
else if (oper == N_XOR || oper == N_AXOR) {
gen_inst1("xor", inmediato, reg1, reg2, constreg);
}
else if (oper == N_AND || oper == N_AAND) {
gen_inst1("and", inmediato, reg1, reg2, constreg);
}
else if (oper == N_CD || oper == N_ACD) {
gen_inst1("sra", inmediato, reg1, reg2, constreg);
```
Transputer的栈架构自动处理寄存器使用,而Am29000中编译器需精确控制每个寄存器。以下是启动C函数的代码,展示处理器复杂性:
```c
/* ** 函数序言:
** o 将虚拟变量分配到寄存器或内存
** o 分配所需空间
** o 复制输入参数(若需要)
*/
prologo_funcion() {
int variable, temp, por_copiar = 0, posicion, registro;
/* ** 分配寄存器(暂不确定是否为局部或全局),
** 同时在栈上分配空间,但需判断是否为复制参数腾空间。
*/
variable = 0;
while (variable < variables_virtuales) {
switch (virtuales[variable] & 3) {
case 0: /* 变量按情况分配 */
if (virtuales[variable + 1] != 0) { /* 是否需要指针? */
virtuales[variable] = (pila << 2) | 1;
pila += virtuales[variable + 2] ? 8 : 4;
} else { /* 否则保留寄存器 */
if (virtuales[variable + 2]) /* 浮点对齐 */
pila_regs = (pila_regs + 1) & ~1;
virtuales[variable] = pila_regs << 2;
pila_regs += virtuales[variable + 2] ? 2 : 1;
}
virtuales[variable + 1] = 0;
break;
case 1: /* 变量必须存内存 */
temp = virtuales[variable] >> 2;
virtuales[variable] = (pila << 2) | 1;
pila += temp;
virtuales[variable + 1] = 0;
break;
case 2: /* 计算需复制的参数量 */
if (virtuales[variable + 1] != 0) /* 是否需要复制? */
por_copiar += virtuales[variable + 2] ? 8 : 4;
break;
}
variable += 3;
}
/* ** 调整栈空间以容纳复制参数,复制参数并预分配寄存器。
*/
pila += por_copiar;
variable = 0;
while (variable < variables_virtuales) {
switch (virtuales[variable] & 3) {
case 1: /* 变量必须存内存 */
virtuales[variable] = (((virtuales[variable] >> 2) + por_copiar) << 2) | 1;
break;
}
variable += 3;
}
if (pila != 0) gen_inst1("sub", SI, 125, 125, pila);
pila_regs = (pila_regs + 1) & ~1;
posicion = 0;
variable = 0;
while (variable < variables_virtuales) {
switch (virtuales[variable] & 3) {
case 2: /* 复制必需参数 */
if (virtuales[variable + 1] != 0) {
virtuales[variable + 1] = 0;
registro = virtuales[variable] >> 2;
virtuales[variable] = (posicion << 2) | 1;
if (posicion == 0) {
gen_inst2("store 0,4,", NO, registro + 128, 125);
posicion += 4;
if (virtuales[variable + 2]) {
gen_inst1("add", SI, 96, 125, posicion);
gen_inst2("store 0,4,", NO, registro + 128, 96);
posicion += 4;
}
} else {
gen_inst1("add", SI, 96, 125, posicion);
gen_inst2("store 0,4,", NO, registro + 128, 96);
posicion += 4;
if (virtuales[variable + 2]) {
gen_inst1("add", SI, 96, 96, 4);
gen_inst2("store 0,4,", NO, registro + 129, 96);
posicion += 4;
}
}
} else {
if (total_regs == -1 && pila_regs <= 4)
temp = 128;
else if (total_regs == -1)
temp = 130 + pila_regs;
else
temp = 130 + total_regs + pila_regs;
virtuales[variable] = (((virtuales[variable] >> 2) + temp) << 2) | 2;
}
break;
case 3: /* 调整内存中的参数 */
virtuales[variable + 1] = 0;
virtuales[variable] = (((virtuales[variable] >> 2) + pila) << 2) | 1;
break;
}
variable += 3;
}
if (total_regs == -1 && /* 若无函数调用且仅使用 */
pila_regs <= 4) { /* 不超过4个寄存器, */
pila_regs = 0; /* 无需寄存器栈 */
variable = 0;
while (variable < variables_virtuales) {
switch (virtuales[variable] & 3) {
case 0: /* 分配gr116-gr119寄存器 */
virtuales[variable] = ((virtuales[variable] >> 2) + 116) << 2;
break;
case 2: /* 参数保持局部性 */
virtuales[variable] &= ~3;
break;
}
variable += 3;
}
} else { /* 申请寄存器栈空间 */
variable = 0;
while (variable < variables_virtuales) {
switch (virtuales[variable] & 3) {
case 0: /* 分配本地寄存器 */
virtuales[variable] = ((virtuales[variable] >> 2) + total_regs + 130) << 2;
break;
case 2: /* 参数已分配位置 */
virtuales[variable] &= ~3;
break;
}
variable += 3;
}
pila_regs += total_regs;
pila_regs += 2;
if (pila_regs > 128)
error("Demasiadas variables locales");
else if (pila_regs + pila_args > 508)
error("Demasiados argumentos");
gen_inst1("sub", SI, 1, 1, pila_regs << 2);
emite_linea("asgeu 64,gr1,gr126");
gen_inst1("add", SI, 129, 1, (pila_regs + pila_args) << 2);
}
}
```
每个C局部变量(包括函数参数)都成为“虚拟”变量(我的思路是:未分配实体的变量即虚拟)。类型0是普通变量(含间接引用计数判断是否复制到内存),类型1是数组,类型2是参数(同样含间接引用计数)。它按需在内存栈(gr125)分配空间,复制需入内存的参数(传入的结构体或使用&运算符的参数),之后为剩余变量分配本地寄存器。
检测零函数调用以完全避免栈帧、直接使用gr116-gr119作为本地寄存器的设计相当超前,最后用三条指令(sub、asgeu、add)创建极简栈帧。
函数尾声则非常简洁:
```c
epilogo_funcion() {
if (buffer_vacio) return;
if (pila_regs != 0) {
gen_inst1("add", SI, 1, 1, pila_regs << 2);
if (pila == 0)
gen_libre(0);
else
gen_inst1("add", SI, 125, 125, pila);
emite_linea("jmpi lr0");
emite_linea("asleu 65,lr1,gr127");
} else {
if (pila != 0)
gen_inst1("add", SI, 125, 125, pila);
estado_buf[total_lineas] = 10;
emite_linea("jmpi \1\1\1\1\1\1\1lr0");
gen_libre(1);
}
vacia_buffer();
}
```
Am29000 C编译器初版源码可在我的git仓库的cc目录获取(https://github.com/nanochess/Am29000)。
在开发编译器的同时,我还在构建处理Am29000指令的汇编器,以及将其与我的窗口化操作系统连接的小型库。由于Am29000指令集正交(寄存器可在任何指令中互换使用),汇编器小巧直接。此早期MS-DOS版汇编器也在我的git仓库的asm目录中。
最终,我开始将编译器移植到我的操作系统。自举过程因内存泄漏充斥小内存而耗时良久。主要漏洞在于未在处理表达式树后释放内存...
相似文章
发掘我为Am29000自制计算机用机器码编写的1996年窗口操作系统
Oscar Toledo G. 讲述了他于1996-1997年间为自制Am29000计算机用机器码开发窗口操作系统的经历,以及近期重新发现并记录这一系统的历程。
用 Zig 写一个 C 编译器
一位开发者记录了用 Zig 语言、按照 Nora Sandler 的教程系列构建名为 paella 的 C 编译器的全过程。
在2026年搭建AmigaOS开发环境
在Linux上设置AmigaOS开发环境的实用指南,涵盖工具链安装、仿真和远程调试。
我的网页版 Mars MIPS 现在内置了 C 编译器
一款网页版的 Mars MIPS 模拟器现在内置了 C 编译器,允许用户在浏览器中编写和模拟 MIPS 汇编及 C 代码。
将我1993年的Amiga游戏移植到Godot,使用LLM解读68000汇编代码
一位工程师描述了他将1993年的Amiga游戏'Babylonian Twins'移植到Godot的过程,使用Claude Fable 5 LLM来辅助读取和翻译68000汇编代码,结合个人历史与AI驱动开发。