16 minute read


I know I’m late, guys. I know you were ready to shred some assembly and dive into new territory. But you also know how it goes: shipping a new version of 0tH is never cheap in terms of time.

Here we go!

Past lessons in this series:

More importantly, this is where I want to introduce a key part of the reverser’s mindset: experimenting, observing, and repeating. Again and again.

The Code

Last time we saw this simple program:

.global _main
.extern _exit

_main:
    mov X11, #0x1723

    mov X0, #0x0000    // exit code
    bl _exit           // invokes exit

and we followed its execution with lldb. Let’s enrich it. For your convenience, I converted the value 0x1723 to binary:

[0 sixteen times] 0001 0111 0010 0011

Boolean operators

If you’re here, you’re supposed to know what boolean operators are and how boolean logic works, so I’ll spare myself the job - and spare you the boring definitions.

As the boolean operator with highest priority is the logical negation, we’ll start from there.

Funny enough, NOT is not implemented directly in the AARCH64 ISA. Instead, it is implemented with a MOV that negates the bits being moved. Considering that, in turn, MOV is just an alias for an ORR (hang on, we’ll see this later. For now, trust me), a NOT is implemented as an inverted or.

Before cursing all boolean gods in search for answers, keep in mind that AARCH64 is designed to be a RISC architecture, so the set of instructions is deliberately kept as small as possibile.

MVN

MVN has the following basic syntax:

MVN dest, source

In this case, dest and source are both registers.

This means: take source, invert its bits, put the result into dest. From now on, I will denote the operation of putting a value into a variable with the symbol , so if there is no ambiguity, operations will be logically denoted as follows:

dest  NOT(source)

Observe that is not assembly language.

There is yet another form of this command; I will show it after presenting the basic forms of the other boolean operators.

AND

The most basic form of AND has the following syntax:

AND dest, op1, op2

where dest, op1, and op2 are registers. The result of this instruction is:

dest  AND(op1, op2)

Like for the MVN operator, there are other forms of this instruction - we’ll discuss them shortly.

OR

In the AArch64 ISA, the OR operator is implemented with the instruction ORR, whose most basic form is

ORR dest, op1, op2

where dest, op1, and op2 are registers. The result of this instruction is:

dest  OR(op1, op2)

Like for the MVN and AND operators, there are other forms of this instruction.

I wrote a little program to show the basic functioning of these instructions. You may want to change it - and you are actually encouraged to do so!

.global _main
.extern _exit

_main:
    mov X11, #0x1723
    mov W12, #0x15

	// playing with NOT
	mvn X13, X11
	mvn W14, W12

    // playing with AND
    // AND X15, X11, W12   // this won't compile
    AND X15, X11, X12   // this would.
    AND X15, XZR, X15   // a new guy!

    // playing with OR
    ORR X14, X15, X11

    mov X0, #0x0000    // exit code
    bl _exit           // invokes exit

Compile and link it:

gabrielebiondo@RevEng3 03-operators % as main.s
gabrielebiondo@RevEng3 03-operators % ld \
  -arch arm64 \
  -platform_version macos 26.0 26.0 \
  -syslibroot "$SDK" \
  -lSystem \
  -o operators \
main.o
gabrielebiondo@RevEng3 03-operators % ls -alh
total 88
drwxr-xr-x  5 gabrielebiondo  staff   160B 22 Dec 06:06 .
drwxr-xr-x@ 9 gabrielebiondo  staff   288B 21 Dec 12:38 ..
-rw-r--r--@ 1 gabrielebiondo  staff   432B 22 Dec 06:06 main.o
-rw-r--r--@ 1 gabrielebiondo  staff   398B 22 Dec 06:06 main.s
-rwxr-xr-x@ 1 gabrielebiondo  staff    33K 22 Dec 06:06 operators

I will debug the code above. If you make changes to it, try to make a sense of it.

Now, launch the debugger and attach it to the newly compiled program:

gabrielebiondo@RevEng3 03-operators % lldb operators
(lldb) target create "operators"
Current executable set to '/Users/gabrielebiondo/MiB/51 - Reversing 101/reversing-101/lessons/03-operators/operators' (arm64).

In the last lesson, I told you the importance of setting a breakpoint in the right place. We will use the same method we used there:

(lldb) breakpoint set --name main
Breakpoint 1: where = operators`main, address = 0x00000001000003c0
(lldb) breakpoint list
Current breakpoints:
1: name = 'main', locations = 1
  1.1: where = operators`main, address = operators[0x00000001000003c0], unresolved, hit count = 0

Execute the program - again, if you have doubts, go to the previous lesson:

