Inspecting xorpd’s assembly snippets

Index | Up


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.