Some ISAs have restrictions for some instructions, like older ARM has some limitations on using the same reg for a source and destination for umull. These hazards are always documented in the instruction-set reference manual1.
Intel and AMD's x86 manuals don't document any such limitation (except for push sp on 8086 / 1862), so you can always assume that an x86-64 instruction reads all its input operands before writing any output operands, exactly like you'd expect. (That's the normal state of affairs for the internal pipeline in CPUs anyway.)
Footnote 1: for example, Keil's ISA reference for UMULL{S}{cond} RdLo, RdHi, Rn, Rm says:
Rn must be different from RdLo and RdHi in architectures before ARMv6.
(The instruction does a full-multiply, like x86 mul, but with an explicit pair of outputs instead of EDX:EAX, and with 2 explicit sources.)
Weird restrictions, if any exist for an ISA, usually involve multiply; for example MIPS also plenty of weirdness around multu and its HI:LO register pair. This old MIPS R3000 manual explains that a multu right after an mflo could still start even if an interrupt happened, resulting in corrupt state after resuming from an interrupt. You need to separate reading one multiply result back into normal regs by 2 instructions before starting another multiply. See also Raymond Chen's blog.
These software "hazards" are the same kind of concept as https://en.wikipedia.org/wiki/Hazard_(computer_architecture), but they're cases the hardware doesn't handle, rather than cases that out-of-order exec needs to detect.
x86 doesn't have any of these software hazards; I only mention other ISAs for comparison and as a point of interest.
Footnote 2:
Early x86 CPUs were microcoded, not pipelined, and the microcode implementation of push actually exposed a weird effect on the earliest x86 CPUs. See the pre-286 note in Intel's manual entry for push:
For IA-32 processors from the Intel 286 on, the PUSH ESP instruction pushes the value of the ESP register as it existed before the instruction was executed. (This is also true for Intel 64 architecture, real-address and virtual-8086 modes of IA-32 architecture.) For the Intel® 8086 processor, the PUSH SP instruction pushes the new value of the SP register (that is the value after it has been decremented by 2).
But for 286 and later (including all x86-64 CPUs), it works as if all operands are read before any are written, including for push rsp or push [rsp + 8].
The PUSH ESP instruction pushes the value of the ESP register as it existed before the instruction was executed. If a PUSH instruction uses a memory operand in which the ESP register is used for computing the operand address, the address of the operand is computed before the ESP register is decremented.
pop also has to document the fact that pop rsp does the stack pointer decrement first, before writing the load result into the register. So pop rsp = mov rsp, [rsp], without add rsp, 8 after. (This is described in the text. Intel's pseudocode doesn't use a temporary, so it doesn't correctly describe that special case.)
And BTW, you can find lots of examples of instructions like that in compiler output. e.g. when the source is a memory operand with a register addressing mode, you're pointer-chasing through a linked list or tree (if this is in a loop, otherwise you're just reusing a register as you wade through levels of indirection).
int foo(int ****p) {
int tmp = ****p;
return tmp * 2;
}
Compiled by clang 10.1 -O3 for x86-64:
foo(int****):
mov rax, qword ptr [rdi]
mov rax, qword ptr [rax]
mov rax, qword ptr [rax] # write-only destination
mov eax, dword ptr [rax]
add eax, eax # EAX += EAX, read-write destination
ret