x86 machine code, 18 bytes
dst=EDI, src=ESI, byte count = ECX. Copies src to dst either forward or reversed.
The only ASCII upper-case byte in the machine code is the 0x71
'q'
, the opcode for jno
, which is always taken (because it follows an xor-zeroing). When lowercased, it becomes 0x51 'Q'
, a push ecx
, which is balanced by a pop ecx
in the fall-through path before reaching the loop.
http://ref.x86asm.net/coder32.html#x71 is a useful opcode map.
Conveniently, most of the jcc
conditional branch opcodes are in the upper-case range.
rev:
xor edx, edx
%if 1
jno .loop ; 71 08 lower case: straight copy
%else
db 0x51, 0x08 ; JNO becomes 0x51 push ecx, leaving the displacement as an opcode
%endif
db 0xd1 ; ModRM for the uppercase case, where 08 is an OR opcode
.fallthrough:
db 0x59 ;pop ecx (or RCX in 64-bit mode on TIO)
; nop ; jump displacement needs to be 8, not 7 POP ES which will fault
; add edi, ecx
; dec edi ; so might as well use a longer single instruction
lea edi, [edi + ecx - 1] ; point to *last* byte of destination
mov dl, 2
.loop:
movsb ; *edi++ = *esi++
sub edi, edx ; 0 for copy forwards, 2 for copy backwards
loop .loop
ret
Try it online! with a _start
that passes buffers and write()s the result.
The code in the fall-through path is 1 byte longer than it needs to be on its own: that makes the jump displacement 08
instead of 07
, because opcode 07
is pop es
. That segfaults on many values we could push.
And 07
is not valid in 64-bit mode (illegal instruction), otherwise we could possibly arrange a jump that turns into a push
of the just-zeroed register, allowing us to pop the null selector into ES. Setting ES to 0 in 32-bit user-space will fault, or lead to movsb
faulting. (Even under a 64-bit kernel where 64-bit user-space does run with ds=es=0 null selectors.)
Disassembly of the uppercased version:
08049000 <rev>:
8049000: 31 d2 xor edx,edx
8049002: 51 push ecx
8049003: 08 d1 or cl,dl # c |= 0 leaves ECX unchanged
08049005 <rev.fallthrough>:
8049005: 59 pop ecx
8049006: 8d 7c 0f ff lea edi,[edi+ecx*1-0x1]
804900a: b2 02 mov dl,0x2
0804900c <rev.loop>:
804900c: a4 movs BYTE PTR es:[edi],BYTE PTR ds:[esi]
804900d: 29 d7 sub edi,edx
804900f: e2 fb loop 804900c <rev.loop>
8049011: c3 ret
(lower-case ASCII ranges from 'a'
0x61 to 'z'
(0x7a
). The 7C ModRM for the LEA is just beyond that. 59
pop ecx is already upper case. If there was a case-flip instead of just upper-case version of this challenge, we could make it 0x79. That byte isn't executed in the upper-case version: jno
jumps over it.
The lower-case version is:
08049000 <rev>:
8049000: 31 d2 xor edx,edx
8049002: 71 08 jno 804900c <rev.loop>
8049004: d1 .byte 0xd1
08049005 <rev.fallthrough>: # having this label here syncs disassembly after the stray byte above
8049005: 59 pop ecx
8049006: 8d 7c 0f ff lea edi,[edi+ecx*1-0x1]
804900a: b2 02 mov dl,0x2
0804900c <rev.loop>:
804900c: a4 movs BYTE PTR es:[edi],BYTE PTR ds:[esi]
804900d: 29 d7 sub edi,edx
804900f: e2 fb loop 804900c <rev.loop>
8049011: c3 ret
heLLO
would becomeHEllo
. Although this question would still be way too hard \$\endgroup\$