Inspecting xorpd’s assembly snippets
Table of Contents
1. Introduction
This is an analysis of each assembly snippet from the book “xchg rax, rax” by xorpd. The book consists of 64 very original assembly snippets. It can be bought physically for a low price and it can be a good gift for some assembly nerd you might know.
If you want to try and figure out the snippets for yourself, I recommend you have the following manuals around:
- Intel Software Developer Manual (SDM), specially volume 2. See the Intel SDM section on the reference index for more information.
- The NASM manual, since it’s the syntax used by the author of the book.
2. Snippets
Each subsection will correspond to a different snippet from the book.
I took the liberty of modifying some indentation/spacing to fit my own preferences.
2.1. Snippet 00
Different ways of setting a register to zero.
xor eax, eax lea rbx, [0] loop $ mov rdx, 0 and esi, 0 sub edi, edi push 0 pop rbp
Note how [0]
doesn’t refer to the value at address 0, it simply indicates that 0
is an effective address. Some instructions, like mov
, operate on the value at
the address (effectively “dereferencing” it); but some others, like lea
, operate
on the address itself, loading it into the specified register. See section 3.3
(Effective Addresses) of the NASM manual.
The loop
instruction decrements the count register (rcx
) and, if it’s not zero,
jumps to the target operand. In this case, the operand is $
, which in NASM
refers to the address of the current instruction. See section 3.5 (Expressions)
of the NASM manual.
2.2. Snippet 01
Calculate Fibonacci or Lucas sequences.
.loop: xadd rax, rdx loop .loop
The xadd
instruction exchanges values in the first and second operand (like
xchg
) and stores the sum of the two values in the first operand. In this case it
will exchange the values at rax
and rdx
, and save the sum in rax
. Because of the
loop
instruction, this process will be repeated rcx
times.
Using the xadd
instruction on the same registers over and over can be used to
generate Fibonacci or similar sequences. For example:
Iteration | rax |
rdx |
---|---|---|
0 | 1 | 0 |
1 | 1 | 1 |
2 | 2 | 1 |
3 | 3 | 2 |
4 | 5 | 3 |
As you can see, the value in each register is the sum of the values that were in that register in the previous two iterations.
By changing the initial state of the registers, the Lucas sequence can be generated:
Sequence | Initial rax |
Initial rdx |
---|---|---|
Fibonacci | 1 | 0 |
Lucas | 1 | 2 |
2.3. Snippet 02
The following code converts non-zero numbers to one.
neg rax sbb rax, rax neg rax
This is equivalent to the following C code:
int val = 123; bool is_non_zero = !!val; /* 1 or 0 */
There are a few key details for understanding the assembly snippet.
The neg
instruction replaces the value of the operand with its two’s
complement. This is equivalent to subtracting the value from zero. It’s
important to note that this instruction also sets the carry flag (CF
) if the
source operand was non-zero.
The sbb
instruction adds the carry flag (CF
) to the source operand, and
subtracts the result from the destination operand. The result is stored into the
destination operand. The operation is:
dst = dst - (src + CF)
Note how the carry flag (CF
) is only added to the source operand. This happens
even if both source and operand registers are the same.
Let’s look at the operations of the assembly snippet step by step.
Step | Last operation | rax = 0 |
rax = 1 |
rax = 5 |
---|---|---|---|---|
1 | Initial state | 0b0000...0000 |
0b0000...0001 |
0b0000...00101 |
2 | First neg |
0b0000...0000 |
0b1111...1111 |
0b1111...11011 |
3 | Add carry flag (sbb ) |
0b0000...0000 |
0b0000...0000 |
0b1111...11100 |
4 | Subtract (sbb ) |
0b0000...0000 |
0b1111...1111 |
0b1111...11111 |
5 | Second neg |
0b0000...0000 |
0b1111...1111 |
0b0000...00000 |
Again, note that the 4th step isn’t subtracting the number of the 3rd step to itself; it is subtracting it to the number of the 2nd step.
Since the source and destination operands of the sbb
instruction are the same,
it’s essentially setting the operand to the negated carry flag (CF
):
x = x - (x + CF) x = -(CF)
After negating this value with the second neg
instruction, the operand becomes
the carry flag (CF
), which was set by the first neg
instruction to either 0 or 1
depending on whether the value was zero or non-zero, respectively.
As we can see, non-zero values are set to one, while zero remains unchanged.