Every claim in the next four concepts is quoted out of a disassembly, and the single most common way to be wrong about one is to read two of them written in two different syntaxes and assume they are the same instruction. They are not, and the difference is one line of code that no tool will warn you about.
$ as -o /dev/null <<'EOF' && objdump -d -M intel /dev/null .text x: addps %xmm1, %xmm0 EOF 0: 0f 58 c1 addps xmm0,xmm1
Those are the same three bytes. The first line is AT&T syntax, the second is Intel syntax, and between them the operands are reversed. Read the AT&T line as “add xmm0 by xmm1, and put the result in xmm0” and you have it right. Read the Intel line as “add xmm0 by xmm1” and you have computed the wrong thing, in a language where addps xmm0, xmm1 really does mean xmm0 = xmm0 + xmm1.
It is worse than a confusion, because the two syntaxes are both correct and a document that mixes them is a document that is wrong in a way that disassembles. The trap is not exotic either: a search for “x86 lea example” returns AT&T lines, a search for “x86 movaps example” returns Intel lines, and they land in the same editor.
There is a single rule that generates both syntaxes, and once you have it the two are not two things to remember but one thing seen from two sides.
Which one is the default flipped for, and it is a toolchain decision rather than an architectural one, which is the point worth keeping. GNU as is AT&T. GNU objdump is Intel. So the assembler and the disassembler that ship in the same binutils tarball speak different languages about the same bytes, and that is why a beginner's first disassembly looks like the assembler has been tampered with. It has not; you are looking at the same three bytes printed by a program that is trying to be helpful to a different audience.
Two prefixes that get mistaken for syntax, and are not:
66 is an operand-size override and it is not decoration: it is the difference between addps and addpd, between mov ax, al and mov eax, al, and it is also a VEX byte's pp field wearing its old costume. The two are the same two bits. The REX concept has the other end of this.lock is a prefix that is not a mode, and the assembler refuses it in a way worth reading closely:$ echo '.text x: lock addq $1, %rax' > /tmp/q.s && as -o /tmp/q.o /tmp/q.s /tmp/q.s: Error: expecting lockable instruction after `lock' $ echo '.text x: lock addq $1, (%rax)' > /tmp/q.s && as -o /tmp/q.o /tmp/q.s && objdump -d /tmp/q.o 0: f0 48 83 00 01 lock add QWORD PTR [rax],0x1
The error names the prefix rather than the reason, and the reason is one clause long: LOCK is defined only for a memory destination, because it makes the read-modify-write atomic in memory and a register has no memory to be atomic about. The message costs the reader a trip to the manual. The bytes are in the corpus for exactly this reason — int_lock_add and int_lock_add_plain are one f0 apart and both are in corpus.s.
The course ships two corpora. One is hand-written: corpus.s, 52 instructions, every one of them chosen because some claim in these pages depends on its encoding. The other is a compiler's: corpus.c, compiled at -O2 and disassembled by the build script. A claim about what a compiler did is a claim about bytes, and the only way to check one is to read them.
$ sed -n '5,20p' corpus_c.txt
shift_left | 48 89 fa 8b f0 89 d6 | mov rdx,rdi
| c1 e2 08 | shl edx,0x8
| 39 f7 | cmp esi,edi
shift_by_one | 48 d1 e2 | shl rdx,1
pick | 4c 0f 4c e1 0f | cmovl r12,r13
bump | 0f b6 00 48 0f be 00 | movzx rax,BYTE PTR [rax]
| 48 83 c0 01 | add rax,0x1
| 88 00 | mov BYTE PTR [rax],al
Four things in eight lines that a reader of the C would not have predicted, and every one of them is a fact about the encoding rather than about the algorithm:
a << b became shl edx, 0x8 — a count in the instruction. The variable form shl %cl, %rdx is one byte longer and needs a register to be free, so the compiler used the immediate where the count was a constant. The next concept measures both and finds the immediate form is not the cheap one, which is a fact about this processor and about nothing in the manual.a << 1 became shl rdx, 1 and 48 d1 e2 became shl rdx,1. The d1 form encodes “shift by one” in the opcode byte with no immediate at all — a third encoding of the same operation, and the reason the opcode space has a hole where d0–d3 are.x < y ? p : q became cmovl. Not a branch. The next-next concept measures that choice and finds that on this machine, in a loop the predictor can learn, the branch was the cheaper of the two — and explains why that is not a contradiction.p[0] = p[0] + 1 on a char * became movzx rax, BYTE PTR [rax] … mov BYTE PTR [rax], al. A 64-bit load of an 8-bit quantity, because the C promotion is to int and int is 32 bits and the compiler chose to carry it in a 64-bit register. Every extension in that sequence is a decision, and the next concept is the reference for what each of them costs and what each of them means when you get the width wrong.Take the row that appears in every disassembly of every compiled program ever written, and read it byte by byte. This is sub %ebx, %eax, which the artifact's corpus records as int_sub with the bytes 29 d8.
29 the opcode. It is in the ONE-BYTE map, and
0x28-0x2d are the SUB group: 28 = r/m8,r8
29 = r/m32,r32 2a = r8,r/m8 2b = r8,r/m32
2c = AL,imm8 2d = eAX,imm32
The suffix IN THE MNEMONIC is the low three bits.
A mnemonic that says `subl` is telling you the
encoding, not the language.
d8 the ModRM byte, and the course that has the whole
layout of this byte is at isa-modrm, so this is
one line of it:
d8 = 11 011 000
| | `-- rm = 000 = EAX
| `----- reg = 011 = EBX
`-------- mod = 11 = REGISTER, so there is
no SIB byte and no displacement
TWO BYTES TOTAL, and the length is not stored anywhere.
A decoder knows it is two bytes because it has read the
opcode and the ModRM and mod says register. That is the
whole of the length arithmetic, and the neutral course
derives it; this is what it looks like applied.
Now the same instruction with a memory operand, from the same corpus: int_lea_mem is 48 8d 44 f7 08, and every byte is doing work.
48 REX.W. One byte that says four things at
once: W (64-bit operands), R (extension for the
reg field), X and B (for the SIB). Without it
the instruction is 32-bit and the address
computation is 32-bit. isa-rex has the rest.
8d LEA. Note that the opcode for LEA is 0x8d,
not 0x8b: MOV to a register and LEA into a
register are DIFFERENT OPCODES, and the
difference is the entire subject of the next
concept.
44 ModRM = 01 000 100
mod = 01 -> a ONE-BYTE displacement follows
reg = 000 -> rax is the destination
rm = 100 -> a SIB byte follows
f7 SIB = 11 111 111
scale = 11 -> x8
index = 111 -> NONE. An index of 111 means
no index register at all
base = 111 -> rdi
08 the one-byte displacement, +8
FIVE BYTES to compute rdi + rsi*8 + 8 and put it in rax,
touching no memory. Compare `mov 8(%rdi,%rsi,8), %rax`,
which is six bytes and DOES touch memory. The next concept
measures the difference between those two and finds it is
not the five-versus-six you would guess.
One trap in that, and it is the one that costs a day. You cannot write a scale of 3. The scale field is two bits and it encodes 1, 2, 4 and 8, and as says so out loud rather than silently computing something else:
$ cat _probe.s .text probe: lea 0(%rdi,%rsi,3), %rax $ as -o _probe.o _probe.s _probe.s:2: Error: expecting scale factor of 1, 2, 4, or 8: got `3'
So lea 0(rdi,rsi,3) is the same shape as lea 0(rdi,rsi,4) — a base, an index, a scale, a displacement, a destination — and there is a value of the scale for which that shape has no encoding at all. A ×3 address is a LEA, a shift and an add, and the assembler makes you write all three. That is the whole ModRM-and-SIB story in one diagnostic, and the neutral course has the byte layout if you want it.
objdump -d f.o and objdump -d -M intel f.o | sed 's/\t/ /g' — or more simply, write the same six instructions in both syntaxes, assemble each, and compare the hex. (Expect the bytes to be identical and the operand order to be reversed. If your assembler is LLVM rather than GNU, `.intel_syntax noprefix` switches it and the exercise becomes the same experiment with a different toolchain — which is the point: the choice of default is a toolchain decision, not an architectural one, and knowing which one you are looking at is the skill.)a[i] with a[3*i] and watch the compiler emit three instructions instead of one. (Expect the array-index form to become a LEA with a scale, a multiply or a shift-and-add, and a register to be spilled if the loop was register-tight. The interesting number is not the speed; it is that the source looks identical in shape and differs in encoding, which is the property the whole architecture is built on.)char loop.unsigned char c = p[i]; use(c); at -O2, then read the disassembly. (Expect a movzx and then nothing, because the zero extension IS the value. Then write char c = p[i] and expect a movsx instead — and then ask what happens to the upper 32 bits when the compiler uses a 32-bit register, which is the trap the next concept opens with.)lock on a register destination, mov %eax, (%rbx,%rsi,3), mov with four operands, and a 66 prefix on an instruction that has no 16-bit form. (Expect four different messages and three of them to be about something other than what you got wrong. Record each one and what the real reason was: this course's whole argument is that a plausible message is worse than a crash, and a collection of eight real assembler messages is the cheapest possible way to believe it.)Backwards, The ISA course's length concept is where the rule this page applied twice is derived rather than illustrated, and its decoder is the thing this course's last concept deliberately tries to break. isa-rex has the four bits in 48 and isa-sib has the three in f7; both are linked rather than repeated, because repeating them is exactly the duplication docs/x86-64-section-plan.md exists to prevent. isa-modes is where the 64-bit mode you have been assuming comes from.
Forwards, this page read three instructions out of a compiled C file and said that each of them encodes a decision. The integer set concept is the reference for what those decisions are: what every one of the instructions in that disassembly writes, what it costs, and the three places where the register name in the source decides the answer rather than the speed. The prefix encodings takes the 66 in addpd and shows that its two bits are the VEX pp field wearing a different hat.
Outward, the neighbouring courses that this one leans on rather than repeats. The vector course measured what happens when the width in movzx is wrong, at a width of 128 bits, and found that an unaligned load is free on this machine — which is not re-measured here and not restated as this course's number. The execution course's frontend is where the "one instruction is a decode slot" claim in the next concept comes from, and where the reason the three-operand encoding buys nothing measurable is properly explained.