(lldb) run
Process 38880 launched: '/Users/gabrielebiondo/MiB/51 - Reversing 101/reversing-101/lessons/03-operators/operators' (arm64)
Process 38880 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: 0x00000001000003c0 operators`main
operators`main:
->  0x1000003c0 <+0>:  mov    x11, #0x1723 ; =5923
    0x1000003c4 <+4>:  mov    w12, #0x15 ; =21
    0x1000003c8 <+8>:  mvn    x13, x11
    0x1000003cc <+12>: mvn    w14, w12
Target 0: (operators) stopped.

The first two instructions have been previously explained. Nothing new. Observe that before running any step, you still can interrogate any register to see that it’s not initialised, for instance:

(lldb) register read x11
     x11 = 0x00000001fe25c0a0

The contents of x11 are just garbage, for us. Move a couple of steps forward:

(lldb) s
Process 38880 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = instruction step into
    frame #0: 0x00000001000003c4 operators`main + 4
operators`main:
->  0x1000003c4 <+4>:  mov    w12, #0x15 ; =21
    0x1000003c8 <+8>:  mvn    x13, x11
    0x1000003cc <+12>: mvn    w14, w12
    0x1000003d0 <+16>: and    x15, x11, x12
(lldb) register read x11 w12
     x11 = 0x0000000000001723
     w12 = 0x00020000

and

(lldb) s
Process 38880 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = instruction step into
    frame #0: 0x00000001000003c8 operators`main + 8
operators`main:
->  0x1000003c8 <+8>:  mvn    x13, x11
    0x1000003cc <+12>: mvn    w14, w12
    0x1000003d0 <+16>: and    x15, x11, x12
    0x1000003d4 <+20>: and    x15, xzr, x15
Target 0: (operators) stopped.
(lldb) register read x11 w12
     x11 = 0x0000000000001723
     w12 = 0x00000015

This should be quite easy for you now to understand. Notice that the next instruction introduces the register x13, so prepare the next register read:

(lldb) register read x11 x13 w12
     x11 = 0x0000000000001723
     x13 = 0x0000000191775000
     w12 = 0x00000015

Moreover: here we will play with bits, and working in binary makes everything more visible. To do so:

(lldb) register read --format binary x11 x13 w12
     x11 = 0b0000000000000000000000000000000000000000000000000001011100100011
     x13 = 0b0000000000000000000000000000000110010001011101110101000000000000
     w12 = 0b00000000000000000000000000010101

Observe the result of the next instruction, writing into x13 the contents of x11, inverted:

(lldb) s
Process 38880 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = instruction step into
    frame #0: 0x00000001000003cc operators`main + 12
operators`main:
->  0x1000003cc <+12>: mvn    w14, w12
    0x1000003d0 <+16>: and    x15, x11, x12
    0x1000003d4 <+20>: and    x15, xzr, x15
    0x1000003d8 <+24>: orr    x14, x15, x11
Target 0: (operators) stopped.
(lldb) register read --format binary x11 x13 w12
     x11 = 0b0000000000000000000000000000000000000000000000000001011100100011
     x13 = 0b1111111111111111111111111111111111111111111111111110100011011100
     w12 = 0b00000000000000000000000000010101

Just as planned. The next instruction will work with 32 bits registers - not very different from the previous one and hence deserving the same treatment:

(lldb) register read --format binary x11 x13 w12 w14
     x11 = 0b0000000000000000000000000000000000000000000000000001011100100011
     x13 = 0b1111111111111111111111111111111111111111111111111110100011011100
     w12 = 0b00000000000000000000000000010101
     w14 = 0b00000000000000000000000000000001
(lldb) s
Process 38880 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = instruction step into
    frame #0: 0x00000001000003d0 operators`main + 16
