Ask HN: Which books/resources to understand modern Assembler?
I’d like to learn more about Assembler in order to be able to work with LLVM and JIT as well as to write high performance low-level code. I’m familiar with the basics of x86 but I haven’t touched Assembler in a while, so I’m wondering which resources and in particular books you’d recommend?
93 comments
[ 4.2 ms ] story [ 119 ms ] threadAs a good refresher on assembly and compilers
The only thing that is not as nice is the tooling, it's GUI based and uses Java. One of the projects on my backburner is to attempt to write a better toolset for it. Or maybe I should just wait for someone else to do it haha (to be clear, there's nothing wrong with the existing tooling, I'd just rather something not based on Java that I could run on a normal IDE, say, VS Code).
https://p.ost2.fyi/courses
Let's see...
"Getting Started with LLVM Core Libraries: Get to Grips With Llvm Essentials and Use the Core Libraries to Build Advanced Tools "
"The Architecture of Open Source Applications (Volume 1) : LLVM" https://aosabook.org/en/v1/llvm.html
"Tourist Guide to LLVM source code" : https://blog.regehr.org/archives/1453
llvm home page : https://llvm.org/
llvm tutorial : https://llvm.org/docs/tutorial/
llvm reference : https://llvm.org/docs/LangRef.html
learn by examples : C source code to 'llvm' bitcode : https://stackoverflow.com/questions/9148890/how-to-make-clan...
kinda amazing to me that amazon cannot fix this but it returns 0 results (at least for me)
From my experience, Intel's x86 manual is better and easier to read than AMD's. It's a free download.
If you are interested in ARM or RISC-V assembly, the concepts are pretty similar but the instructions are different. For any architecture, you're going to have to read the architecture manuals to get a good working knowledge of the instructions and how to use them. An easy way to get started is to write a program in C, then replace the functions with assembly code one by one until your C code is just main() and a header.
ARMv7: https://developer.arm.com/documentation/100076/0200/a32-t32-...
ARMv8: https://developer.arm.com/documentation/ddi0602/2024-03/Base...
alternative: https://www.scs.stanford.edu/~zyedidia/arm64/
RISC-V: https://riscv.org/technical/specifications/
x86: https://www.intel.com/content/www/us/en/developer/articles/t...
web format: http://x86.dapsen.com/
If you like to learn by example (most of these are not great, but good enough to get started):
https://rosettacode.org/wiki/Assembly
https://github.com/TheAlgorithms/AArch64_Assembly
I was expecting a bit more of a technical approach and was disappointed. If you’re in the same boat reference manuals and compiling to .s files may be the way to go.
Assemble the emitted .S file:
Link with c runtime If using -nostdlib, add your own start.S with a simple entry which calls main with no command line arguments. Assemble with: And link both: Check the result: To keep even more minimal, add your own link.ld file instead of using the default: Link with:PSA: Read Hongjiu Lu's classic paper ELF: From The Programmer's Perspective as a preliminary to get an idea of how the above steps fit together.
They can go for it later, after learning assembly basics.
A typical function has two kinds of assembly code:
(1) The ABI-required logic for functions and function calls, and
(2) Everything else, which can be more or less whatever you want. As long as you don't stomp on the details required by the ABI.
Any suggestions there?
I imagine similar documents exist for each OS-architecture pairing, since the ABI is a convention that (ideally) everyone will agree on.
If you need a definitive answer for some particular OS-architecture pairing, you might ask within a development community that also needs to support that ABI. E.g., gcc, LLVM, lldb, gdb.
[0] IIRC: Even after reading that doc I was unclear on a few details. I got clarification by writing some minimal examples and looking at the assembly that gcc produces. Note that you may need to disable inlining and optimization, or else you might draw the wrong conclusions from the generated assembly.
Actually, I ended up finding the answer to my question (“why doesn’t every assembly tutorial cover calling conventions on like, page 2?”), and it was because I was coming at this topic from a C perspective.
https://stackoverflow.com/a/17309038
“ If you're writing in assembly language, you can do whatever you want. It's only when you want to interact with some external code (maybe a library, maybe a system call) that you need to obey the calling convention.”
If you're first learning assembly in some little simulator, or on bare metal, I can see the point in deferring any discussion about the ABI until later.
My first assembly project needed to be invoked as a function call from a C/C++ program. So I'd have really benefited from at least a brief explanation about the boilerplate asm you need to be a callable function, how to make function calls / syscalls to print some output, etc.
Step #1: read the arch manual for some CPU. Read most if not all of it. It’s a lot of reading but it’s worth it. My first was PowerPC and my second was x86. By the time I got to arm, I only needed to use the manual as a reference. These days I would start with x86 because the manuals are well written and easily available. And the HW is easily available.
Step #2: compile small programs for that arch using GCC, clang, whatever and then dump disassembly and try to understand the correspondence between your code and the instructions.
.. and because the ARM system manuals are just unimaginably awful. ;-P
The other is the assembler - what syntax it gives you, how it handles macros, whether it optimises, whether it does any semantic analysis. GNU AS is different to NASM is different to flat assembler.
I didn't get much out of reading compiler disassembly relative to handwritten assembly. I'd recommend trying to find some of the latter, might need to be maths libs or video codecs or similar. I'd be interested in recommendations here, the asm I learned from was proprietary.
If you want the full monty, I think you'll have to read the LLVM documentation on JIT linking: https://llvm.org/docs/JITLink.html
I haven't found any academic papers or tutorials on JIT linking, unfortunately.
It could be one option.
Funny enough, on large CPUs this can be slower than recomputing something, because they don't like long dependency chains and sometimes even have penalties for reading a register that hasn't been written for hundreds of instructions.
I think you have to include an understanding of the underlying processor architecture as part of "learning more about assembly". If you're writing assembler without instruction scheduling in mind, you would be better off not writing assembler at all, and letting the LLVM optimizers do instruction scheduling for you.
The heuristics go something like:
1. Find out what execution ports your processor has. E.g. it can probably do two 256bit loads from L1 cache each cycle and probably can't do two stores. It can do arithmetic at the same time. Beware collisions between your arithmetic and address calculations.
2. Look for some indication of what the register files are - you don't want to read from a register immediately and probably don't want to wait too long either, and there's a load of latency hiding renaming going on in the background. This one seems especially poorly documented.
3. Aim is to order instructions so that the dynamic scheduler has an easier time keeping the ports occupied and so that stalls on register access are unlikely
4. Choosing different instructions may make that work better in a gnarly NP over NP sort of fashion
5. Moving redundant or reversible calculations across branches can be a good idea
The DSP chips are much more fun to schedule in the compiler as branches are usually cheaper and there's probably no reordering happening at runtime.
For really detailed insight into code optimization, the Intel Profiler ($$) gives you a lot of tools for precise instruction scheduling (e.g. an indication of which instructions are stalling during execution of your code, useful analysis of cache miss rates, and which instructions caused those cache misses). ARM also provides a profiler that may do the same for ARM chips, but it is insanely expensive.
You can make do with LINUX stochastic profilers, but it may be helpful to have some utility code that provides dumps of relevant profiling registers for your CPU (e.g. L1, L2,L3 cache missed counts, missed-branch counts, processor stall counts, &c.) I'm not sure what x86 processors provide; but writing code to dump ARM profiling registers proved to be incredibly useful in a recent profiling and optimization misadventure.
Fwiw, unless you're using instructions that don't map well onto high-level languages, it's pretty difficult to beat well-tweaked GCC-generated code by more than a few percent. I imagine LLVM is the same. Unless you're writing code whose wellfare depends on whether it's 3% faster than a competitor, it's probably not worth it to drop into assembler.
With a bit of tweaking you can even get all the major C/C++ compilers to generate SIMD code that's consistently annoyingly good from non-SIMD C/C++ by encouraging the compilers to perform SIMD vectorization optimizations.
The other way to learn is to do. Profile EVERYTHING with a stochastic profiler. Tweak based on your necessarily limited understanding of the architecture. Profile again to confirm that your optimization actually is valid. Repeat until done.
First, Godbolt is a useful tool for understanding what a particular compiler will do with your code with different switch set.
Second, it's a useful tool for communicating because you can enter source, set some switch and see some output. You can (and we do) get a persistent shortened url to submit on StackExchange, HN, … so that people concretely know what you're complaining about.
Godbolt should get the ACM System Software Award.
https://ofrak.com/tetris/
I didn't do much ARM before working on the game, but since playing a lot, I'm very quick at reading disassembly, even for instructions not present in the game. It might help you to do the same – the timed game aspect forces you to learn to read the instructions quickly.
The game is like Tetris, but the blocks are ARM assembly instructions. As instructions fall, you can change the operand registers. Locking instructions into the .text section executes them in a CPU emulator running client-side in the browser, so you can immediately see the effects of every action. Your score is stored in memory at the address pointed to by one of the registers, so even though you earn points for each instruction executed without segfaulting, the true goal is to execute instructions that directly change the memory containing the score value.
When I released it a bit less than a year ago, I posted it to Hacker News as a Show HN:
https://news.ycombinator.com/item?id=37083309
What kind of job?
It very thoroughly describes Cortex M0 assembly language and it also touches the concept of multiprocessor programming. And you just need two Raspi Picos (one to serve as programmer) which are very available.
GCC does, however, know what to do with a .s file, so you can write your assembly routines outside your C(++) source and just compile them in like a C module, which is what I did last time I was hardcore slinging x86 opcodes.
It's very easy to get the constraints wrong and have the aggregate still "work", until unrelated changes months later perturb the register allocation slightly such that it no longer runs as hoped.
It's documented and usable but working with it is never a very good time.
A hidden feature of the D inline assembler is the compiler knows which registers are read/written, so there's no need for the programmer to explicitly specify it. The compiler will also figure out the right addressing mode for local variables, so the programmer is relieved of that, too.
I proposed doing the same for printf - have the compiler example the arguments and insert the appropriate formats in the format string.
The one in the parent comment does not; it's basically a black box and the compiler has to save and restore the entire state of the function around it. Total performance killer unless you write large chunks of code in inline assembly (and then you're probably better off just using an assembly file).
> an instruction set reference
I think the programming manual just as important, especially if you're working with mixed asm and c code.
One way to think about it: the C programming language has a runtime environment that is everyone's responsibility to maintain. A bug-free compilation should ensure this is protected.
Without the all seeing machine help, it's all on you not to interfere with this environment by following the specified calling conventions.
Also, don't try to get too tricky. I saw a really bad bug where someone thought it wise to use a macro, to place a (C language) add operation directly before a function, then it calls it, and first instruction checks the overflow flag. The thing is C has no idea what you're doing, and doesn't guarantee that it will happen like that. In fact the assembly language couldn't even do certain register swaps for function calls without clearing the flag.
So, basically it "seemed to work" for years. A new compiler caused it to fail testing. Yes, this was a terrible idea and you should no better, but it's important to understand this challenging to predict and "testing resistant" risk. Extreme misuse can help illustrate the point.
P.S. I actually have come to favor asm code for asm, for the writing of asm code that is not trivial. It's mostly a preference and a style thing. Developers are more likely to think about the fact that the code has to play nice with C, instead of assuming some help. You should know better, but it can feel like everything is playing nice.
For long out of date assembler this YouTube channel: https://www.youtube.com/@ChibiAkumas
For modern assembler this YouTube channel: https://www.youtube.com/@WhatsACreel
Note that chibiakumas also covers RISC-V, which is about as modern as it gets.
https://www.agner.org/optimize/#manuals
The one resource they don't list is the ISA manual which is called the Principles of Operation which the latest version can be found here: https://publibfp.dhe.ibm.com/epubs/pdf/a227832d.pdf
It is actually pretty amazing at how easy it is to learn other architectures once you understand how one or two work.
6502 assembly on the Commodore 64 was good fun.
Pretty simple ISA.
Its tough to learn on x86 or ARM, because there has been a lot of standardization towards C, and that is sorta the expected low level API/ABI even at the kernel level in 2024. Calling conventions have been standardized, and really very little actual programming is done manually in assembly these days. So its sort of artificial trying to learn assembly on those ISAs these days. Certainly there are not many APIs specified in assembly. Perhaps one exception would be compiler/linker backends, but that is a whole other can of worms you've gotta learn if you go that route.
IBM Z is different in that respect. It makes it probably a more difficult and unwieldy platform overall if you just wanna make a quick app and ship (why I'm not advocating you all go out and buy mainframes lol), but for learning how to program assembler "in the real world", its the last bastion of a much older school of programming and a great learning environment that will challenge some of the assumptions you maybe have picked up that actually come from Unix or C and not the ISAs themselves.
Its also, in my humble opinion, a pretty nice macro assembler to actually work with. Its had a lot of development over the years (perhaps as a result of it remaining a significant force in the systems programming world for mainframes long after similar programming interfaces were quietly retired on newer platforms), and so I quite like the actual assembler itself, now known as IBM HLASM.
I tend to recommend newcomers pick up a free pdf book from here by the esteemed John Ehrman (RIP)
https://idcp.marist.edu/assembler-resources
along with the POPs specified above. And make an account on zXplore or similar and play around after doing the (very short) introductory portion.
I think for compiler+linker backends it's really important to have a decent reading level of assembly. In llvm, most of the work will be in instruction selection and you'll then read the results. With lld you write a very stylized handler with some switch statements and very stylized macro incantations. And read the results with dis and elfdump.