感谢@Lynn -16个字节感谢@Neil
-2个字节
由于这只是Z80机器码,因此这是很多不可打印的内容,因此有一个- xxd -r
可逆的十六进制转储:
00000000: ddb6 2120 10dd b615 280c 003e 62ff 3e65 ..! ....(..>b.>e
00000010: ffff 3e70 ff76 003e 62ff 3e65 ffff 3e70 ..>p.v.>b.>e..>p
00000020: ff76 .v
在线尝试!(Python中的详尽测试器)
说明
z80golf是Anarchy Golf假设的Z80机器,其中call $8000
是putchar,call $8003
是getchar,halt
使解释器退出,您的程序放置在$0000
,而所有其他内存都填充零。使程序在汇编时防辐射非常困难,但是一种通用的有用技术是使用一个字节的幂等指令。例如,
or c ; b1 ; a = a | c
只是一个字节,而a | c | c == a | c
,因此只需重复执行指令即可使其防辐射。在Z80上,8位立即数是两个字节(其中立即数在第二个字节中),因此您也可以可靠地将某些值加载到寄存器中。这是我最初在程序开始时所做的,因此您可以分析答案底部存储的较长的变体,但后来我意识到有一种更简单的方法。
该程序由两个独立的有效载荷组成,其中一个有效载荷可能已被辐射损坏。我通过检查一些绝对内存地址的值来检查是否删除了一个字节,以及删除的字节是否在有效负载的第二个副本之前。
首先,如果没有观察到辐射,我们需要退出:
or a, (ix+endbyte) ; dd b6 21 ; a |= memory[ix+0x0021]
jr nz, midbyte ; 20 10 ; jump to a halt instruction if not zero
如果删除了任何字节,则所有字节将移位并$0020
包含last 76
,因此$0021
将为零。即使实际上没有冗余,我们也可以负担辐射程序的开始时间:
- 如果
$10
消除了跳变偏移,则可以正确检测到辐射,不会发生跳变,并且该偏移也无关紧要。下一条指令的第一个字节将被消耗,但是由于它被设计为可抵抗字节删除,因此这无关紧要。
- 如果
$20
删除了跳转操作码,则跳转偏移量$10
将解码为djnz $ffe4
(将下一个指令字节用作偏移量-参见上文),这是一个循环指令-递减B,如果结果不为零,则跳转。因为ffe4-ffff
用零填充(nop
s),并且程序计数器回绕,所以它将在程序的开头运行256次,然后最终继续。我对此工程感到惊讶。
- 删除
$dd
时,片段的其余部分将解码为or (hl) / ld ($1020), hl
,然后滑入程序的下一部分。该or
不会改变任何重要的寄存器,而且由于HL为零,在这一点上,写也会抵消。
- 删除
$b6
将使其余部分解码ld ($1020), ix
并按上述步骤进行。
- 删除
$21
将使解码器吃掉$20
,从而触发djnz
行为。
请注意,由于集成了零校验功能,使用or a, (ix+*)
节省了两个字节ld a, (**) / and a / and a
。
现在,我们需要确定要执行的有效负载的两个副本中的哪个:
or (ix+midbyte) ; dd b6 15
jr z, otherimpl ; 28 0c
nop ; 00
; first payload
ld a, 'b' ; 3e 62
rst $0038 ; ff
ld a, 'e' ; 3e 65
rst $0038 ; ff
rst $0038 ; ff
ld a, 'p' ; 3e 70
rst $0038 ; ff
midbyte:
halt ; 76
otherimpl:
nop ; 00
ld a, 'b' ; 3e 62
; ... ; ...
rst $0038 ; ff
endbyte:
halt ; 76
由于使用了相对跳转来在两个副本之间进行选择,因此两个副本之间用nop分隔,并且辐射可能会使程序移位,从而使跳转跳过了目的地之后的第一个字节。另外,nop被编码为零,这使得检测移位字节变得容易。请注意,如果交换机本身已损坏,则选择哪个有效负载都没有关系,因为这样两个副本都是安全的。但是,请确保它不会跳入未初始化的内存:
- 删除
$dd
将使接下来的两个字节解码为or (hl) / dec d
。ClobbersD。没什么大不了的。
- 删除
$b6
将为创建未记录的更长编码dec d
。同上。
- 删除
$15
将改为读取,$28
作为偏移量,然后执行将在处进行$0c
,如下所示。
- 当
$28
消失时,$0c
解码为inc c
。有效负载并不在乎c
。
- 删除
$0c
-这就是nop的目的。否则,将读取有效载荷的第一个字节作为跳转偏移量,并且程序将跳转到未初始化的内存中。
有效负载本身非常简单。我认为字符串的小尺寸使该方法比循环小,并且以这种方式使位置无关更加容易。将e
在beep
重复的,因此我可以剃去一个ld a
。同时,由于之间的所有内存$0038
和$8000
归零,我可以告吹,并使用更短rst
的变体call
指令,该指令只适用于$0
,$8
,$10
等等,最多$38
。
较旧的方法
64字节
00000000: 2e3f 3f2e 3f3f 7e7e a7a7 201f 1e2b 2b1e .??.??~~.. ..++.
00000010: 2b2b 6b00 7ea7 2814 003e 62cd 0080 3e65 ++k.~.(..>b...>e
00000020: cd00 80cd 0080 3e70 cd00 8076 003e 62cd ......>p...v.>b.
00000030: 0080 3e65 cd00 80cd 0080 3e70 cd00 8076 ..>e......>p...v
58个字节
00000000: 2e39 392e 3939 7e7e a7a7 2019 3a25 00a7 .99.99~~.. .:%..
00000010: 2814 003e 62cd 0080 3e65 cd00 80cd 0080 (..>b...>e......
00000020: 3e70 cd00 8076 003e 62cd 0080 3e65 cd00 >p...v.>b...>e..
00000030: 80cd 0080 3e70 cd00 8076 ....>p...v
53个字节
这个在编辑历史中有一个解释,但是并没有太大的区别。
00000000: 3a34 00a7 a720 193a 2000 a728 1400 3e62 :4... .: ..(..>b
00000010: cd00 803e 65cd 0080 cd00 803e 70cd 0080 ...>e......>p...
00000020: 7600 3e62 cd00 803e 65cd 0080 cd00 803e v.>b...>e......>
00000030: 70cd 0080 76 p...v
如果发生以下情况,该怎么办:任何非空输出都可以,而不是发出哔声
1个字节
v
halt
s通常是该程序,但是如果辐射将其删除,则内存将充满零,从而$8000
执行无限次,并打印很多空字节。