The headline is sort of misleading. It is (appropriately) primarily about inappropriate abstractions. As you say, some abstractions are unavoidable. Even assembly language is an abstraction.
I think arguing that assembly is an abstraction is going the wrong direction on why it is hard. Assembly largely forces numerical abstraction on your problem. Which is a lot harder to reason about than folks want to acknowledge.
That is, higher level languages let you get farther away from the abstraction that is the computer itself. As such, you can have a less abstract program in a language that has higher abstraction away from the execution environment.
>I thought assembly mapped 1-to-1 with actual HW instructions.
Assembly encodes a wide varity of abstractions (it is a human readable format after all) and lots of assembly instructions have a clear relation to an instruction on the hardware, but definitely not all.
E.g. a CPU does not understand what a "label" is and the semantics of a labels and jumping to them is removed by the assembler.
But not even the assembled binary actually maps to executed instructions. The CPU is actually a virtual machine which presents itself as e.g. an x86 ISA interpreter, but internally it uses microcode executed in various performance enhancing ways to speed up the process.
First of all, you normally use symbolic labels, not offsets for jumps; the assembler will calculate them for you, and a linker will possibly build a relocation table based on them. Then, any good assembler has macros. Also, data / text blocks, etc that are not code but an abstraction which the linker later uses.
Writing machine code directly, as a byte stream, is fun, but is exhausting.
Arguably it's more of translation layer not abstraction. You're not abstacting away any concepts or code blocks here, you still have to write every single instruction, you just get a bit of a help with math.
Nope, the CPU presents you an interface of the available instructions but those instructions are a complete fiction on modern processors and don't have to match at all what the underlying hardware does. The CPU guarantees that the observable effects of the instructions are consistent (i.e if the processor wants to do some fuckery it can't change the instruction semantics) but beyond that is free to run your code however it wants.
Simpler RISC designs, like simpler ARM cores, more or less directly execute instructions; same for old 8-bit cores you can still widely find in MCUs. Complex high-performance cores, with pipelining, instruction fusion, and OoO execution, turn instruction stream into a microcode lava.
So maybe the right thing to say is that assembly is a contract between you and the processor and the underlying implementation can map directly to hardware when appropriate but it doesn't have to. It is an abstraction over different underlying hardware implementations.
It is true. Abstraction means that you dont have to worry about some lower level concerns. With computers, the rabbit hole goes quite deep.
High-level language (C/python)
Assembly language (att/intel)
Byte code
Micro code
So, with assembly code, you dont care about what the actual bytes that get generated are (they can change and you'd never know if the system was backwards compatible). Similarly, if the microcode that the CPU generates to implement the instruction changes youd also never know.
> I thought assembly mapped 1-to-1 with actual HW instructions.
on x86 it's a 1-to-many relationship. There are, for example, many different MOV instructions that can be encoded based on the parameters used. All assemblers I know of hide this from the user, as well as featuring labels, macros, etc. which are not defined by the hardware at all. For the most part, the x86 assembler hides the details of prefix bytes and ModR/M+SIB from the user. Some assemblers are quite advanced and if you took them just a few logical steps beyond where they are you would end up with C.
> Is this true? I thought assembly mapped 1-to-1 with actual HW instructions. If so, assembly wouldn't be an abstraction, it would be an interface.
Assembly gives you a nice sequential list of instructions, completely hiding pipelining, speculative execution, and all the magic that modern (since the 90s at least) CPU do to do the job fast.
And as the Spectre family of CPU vulnerabilities, these abstractions are in fact more leaky than most people assume.
And cache management, thread switching optimization/prioritization, and so on.
If a CPU were to ONLY do the instructions that were written in the assembly code, the code could be maybe 5-10 times slower when using cached memory and 100x slower if accessing RAM constantly.
And this is part of the reason why it's hard to write assembly that is faster than a well optimized C/C++ program. The C compiler "knows" (to some extent) what the machine code leads to at the hardware level, and will often create machine code that is more liklely to allow the CPU to reap all such advantages in a way many assembly programmers wouldn't know about or think of.
This is true. Ignoring the labels, macros, and directives, there are many ways in machine code to encode most common x86 instructions (zeroing a register even has many possible assembly instructions!). Assemblers pick the best encoding.
Is that true? They swap actual instruction mnemonics as written in the assembly source into different instructions, for performance? I hope that is controlled by some flag or something, seems like a strange thing for an assembler to be doing unless asked.
I seem to remember the venerable combined assembler/monitor/editor ASM-One [1] on the Amiga having a mode to do that, an "optimizing assembler", but I think it mainly worked with instruction and data sizes, i.e. optimizing short jumps into branches which were cheaper on the 68k.
I believe that there are some assemblers that will swap "mov rax, 0" for "xor eax, eax" (which is smaller and faster), if you let them, but not all of them. Some instructions like "jmp LABEL" correspond to many different options (short relative jumps, long relative jumps, absolute jumps, etc.) and the assembler picks the best one.
As another example, almost all of the vector instructions have an encoding with a VEX prefix (an AVX encoding) as well as the older SSE encoding. If you mix the VEX and SSE encoded instructions, there can be a big slowdown, so an assembler will give you VEX encodings if you have AVX-only instructions in the stream, and default to SSE encodings if you don't (they are often smaller).
Some instructions, like LEAs and ADDs, have several different encoding options corresponding to different operand orderings, and an assembler will pick the best one - some of these encodings will force an extra SIB byte when you use R12 as an operand, for example.
This is kind of assembler-specific in terms of how smart it is. I'm not sure that the dumber assemblers do this for you.