Why ARM Disassembly Looks "Too Simple"

01-_-1 pts0 comments

Why ARM Disassembly Looks "Too Simple" (And What x86 Is Actually Doing Behind Your Back) | Comuniq

This site requires Javascript to work properly. Please enable Javascript in your browser.

-->

/Programming

Learn to develop things, get out of debt, find answers and take part in coding and programming challenges.

Members: 10 Join

Moderated by: mozzapp

Why ARM Disassembly Looks "Too Simple" (And What x86 Is Actually Doing Behind Your Back)

Manon_code<br>1786043737<br>[Programming]<br>0 comments

Every time someone opens an ARM disassembly for the first time after years of dealing only with x86, the same doubt hits: why does this look so much simpler? And right after that comes the suspicion, because things that look too simple usually mean you're missing something. It's not that you're missing anything, it's a genuine difference in philosophy, and that's worth unpacking slowly, because the "simplicity" of ARM comes at a cost, and the "messiness" of x86 exists for a reason that still, to this day, keeps half the planet's server infrastructure running.

The root of it all is that x86 lets you add a value straight from memory to a register in a single instruction, something like add eax, [ebx]. Sounds like a small convenience until you realize that under the hood, this one instruction is doing a load, an addition, and potentially dealing with memory alignment all at once, and the processor has to decode something far more elaborate as a result. ARM doesn't allow that. In ARM, if you want to add something sitting in memory, you first load that value into a register with an LDR, do the math, and if you need to write it back, you use a STR. No arithmetic instruction in ARM ever touches memory directly, full stop. That's the load/store design, and it's the one decision that explains pretty much every other difference that actually matters in practice.

A direct consequence of this is the number of registers. Since every operation in ARM depends on values already sitting in registers, it makes sense the architecture gives you plenty of room for that, and ARM64 offers 31 general-purpose registers. x86, even in its 64-bit form with the extension that brought registers like r8 through r15, still carries some of the weight of having been born with really only about 4 usable registers, and that shows up in generated assembly: x86 code tends to reuse the same registers over and over inside a function, pushing and pulling things off the stack, while the same code compiled for ARM can often keep almost everything in registers from start to finish. It's not that ARM is "better written," it just has more drawers to work with.

Instruction size is another thing that catches people off guard. x86 has variable-length instructions, anywhere from 1 byte to a mess of up to 15 bytes with every possible prefix stacked on, which is great for code density but terrible for predictability: the processor doesn't know where one instruction ends without decoding it first. ARM fixed everything at 4 bytes per instruction (at least in A64, since the Thumb side of 32-bit ARM is a whole other story). In practice this means navigating an ARM disassembly is mechanical in a good, boring way, every line takes up exactly the same space, and you can jump straight to the address you want without counting bytes along the way. Anyone who's tried manual reverse engineering on x86 knows the pain of miscalculating an offset because of some instruction prefix that slipped by unnoticed.

Then there's the flags situation, which completely changes how you read a comparison in code. In x86, most arithmetic instructions already touch the flags as a side effect, so a sub followed by a conditional jump is often enough on its own. In ARM you have to be explicit about it: either use a dedicated CMP instruction, or add the S suffix to a normal instruction to say "yes, I actually want this to affect the flags this time." That makes ARM code a bit more verbose in this specific spot, but also easier to audit, since it's obvious exactly where a comparison is meant to happen.

Calling conventions are usually where people debugging real code get stuck first, more than anything theoretical. In x86-64 on Linux, the first arguments to a function go in rdi, rsi, rdx, rcx, r8, r9, in that order, with the rest on the stack. ARM64 follows similar logic but the registers shift to x0 through x7, and anyone who's debugged both knows the first instinct when staring at a crash dump is always to check the wrong register if you just switched architectures. It's worth opening gdb or lldb on some function and just watching the arguments arrive, that teaches more than any table ever will.

If you compile the same simple C function, say adding two integers from an array, on both architectures, the difference jumps out fast. The x86 code will have one instruction touching memory in the middle of the math. The ARM code will have an LDR beforehand, the operation on its own after that, and if...

instruction registers code first disassembly simple

Related Articles