Branch differently in x86 / x86-64 using only printable visible ASCII characters in the machine code

7 bytes

0000000: 6641 2521 2173 21                        fA%!!s!

As 32 bit

00000000  6641              inc cx
00000002  2521217321        and eax,0x21732121

As 64 bit

00000000  6641252121        and ax,0x2121
00000005  7321              jnc 0x28

and clears the carry flag so the 64 bit version always jumps. For 64-bit the 6641 is the operand size override followed by rex.b so the operand size for the and comes out as 16 bit. On 32-bit the 6641 is a complete instruction so the and has no prefix and has a 32-bit operand size. This changes the number of immediate bytes consumed by the and giving two bytes of instructions that are only executed in 64-bit mode.


11 bytes

ascii: j6Xj3AX,3t!
hex: 6a 36 58 6a 33 41 58 2c 33 74 21

Uses the fact that in 32-bit, 0x41 is just inc %ecx, whereas in 64-bit it is the rax prefix that modifies the target register of the following pop instruction.

        .globl _check64
_check64:
        .byte   0x6a, 0x36      # push $0x36
        .byte   0x58            # pop %rax
        .byte   0x6a, 0x33      # push $0x33

        # this is either "inc %ecx; pop %eax" in 32-bit, or "pop %r8" in 64-bit.
        # so in 32-bit it sets eax to 0x33, in 64-bit it leaves rax unchanged at 0x36.
        .byte   0x41            # 32: "inc %ecx", 64: "rax prefix"
        .byte   0x58            # 32: "pop %eax", 64: "pop %r8"

        .byte   0x2c, 0x33      # sub $0x33,%al
        .byte   0x74, 0x21      # je (branches if 32 bit)

        mov     $1,%eax
        ret

        nop
        nop
        nop
        nop
        nop
        nop
        nop
        nop
        nop
        nop
        nop
        nop
        nop
        nop
        nop
        nop
        nop
        nop
        nop
        nop
        nop
        nop
        nop
        nop
        nop
        nop
        nop
        mov     $0,%eax
        ret

Wrote this on OSX, your assembler might be different.

Call it with this:

#include <stdio.h>
extern int check64(void);
int main(int argc, char *argv[]) {
  if (check64()) {
    printf("64-bit\n");
  } else {
    printf("32-bit\n");
  }
  return 0;
}

7 bytes

Not relying on 66 prefix.

$$@$Au!

32-bit:

00000000 24 24 and al,24h
00000002 40    inc eax
00000003 24 41 and al,41h
00000005 75 21 jne 00000028h

AL will have bit 0 set after the INC, the second AND will preserve it, the branch will be taken.

64-bit:

00000000 24 24    and al,24h
00000002 40 24 41 and al,41h
00000005 75 21    jne 00000028h

AL will have bit 0 clear after the first AND, the branch will not be taken.