🧩 29 字节说你好:在 SuperHack 中极限压缩 “Hello, World!” 🔗

hacker.org 上“超小 Hello World”(Super Small Hello World)挑战赛解题实录:那些奏效的方案、功亏一篑的尝试,以及最终压进 29 字节的奇技淫巧。

总览

📑 目录 🔗


🎯 挑战内容 🔗

hacker.org 上有一门叫做 SuperHack 的小众深奥编程语言(esolang)。任务听起来再简单不过:编写一个打印出 Hello, World! 的 SuperHack 程序。

难点在于计分规则。你的得分取决于代码外接矩形的面积,越小越好。此外,程序还必须正常结束:即通过执行 !(停机)指令退出。输出文本后因虚拟机异常而崩溃是不算数的,多打印出任何多余字节也同样违规。

我们要打破的目标纪录是 29。


🧭 两分钟速览 SuperHack 🔗

SuperHack 是一台二维栈式状态机,设计精神与 Befunge 类似:

指令效果
0-9将数字压入栈中
+ - d加法、减法、向下取整除法(除以零的 d 会返回 0)
x复制栈顶元素
n ^复制栈顶往下第 n 个元素并压入栈顶(1^ 表示复制栈顶下方的第二个元素)
,读取输入。当没有输入时,它会压入 0
g弹出 x,弹出 y,将坐标 (x, y) 处的字符编码压入栈中。若超出网格边界则压入 0
P弹出栈顶并将其作为字符打印出来(仅保留低 7 位)
s跳过下一个单元格
?弹出栈顶;如果值为 0,则跳过下一个单元格
@将当前位置(及移动方向)压入调用栈
$从调用栈弹出一个位置,跳转到该处,并从往前两个单元格的位置继续执行
\反射镜:将“向右移动”折射为“向下移动”
!停机(halt)
其他任何字符空操作(no-op)

由此引出的两个特性决定了后文的所有思路:

  1. 字符即数据。 g 可以读取程序自身的源代码,因此字符串 Hello, World! 可以直接放在代码网格中,由程序逐个字符读取出来。其中绝大多数字符(H e l o W r)在被执行时都是空操作(no-op)。但有三个例外:, 会压入 0,d 会执行除法,而 ! 会直接停机。
  2. @ 与 $ 构成循环,且循环次数取决于 @ 的排列方式。 $ 会在它所返回的那个 @ 往后两个单元格处恢复执行。如果那个位置恰好是下一个 @,就会再次把位置压栈,因此只需几个 @ 就能将循环体重复执行很多次:
    • 紧密相邻的 @@@@@ 会产生斐波那契数列般的执行次数。5 个相邻的 @ 会让循环体恰好执行 13 次。
    • 每隔一格放置的 @ 会产生 2 的幂次的执行次数。4 个这样的 @ 可以跑满 16 轮循环。

13 个字符,13 次循环。请记住这个数字。

由于面积等于宽乘以高,而 29 是一个质数,因此 29 格的程序必然只能是一整行或者一整列。


1️⃣ 方案 1:直截了当的循环(面积 32) 🔗

方案 1 注解图

Hello, World!%0@s!@@@@@ 01^gP1+$

第一个真正可行的方案把数据载荷放在最前面,并从数据之后开始执行代码:

部分作用
Hello, World!纯数据,永远不会被执行,因为程序计数器(PC)从 % 处启动
%程序入口点
0初始索引 i = 0
@ s !停机小机关(halt gadget):见下文解析
@@@@@5 个相邻的蹦床指令,使循环体恰好运行 13 次
填充空格(原因见下文)
0 1^ g压入行号 0,复制索引 i,然后读取第 i 列的字符
P打印该字符
1+i += 1
$循环回跳

停机小机关。 第一个 @ 把它自己的位置压在调用栈的最底部,紧接着 s 跳过 !。随后,斐波那契链条开始执行它的 13 轮循环。当调用栈最终层层回退、弹出最底部那个 @ 时,$ 会在它后面两个单元格处恢复执行——那里正好就是 !。程序干脆利落地停机,全程甚至不需要任何字符串结束判断。

填充空格。 当多个 @ 相邻时,从最后一个 @ 返回时会向后跳两格恢复执行,从而跳过紧挨着 @ 链条的第一个单元格。因此,那一格必须是无害的字符,这里用了一个空格。

运行结果:输出 Hello, World!,干净停机,面积 32。这个方案完全正确,但略显宽裕:数据载荷是纯粹的死重,而且 %、填充空格和 0 加起来白白占用了 3 个单元格。


2️⃣ 方案 2:代码与数据交错编织(面积 28,但无效) 🔗

