我是如何开发Am29000的C编译器和网络浏览器的

Hacker News Top 工具

摘要

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目录中。 最终,我开始将编译器移植到我的操作系统。自举过程因内存泄漏充斥小内存而耗时良久。主要漏洞在于未在处理表达式树后释放内存...

相似文章

用 Zig 写一个 C 编译器

Hacker News Top

一位开发者记录了用 Zig 语言、按照 Nora Sandler 的教程系列构建名为 paella 的 C 编译器的全过程。