x86-32 machine code (Linux executable) 27 bytes
cut-down NASM listing: Address | machine code | source
global _start
_start: ; Linux processes (and thus static executables) start with registers zeroed, except ESP
asciitable:
00 B020 mov al, ' '
02 B20A mov dl, 0xa
04 89E7 mov edi, esp ; space above ESP on the stack is argc, argv[] and env[] array elements, and the env strings. We overwrite that.
.loop:
06 AA stosb
07 40 inc eax ; there is no branch condition for AF, the half-carry flag :/
08 A88F test al, 0x8f ; detect mod16 and when we've gone past printable
0A 7503 jnz .nonewline
0C 92 xchg eax, edx
0D AA stosb
0E 92 xchg eax, edx
.nonewline:
; FLAGS still set from earlier TEST
0F 79F5 jns .loop ; stop at 0x80, beyond what the syscall uses
; mov [edi-1], 0xa ; no trailing newline
11 B004 mov al, 4 ; __NR_write
; ebx=0 from process startup; writing to stdin happens to work on a terminal
13 89E1 mov ecx, esp
15 8D5060 lea edx, [eax-4 + 0x7f-0x20 + 5] ; exclusive range, 5 newlines; final line does not end with newline as whitespace isn't required
18 CD80 int 0x80 ; write(0, ecx, 100)
; mov eax,1 ; exit(ebx)
; int 0x80
1A CC int3 ; abort program
Fill bytes into a buffer (overwriting stack memory above ESP, starting with argc, argv[], and into env[]). We go a bit beyond 0x7e because that makes the stop condition cheaper, but those bytes aren't printed: we have to generate an explicit length in EDX anyway, so we just omit it.
We make one write(0, buf, 100)
system call. 0
is STDIN_FD, but on a normal xterm/konsole/gnome_terminal, FD 0,1, and 2 are all read+write duplicates of the same file description. So we save 1 byte for inc ebx
. If you want to pipe the output into something to hexdump for example, ./asciitable 0>&1 | hexdump -C
Instead of existing cleanly, we use int3
to raise a debug exception, resulting in the OS delivering SIGTRAP, killing the process. A typical shell will then print Trace/breakpoint trap (core dumped)
, but that's the shell, not this program. This saves 3 bytes vs. mov al, 1
/ int 0x80
(the upper bytes of EAX are zeroed, unless write
returned an error).
I considered letting execution just fall off the end (to probably 00 00
padding added by the linker, which decodes as add [eax], al
and will segfault), but we're already stretching things a bit by only counting the .text
section of the executable, not the whole file size. (Unlike a DOS .com
, there is metadata)
Our output doesn't end with a newline; as the question says, no trailing whitespace is required after the 0x7e
~
character.
The mod16 detection via checking the low bits 4 of AL is fairly straightforward, but was borrowed from ErikF's the x86-16 MS-DOS answer which I read first before thinking about how I'd do it.
But to enable a jns
as the loop branch without a separate cmp
, we also set the MSB in our mask for test
. That's not a spot we wanted a newline anyway. I'd hoped to be able to branch on FLAGS from inc al
(2 bytes), but since we'd also need something for newlines, this is even better: we can now use inc eax
(1 byte) and still branch on the MSB of AL. Using mov dl, 0xa (2B) / xchg/stosb/xchg (1B each) instead of mov byte [edi], 0xa
(3 bytes) / inc edi
(1 byte but clobbers FLAGS) was the key to that, saving another test
or cmp
for a net saving of 1 byte.
Demo:
$ nasm -felf32 asciitable.asm
$ ld -melf_i386 -o asciitable asciitable.o
$ ./asciitable
!"#$%&'()*+,-./
0123456789:;<=>?
@ABCDEFGHIJKLMNO
PQRSTUVWXYZ[\]^_
`abcdefghijklmno
pqrstuvwxyz{|}~Trace/breakpoint trap (core dumped)
$ ./asciitable 0>&1 | cat # suppresses signal diagnostic from shell
!"#$%&'()*+,-./
0123456789:;<=>?
@ABCDEFGHIJKLMNO
PQRSTUVWXYZ[\]^_
`abcdefghijklmno
pqrstuvwxyz{|}~peter@volta:/tmp$
$ ./asciitable 0>&1 | hexdump -C
00000000 20 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f | !"#$%&'()*+,-./|
00000010 0a 30 31 32 33 34 35 36 37 38 39 3a 3b 3c 3d 3e |.0123456789:;<=>|
00000020 3f 0a 40 41 42 43 44 45 46 47 48 49 4a 4b 4c 4d |?.@ABCDEFGHIJKLM|
00000030 4e 4f 0a 50 51 52 53 54 55 56 57 58 59 5a 5b 5c |NO.PQRSTUVWXYZ[\|
00000040 5d 5e 5f 0a 60 61 62 63 64 65 66 67 68 69 6a 6b |]^_.`abcdefghijk|
00000050 6c 6d 6e 6f 0a 70 71 72 73 74 75 76 77 78 79 7a |lmno.pqrstuvwxyz|
00000060 7b 7c 7d 7e |{|}~|
00000064
$ strace ./asciitable 0>/dev/null
execve("./asciitable", ["./asciitable"], 0x7ffc63d167c0 /* 55 vars */) = 0
[ Process PID=2670503 runs in 32 bit mode. ]
strace: WARNING: Proper structure decoding for this personality is not supported, please consider building strace with mpers support enabled.
write(0, " !\"#$%&'()*+,-./\n0123456789:;<=>"..., 100) = 100
--- SIGTRAP {si_signo=SIGTRAP, si_code=SI_KERNEL} ---
+++ killed by SIGTRAP (core dumped) +++
Trace/breakpoint trap (core dumped)
Try it online! with an inc ebx
added so it writes to stdout and thus shows up in the tio.run output pane. IDK why it didn't show in the debug pane. (32-bit code supported by having FASM make an executable directly, instead of NASM.)
TODO: a function instead of program, filling a buffer
We'd obviously save the syscall code, but could no longer write past the end of the actual stop point. So would need maybe 1 more byte in the loop to go back to cmp al, 0x7e
/ jne
as the loop condition.