operators`main:
->  0x1000003d0 <+16>: and    x15, x11, x12
    0x1000003d4 <+20>: and    x15, xzr, x15
    0x1000003d8 <+24>: orr    x14, x15, x11
    0x1000003dc <+28>: mov    x0, #0x0 ; =0
Target 0: (operators) stopped.
(lldb) register read --format binary x11 x13 w12 w14
     x11 = 0b0000000000000000000000000000000000000000000000000001011100100011
     x13 = 0b1111111111111111111111111111111111111111111111111110100011011100
     w12 = 0b00000000000000000000000000010101
     w14 = 0b11111111111111111111111111101010

The next instruction uses three registers: x15, x11, and x12. We will shortly prepare the next register read instruction, but first we have to observe explicitly that the objection “but we did never instantiate x12” is de facto wrong. We instantiated w12, which contains the less significant bytes of x12. The most significant bytes of the latter are padded with zeroes, as the following shows:

(lldb) register read --format binary x11 x13 w12 w14 x12
     x11 = 0b0000000000000000000000000000000000000000000000000001011100100011
     x13 = 0b1111111111111111111111111111111111111111111111111110100011011100
     w12 = 0b00000000000000000000000000010101
     w14 = 0b11111111111111111111111111101010
     x12 = 0b0000000000000000000000000000000000000000000000000000000000010101

so we will keep the notation short and proceed:

(lldb) register read --format binary x11 x12 x15
     x11 = 0b0000000000000000000000000000000000000000000000000001011100100011
     x12 = 0b0000000000000000000000000000000000000000000000000000000000010101
     x15 = 0b0000000000000000000000000000000000000000000000000000000001010101
(lldb) s
Process 38880 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = instruction step into
    frame #0: 0x00000001000003d4 operators`main + 20
operators`main:
->  0x1000003d4 <+20>: and    x15, xzr, x15
    0x1000003d8 <+24>: orr    x14, x15, x11
    0x1000003dc <+28>: mov    x0, #0x0 ; =0
    0x1000003e0 <+32>: bl     0x1000003e4    ; symbol stub for: exit
Target 0: (operators) stopped.
(lldb) register read --format binary x11 x12 x15
     x11 = 0b0000000000000000000000000000000000000000000000000001011100100011
     x12 = 0b0000000000000000000000000000000000000000000000000000000000010101
     x15 = 0b0000000000000000000000000000000000000000000000000000000000000001

This is correct and expected. Now, the next instruction is a bit strange. There is a new kid in town, xzr. Time to introduce this virtual register - or pseudo-register. First, the behaviour: if the instruction requires a source register (like in the case under analysis), the hardware returns a 0 (in fact, xzr can be seen as the mnemonics for zero register). When the instruction requires a destination register, the hardware simply ignores the result (pretty much, like writing to /dev/null).

xzr is also implemented differently from the x0x30 registers. These are typically built with flip-flops or SRAM. xzr is hard wired, instead. This is also reflected on the following result:

(lldb) register read --format binary x11 x12 x15 xzr
     x11 = 0b0000000000000000000000000000000000000000000000000001011100100011
     x12 = 0b0000000000000000000000000000000000000000000000000000000000010101
     x15 = 0b0000000000000000000000000000000000000000000000000000000000000001
error: Invalid register name 'xzr'.

Now, we have:

error: Invalid register name 'xzr'.
(lldb) s
Process 38880 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = instruction step into
    frame #0: 0x00000001000003d8 operators`main + 24
operators`main:
->  0x1000003d8 <+24>: orr    x14, x15, x11
    0x1000003dc <+28>: mov    x0, #0x0 ; =0
    0x1000003e0 <+32>: bl     0x1000003e4    ; symbol stub for: exit

operators`exit:
    0x1000003e4 <+0>:  adrp   x16, 4
Target 0: (operators) stopped.
(lldb) register read --format binary x14 x15
     x14 = 0b0000000000000000000000000000000011111111111111111111111111101010
     x15 = 0b0000000000000000000000000000000000000000000000000000000000000000

I wanted to show you this operation (zeroing a register) because it’s one of the foundations of shellcoding. Not important right now, but keep it in the back of your mind, it’ll come back.

The rest of the program is quite similar to what we have seen already, I believe it does not need any comment.


So far, this post has been full of “forward declarations”. To close the loop, we first need to introduce another family of operators: shifts. Also in this context we present the results before the theory.

So let’s expand the previous program with this:

.global _main
.extern _exit

_main:
    mov X11, #0x1723
    mov W12, #0x15

	// playing with NOT
	mvn X13, X11
	mvn W14, W12

    // playing with AND
    // AND X15, X11, W12   // this won't compile
    AND X15, X11, X12   // this would.
    AND X15, XZR, X15   // a new guy!

    // playing with OR
    ORR X14, X15, X11

    // Shifts
    mov X0, #-230
    mov X1, #230

    // and, were you wondering how to store a negative hex number:
    mov X2, #-0x8197

    LSL X3, X1, #2
    LSL X4, X2, #2

    LSR X5, X1, #2
    LSR X6, X2, #2

    ASR X7, X1, #2
    ASR X8, X2, #2


    mov X0, #0x0000    // exit code
    bl _exit           // invokes exit

Let’s focus on the second part - from the comment // Shifts onwards. Uh, yes, I forgot to mention that, but it should be obvious by now: as supports C++ comments (introduced by //).

To properly understand what follows you should know how the negative numbers are represented into memory. For more information, revert to Two’s complement and come back if and only if you understood what’s there.