方案 2 注解图

1H@e@l@l@o ,1 ^WgoPr2lsds!+$

接下来的思路是不再为单独存放数据载荷浪费空间,而是把字符串编织进代码里。Hello, World! 的各个字符坐在奇数列(1, 3, …, 25),而执行代码则填满它们之间的偶数列:

问题在于,16 轮循环比 13 轮多出了 3 轮。打印完目标字符串后,循环还会继续越界读取,额外打印出 $ 和两个空字节(NUL)。接着,$ 发现调用栈已空,虚拟机报错崩溃:

output:  "Hello, World!$\0\0"
message: call stack underflow

虽然体积很小,但它不是一个合法解。不过它引入了一个关键思路:数据载荷中那些恰好也是实用指令的字符(如 ,),可以同时兼任数据与代码的双重角色。


3️⃣ 方案 3:纵向排列(面积 27,但无效) 🔗

方案 3 注解图

\
2
H
@
e
@
l
@
l
@
o
x
,
g

P
W
2
o
+
r
$
l

d

!

(每行一个字符。空行代表单个空格字符,属于数据的一部分。)

把程序竖过来放带来了一个意想不到的好处。在单列布局中,g 需要将 x 坐标设为 0、行号设为 i。数据载荷自带的 , 免费提供了那个 0,于是读取操作简化成了 x , g:复制 i、压入 0、读取字符。唯一的代价是在最顶部加一个 \,把程序计数器(PC)的方向折向下。

不过,它有着和方案 2 相同的致命缺陷:循环次数超标,会多打印出空字节,并最终因调用栈下溢(call stack underflow)而崩溃。面积 27 的它是本文中体积最小的程序,只可惜不算数。

方案 2 和方案 3 给我们的教训是:体面地停机是需要消耗单元格的。 交错布局虽然让循环本身变得廉价,但它在你只需要 13 轮循环的时候硬塞给了你 16 轮。


4️⃣ 方案 4:为退出条件买单(面积 30) 🔗

方案 4 注解图

\
2
H
@
e
@
l
@
l
@
o
x
,
g
 
P
W
x
o
5
r
d
l
5
d
?
!
2
+
$

方案 4 保留了方案 3 的纵向交错布局,并老老实实加上了字符串结束检测:

行号单元格作用
0\向下转向
12i = 2
3, 5, 7, 9@4 个蹦床指令,最多支持 16 轮循环
11-13x , g读取第 i 行(其中的 , 来自数据载荷)
15P打印字符
17-24x 5 d 5 d计算检测值 floor(floor(i/5)/5) = floor(i/25)(最后一个 d 是数据载荷)
25?如果检测值为 0,则跳过下一个单元格……
26!……也就是数据载荷自带的那个 !
27-282 +i += 2
29$循环回跳

索引 i 依次取 2, 4, …, 26。在到达最后一个字符(i = 26)之前,floor(i/25) 始终为 0,因此在除最后一轮之外的所有循环中,? 都会跳过停机指令。而在最后一轮循环中,执行流恰好落到数据载荷的 ! 上——它既是最后一个被打印出来的字符,又是让程序停机的指令。

这是一次极其漂亮的代码复用:数据载荷中的 ,、d 和 ! 全部兼职充当了代码。但结束检测还是花掉了 5 个单元格,总面积停在了 30。

🧱 为什么 29 看起来像是天方夜谭 🔗

到了这个阶段,我的 AI 助手甚至试图证明 30 就是理论最优解。几乎所有思路都撞上了同一堵墙:

对更小面积的穷举搜索一无所获,围绕方案 4 布局在面积 29 上的搜索也同样颗粒无收。“30 就是极限下界”看起来越来越像一条铁律了。

然而并不是。


🏆 方案 5:29 字节绝杀(面积 29) 🔗

方案 5 注解图

9@s!@@@@@Hello, Worlä¡1^gP1+$

破局的转机来自于重新研读评测机(judge)的源码。其中有两行代码至关重要:

case 'P':
  $output .= chr($this->Pop() & 0x7F); break;
...
default:
  break;   // every unknown character is a no-op

这意味着,数据载荷中的每一个字符其实都有两种编码方式:普通 ASCII 编码在被执行时可能会触发某些操作;而 c | 0x80 编码则保证是完全惰性的空操作。这就是“数据的魔法公式”:把危险字符存储为 c + 128,再让 P 指令免费帮我们剥离最高位。

数据载荷里只有两个危险字符:d 变成了 ä(0xE4),! 变成了 ¡(0xA1)。改掉这两个字符之后,整个数据载荷就可以直接当作代码穿行而过了。这一下子消除了上一节第三条里的障碍。

