I did a bit of x86 as stuff 20 years ago but hated it, now I wanted to teach my daughter some c and assembler and was thinking between arm and riscv, but riscv is just a joy to teach (I made a riscv assembler boardgame to help with the task https://punkx.org/overflow/)
Recently I was rewatching Hackers(1995) and I also got excited about the same quote:
> “RISC architecture is going to change everything.” — Acid Burn
After spending some time with esp32s and riscv assembler, I think its more true than before :)
Note this is an older (pre-ratification) cheatsheet.
You can tell because sbreak/scall. They are ebreak/ecall in the final version.
AIUI, the reason the name was changed is that they're not necessarily calls to the supervisor. E.g. they could be calls to machine mode, or the hypervisor. And some chips only implement M mode.
68000 is, in many ways, the pinnacle of assembler for programming, but RISC-V is pretty fun, too. I hope RISC-V tempts a few more people to try asm programming (again).
I got into 68000 programming quite late (6yr ago), but I have been enjoying it (so far Amiga, Atari ST, rosco-m68k). It is a very programmer-friendly instruction set architecture.
RISC-V I started playing with more recently (early 2023, thanks to VisionFive 2), and it feels like my old favorite (MIPS), without the baggage MIPS carried.
It is a pleasure to work with this amount of GPRs and the comfortable alternative names for them that the official abi offers. I am loving it so far.
I expect to have RVA22+V hardware soon (Milk-V Oasis). Very much looking forward to playing with the vector extension on that.
Yep, same, I am keeping an eye on Oasis, but to run powerful GPU drivers (much user space would have to be ported from c++ to hand written risc-v assembly, SDK included). Don't rush it though, concurrent access and memory coherence of device memory is still not finalized.
I have been coding kind of a lot x64 recently, the limitation of 16 GPRs has been painful. I am sure that when I will crank up on rv64 assembly programming, those 32GPRs will feel like fresh air.
In the other hand, I am not fond of the ABI register names, and the pseudo-instructions involving mini-compilation. I'll stick to xNUMBER register names and won't use pseudo-instructions. Like I will avoid any abuse of the preprocessor.
>In the other hand, I am not fond of the ABI register names
Why? They're simple substitution, and very helpful with following ABI.
>and the pseudo-instructions involving mini-compilation.
Again, why? These aren't specific to the assembler used, but rather, defined in the specification itself. This means they are reliable, and will always be there for as long as you use a RISC-V compliant assembler.
They are thus also the register names you will see in disassembler output, debuggers and other tools.
Also, you might be interested in this new RVA22+V board[0].
The standard pseudo-instructions are not just standard. They express idioms that get treated differently, sometimes also by hardware.
For example `li` gets expanded by the assembler into `liu` and `addi` which on larger RISC-V cores get recognised and fused back into a single op.
Using `xori` instead of `addi` would have had the same result but wouldn't get fused.
Next, some idioms get recognised and automatically assembled into "compressed" 16-bit instructions to save space. For example "mv rd,rs" and "addi rd, rs, 0" both get assembled into "c.mv rd,rs". And on a larger RISC-V core, "c.mv" could be only a register rename in the decoder, thus taking 0 cycles.
RISC-V, being an ISA worldwide royalty free standard, is meant for assembly writting. The main reason, above the "comfort" reason of C and similar, was ISA abstraction which has no meaning in the RISC-V realm. The middle ground is those very high level language interpreters (python/lua/ruby/javascript/etc) written directly in RISC-V assembly (without abuse of any assembler preprocessor, ofc).
I am currently writting my own rv64 on x64 virtual machine process, to code my programs in rv64 and not anymore in x64 in order to be "real hardware ready".
BTW, anybody knows a EU based distributor of milk-v duo boards with the cv1800 SOC (the one without ARM cores and a rv64 MCU) which I can contact using my domestic email server?
> ProTip: Hexadecimal literals are prefixed with 0x.
I love the idea that someone could get to this page and not already know that!
Also this nicely highlights my pet peeve with assembly:
add rd, rs1, rs2 # rd = rs1 + rs2
It's very difficult to remember which parameter is the destination etc. IMO it would be much nicer if assembly had just a little more syntax for that sort of thing. E.g.
rd = add rs1, rs2
t0 = li 5
Just so the destination register is obvious. Ah well, nobody's going to do that. Assembly parsing is a total mess; there's no official grammar or anything - it's just "what GCC/LLVM do".
If you want fun, there's the x86 assembly syntax where the destination is the first register, and the x86 assembly syntax where the destination is the last register. One is the syntax as is used in official documentation (the Intel and AMD manuals), most reverse engineering tools, etc. The other is the syntax most commonly used in practice because it's what gcc defaults to and actually isn't documented (which gets into a problem when you start running into what gcc figured was the best way to adapt AVX-512 EVEX stuff into assembly).
So there's a lot of times where I'm staring at x86 assembly and going "wait, which version is this? the one that does destination first or destination second?"
> and the x86 assembly syntax where the destination is the last register.
ITYM AT&T :). The idea is that the basic grammar is common across architectures to help compiler backend authors. The historical reason for the ordering is because that’s how it was on the PDP-11, the “mother” assembly. And all AT&T/GNU versions preserve this ordering regardless of the vendor format.
> The other is the syntax most commonly used in practice
It didn’t always used to be this way. In the dark ages before NASM, MASM was a top warez.
And depending on what you’re doing I don’t think Intel syntax is uncommon, gas will even accept it for the most part these days.
> "wait, which version is this? the one that does destination first or destination second?"
There must be some mnemonic to associate sigil vomit with destination last. Shitty sigils come out the ass?
> The historical reason for the ordering is because that’s how it was on the PDP-11
True, I think. I mean, that certainly was the case for PDP-11 and VAX asm. 68000 too (pretty much a 32 bit, 16 register PDP-11). Whether that was the actual reason is more debatable.
> And all AT&T/GNU versions preserve this ordering regardless of the vendor format.
False. GNU `as` puts the destination register first for all of Arm32, Arm64, MIPS, PowerPC, RISC-V. Every RISC ISA, as far as I know. Except for store instructions, where the source register is first.
Yes absolutely right about the modern RISC archs. I should have just said AT&T not AT&T/GNU. The difference is that x86, VAX and SPARC were AT&T UNIX porting efforts prior to or alongside GNU, and GNU sort of chose to inherit that. The newer RISC architectures did not have the hand/influence of AT&T as far as I know.
As far as the operand ordering with the x86, the thing is they did flip it from the Intel syntax and the beginnings of this port date back to Version 7 (1978), a few years before GNU.
So what was the reason other than it looked like the incumbent/precedent PDP-11? I haven't heard it being much more than circumstance.
> I love the idea that someone could get to this page and not already know that!
Might be worth noting because it is different in other assembly languages. In e.g. 6502 and MC680x0 asm, you'd first prefix with # to differentiate between an address (to load from and/or store to) and an "immediate" constant number, and then $ to denote hexadecimal.
In x86 assembly (Intel style), you instead suffix the number with the letter h.
> rd = add rs1, rs2
There are assembly languages for a few architectures that did/have that form or a similar one. Itanium assembly had instead the form:
Most architectures have something. Sparc had one (I still have the manual), PPC, 68k. I would even say x86 does as well but you can’t force its adoption, what AT&T and GNU wants to do on their own can’t be prevented. AT&T I suppose had the goal of making it all consistent, but I’m not sure if that was an improvement. Though I know of its vocal defendants.
RISC-V might be the exception more than anything, but they have a defacto syntax used throughout the spec.
Analog Devices DSPs and Itanium are examples with the = token.
That document is grossly unfinished and doesn’t even appear to specify syntax outside pseudo ops and a few other things. In a few places it refers to the output of objdump, which I think is close enough to saying “whatever gcc does”.
That's a valid idea for RISC-V, but not always applicable. For example x86 `ADD` only accepts two operands, so `add rax, rbx` would add `rbx` to `rax` in place. `rax = add rax, rbx` would be a "faithful" translation, but `rax = add rbx, rcx` would be invalid (AFAIK there is still no three-operand version of scalar `ADD` in x86). `rax = add rbx` would be less redundant but way more confusing.
Another alternative is to mark the output in the operand position. Say, `add rd!, rs1, rs2`. I relate this to x86 `LEA` which is effectively an arithmetic operation but encoded like a load operation, hence a weird syntax `lea rax, [rbx + 42]` even though no memory access happens. Maybe that might be useful in the other way around.
> (AFAIK there is still no three-operand version of scalar `ADD` in x86)
Indeed, a 3-operand ADD is one of the extensions planned for Intel APX. But unless you need the output flags, an ordinary LEA suffices for adding two registers and storing the result in a third.
Because from the CPU perspective, «+» is ambiguous as there is not one «addition» but many:
- signed add
- unsigned add
- add and carry
There are a few others in other ISA', then there are the operand sizes (byte, half-word, word, long word etc) and the «+» operator does not capture the operand size nor the specifics whereas
- rd = rs1 addu8 rs2
makes the intention clear: «add the lower 8 bits from rs2 to rs1, don't set the sign bit when overflown and store the product in rd».
Moreover, the «+» operation is commutative and the CPU instructions are not, e.g.
- rd = rs + #10
and
- rd = #10 + rs
mean two completely different things for the CPU and the latter does not even have an encoding for it. The assembly processor is not the right place to place the smarts in to figure out the programmer's intention, either, as it is a very straightforward 1:1 assembly syntax to the ISA encoding translator.
etc. The + stands out, more clearly indicating addition. (to me anyway)
As for commutativity of +, it's not necessarily true. It depends entirely on what underlying operation + denotes. It's perfectly reasonable to use it for string concatenation, and that clearly isn't commutative. But if it's not in the case of an ISA, that's fine, just have the assembler reject it.
AMD 29k (its descendants are still alive) has two further ADD operations:
– ADDU – IF unsigned overflow THEN trap (out of range), and
– ADDCS – IF signed overflow THEN trap (out of range).
The ADD instruction family in the HP PA-RISC 2.0 is, of course, one of the best ones out there:
ADD,cmplt,carry,cond r1,r2,t
Purpose: To do 64-bit integer addition and conditionally nullify the following instruction.
General register r1 and general register r2 are added. If no trap occurs, the result is placed in general register 1. The variable «carry_borrows» in the operation section captures the 4-bit carries resulting from the add operation. The completer, «complt», specifies whether the carry/ borrow bits in the PSW (the processor status word) are updated and whether a trap is taken on signed overflow. The completer, «carry», specifies whether the addition is done with carry in.
So, under a certain set of conditions, a PA-RISC «add» operation, other than yielding an add product, can:
– Can result in a trap on a signed overflow.
– Can nullify the following instruction.
An instruction mnemonic would like this:
ADD,DC,TSV,C,<=,N r1, r2 – «add where r1 is less than or equal to negative of r2, and trap if specified conditions (DC,TSV) were met and nullify the following instruction»
HP PA-RISC 2.0 also has an «add and branch» instruction, naturally, embellished with branch conditions, «cond»:
ADDB,cond,n r1, r2, target
If «n» flag is set, the «add and branch» will also nullify the following instruction, e.g.
ADDB,*<=,N r1, r1, label_1
There are also an «add immediate left» instruction (a left shift and add a constant) and «halfword parallel add» (adds multiple halfwords in parallel with optional saturation).
How does one encode all of that with a «+» operator?
What about adding two decimals? The decimals are not used in modern CPU architectures, but they used to be a commonplace and were encoded by completely separate instructions.
The bottom line is: the assembly language is a slightly more human friendly (e.g. «add r1, r2, r3») interface into the bit code (e.g. a fictional opcode of «0xf500010203» for «add r1, r2, r3») and, consequently, into the internal CPU machinery and its state, and is not a high programming language nor a testamenet to the laws of mathematics.
I appreciate the detail you've given and you clearly know your stuff, but I think your looking at it too deeply, at least compared to me. Basically, use a familiar notation where a familiar notation would be appropriate. Your complex ad example is a good case where it probably isn't.
> What about adding two decimals?
rx = ry +bcd rz
I'm really thinking of simple, simple changes. As you point out some situations aren't appropriate for it, in other cases maybe it is. Or maybe I'm just plain wrong and it never is appropriate. But it's just a suggestion and worth considering, no?
> […] where a familiar notation would be appropriate.
And this is the problem I have been trying to point out. «+» comes from math, and – in the world of math – there are no bytes, no half-words, no overflows, no carry overs, no branches and no traps, and the imaginary «register» size is infinite – an addition always succeeds however large is the number. There are no decimals, no floating point numbers, either – in math, there is no distinction. There is just a lone exception being the handling of the explicit infinity values (-∞ and +∞). «Add and branch» or «add and trap» simply do not exist in math – math is abstract, and computing is concrete.
That is not the case in computing. The semantics of «add» varies across different CPU architectures, as the assembly language exposes the internal machinery and the internal state of a given CPU which may or may not be appropriate for another CPU. For example, RISC-V does not support the integer overflow flag, and most other CPU's do. Assembly is a 1:1 representation of the binary code for a given CPU, and that is where it stops.
In fact, I do find the charm in your proposal being along the lines of «r3 = r1 +.u8 r2», «r3 = r1 +.c r2 or branch label1», «r3 = r1 +.c r2 or trap overflow» etc. It will not be assembly though, more of a meta-assembly, which is fine. The real trouble is that the grammar will be able to handle just one ISA and might quickly become complex and unwieldy if ported to another ISA that has a rich set of addition operations that do not fit in the narrow constraints of such a design.
I'm glad you like the possibility of using the addition symbol or some variation on it. The idea that it would be ISA independent never crossed my mind, and perhaps just as well because as you pointed out, it would bomb. For that (ie. portability), use an HLL.
I fully understand your criticism, entirely correct, that machine arithmetic does not behave like mathematical arithmetic. I diverged from you where you say that '+' is mathematical therefore has a predetermined meaning. It has a conventional mathematical interpretation, often related to number addition, but it is a convention only and can be bent as far as you like provided you're clear about it (and a bit of common sense too; defining '+' as a square root operation would be pretty bloody stupid). No symbol in mathematics has any intrinsic meaning, including '+'.
Obviously that doesn't generalize for opcodes with 0, 1, 3 or more input operands. As a concrete example, consider a fused multiply-add operation: `rd = fma rs1, rs2, rs3` is consistent, but how would you convert it to an infix notation?
pre-fix is the finest notation for many reasons.
So, Intel syntax makes more sense than ATT syntax.
Even C functions are pre-fix. [function_name arg1, arg2 ...]
If schools were teaching pre-fix, we would be in better position to appreciate Intel syntax.
Higher maths uses pre-fix, for example:
y=f(x)
unfortenately, above line is not pure prefix...
= y f(x) would be pure pre-fix.
Assembly is meant to be 1:1 with machine code, which makes writing an assembler extremely easy as long as you know the architecture. Machine code doesn’t have things like equal signs, it’s literally just a series of bytes (an opcode and operands)
Nonsense. Machine code doesn't have things like commas, brackets and letters, yet we use those in assembly. There's zero reason why you couldn't do my proposal.
Also assembly mnemonics aren't even 1:1 with instructions. Pseudoinstructions do pretty much anything, and even something like `add` can assemble to two different instructions depending on the arguments.
As for writing an assembler being "extremely easy"... yeah no. There's no formal grammar so you're going to be reverse engineering LLVM and GCC's hilariously messy assemblers. Or more realistically, guessing and building an enormous test suite. Not easy at all.
I mean if you want to be 1:1 with machine code, then don't look at the "J" Jump or "B" Branch instruction formats where the constant value is split into pieces and packed in around the registers.
With a '=' all OP is asking for is the wee-est bit of grammar (no more than in some addressing modes in CISC assemblers) and to change the order of the registers, which isn't crazy since immediates are already split.
PS- while the instruction formats look crazy in isolation, there is a nice symmetry between them and if you start thinking about instruction decoding in discrete logic, they are actually quite an elegant design.
Machine code doesn’t have mnemonics either. What’s your point? There are assembly syntaxes with equal signs (The Itanium though ultimately a failure was not obscure and there are a handful of DSP ISAs that use this syntax). Not my cup of tea but your argument is specious.
> Assembly is meant to be 1:1 with machine code,
And that is nonsense as well. RISC-V is a perfect example as it has plenty of pseudo ops. (Or do you actually think that the literal machine code bytes of an add instruction are 0x41 0x44 0x44? - they’re not)
Equal signs or not, macro assemblers have plenty of ergonomic conveniences layered above machine code.
How does an equal sign break any concordance with assembly and the underlying machine code anyway?
It isn’t my definition so your entire rant here is just being argumentative and defensive for no reason. Just because some fringe ISAs have equal signs in their assembler doesn’t change anything either. Assembly is meant to map directly to the way the machine code is written, and ran, so having [opcode] [operand(s)] makes perfect sense and quality of life/syntactic sugar beyond very simple things like variables (which don’t add some crazy ast or other abstractions that make it a compiler) do not make sense for the tool an assembler is.
Very fringe. A huge market failure. Hardware discontinued. Support removed in LLVM (2.6), and the Linux kernel (6.7). Still seems to be hanging on in GCC, though it was initially going to be deprecated in GCC 10.
One of the very few ISAs I've never actually seen a real machine of, let alone used. And I've worked professionally on i960 (Stratus fault-tolerant computer), which not many people can say. Not to mention of course PA-RISC and Alpha and Pr1me and DG Nova/Eclipse (and an M88000 PC) as well as common-as-mud (and which I own examples of) SPARC and MIPS.
That it was a failure is well known. But fringe, as in obscure (which is what I think is salient in this context)? I think that is misleading - that it has an official gcc backend to remove is a sufficient counterargument. Funny you mentioned PA-RISC and Alpha, as both were abandoned by HP and Compaq for it. Stupid (at least HP did backpedal, but not after burning a mountain of cash, it was to replace the PA), but not consistent with fringe. Getting it into the Linux kernel and gcc support was a big deal at the time thousands of man-hours were expended on all of this, all the investment and work was done for the dominant architecture.
Obviously Microsoft was hugely invested in it, but some there thankfully had better ideas and didn’t miss the x86-64 boat, since it already stank years prior to its release.
DEC Alpha was also in the long run a commercial failure. wouldn't call it fringe though. PA-RISC was commercially middling, I also wouldn't call it fringe (both shitcanned for Itanic BTW, for which people are still bitter).
Absolutely correct that mega-millions or billions went into it, both hardware and software, and that it FUD'ed many other ISAs to premature extinction.
It's not that hard to get an ISA into gcc and Linux -- you just need shipping hardware (or a convincing story that there will be) and someone to do the work. For example Andes (Taiwan) and C-Sky (China) both got their proprietary ISAs (nds32, c-sky) into GCC and the Linux kernel. And both switched to RISC-V not long after :_)
Ok I’ll concede that’s a graveyard of obscurity in the gcc tree, but some group of people thought each one of those were worth the effort at some point, possibly delusionally. Do you need shipping hardware even? MMIX has a backend. Itanium doesn’t deserve this label though, shall it live forever in infamy.
I did a bit of x86 as stuff 20 years ago but hated it, now I wanted to teach my daughter some c and assembler and was thinking between arm and riscv, but riscv is just a joy to teach (I made a riscv assembler boardgame to help with the task https://punkx.org/overflow/)
Recently I was rewatching Hackers(1995) and I also got excited about the same quote:
> “RISC architecture is going to change everything.” — Acid Burn
After spending some time with esp32s and riscv assembler, I think its more true than before :)
If you havent given it a try yet, there are many articles like the one, and also projects like https://luplab.gitlab.io/rvcodecjs/ or https://riscv.vercel.app/ where you can play with it, or even make your own emulator by learning from other emulators like https://github.com/OpenMachine-ai/tinyfive/blob/main/machine...
this cheatsheet is also very useful: https://www.cl.cam.ac.uk/teaching/1617/ECAD+Arch/files/docs/...