includes 3 different answers, smallest first
x86-64 machine code "function", 4 bytes
("works" in all 3 modes: 16-bit, 32-bit, and 64-bit. In other modes, FE 00 is a jmp to eax or ax.)
401000: 0f 31 rdtsc # EDX:EAX = timestamp counter
401002: ff e0 jmp rax # "return" with jmp to register
This function can be called with
jmp instead of
call; it doesn't need you to pass it a return address on the stack. It uses the low 32 bits of the time counter as a jump target, which might or might not be the correct return address (or somewhere else useful).
The crash possibility is code-fetch from an unmapped or non-executable page, or jumping to instructions that fault (e.g.
00 00 add [rax],al), or to an illegal instruction, like a
1F or other byte somewhere in 64-bit mode, or a multi-byte illegal sequence in 16 or 32-bit mode, that will raise #UD.
RDTSC sets EDX:EAX = the number of reference cycles since power-on (i.e. the TSC = TimeStamp Counter, SO canonical Q&A about it. Note that it doesn't count core clock cycles on modern x86). The reference frequency is normally close to the CPU's sticker frequency (e.g. 4008MHz on a nominally 4GHz i7-6700k) so the low 32 bits wraps around in just over 1 second, which is close enough to random for interactive use. Or every few seconds on chips with lower "base" frequencies.
Assuming a valid return address or other jump target exists in the low 32 bits of virtual address space, we have a
2^32-1 chance of reaching it. Or higher if there are multiple useful targets to dispatch to. (Assuming TSC is uniformly distributed, and fine-grained enough that every 32-bit low half is actually possible. I think this is the case.)
In 32 and 16-bit mode, every possible address (in the same code segment) is reachable, but 64-bit mode unfortunately still splits the TSC between EDX and EAX so most of the 64-bit (or 48-bit) address space is unreachable.
On systems like MacOS where 64-bit processes normally have all their code outside the low 4GiB of address space, use 32-bit mode. Linux non-PIE executables are mapped in the low 2GiB of virtual address space so any non-library code will be reachable.
x86 32-bit machine code function, 5 bytes
8049000: 0f 31 rdtsc # EDX:EAX = timestamp counter
8049002: 40 inc eax # EAX++
8049003: ce into # trap if OF==1
8049004: c3 ret
On most x86 CPUs, the TSC is fine-grained and really can be any value in the low half, including 231-1. So incrementing it can produce signed integer overflow, setting OF.
Also works in 16-bit mode (incrementing only AX with this machine code), but not 64-bit mode where
into isn't a valid opcode.
x86-64 machine code function, 6 bytes
(same machine code works in all 3 modes, using the default operand size for the mode; 16, 32, and 32.)
divides 64-bit user input by a random number: can overflow or divide by 0.
0000000000401000 <divrandom>: # input in EDX and EAX
401000: 0f c7 f1 rdrand ecx
401003: f7 f1 div ecx # return EDX:EAX / ECX
401005: c3 ret
Yup, x86 has a true RNG built in (Intel since IvyBridge, and AMD since at least Zen).
x86 division of 64-bit EDX:EAX / 32-bit ECX => 32-bit quotient and remainder faults (with a #DE exception -> SIGFPE or other OS signal) if the quotient doesn't fit in 32-bit EAX. With a small dividend, this can only happen on divisor = 0, 1 chance in 2^32.
With function input in EDX:EAX above 2^32-1, small divisors could leave a quotient larger than 2^32-1. So the chance of faulting depends on the input value. Specifically, division runs without faulting if ECX > EDX, where ECX is the random divisor and EDX is the high half of the 64-bit input.
rdrand always sets OF to 0 so we can't use 1-byte
into conditionally trap on overflow. (It only sets CF = success, 0 means HW RNG temporarily exhausted).
I can't think of any "unpredictable / undefined behaviour" situation that could actually give different results on different runs, other than meltdown-style timing that depends on microarchitectural conditions.
Some old ARM and MIPS CPUs have unpredictable behaviour that depends on timing if you for example use a multiply where the destination is one of the inputs, or on MIPS I read the result of a load in the next instruction (in the load delay slot). So for example on MIPS
lw $ra, ($a0) ;
jr $ra (4 bytes each) might use the original return address in
$ra (the link register) if the load hits in cache, otherwise it stalls and we'd return to wherever the load points.