现在,我们拿起方案 1,把数据载荷直接搬进循环体内部:

列号单元格作用
09i = 9(即字符 H 所在的列号)
1-3@ s !停机小机关,与方案 1 相同
4-8@@@@@5 个相邻的蹦床指令,让循环体恰好执行 13 次
9-21Hello, Worlä¡数据载荷,在每一轮循环中都会被程序计数器穿过执行
22-271 ^ g P 1 +复制 i、读取字符、打印、将索引加一
28$循环回跳

让我们单步追踪一轮循环的执行过程:

  1. 程序计数器(PC)落在 @ 链条的某个位置,然后一路穿过数据载荷。H e l l o、空格以及 W o r l 什么也不做。数据载荷自带的 , 压入了 0——刚好作为 g 所需的行号。接下来的 ä 和 ¡ 也什么都不做。
  2. 1^ 将索引 i 复制到那个 0 的上方,随后 g 读取出第 i 列的字符。
  3. P 将其打印出来。对于最后两个特殊字符,它会分别计算 0xE4 & 0x7F = 'd' 和 0xA1 & 0x7F = '!' 并打印。
  4. 1+ 将 i 加一,$ 跳回 @ 链条中继续下一轮。

与方案 1 相比,有 3 个单元格凭空消失了:

32 − 3 = 29。在第 13 轮循环结束后,调用栈回退到停机小机关,程序稳稳停在第 3 列的 ! 上。

输出 Hello, World!,无任何虚拟机报错,耗时 271 个周期,面积 29。

🌐 最后一道坎:把原始字节完好无损地送达评测机 🔗

第一次提交时,服务器返回了 got: 'Hello, WorlC$',得分 31。原因是网页表单默认以 UTF-8 编码提交数据,把每个特殊字符都拆成了两个字节(ä → C3 A4,¡ → C2 A1)。在被掩码截取低 7 位后,0xC3 被打印成了 C,而 0xA4 被打印成了 $。

解决办法是强制浏览器以 Latin-1(ISO-8859-1)编码发送表单,在该编码下这两个字符各只占一个字节。在挑战页面上,我打开浏览器开发者工具(DevTools)控制台并运行了以下代码:

document.querySelectorAll('form').forEach(
  f => f.acceptCharset = 'ISO-8859-1');
document.querySelector('textarea').value =
  "9@s!@@@@@Hello, Worl\u00e4\u00a11^gP1+$";

然后点击普通的提交按钮。评测机顺利打印出 Hello, World!,占山为王(King of the Hill)排行榜上的得分定格在了 29。


📊 战绩汇总表 🔗

#程序代码面积是否正常结束?
1Hello, World!%0@s!@@@@@ 01^gP1+$32✅
21H@e@l@l@o ,1 ^WgoPr2lsds!+$28❌ 多打印额外字节,且调用栈下溢
3\2H@e@l@l@ox,g PW2o+r$ld!27❌ 多打印额外字节,且调用栈下溢
4\2H@e@l@l@ox,g PWxo5rdl5d?!2+$30✅
59@s!@@@@@Hello, Worlä¡1^gP1+$29✅

💡 经验总结 🔗


📚 免费下载我的电子书 🔗

获取《卡乌埃的生活方式》精选离线版——一份全面优化 🍏 健康、💰 财富、❤️ 爱 与 🧘 意义 的实践指南。

卡乌埃的生活方式

卡乌埃的生活方式

📬 订阅邮件通讯 🔗

当我发布关于美好生活的新随笔、读书笔记和实践指南时接收通知。绝无垃圾邮件——可随时退订。


💬 评论与讨论 🔗

有任何问题、见解或建议吗?欢迎在下方分享您的想法。评论功能由 GitHub Discussions 驱动(无广告、无垃圾信息)——点击下方的使用 GitHub 登录即可发表评论或互动(注册免费 GitHub 账号不到一分钟即可完成)。



最后更新于 2026年10月2日.

免责声明:本站所有内容仅供信息参考与教育用途,不构成任何医疗、心理、财务、投资、税务或法律建议。此处呈现的观点、整理与见解仅代表我个人,不代表我雇主的立场。书籍、演讲及文章摘要均为独立的教育性评论,其知识产权归原作者所有。本站可能使用 AI 工具辅助编辑、排版与归纳;尽管我们竭力确保准确无误,但仍可能存在疏漏。隐私政策:本站使用第三方服务提供可选的邮件通讯订阅(follow.it)、评论(giscus / GitHub Discussions)以及嵌入媒体(YouTube),这些服务可能会依据各自的隐私政策处理访客的 IP 地址和 Cookie。