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.
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.
Writing machine code directly, as a byte stream, is fun, but is exhausting.
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.
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.
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.
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.
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.
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.
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.