x86-64机器码功能,30字节。
使用与@Level River St的C答案相同的递归逻辑。(最大递归深度= 100)
使用puts(3)
libc中的函数,无论如何,该函数都链接到普通可执行文件。它可以使用x86-64 System V ABI来调用,即从Linux或OS X上的C语言调用,并且不会破坏不应有的任何寄存器。
objdump -drwC -Mintel
输出,带有解释的评论
0000000000400340 <g>: ## wrapper function
400340: 6a 64 push 0x64
400342: 5f pop rdi ; mov edi, 100 in 3 bytes instead of 5
; tailcall f by falling into it.
0000000000400343 <f>: ## the recursive function
400343: ff cf dec edi
400345: 97 xchg edi,eax
400346: 6a 0a push 0xa
400348: 5f pop rdi ; mov edi, 10
400349: 0f 8c d1 ff ff ff jl 400320 <putchar> # conditional tailcall
; if we don't tailcall, then eax=--n = arg for next recursion depth, and edi = 10 = '\n'
40034f: 89 f9 mov ecx,edi ; loop count = the ASCII code for newline; saves us one byte
0000000000400351 <f.loop>:
400351: 50 push rax ; save local state
400352: 51 push rcx
400353: 97 xchg edi,eax ; arg goes in rdi
400354: e8 ea ff ff ff call 400343 <f>
400359: 59 pop rcx ; and restore it after recursing
40035a: 58 pop rax
40035b: e2 f4 loop 400351 <f.loop>
40035d: c3 ret
# the function ends here
000000000040035e <_start>:
0x040035e - 0x0400340 = 30 bytes
# not counted: a caller that passes argc-1 to f() instead of calling g
000000000040035e <_start>:
40035e: 8b 3c 24 mov edi,DWORD PTR [rsp]
400361: ff cf dec edi
400363: e8 db ff ff ff call 400343 <f>
400368: e8 c3 ff ff ff call 400330 <exit@plt> # flush I/O buffers, which the _exit system call (eax=60) doesn't do.
内置 yasm -felf64 -Worphan-labels -gdwarf2 golf-googol.asm &&
gcc -nostartfiles -o golf-googol golf-googol.o
。我可以发布原始的NASM源代码,但是看起来似乎很混乱,因为反汇编中就存在asm指令。
putchar@plt
距离不到128个字节jl
,因此我可以使用2字节的短跳转而不是6字节的近跳转,但这仅适用于小型可执行文件,而不是较大程序的一部分。因此,如果我还利用简短的jcc编码来实现它,那么我认为没有理由不计算libc的puts实现的大小。
每个递归级别都使用24B的堆栈空间(两次压入和CALL压入返回地址)。每隔一个深度调用一次putchar
,堆栈仅对齐8,而不是16,因此这确实违反了ABI。使用对齐存储将xmm寄存器溢出到堆栈的stdio实现可能会出错。但是glibc putchar
不会这样做,而是使用完全缓冲写入管道或使用行缓冲写入终端。在Ubuntu 15.10上测试。可以使用中的虚拟push / pop来解决此问题.loop
,以在递归调用之前使堆栈再偏移8。
证明可以打印正确数量的换行符:
# with a version that uses argc-1 (i.e. the shell's $i) instead of a fixed 100
$ for i in {0..8}; do echo -n "$i: "; ./golf-googol $(seq $i) |wc -c; done
0: 1
1: 10
2: 100
3: 1000
4: 10000
5: 100000
6: 1000000
7: 10000000
8: 100000000
... output = 10^n newlines every time.
我的第一个版本是43B,用于puts()
9个换行符(以及一个终止的0字节)的缓冲区,因此看跌期权将附加在第10个之后。递归的基本情况更接近于C的灵感。
以不同的方式分解10 ^ 100可能会缩短缓冲区,也许减少到4个换行符,节省5个字节,但是到目前为止使用putchar更好。它只需要一个整数arg,不需要指针,也根本不需要缓冲区。C标准允许在其中将其作为宏的实现putc(val, stdout)
,但是在glibc中,它作为可从asm调用的真实函数存在。
每个调用仅打印一个换行符而不是10,这意味着我们需要将递归最大深度增加1,以获得另一个10换行符的因子。由于99和100都可以用符号扩展的8位立即数表示,push 100
因此仍然只有2个字节。
更好的是,10
在寄存器中用作换行符和循环计数器,可以节省一个字节。
保存字节的想法
32位版本可以为节省一个字节dec edi
,但是stack-args调用约定(用于putchar之类的库函数)使尾部调用工作变得不那么容易,并且可能在更多地方需要更多字节。我可以使用private的register-arg约定f()
,仅由调用g()
,但是我不能尾调用putchar(因为f()和putchar()会使用不同数量的stack-args)。
f()可以保留调用者的状态,而不是在调用者中进行保存/恢复。但是,这可能很糟糕,因为它可能需要在分支的每一侧分别获取,并且与尾调用不兼容。我尝试过,但没有发现任何节省。
在堆栈上保留循环计数器(而不是在循环中推送/弹出rcx)也没有帮助。使用puts的版本差了1B,而使用该版本更便宜地设置rcx的损失可能更大。