x86 32位机器码功能,21字节
x86-64机器码功能,22字节
以32位模式保存1B时,需要使用分隔符= filler-1,例如fill=0
和sep=/
。22字节版本可以使用分隔符和填充符的任意选择。
这是21字节的版本,其中输入分隔符= \n
(0xa),输出填充0
符=,输出分隔符= /
=填充符-1。这些常数可以轻松更改。
; see the source for more comments
; RDI points to the output buffer, RSI points to the src string
; EDX holds the base
; This is the 32-bit version.
; The 64-bit version is the same, but the DEC is one byte longer (or we can just mov al,output_separator)
08048080 <str_exp>:
8048080: 6a 01 push 0x1
8048082: 59 pop ecx ; ecx = 1 = base**0
8048083: ac lods al,BYTE PTR ds:[esi] ; skip the first char so we don't do too many multiplies
; read an input row and accumulate base**n as we go.
08048084 <str_exp.read_bar>:
8048084: 0f af ca imul ecx,edx ; accumulate the exponential
8048087: ac lods al,BYTE PTR ds:[esi]
8048088: 3c 0a cmp al,0xa ; input_separator = newline
804808a: 77 f8 ja 8048084 <str_exp.read_bar>
; AL = separator or terminator
; flags = below (CF=1) or equal (ZF=1). Equal also implies CF=0 in this case.
; store the output row
804808c: b0 30 mov al,0x30 ; output_filler
804808e: f3 aa rep stos BYTE PTR es:[edi],al ; ecx bytes of filler
8048090: 48 dec eax ; mov al,output_separator
8048091: aa stos BYTE PTR es:[edi],al ;append delim
; CF still set from the inner loop, even after DEC clobbers the other flags
8048092: 73 ec jnc 8048080 <str_exp> ; new row if this is a separator, not terminator
8048094: c3 ret
08048095 <end_of_function>
; 0x95 - 0x80 = 0x15 = 21 bytes
使用2字节DEC或a的64位版本要长1字节mov al, output_separator
。除此之外,两个版本的机器代码都相同,但是某些寄存器名称发生了变化(例如,rcx
而不是ecx
中的pop
)。
运行测试程序(基于3)的样本输出:
$ ./string-exponential $'.\n..\n...\n....' $(seq 3);echo
000/000000000/000000000000000000000000000/000000000000000000000000000000000000000000000000000000000000000000000000000000000/
算法:
循环输入,exp *= base
处理每个填充符。在定界符和终止的零字节上,将exp
填充符字节附加在后面,然后将分隔符附加到输出字符串,然后重置为exp=1
。确保输入不以换行符和终止符结尾非常方便。
输入时,分隔符上方的任何字节值(无符号比较)均视为填充符,分隔符下方的任何字节值均视为字符串结尾标记。(显式检查零字节将对test al,al
内部循环设置的标志进行额外的分支。)
规则仅在尾随换行符时才允许尾随分隔符。我的实现总是附加分隔符。 要在32位模式下保存1B,该规则要求分隔符= 0xa('\n'
ASCII LF =换行符),填充符= 0xb('\v'
ASCII VT =垂直制表符)。 那不是很人性化,但是满足法律要求。(您可以进行十六进制转储或
tr $'\v' x
输出以验证其是否有效,或更改常数以使输出分隔符和填充符可打印。我还注意到规则似乎要求其接受与用于输出的填充/分隔符相同的输入。 ,但我认为违反该规则不会有任何收获。)
NASM / YASM源。使用%if
测试程序附带的内容构建为32位或64位代码,或者仅将rcx更改为ecx。
input_separator equ 0xa ; `\n` in NASM syntax, but YASM doesn't do C-style escapes
output_filler equ '0' ; For strict rules-compliance, needs to be input_separator+1
output_separator equ output_filler-1 ; saves 1B in 32-bit vs. an arbitrary choice
;; Using output_filler+1 is also possible, but isn't compatible with using the same filler and separator for input and output.
global str_exp
str_exp: ; void str_exp(char *out /*rdi*/, const char *src /*rsi*/,
; unsigned base /*edx*/);
.new_row:
push 1
pop rcx ; ecx=1 = base**0
lodsb ; Skip the first char, since we multiply for the separator
.read_bar:
imul ecx, edx ; accumulate the exponential
lodsb
cmp al, input_separator
ja .read_bar ; anything > separator is treated as filler
; AL = separator or terminator
; flags = below (CF=1) or equal (ZF=1). Equal also implies CF=0, since x-x doesn't produce carry.
mov al, output_filler
rep stosb ; append ecx bytes of filler to the output string
%if output_separator == output_filler-1
dec eax ; saves 1B in the 32-bit version. Use dec even in 64-bit for easier testing
%else
mov al, output_separator
%endif
stosb ; append the delimiter
; CF is still set from the .read_bar loop, even if DEC clobbered the other flags
; JNC/JNB here is equivalent to JE on the original flags, because we can only be here if the char was below-or-equal the separator
jnc .new_row ; separator means more rows, else it's a terminator
; (f+s)+f+ full-match guarantees that the input doesn't end with separator + terminator
ret
该函数遵循x86-64 SystemV ABI,带有签名
void str_exp(char *out /*rdi*/, const char *src /*rsi*/, unsigned base /*edx*/);
它仅在输出字符串的末尾保留一个指针,以告知输出字符串的长度rdi
,因此您可以将其视为非字符串的返回值。 -标准的调用约定。
xchg eax,edi
以eax或rax返回端点指针将花费1或2个字节()。(如果使用x32 ABI,则保证指针只能是32位,否则xchg rax,rdi
,在调用者将指针传递到低32位之外的缓冲区的情况下,我们必须使用它。)发布,因为有一些解决方法可以在不从中获取值的情况下调用方使用rdi
,因此您可以在不使用包装的情况下从C进行调用。
我们甚至不对输出字符串或其他任何内容进行null终止,因此仅以换行符终止。需要花费2个字节来解决:(xchg eax,ecx / stosb
rcx从开始为零rep stosb
)。
找出输出字符串长度的方法是:
- rdi指向返回时字符串的最后一位(因此调用方可以执行len = end-start)
- 调用方可以知道输入中有多少行并计算换行符
- 调用者可以使用较大的归零缓冲区,
strlen()
然后再使用。
它们不是很漂亮或效率很高(除了使用来自asm调用者的RDI返回值),但是如果需要,则不要从C调用golfed asm函数。
大小/范围限制
最大输出字符串大小仅受虚拟内存地址空间限制。(主要是当前的x86-64硬件仅支持虚拟地址中的48个有效位,将它们分成两半,因为它们进行符号扩展而不是零扩展。请参见链接的答案中的图。)
每行最多只能有2 ** 32-1个填充字节,因为我将指数存储在32位寄存器中。
该函数适用于从0到2 ** 32-1的基数。(基数0的正确是0 ^ x = 0,即只是空行,没有填充字节。基数1的正确是1 ^ x = 1,因此始终每行1个填充符。)
在Intel IvyBridge和更高版本上,它的运行速度也非常快,尤其是对于将大行写入对齐内存的情况。 rep stosb
是具有memset()
ERMSB功能的CPU上具有对齐指针的大量计数器的最佳实现。例如180 ** 4为0.97GB,在我的i7-6700k Skylake(带有〜256k个软页面错误)上需要0.27秒的时间写入/ dev / null。(在Linux上,用于/ dev / null的设备驱动程序不会将数据复制到任何地方,它只会返回。因此,所有时间都在rep stosb
和中,而软页面错误则是在首次触摸内存时触发的。它是不幸的是,没有对BSS中的阵列使用透明的大页面。可能madvise()
系统调用会加快它的速度。)
测试程序:
生成一个静态二进制文件并按./string-exponential $'#\n##\n###' $(seq 2)
基本2 运行。为避免实现atoi
,它使用base = argc-2
。(命令行长度限制阻止测试可笑的大基数。)
该包装器适用于最大1 GB的输出字符串。(即使对于巨大的字符串,它也只进行单个write()系统调用,但是Linux甚至为写入管道也支持此调用)。要计算字符,请通过管道wc -c
或使用strace ./foo ... > /dev/null
来查看写入系统调用的arg。
这利用RDI返回值将字符串长度计算为的arg write()
。
;;; Test program that calls it
;;; Assembles correctly for either x86-64 or i386, using the following %if stuff.
;;; This block of macro-stuff also lets us build the function itself as 32 or 64-bit with no source changes.
%ifidn __OUTPUT_FORMAT__, elf64
%define CPUMODE 64
%define STACKWIDTH 8 ; push / pop 8 bytes
%define PTRWIDTH 8
%elifidn __OUTPUT_FORMAT__, elfx32
%define CPUMODE 64
%define STACKWIDTH 8 ; push / pop 8 bytes
%define PTRWIDTH 4
%else
%define CPUMODE 32
%define STACKWIDTH 4 ; push / pop 4 bytes
%define PTRWIDTH 4
%define rcx ecx ; Use the 32-bit names everywhere, even in addressing modes and push/pop, for 32-bit code
%define rsi esi
%define rdi edi
%define rsp esp
%endif
global _start
_start:
mov rsi, [rsp+PTRWIDTH + PTRWIDTH*1] ; rsi = argv[1]
mov edx, [rsp] ; base = argc
sub edx, 2 ; base = argc-2 (so it's possible to test base=0 and base=1, and so ./foo $'xxx\nxx\nx' $(seq 2) has the actual base in the arg to seq)
mov edi, outbuf ; output buffer. static data is in the low 2G of address space, so 32-bit mov is fine. This part isn't golfed, though
call str_exp ; str_exp(outbuf, argv[1], argc-2)
; leaves RDI pointing to one-past-the-end of the string
mov esi, outbuf
mov edx, edi
sub edx, esi ; length = end - start
%if CPUMODE == 64 ; use the x86-64 ABI
mov edi, 1 ; fd=1 (stdout)
mov eax, 1 ; SYS_write (Linux x86-64 ABI, from /usr/include/asm/unistd_64.h)
syscall ; write(1, outbuf, length);
xor edi,edi
mov eax,231 ; exit_group(0)
syscall
%else ; Use the i386 32-bit ABI (with legacy int 0x80 instead of sysenter for convenience)
mov ebx, 1
mov eax, 4 ; SYS_write (Linux i386 ABI, from /usr/include/asm/unistd_32.h)
mov ecx, esi ; outbuf
; 3rd arg goes in edx for both ABIs, conveniently enough
int 0x80 ; write(1, outbuf, length)
xor ebx,ebx
mov eax, 1
int 0x80 ; 32-bit ABI _exit(0)
%endif
section .bss
align 2*1024*1024 ; hugepage alignment (32-bit uses 4M hugepages, but whatever)
outbuf: resb 1024*1024*1024 * 1
; 2GB of code+data is the limit for the default 64-bit code model.
; But with -m32, a 2GB bss doesn't get mapped, so we segfault. 1GB is plenty anyway.
这是一个有趣的挑战,非常适合于asm,尤其是x86 string ops。很好地设计了规则,以避免必须先处理换行符,然后再处理输入字符串末尾的终止符。
具有重复乘法的指数就像具有重复加法的乘法一样,无论如何,我都需要循环以计算每个输入行中的字符数。
我考虑使用单操作数mul
或imul
代替更长的操作数imul r,r
,但是其隐式使用EAX会与LODSB冲突。
我还尝试了SCASB而不是load and compare,但是我需要xchg esi,edi
在内循环之前和之后,因为SCASB和STOSB都使用EDI。(因此,64位版本必须使用x32 ABI以避免截断64位指针)。
避免使用STOSB并不是一种选择。没有什么比这更短了。使用SCASB的一半好处是在离开内循环之后AL = filler,因此我们不需要为REP STOSB进行任何设置。
SCASB与我一直在做的事情相反,因此我需要颠倒比较。
我最好的尝试与xchg和scasb。可行,但并不短。(32位代码,使用inc
/ dec
技巧将填充符更改为分隔符)。
; SCASB version, 24 bytes. Also experimenting with a different loop structure for the inner loop, but all these ideas are break-even at best
; Using separator = filler+1 instead of filler-1 was necessary to distinguish separator from terminator from just CF.
input_filler equ '.' ; bytes below this -> terminator. Bytes above this -> separator
output_filler equ input_filler ; implicit
output_separator equ input_filler+1 ; ('/') implicit
8048080: 89 d1 mov ecx,edx ; ecx=base**1
8048082: b0 2e mov al,0x2e ; input_filler= .
8048084: 87 fe xchg esi,edi
8048086: ae scas al,BYTE PTR es:[edi]
08048087 <str_exp.read_bar>:
8048087: ae scas al,BYTE PTR es:[edi]
8048088: 75 05 jne 804808f <str_exp.bar_end>
804808a: 0f af ca imul ecx,edx ; exit the loop before multiplying for non-filler
804808d: eb f8 jmp 8048087 <str_exp.read_bar> ; The other loop structure (ending with the conditional) would work with SCASB, too. Just showing this for variety.
0804808f <str_exp.bar_end>:
; flags = below if CF=1 (filler<separator), above if CF=0 (filler<terminator)
; (CF=0 is the AE condition, but we can't be here on equal)
; So CF is enough info to distinguish separator from terminator if we clobber ZF with INC
; AL = input_filler = output_filler
804808f: 87 fe xchg esi,edi
8048091: f3 aa rep stos BYTE PTR es:[edi],al
8048093: 40 inc eax ; output_separator
8048094: aa stos BYTE PTR es:[edi],al
8048095: 72 e9 jc 8048080 <str_exp> ; CF is still set from the inner loop
8048097: c3 ret
对于的输入../.../.
,产生..../......../../
。我不会费心地显示带有eparator = newline的版本的十六进制转储。
"" <> "#"~Table~#
比短3个字节"#"~StringRepeat~#
,可能还可以打高尔夫球。