Compile exactly as you did before. Attach lldb to the program, set the usual breakpoint.

Now, let’s proceed with the debugging from where we stopped to introduce the new instructions.

LSL

LSL stands for Logical Shift Left. The most generic form of this instruction is

LSL dest, source, #imm

and the effect is

dest  source << #imm

in simple terms, this instruction shifts the bits in the register source to their left, padding the result with zeros, and writing into dest. In the context of shift/rotating operations, dest and source are always registers unless differently specified.

Let’s see what happens in our debug session - if you followed the example, you should be in this situation:

Process 40944 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = instruction step into
    frame #0: 0x00000001000003dc operators`main + 28
operators`main:
->  0x1000003dc <+28>: mov    x0, #-0xe6 ; =-230
    0x1000003e0 <+32>: mov    x1, #0xe6 ; =230
    0x1000003e4 <+36>: mov    x2, #-0x8197 ; =-33175
    0x1000003e8 <+40>: lsl    x3, x1, #2
Target 0: (operators) stopped.

so hit step (or s) three times, to see this:

Process 40944 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = instruction step into
    frame #0: 0x00000001000003e8 operators`main + 40
operators`main:
->  0x1000003e8 <+40>: lsl    x3, x1, #2
    0x1000003ec <+44>: lsl    x4, x2, #2
    0x1000003f0 <+48>: lsr    x5, x1, #2
    0x1000003f4 <+52>: lsr    x6, x2, #2
Target 0: (operators) stopped.
(lldb) register read --format binary x0 x1 x2
      x0 = 0b1111111111111111111111111111111111111111111111111111111100011010
      x1 = 0b0000000000000000000000000000000000000000000000000000000011100110
      x2 = 0b1111111111111111111111111111111111111111111111110111111001101001

If you didn’t know about the 2’s complement, you may want to take a deeper look at the contents of the registers x0 and x1. It’s evident noticing what happens if you add the contents - this also justifies the choice of the 2’s complement to represent negative integers.

At this point hitting s again gives

* thread #1, queue = 'com.apple.main-thread', stop reason = instruction step into
    frame #0: 0x00000001000003ec operators`main + 44
operators`main:
->  0x1000003ec <+44>: lsl    x4, x2, #2
    0x1000003f0 <+48>: lsr    x5, x1, #2
    0x1000003f4 <+52>: lsr    x6, x2, #2
    0x1000003f8 <+56>: asr    x7, x1, #2
Target 0: (operators) stopped.
(lldb) register read --format binary x1 x3
      x1 = 0b0000000000000000000000000000000000000000000000000000000011100110
      x3 = 0b0000000000000000000000000000000000000000000000000000001110011000

as expected. Bits moved to the left by 2 positions, zero padded. You should ask yourself what is the practical implication of this operation, from a numerical standpoint.

Negative numbers are treated similarly: hitting s gives

Process 40944 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = instruction step into
    frame #0: 0x00000001000003f0 operators`main + 48
operators`main:
->  0x1000003f0 <+48>: lsr    x5, x1, #2
    0x1000003f4 <+52>: lsr    x6, x2, #2
    0x1000003f8 <+56>: asr    x7, x1, #2
    0x1000003fc <+60>: asr    x8, x2, #2
(lldb) register read --format binary x2 x4
      x2 = 0b1111111111111111111111111111111111111111111111110111111001101001
      x4 = 0b1111111111111111111111111111111111111111111111011111100110100100
Exercise 1:

Enrich the program in the following manner:

  • copy the value 0x2000000000000000 in x0
  • Shift it left 2 positions
  • Observe what happens and find a reason

LSR

The dual operation of LSL is shifting the sequence of bits right by a given number of positions. Unsurprisingly, this is called LSR (logical shift right), the base syntax is:

LSR dest, source, #imm

and the effect is

dest  source >> #imm

with zero padding on the most significant bits. This poses a problem that will be evident in the debug session. From here:

Process 40944 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = instruction step into
    frame #0: 0x00000001000003f0 operators`main + 48
operators`main:
->  0x1000003f0 <+48>: lsr    x5, x1, #2
    0x1000003f4 <+52>: lsr    x6, x2, #2
    0x1000003f8 <+56>: asr    x7, x1, #2
    0x1000003fc <+60>: asr    x8, x2, #2

the next steps gives

Process 40944 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = instruction step into
    frame #0: 0x00000001000003f4 operators`main + 52
