Some of the other low level systems (like the Mac) had a trap system that wasn’t so far away in cycle count from user code. But in these days of needing 10,000 cycles to bridge a system call it’s best to do whatever you can to avoid calling the OS.
You might as well ask why dogs bark or puzzle with furrowed brow over the croaking of frogs.
Then I sort of turned the problem around in my mind.
When is a compiler prevented from optimizing? Maybe pointer aliasing?
Another thing I wondered. I've written hello.c and it's about 84 bytes of C and 8.3k as an executable. Would hand-coded assembler be that large?
maybe it's that compilers CAN do well, but because of requirements, they can't do some things well and necessarily create a lot of "boilerplate infrastructure".
The answer is generally no. A lot of that overhead is due to the C std library. If memory serves from a blog post I read long ago, with very very aggressive tuning, you can get a hello world binary down under 50 bytes (which is smaller than the ELF header). A “normal” ASM coded “hello world” binary could easily be in the range of a few hundred bytes without anything special.
I just compiled my own 'hello.c' on Linux, with the optimisation flag set for 'code size', and the binary weighed in around 8.3k. I then removed the `printf` call and the inclusion of `stdio.h`. The resulting binary was 8.1k. This is exactly as expected. Why would the inclusion of a call to a dynamic library bloat the final binary size?
Here’s two good links to read on it:
https://www.muppetlabs.com/~breadbox/software/tiny/teensy.ht...
hello.c: 4 loc, 84 bytes
hello.s: 30 loc, 517 bytes
hello: 8.3k
so it must be data structures for linking.
oh, and if I use -static, hello becomes 844704 bytes. :)
Abstractions like functions are still present, just not in the way that you might think of them in a higher-level language. You use 'branching' instructions to jump from one part of the code to another. Either by referencing labeled sections of the code to perform 'absolute' jumps, or by jumping 'relative' to the current instruction. This forms the building blocks that can be used to implement more abstract constructs such as loops, functions and conditionals. This is a gross simplification, I hope it helps answer your question though.
You can write tests like in any language, they just happen to be more tedious.
If you make use of powerful macro assemblers, like MASM and TASM, you can write what looks like high level functions.
You can have a look here for a couple of MASM examples,
Optimised ASM can still be tested but one problem is that when the code doesn't work, it can simply crash. This makes it a pain to debug.
Edit: I have a little bit of experience in the area of operating-system development in assembly. This is purely a hobby project that I work on half-heartedly. Nevertheless, it does demonstrate what I know about bare-metal x86 assembler.
https://github.com/ajxs/hxos