operators`main:
->  0x1000003f4 <+52>: lsr    x6, x2, #2
    0x1000003f8 <+56>: asr    x7, x1, #2
    0x1000003fc <+60>: asr    x8, x2, #2
    0x100000400 <+64>: mov    x0, #0x0 ; =0
Target 0: (operators) stopped.
(lldb) register read --format binary x1 x5
      x1 = 0b0000000000000000000000000000000000000000000000000000000011100110
      x5 = 0b0000000000000000000000000000000000000000000000000000000000111001

as planned; but another step gives:

->  0x1000003f8 <+56>: asr    x7, x1, #2
    0x1000003fc <+60>: asr    x8, x2, #2
    0x100000400 <+64>: mov    x0, #0x0 ; =0
    0x100000404 <+68>: bl     0x100000408    ; symbol stub for: exit
Target 0: (operators) stopped.
(lldb) register read --format binary x2 x6
      x2 = 0b1111111111111111111111111111111111111111111111110111111001101001
      x6 = 0b0011111111111111111111111111111111111111111111111101111110011010

The value just changed sign. In reversing this is not a great problem, usually - but if you were to write code, something like this could cause you severe headaches. This is the typical insidious bug: hard to find, completely legal, makes debugging a nightmare.

This explains why there’s need for another shift right operator, a sign-savvy one.

ASR

ASR means Arithmetic Shift Right. The syntax is

ASR dest, source, #imm

and the effect is

dest  source >> #imm

but it preserves the sign. Thus a step after

Process 40944 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = instruction step into
    frame #0: 0x00000001000003f8 operators`main + 56
operators`main:
->  0x1000003f8 <+56>: asr    x7, x1, #2
    0x1000003fc <+60>: asr    x8, x2, #2
    0x100000400 <+64>: mov    x0, #0x0 ; =0
    0x100000404 <+68>: bl     0x100000408    ; symbol stub for: exit

we would see:

Process 40944 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = instruction step into
    frame #0: 0x00000001000003fc operators`main + 60
operators`main:
->  0x1000003fc <+60>: asr    x8, x2, #2
    0x100000400 <+64>: mov    x0, #0x0 ; =0
    0x100000404 <+68>: bl     0x100000408    ; symbol stub for: exit

operators`exit:
    0x100000408 <+0>:  adrp   x16, 4
Target 0: (operators) stopped.
(lldb) register read --format binary x1 x7
      x1 = 0b0000000000000000000000000000000000000000000000000000000011100110
      x7 = 0b0000000000000000000000000000000000000000000000000000000000111001

and another step would give us:

Process 40944 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = instruction step into
    frame #0: 0x0000000100000400 operators`main + 64
operators`main:
->  0x100000400 <+64>: mov    x0, #0x0 ; =0
    0x100000404 <+68>: bl     0x100000408    ; symbol stub for: exit

operators`exit:
    0x100000408 <+0>:  adrp   x16, 4
    0x10000040c <+4>:  ldr    x16, [x16]
Target 0: (operators) stopped.
(lldb) register read --format binary x2 x8
      x2 = 0b1111111111111111111111111111111111111111111111110111111001101001
      x8 = 0b1111111111111111111111111111111111111111111111111101111110011010

finally.

Conclusions

This was a dense lesson, and deliberately so.

At this point, we are no longer just learning instructions: we are learning how small, perfectly legal details can radically change the behaviour of a program. Zero-extension, logical versus arithmetic shifts, implicit assumptions about signedness — these are exactly the kind of details that matter when reversing real binaries.

If there is one takeaway from this lesson, it is this: never trust intuition alone. Always verify. The debugger is not a support tool; it is the ground truth.

You may have noticed that I often rely on “forward declarations” and end up splitting lessons more than I initially planned. This is intentional. Some concepts only make sense once you have seen their consequences first. Teaching assembly bottom-up is tempting, but it is rarely effective.

We are now close to completing the foundational layer of this series. Once that is done, the format will become more compact and less guided. Fewer explanations, more observations — closer to how reversing is actually done in practice.

Next Lesson

The next lesson closes all remaining forward declarations and ties together the concepts introduced here.

See you next time. ‘til then… Have fun!


Want the deep dive?

If you’re a security researcher, incident responder, or part of a defensive team and you need the full technical details (labs, YARA sketches, telemetry tricks), email me at info@bytearchitect.io or DM me on X (@reveng3_org). I review legit requests personally and will share private analysis and artefacts to verified contacts only.

Prefer privacy-first contact? Tell me in the first message and I’ll share a PGP key.


Subscribe to The Byte Architect mailing list for release alerts and exclusive follow-ups.


Gabriel(e) Biondo
ByteArchitect · RevEng3 · Rusted Pieces · Sabbath Stones