How does the OS and the hardware draw on the screen, actually? All they have is also just calculator stuff, super basic primitives. You can't even do loops in hardware, or even real branches (hardware always "executes both sides" of a branch at once)
Anyways, if you keep digging long enough you eventually end up finding this XKCD https://xkcd.com/435/ =)
Unless you're talking about quantum hardware, that is very much not true. The whole point of transistors is to choose whether to power one part of a circuit or another.
Plus, even for hardware, the solution to all this is to modularize all the way down. One piece of hardware sets up the right state and powers up another piece of hardware - this type of logic doesn't stop at the OS level. For drawing on the screen, ultimately you reach a piece of hardware that lights up in one of three colors based on that state - but all the way there, it's the same kind of "function calls" (and even more indirection, such as communication protocols) on many levels.
Even if you add pipelining, the basic textbook design will stall the pipeline when it encounters a conditional/computed jump instruction. Even if you add basic speculative execution, you still don't get both branches executed at once, necessarily - assuming you had a single ALU, you'd still get only one branch executed at once, and if the wrong one was predicted, you'll revert and execute the other one once the conditional finishes being computed.
I'm talking on a lower level than the clock cycle or instruction. Let's say circuit A takes X and outputs foo(X), and circuit B takes X and outputs bar(X). We want to build a circuit that computes baz(X, Y) = Y ? foo(X) : bar(X), where X is available before Y is. Instead of letting Y settle, powering up one of the circuits A or B, and sending X into it, we can instead send X to circuits A and B at the same time, then use Y to select whichever output we want.
One other common pattern for implementing conditional logic is to compute a boolean expression in which the control signal is just another input variable. In that model, to compute Y ? foo(X) : bar(X), we actually compute baz(X, Y) whose result is the same. This is very commonly how ALUs work.
And the other very common pattern is to split this in multiple clock cycles and use registers for intermediate results. If you don't have two circuits A and B, only one circuit that can compute either A or B based on a control signal (such as simple processors with a single ALU), this is the only option: you take one clock cycle to put Y in a register, and in the next clock cycle you feed both Y and X into your single circuit which will now compute either A or B.
In hardware the equivalent of a ternary is a mux, which can be made from a lot of parallel instances of
out0 = (a0 & cond) | (b0 & ~cond)
Or in other words, both branches must be computed and the correct value is chosen based on the condition.Similarly, a two bit adder is not going to have all 4 possible states internally or for some time - as soon as the input voltage is applied to its inputs, its output voltages will correspond to the single result (disregarding the analog signal propagation, which I assume is not what you were talking about).
Similarly, a conditional jump instruction will not be implented natively by computing both "branches". It will do a single computation to set the instruction pointer to the correct value (either current + 1 or destination). Now sure, speculative execution is a different matter, but that is extra hardware that was added late in the processor design process.
The conditional jump is a great example actually. Typically this would be implemented by having one block compute PC+<instruction size> and another block compute the jump target and then choosing between the two using a mux
Secondly, if we think about the instruction decoding itself, it should become pretty clear that even if the hardware of course always exists and is always going to output something, that's not equivalent to saying it will compute all options. If the next instruction is `add ax, bx`, the hardware is not going to compute both ax + bx and ax - bx and ax & bx and ax | bx and so on, and then choose which result to feed back to ax through a mux. Instead, the ALU is constructed of logic gates which evaluate a single logical expression that assigns each output bit to a logical combination of the input bits and the control signal.
This non-optimising JIT has been far, far easier than all the scary articles and comments I've seen led me to believe.
I'm already in the middle of making it work on both Aarch64 and RISC-V, a couple weeks in.
But for a simple taste, the push to stack function currently looks like this. (All the emit stuff just writes bytes into a mmap that gets executed later.)
void compile_push_literal(Value val) {
#if ARCH_X86_64
emit_bytes((uint8_t[]){X86_MOV_RDI_IMM64_0, X86_MOV_RDI_IMM64_1}, 2); emit_uint64_le(val);
emit_bytes((uint8_t[]){X86_MOV_RAX_IMM64_0, X86_MOV_RAX_IMM64_1}, 2); emit_uint64_le((uint64_t)push);
emit_bytes((uint8_t[]){X86_CALL_RAX_0, X86_CALL_RAX_1}, 2);
#elif ARCH_ARM64
uint64_t imm = val;
emit_uint32_le(ARM64_MOVZ_OP | (ARM64_REG_X0 << 0) | ((imm & 0xFFFF) << 5));
emit_uint32_le(ARM64_MOVK_OP_LSL16 | (ARM64_REG_X0 << 0) | (((imm >> 16) & 0xFFFF) << 5));
emit_uint32_le(ARM64_MOVK_OP_LSL32 | (ARM64_REG_X0 << 0) | (((imm >> 32) & 0xFFFF) << 5));
emit_uint32_le(ARM64_MOVK_OP_LSL48 | (ARM64_REG_X0 << 0) | (((imm >> 48) & 0xFFFF) << 5));
uint64_t func_addr = (uint64_t)push;
emit_uint32_le(ARM64_MOVZ_OP | (ARM64_REG_X1 << 0) | ((func_addr & 0xFFFF) << 5));
emit_uint32_le(ARM64_MOVK_OP_LSL16 | (ARM64_REG_X1 << 0) | (((func_addr >> 16) & 0xFFFF) << 5));
emit_uint32_le(ARM64_MOVK_OP_LSL32 | (ARM64_REG_X1 << 0) | (((func_addr >> 32) & 0xFFFF) << 5));
emit_uint32_le(ARM64_MOVK_OP_LSL48 | (ARM64_REG_X1 << 0) | (((func_addr >> 48) & 0xFFFF) << 5));
emit_uint32_le(ARM64_BLR_OP | (ARM64_REG_X1 << 5));
#elif ARCH_RISCV64
emit_load_imm_riscv(val, RISCV_REG_A0, RISCV_REG_T1);
emit_load_imm_riscv((uint64_t)push, RISCV_REG_T0, RISCV_REG_T1);
emit_uint32_le((0 << 20) | (RISCV_REG_T0 << 15) | (RISCV_F3_JALR << 12) | (RISCV_REG_RA << 7) | RISCV_OP_JALR);
#endif
}
Creating an assembler with Lisp syntax and then using that to bootstrap a Lisp compiler (with Lisp macros instead of standard assembler macros) is one of those otherwise pointless educational projects I’ve been wanting to do for years. One day perhaps.
You already have the assembler with Lisp syntax covered.
Add some macro support on top, and you can start already implementing the upper layer for your Lisp.
Naturally there are already a couple of attempts at that.
Our approach was to model the compiler IR into Assembly macros, and follow the classical UNIX compiler build pipeline, thus even though it wasn't the most performant compiler in the world, we could nonetheless enjoy having our toy compiler generate real executables in the end.
I mmap, insert, mark as executable and done. Patchjumping and everything "just works".
I'm not modifying my own process, so there's no hardening issues. Just modifying an anonymous memory map.
IIRC, debug.com could be used to create programs using machine lang.
The reasoning being that COM files were a plain memory dump starting at offset 100H, thus you would type the code in memory and then dump it.
To me, it looks like some kind of complex tetris game. I guess we could maybe represent a program as such, with pieces for registers, instructions, etc.
Yet, the tooling we have is very terse, and textual.
It quickly becomes tedious to do large programs, not really hard, just unmanagable, which is precisely it should be taught as a first language. You learn how do do simple things and you learn why programming languages are used. You teach the problem that is being solved before teaching more advanced programming concepts that solve the problem.
The best ISA for learning is probably the Motorola 68000, followed by some 8-bit CPUs (6502, 6809, Z80), also probably ARM1, although I never had to deal with it. I always thought that x86 assembly is ugly (no matter if Intel or AT&T).
> It quickly becomes tedious to do large programs
IME with modern tooling, assembly coding can be surprisingly productive. For instance I wrote a VSCode extension for 8-bit home computers [1], and dog-fooded a little demo with it [2], and that felt a lot more productive than back in the day with an on-device assembler (or even typing in machine code by numbers).
[1] https://marketplace.visualstudio.com/items?itemName=floooh.v...
[2] https://floooh.github.io/kcide-sample/kc854.html?file=demo.k...
Also, the Z8000 looks quite interesting and like the better 16-bit alternative to the x86, but it never took off: https://en.wikipedia.org/wiki/Zilog_Z8000
- assembly instructions are executed in the order they appear in in the source code
- an x86 processor only has a handful of registers
- writing to a register is an instruction like any other and will take roughly the same time
- the largest registers on an x86 processor are 64-bit
It doesn’t, and out-of-order CPUs don’t do that. https://en.wikipedia.org/wiki/Out-of-order_execution: “In this paradigm, a processor executes instructions in an order governed by the availability of input data and execution units, rather than by their original order in a program.”
For instance you don't need to be afraid that an instruction uses garbage inputs just because a previous instruction hadn't finished computing an input value to the instruction. At worst you'll get a pipeline stall if the CPU can't fill the gap with out-of-order executed instructions.
On some CPUs it does get tricky once memory is involved though (on ARM, but not on x86).
> […]
> On some CPUs it does get tricky once memory is involved though (on ARM, but not on x86).
https://en.wikipedia.org/wiki/Memory_ordering:
“Among the commonly used architectures, x86-64 processors have the strongest memory order, but may still defer memory store instructions until after memory load instructions.”
The reality is that any assembler simple enough to be taught as your first contact with programming will leave you with a wrong intuition about how modern processors work, and thus a wrong intuition about the relative performance of various operations.
Having no intuition about something is better than building a bad intuition, especially at the beginning of your learning journey.
I agree about tooling, I made a pacman game in a dcpu16 emulator in a couple of days.
https://fingswotidun.com/dcpu16/pac.html
I experimented with a fantasy console idea using an in-browser assembler as well. https://k8.fingswotidun.com/static/ide/I think you can build environments that give immediate feedback and the ability to do real things quickly in ASM. I would still recommend moving swiftly on to something higher level as soon as it started to feel like a grind.
I wouldn't teach it first, but after a person knows the basics in another language, seeing how it all actually works can be fun.
So why teach someone a language that doesn't have if, while, (local) variables, scopes, types, nor even real function calls?
It's a very nice exercise for understanding how a computer functions, and it has a clear role in education - I'm not arguing people shouldn't learn it at all. But I think it's a terrible first language to learn.
You can teach them how to implement function calls, variables and loops using assembly, to show them how they work under the hood and how they should be thankful for having simple if in their high level languages like C.
They would understand code and data are in the same place, that all flow control effectively boils down to a jump, and they have a _more_ accurate picture of the inside of a machine than anyone starting out with Python or JavaScript could hope for.
Having spent 25 years to get to assembler, I wish I'd started it sooner. It's truly a lovely way to look at the machine. I'll definitely be teaching my kids how to program in assembly first (probably x86-16 using DOS as a program launcher)
Be very careful that you're not going to just kill enthusiasm for programming as an activity entirely with this approach.
I see this happen a lot (I did a lot of robotics/programming mentoring), and then adults wonder why their kids don't like any of the stuff they like - and the reason is that the adult was really a dick about making them learn the things the adult liked, and ignored most of the fun aspects of the activity, or the wishes of the kid.
This can be done with any programming language.
The point of teaching assembly isn't for someone to memorize all the details of any particular instruction set. It's about conceiving of the decomposition of problems on that level. It's about understanding what data is, so that when the student later learns a higher-level programming language, it sets expectations for what happens when you open a file, what kind of processing has to be done, etc. It's the basis for understanding abstractions that are built upon all those 1s and 0s, about the way that a program implicitly assigns semantics to them.
(This is best done with a toy assembly language, not one that comes anywhere near reflecting the complexity of modern CPUs. Anything to do with the practical considerations of modern optimizing compilers is also missing the point by a mile.)
These are all things that are your goals, as the adult and teacher.
The student who wants to engage with programming and software likely has other goals in mind.
Skip all the crap you just mentioned, focus on helping them achieve their goals. I think you'll find those are usually more in the realm of "I want to make a game" or "I want to show my stuff to friends on a website" or "I want to make the computer play music" or [insert other high level objective that's not "learn about bits and bytes"].
Will that involve the stuff you mentioned? Sure will, and a student who feels like they're achieving the thing they want by learning that stuff is engaged.
But a student who gets to just sit there and listen to you drone on and on about "abstractions" and "instructions sets" and "data is code" and "semantics" all to end up with a complicated file that functionally just adds two numbers together? That student is usually bored and disengaged.
And the student who doesn't learn these concepts will inevitably run into a roadblock soon thereafter.
> But a student who gets to just sit there and listen to you drone on and on about "abstractions" and "instructions sets" and "data is code" and "semantics"
You don't "drone on" about these things. You introduce them as it makes sense, by pointing things out about the first programs as they are developed. You don't talk about abstracting things and assigning semantics; you do it, and then point out what you've done.
So we agree that maybe dragging them right into the start by teaching assembly (because it's good at teaching those things) as the first time language isn't the best strategy?
At no point will I argue against learning it. Knowing how machines work is great, and I think going "down" the stack benefits a lot of developers ONCE they're developers and have an understanding that programming and computers are things they like and want to do.
But first you have to foster enthusiasm and nurture interest. You don't do that by declaring that you're going to teach your kids assembly... you do that by listening to your kids interests in the space and helping them achieve their goals.
Reading is easier when you know your latin roots, but we don’t make kids speak latin before See Spot Run even if it would help.
Frankly, a more accurate picture than those starting in C have, too.
That sounds like the perfect beginner language! If they survive that experience, they'll do very well in almost any type of programming, as it's mostly the same just a tiny bit less tedious. A bit like "hardening" but for programmers.
I guess it's as much "gatekeeping" as being required to formulate plans and balance tradeoffs is "gatekeeping".
This is like learning to read by first being asked to memorize all the rules of grammar and being quizzed on them, or being forced to learn all the ins and outs of book binding and ink production.
It's tedious, unproductive, miserable.
There's very little reward for a lot of complexity, and the complexity isn't the "stimulating" complexity of thinking through a problem; it's complexity in the sense of "I put the wrong bit in the wrong spot and everything is broken with very little guidance on why, and I don't have the mental model to even understand".
There's a perfectly fine time to learn assembly and machine instructions, and they're useful skills to have - but they really don't need to be present at the beginning of the learning process.
---
My suggestion is to go even farther the other way. Start at the "I can make a real thing happen in the real world with code" stage as soon as possible.
Kids & adults both light up when they realize they can make motor turn, or an LED blink with code.
It's similarly "low level" in that there isn't much going on and they'll end up learning more about computers as machines, but much more satisfying and rewarding.
> it's complexity in the sense of "I put the wrong bit in the wrong spot and everything is broken with very little guidance on why, and I don't have the mental model to even understand
That's the nice thing about assembly, it always works, but the result may not be as expected. But instead of having a whole lot of magic between what is happening and how you model it, it's easy to reason about the program. You don't have to deal with stack trace, types, garbage collection and null pointer exception. Execution and programming is the same mental model, linear unless you said so.
You can start with assembly and then switch to C or Python and tell them: For bigger project, assembly is tedious and this is what we invented instead.
AVR's assembly is quite mediocre, with 120+ something instructions, with lots of duplication among them (IIRC — it's been... many years already), and some people swore by PIC which only had 35 instructions to remember. But it was still easier than lobotomizing oneself by trying to write a Win32 application in x86 assembly (which came later... and went to the trash bin quickly while microcontrollers stuck for much longer).
I see lots of people become pretty helpless when their framework isn’t working as expected or abstraction becomes leaky. Most people don’t really need to know assembly in order to get past this, but the general intuition of “there is something underneath the subtraction that I could understand” is very useful.
I don’t think that’s a fitting analogy.
Nearly everyone on the planet uses (basic) arithmetic. Very few use calculus.
By contrast, very few (programmers) use ASM, but nearly all of them use higher level languages.
My first language was BASIC on a V-tech. It's not quite the same but it still was such a fantastic starting point.
I've tried luring people into programming with Python for example and see them get frustrated by the amount of abstractions and indirection going on. I am really starting to like this idea of starting with assembly.
My point is that the analogy with arithmetic vs. calculus doesn't hold.
Nearly everyone uses basic arithmetic in everyday life, and a tiny fraction of those use calculus.
No programmer needs to learn ASM to be able to know how to use higher level languages. And a tiny fraction of them are using actual ASM in their everyday jobs.
Also, I think you can still learn the basic constructs of how languages work at a lower level without every learning actual ASM. There's no way you can learn calculus without an understanding of arithmetic.
The fact that our high level languages compile down to assembly doesn't mean we use assembly in any meaningful sense. My C code will be correct or not based on whether it conforms to the semantics of the C abstract machine, regardless of whether those semantics match the semantics of the assembly language that it happens to compile down to. Even worse, code that is perfectly valid in assembler may be invalid C, even if the C code compiles down to that same assembler code. The most clear example is adding 1 to an int variable that happens to have the value MAX_INT. This code will often compile down to "add, ax, 1" and set the variable to MIN_INT, but it is nevertheless invalid C code and the compiler will assume this value is impossible to happen.
This relationship between a programming language and assembler is even more tenuous for languages designed to run on heavy runtimes, like Java or JavaScript.
And I could go on with other topics. High-level languages, even something like C, are just a completely different model of looking at the world from machine language, and truly understanding how machines work is actually quite an alien model. There's a reason that people try to pretend that C is portable assembler rather than actually trying to work with a true portable assembler language.
The relationship you're looking for is not arithmetic to calculus, but set theory to arithmetic. Yes, you can build the axioms of arithmetic on top of set theory as a purer basis. But people don't think about arithmetic in terms of set theory, and we certainly don't try to teach set theory before arithmetic.
In contrast, you can have a very successful, very advanced career in computer science or in programming without once in your life touching a line of assembler code. It's not very likely, and you'll be all the poorer for it, but it's certainly possible.
Assembly language is much more like learning the foundations of mathematics, like Hilbert's program (except, of course, historically that came a few millenia after).
- machine code
- assembly
- Lisp and Forth
- C
- Pascal
- maybe a short detour into OOP and functional languages
...but in the end, all you need to understand for programming computers are "sequences, conditions and loops" (that's what my computer club teacher used to say - still good advice).
I fully agree - and assembly language teaches you precisely 0 of these.
I think there's value in understanding how high level language constructs like if-else and loops can all be constructed from simple conditional jumps, and that a function call is just a CALL/RET pair with the return address being stored on the stack.
Also, structured programming had to be invented, and working in assembly code makes it clearer why.
It's also food for thought why CPU ISAs never made the leap to structured programming.
And I absolutely agree there is value in understanding the mechanics of how languages are executed. What I disagree with is that this is necessary for being a good programmer, and that it is useful as an initial learning experience.
Programming itself didn't start with assembly. It started with pseudo-code, which was always expressed in a high-level format, and then manually translated to some form of assembly language for a particular physical machine. But people have never designed their programs in terms of assembly - they have always designed them in higher level terms.
But in the end no one learns "assembler". Everyone learns a specific ISA, and they all have different strengths and limitations. Assembler on a 36-bit PDP-10, with 16 registers and native floating point, is a completely different experience to assembler on a Z80 with an 8-bit accumulator and no multiply or divide.
You can learn about the heap and the stack and registers and branches and jumps on both, but you're still thinking in terms of toy matchstick architecture, not modern building design.
It may put people off a programming career, but perhaps that is good. There are a lot of people who work in programming who don't understand the machines they use, who don't understand algorithms and data structures, they have no idea of the impact of latency, of memory use, etc. They're entire career is predicated on being able to never have to solve a problem that hasn't been solved in general terms already.
Plus, it is literally impossible to do any kind of math without knowing arithmetic. It is very possible to build a modestly advanced career knowing no assembly language.
The first graders in my neighbourhood school are currently leaning about probability. While they did cover addition earlier in the year, they have not yet delved into topics like multiplication, fractions, or anything of that sort. What you suggest is how things were back in my day, to be fair, but it is no longer the case.
I've taught people Python as their first language, and this was their exact opinion of it.
When you're an experienced programmer you tend to have a poor gauge of how newcomers internalize things. For people who are brand new it is basically all noise. We're just trying to gradually get them used to the noise. Getting used to the noise while also trying to figure out the difference between strings, numbers, booleans, lists, etc. is more difficult for newcomers than many people realize. Even the concept of scoping can sometimes be too high-level for a beginner, IME.
I like asm from the perspective that, its semantics are extremely simple to explain. And JMP (GOTO) maps cleanly from the flowchart model of programming that most people intuit first.
In particular, Python having generators and making range() be a generator means that in order to fully explain a simple for loop that's supposed to do something X times, I have to explain generators, which are conceptually complicated. When range() just returned a list, it was much easier to explain that it was iterating over a list that I could actually see.
Like if range was used like this:
for i in range 1 to 100:
pass
No one is going to ask how that works internally, so I don't think it's necessary to treat range(1, 100) any differently. For this usage it makes no difference if it's a generator, a list (excepting performance on large ranges), or if the local variable is incremented directly like a C-style for loop.It was already much more powerful than most people writing simple shell script replacements were aware of.
Thing is, very few bother to read the reference manuals cover to cover.
However, I don't agree at all that having strings and numbers as different things was ever a problem. On the contrary, explaining that the same data can be interpreted as both 40 and "0" is mistifying and very hard to grok, in my experience. And don't get me started on how hard it is to conceptualize pointers. Or working with the (implicit) stack in assembly instead of being able to use named variables.
That's why it's such an important first language! Pedagogically it's the foundation motivating all the fancy things languages give you.
You don't teach a kid to cut wood with a table saw. You give them a hand saw!
But hey, what do I know. Im the kind of guy who gets to play with TMA and seriously considers purchasing hydrazine for work. What do I know?
Programming languages are usually designed based on formal semantics. They include constructs that have been found either through experience or certain formal reasons to be good ways to structure programs.
Haskell's lazy evaluation model, for example, has no relationship to assembly code. It was not in any way designed with thought to how assembly code works, it was designed to have certain desirable theoretical properties like referential transparency.
It's also important to realize that there is no "assembly language". Each processor family has its own specific assembly code with its own particular semantics that may vary wildly from any other processor. Not to mention, there are abstract assembly codes like WebAssembly or JVM bytecode, which often have even more alien semantics.
You don't teach Haskell to seventh grader.
But 4 bit assembly driving a few LEDs? That works
Okay but that's not for pedagogical reasons, it's because power saws are MUCH more dangerous than hand saw.
Contrariwise, you don't teach a kid to drill wood with a brace & bit, because a power drill is easier to use.
Not doing this is how you get Electron.
Avoid:
- Z80: at least as a first language. Extended 8080 with completely different syntax, even more messy and unorthogonal than x86!
LD A,(HL) ;load A from address in HL register pair
LD A,(DE) ;load A from address in DE
LD B,(HL) ;load B from address in HL
LD B,(DE) ;invalid!
JP (HL) ;load program counter with contents of HL (*not* memory)
ADD A,B ;add B to A
ADC A,B ;add B to A with carry
SBC A,B ;subtract B from A with borrow
SUB B ;subtract B from A
OR B ;logical-or B into A
etc.
- RISC-V: an architecture designed by C programmers, pretty much exclusively as a target for compiling C to & omitting anything not necessary for that goalStart out at "here's your machine code. Let's understand how x86_64 gets started" and work your way up to "now you have the automation to compile Linux and a modern compiler".
Which would certainly have stops most of the way up for things we usually include.
If I were teaching a general-interest programming course, I'd probably start with just a bit of BASIC to introduce a few broad concepts like variables and looping, then a little assembly to say, "And this is what's going on when you do those things," and then move up the chain. Then when they get to something like C and go to look at the assembly it produces for debugging, they'll at least be familiar with concepts like registers and branching. So not quite the order I happened to do it in, but similar.
Load registers, call DOS or BIOS with 'int', etc. all interactively and with a nice full screen display of registers, flags and memory. Of course entering single instructions to run immediately only gets you so far, but you can also enter short programs into memory and single step through them.
It's too bad nothing like this seems to exist for modern systems! With the CPU virtualization features now available, you could even experiment with ring 0 code without fear of crashing your machine.
[2] to run it under DOSBox, you may need to add the '+b4' command line switch in order to bypass some machine detection code
https://edsim51.com/about-the-simulator/
Very immediate and positive feedback.
Unless you're teaching people preparing for engineering hardware perhaps, I think ASM is absolutely the wrong language for this. The first reason is that programming is about problem solving, not fiddling with the details of some particular architecture, and ASM is pretty bad at clearly expressing solutions in the language of the problem domain. Instead of programming in the language of the domain, you're busy flipping bits which are an implementation detail. It is really a language for interfacing with and configuring hardware.
The more insidious result is that teaching ASM will make an idol out of hardware by reinforcing the notion that computer science or programming are about computing devices. It is not. The computing device is totally auxiliary wrt subject matter. It is utterly indispensable practically, yes, but it is not what programming is concerned with per se. It is good for an astronomer to be able to operate his telescope well, but he isn't studying telescopes. Telescope engineers do that.
In order to successfully program a solution to a problem, it is necessary to understand the system you are working with. A machine-level programming language cuts through the squishiness of that and presents a discrete and concrete system whose state can be fully explained and understood without difficulty. The part where it's all implementation details is the benefit here.
From a functional perspective, you see things properly as a matter of language. When you describe to someone some solution in English, do you worry about a "computational system"? When writing proofs or solving mathematical problems using some formal notation, are you thinking of registers? Of course not. You are using the language of the domain with its own rules.
Computer science is firmly rooted in the formal language tradition, but for historical reasons, the machine has assumed a central role it does not rightly possess. The reason students are confused is because they're still beholden to the machine going into the course, causing a compulsion to refer to the machine to know "what's really going on" at the machine level. Instead of thinking of the problem, they are worrying about distracting nonsense.
The reason why your students might feel comforted after you explain the machine model is because they already tacitly expect the machine to play a conceptual role in what they're doing. They stare and think "Okay, but what does this have to do with computers?". The problem is caused by the prior idolization of the machine in the first place.
But machine code and a machine model are not the "really real", with so-called "high-level languages" hovering above them like some illusory phantom that's just a bit of theater put on by 1s and 0s. The language exists in our heads; machines are just instruments for simulating them. And assembly language itself is just another language. It's domain just is, loosely, the machine architecture.
So my view is that introductory classes should beat the machine out of students' heads. There is no computer, no machine. The first few classes in programming should omit the computer and begin with paper and pencil and a small toy language (a pure, lispy language tends to be very good here). They should gain facility in this small language first. The aim should be to make it clear that the language is about talking about the domain, and that it stands on its own, as it were; the computer is to programming as the calculator is to mathematical calculation. Only once this have been achieved are computers permitted, because large programs are impractical to deal with using pen and paper.
This intuition is the foundation difference between a bona fide compute science curriculum and dilettante tinkering.
Pointers are an abstraction that are no more or less real than any other abstraction. They belong to particular languages, but they are not intrinsic to computer science as such as if they were some kind of atomic construct of the field.
> you can't just put a LISP book on top of an x86 chip [...the rest is confusing...]
I'm not talking about what, in today's contingent market and incidental state of the art, is practical. Obviously, if you want to run any program in any language, you have to target some architecture. The point is that the architecture is utterly incidental as far as the language per se is concerned. Lisp is not "less real" because you need to translate it into machine code. The machine code of a particular architecture is only there to simulate Lisp on that architecture. You can in principle have different architectures with their own machine code that can be used to simulate the very same Lisp.
> Computer science is, to be honest, not interesting or useful without a machine to use it on.
Computer science is very interesting without a machine, but how interesting you find it is neither here nor there. The point isn't to do away with machines, or that the machine has no practical importance. The point is to say that the machine is only a tool, and not the subject matter of computer science.
We give a lot of attention to pointers because electronic computers feature random access memory consisting of small, equal-sized cells of bits, keyed by binary numbers.
"How do I use bits to represent concepts in the problem domain?" is the fundamental, original problem of computer science.
And to teach this, you use much simpler problems.
> ... reinforcing the notion that computer science or programming are about computing devices. It is not.
It is, however, about concepts like binary place-value arithmetic, and using numbers (addresses) as a means of indirection, and about using indirection to structure data, and about being able to represent the instructions themselves as data (such that they can be stored somewhere with the same techniques, even if we don't assume a Von Neumann machine), and (putting those two ideas together) about using a number as a way to track a position in the program, and manipulating that number to alter the flow of the program.
In second year university I learned computer organization more or less in parallel with assembly. And eventually we got to the point of seeing - at least in principle - how a basic CPU could be designed, with its basic components - an ALU, instruction decoder, bus etc.
Similarly:
> It is good for an astronomer to be able to operate his telescope well, but he isn't studying telescopes.
The astronomer is, however, studying light. And should therefore have a basic mental model of what a lens is, how lenses relate to light, how they work, and why telescopes need them.
> It is, however, about concepts like binary place-value arithmetic
That is the original problem of using a particular digital machine architecture. One shouldn't confuse the practical/instrumental problems at the time with the field proper. There's nothing special about bits per se. They're an implementation detail. We might study them for practical reasons, we may study the limits of what can be represented by or computed using binary encodings, or efficient ways to do so or whatever, but that's not the central concern of computer science.
> In second year university I learned computer organization more or less in parallel with assembly.
Sure. But just because a CS major learns these things doesn't make it computer science per se. It's interesting to learn, sure, and has practical utility, but particular computer architectures are not the domain of computer science. They're the domain of computer engineering.
> The astronomer is, however, studying light.
No, physicists studying optics study light in this capacity. Astronomers know about light, because knowledge of light is useful for things like computing interstellar distances or determining the composition of stellar objects or for calculating the rate of expansion or whatever. The same goes for knowledge of lenses and telescopes: they learn about them so they can use them, but they don't study them.
Indeed - you don't actually need to work on difficult tasks to get the intellectual benefit. Once you've properly understood what a computer is, you can absorb the ideas of SICP.
Python is much easier to introduce someone to because there's no boilerplate and the tooling is very simple. Assembly on x86 machines is a royal PITA to set up, and you also need some kind of debugger to actively inspect the program counter and registers.
When I took Computer Organization & Architecture, they had us play around with MARIE[1] which really made assembly make sense to me. After that, I wrote an 8080 emulator and it made even MORE sense to me.
---
Assembly language is not a reasonable first programming language. There's just so many things about it that make it a poor choice for programming instruction.
Chiefly, assembly lacks structure. There's no such thing as variables. There's no such thing as functions. You can fake some of this stuff with convention, but if you make mistakes--and students in intro-to-programming will make mistakes--there is nothing that's going to poke you that you did something wrong, you just get the wrong result.
http://www.z80.info/decoding.htm
For actually programming in machine code this understanding of the internal opcode structure isn't all that useful though, usually - without an assembler at hand - you had a lookup table with all possible assembly instructions on the left side, and the corresponding machine code bytes on the right side.
Programming by typing machine code into a hex editor is possible, but really only recommended as absolute fallback if there's no assembler at hand - mainly because you had to keep track of all global constant and subroutine entry addresses - e.g. the main thing that an assembler does for you, and you had to leave gaps at strategic locations so that it is possible to patch the code without having to move things around.
The thought came to me when testing the new Jules agentic coding platform. It occured to me that we are just in another abstraction cycle, but like those before, it's so hard to see the shift when everything is shifting.
My conclusion? There will be non-AI coders, but they will be as rare as C programmers before we realize it.
When all the values were POKEd in, I'd save to tape and execute it with RAND USR 16514.
That memory address is permanently etched in my brain even now.
It wasn't good, bad or scary it was just what I had to do to make the programs I wanted to make.
This is whacky. So presumably adding with a 24-bit constant whose value isn't representable in this compressed 13-bit format would then expand to two add instructions? Or would it store a constant to a register and then use that? Or is there a different single instruction that would get used?
(Also, typo, 42 << 12 is 172032).
add_large_const:
add w8, w0, #43, lsl #12
add w0, w8, #42
ret
The same happens e.g. on RISC-V: add_large_const:
lui a5,43
addi a5,a5,42
add a0,a0,a5
ret
Because there are only 32 bits in an instruction, you can't fit a whole 32-bit immediate into it. Contrast it with x86 which uses variable-length instructions (up to 15 bytes per instruction): add_large_const:
lea eax, [rdi+176170] # this instruction takes 6 bytes
ret
P.S. Some people seem to be really puzzled by LEA instructions. It's intended use is to correspond to C's "dest = &base[offset]" which semantically is just "dest = base + offset", of course — but it allows one to express base and/or offset using available addressing modes.I am thinking about showing them what is under the hood, that python itself is just a program. When I learned to program it was the late 70s, and trs-80s and apple-IIs were easy to understand at the machine code level.
I could recapitulate that experience for them, via an emulator, but that again just feels like an abstraction. I want them to have the bare-metal experience. But x86 is such a sprawling, complicated instruction set that it is very intimidating. Of course I'd stick to a simplified subset of the instructions, but even then, it seems like a lot more work to make output running on a PC vs on the old 8-bit machines where you write to a specific location and it shows up on the screen.
Show them a CPU running on Logisim (or the like, such as the newer Digital) and show how when you plug a program into a ROM, it turns into wires lighting up and flipping gates/activating data lines/read registers etc.
For the overwhelming majority of programmers, assembly offers absolutely no benefit. I learned (MC6809) assembly after learning BASIC. I went on to become an embedded systems programmer in an era where compilers were still pretty expensive, and I worked for a cheapskate. I wrote an untold amount of assembly for various microcontrollers over the first 10 years of my career. I honestly can't say I got any more benefit out of than programming in C; it just made everything take so much longer.
I once, for a side gig, had to write a 16-bit long-division routine on a processor with only one 8-bit accumulator. That was the point at which I declared that I'd never write another assembly program. Luckily, by then gcc supported some smaller processors so I could switch to using Atmel AVR series.
I don't follow. Why should assembly have to be useful or pleasant in a production environment, for learning it to be useful?
I was taught a couple different flavours of assembly in university, and I found it quite useful for appreciating what the machine actually does. Certainly more so than C. Abstractions do ultimately have to be rooted in something.
Your point about education is orthogonal to the point made. I agree with you that learning assembly can be a good way to teach people how computers work on a low level, but that has nothing to do with whether it is useful as a skill to learn.
As someone teaching similar things at the university level to a non-tech audience I have to always carefully wheigh how much "practically useless" lessons a typical art student can stomach. And which kind of lesson will just deter them, potentially forever.
I don't understand the distinction you're trying to make. The post I was replying to specifically discussed "learning assembly language". My entire point is that "learning assembly language" has purposes other than creating practical real-world programs in assembly.
If you can't see through field accesses and function calls to memory indirections, anything you might read about how TLBs and caches and branch prediction work doesn't connect to much.
It absolutely sucks. But it's not scary. In my world (Zephyr RTOS kernel) I routinely see people go through all kinds of contortions to build abstractions around what are actually pretty straightforward hardware interfaces. Like, here's an interrupt controller with four registers. And here's a C API in the HAL with 28 functions and a bunch of function pointer typedefs and handler mechanics you're supposed to use to talk to it. Stuff like that.
It's really common to see giant abstractions built around things that could literally be two-instruction inline assembly blocks. And the reason is that everyone is scared of "__asm__".
In, say, the interrupt controller case: there's a lot of value in having a generic way for boards to declare what the IRQ for a device actually is. But the driver should be stuffing that into the hardware or masking interrupt priorities or whatever using appropriately constructed assembly where needed, and not a needless layer of C code that just wraps the asm blocks anyway.
[1] And to be clear I'm not that much of a devicetree booster and have lots of complaints within the space, both about the technology itself and the framework with which it's applied. But none that would impact the point here.
This is exactly the kind of job I'd enjoy! A perfectly doable technical challenge with clear requirements. Some people like solving Sudoku puzzles, I like solving programming puzzles.
I guess I'm just not "the overwhelming majority of programmers".
But of course, might not be that rosy if under great time constraints.
That's a Project Management issue, not an implementation concern.
In my case, there was no requirement that said "use 16-bit long division." However, we had committed to a particular processor family (MC68HC05), and the calculation precision required 16-bit math. IIRC, there was a compiler available, but it cost more than the rest of the project and the code it produced wouldn't have fit into the variant of the processor that I was using anyway.
The actual requirement would have looked more like "detect a 0.1% decrease in signal that persists for 10 seconds, then do X."
I feel the same way, but I also can't help but imagine the boss jumping up and down and throwing chairs and screaming "how can you not be done yet? You're a programmer and this is a program and it's been three _hours_ already".
For most folks, that's going to be a couple days of prep work before they can get to the fun part of solving the puzzle.
The "overwhelming" majority of programmers may be underwhelming
Some readers may be unimpressed by programmers who complain about and criticise assembly language, e.g., claiming it offers "no benefit" to others, especially when no one is forcing these programmers to use it
But the context where I'm doing it is very different from the context where you had to write a division routine from scratch! We never use assembly where a higher-level language would be good enough. It's only used for things that can't be written in C at all, either because it needs an instruction that the C compiler won't emit, or it involves some special calling convention that C can't express.
However, I read assembly in production all the time. Literally every day on the job. It's absolutely essential for crashes that won't reproduce locally, issues caused by compiler bugs, or extremely sensitive performance work. Now, lots of programmers very rarely have to deal with those sorts of things, but when it comes up, they'll be asking for help from the ones who know this stuff.
I hardly consider myself an expert ARM ASM programmer (or even an amateur...), but a baseline level of how to read it even if you have to look up a bunch of instructions every time can be super useful for performance work, especially if you have the abstract computer engineering know how to back it up.
For example, it turns out that gcc 7.3 (for arm64) doesn't optimize
foo() ? bar() : baz();
the same as if (foo()) {
bar();
} else {
baz();
}
!The former was compiled into a branchless set of instructions, while the latter had a branch!
Forth was quite acceptable performance wise, but that's barely above a good macro assembler.
And after the 8-bitters, assembly coding on the Amiga was pure pleasure - also for large programs, since apart from the great 68k ISA the entire Amiga hardware and operating system was written to make assembly coding convenient (and even though C was much better on the 68k, most serious programs used a mix of C and assembly).
(also, while writing assembly code today isn't all that important, reading assembly code definitely is when looking at compiler output and trying to figure out why and how the compiler butchered my high level code).
I am curious what specific examples do you have of the HW and OS being made/written to make ASM convenient?
btst #6, $bfe001
The OS used a simple assembly-friendly calling convention, parameters were passed in registers instead of the stack (and the API documentation mentioned which parameters are expected in which registers), and the reference manuals usually had both C and assembly examples, etc... basically lots of little things to make the lives of assembly coders easier.This YouTube playlist gives a nice overview of assembly coding on the Amiga (mostly via direct hardware access though): https://www.youtube.com/playlist?list=PLc3ltHgmiidpK-s0eP5hT...
Anyone curious how their JVM, CLR, V8, ART, Julia,.... gets massaged into machine code only needs to learn about the related tools on the ecosystem.
Some of them are available on online playgrounds like Compiler Explorer, Sharpio,....
Agreed - I wouldn't be able to write any x86 assembly without a bit of help, but having done some game reverse engineering I've learned enough to make sense of compiler generated code.
We are also getting burned out by the modern Agile web/data/whatever development scene and would like to drill really deep into one specific area without stakeholders breathing down our necks every few hours, which assembly programming conveniently provides.
I also consider the grit (forced or voluntary) to be a bath of fire which significantly improved two important things - the programmer's understanding of the system, and the programmer's capability to run low level code in his brain. Is it suffering? Definitely, but this is a suffering that bring technical prowess.
Most of us do not have the privilege to suffer properly. Do you prefer to suffer from incomplete documentation, very low level code and banging your head on a wall for tough technical problems, or do you prefer to suffer from incomplete documentation, layers and layers of abstraction, stakeholders changing requirements every day and actually know very little about technical stuffs? I think it is an easy choice, at least for me. If there is an assembly language / C job that is willing to take me in, I'll do it in half of the salary I'm earning.
Not knowing assembler means programmers have a bit of a blind spot towards what are expensive things to do in C vs what generates the best code.
For example, debugging a program sometimes requires looking at the generated assembler. Recently I was wondering why my deliberate null pointer dereference wasn't generating an exception. Looking at the assembler, there were no instructions generated for it. It turns out that since a null pointer dereference was undefined behavior, the compiler didn't need to generate any instructions for it.
I do need to understand how indexes in db engine work, I need to understand there might be socket exhaustion in the system, I do need to understand how my framework allocates data on heap vs stack.
Having to drop to instructions that is for web servers, db, frameworks developers not for me to do. I do have a clue how low level works but there is no need for me.
That is part where parent poster is correct there are better ways for developers to spend time on - trust your database, web servers and framework and learn how those work in depth you can skip asembler, because all of those will take a lot of time anyway and most likely those are the ones you should/can tweak to fix performance not assembler.
Further, changes in the ISA can open up gains in performance that weren't available in yesteryear. An example of this would be SIMD instruction usage.
It's not a bad idea to know enough assembly language to understand why code is slow. However, the general default should be to avoid writing it. Your time would be better spent getting a newer version of your compiler and potentially enabling things like PGO.
I deeply hate this attitude in modern compiler design.
Enables debugging binaries and crash dumps without complete source codes, like DLLs shipped with Windows or third-party DLLs. Allows to understand what compilers (both traditional and JIT) did to your source codes, this is useful when doing performance optimizations.
But for tinkering (e.g writing GBA/NES games), hell why not? It's fun.
If you have a six-day deadline, probably it would be better to use a high-level language instead.
But, when you have time for them, all of these things are intrinsically rewarding. Not all the time! And not for everyone! But for some of us, some of the time, they can all be very enjoyable. And sometimes that slow effort can achieve a result that you can't get any other way.
I haven't written that much assembly, myself. Much less than you have. If I had to write everything in assembly for years, maybe I wouldn't enjoy it anymore. I've written a web server, a Tetris game, some bytecode interpreters, a threading library, a 64-byte VGA graphics demo, a sort of skeletal music synthesizer, and an interpreter for an object-oriented language with pattern-matching and multiple dispatch, as well as a couple of compilers in high-level languages targeting assembly or machine code. All of these were either 8086, ARM, RISC-V, i386, or amd64; I never had to suffer through 6809 or various microcontrollers.
Maybe most important, I've never written assembly code that someone else depended on working. Those programs I've mostly written in Python, which I regret now. It's much faster that way. However, I've found it useful in practice for debugging C and C++ programs.
I think that a farmer who says, "For the vast majority of consumers, gardening offers absolutely no benefit," is missing the point. It's not about easier access to parsley and chives. Similarly for an author who says, "For the vast majority of readers, solving crossword puzzles offers absolutely no benefit."
So I don't think assembly sucks.
But C is slow to create -- it is like using a toothpick
Writing from scratch is slow, and using C libraries also sucks. Certainly libc sucks, e.g. returning pointers to static buffers, global vars for Unicode, etc.
So yeah I have never written Assembly that anybody needs to work, but I think of it as "next level slow"
---
Probably the main case where C is nice is where you are working for a company that has developed high quality infrastructure over decades. And I doubt there is any such company in existence for Assembly
I was taught assembler
in my second year of school.
It's kinda like construction work —
with a toothpick for a tool.
So when I made my senior year,
I threw my code away,
And learned the way to program
that I still prefer today.
Now, some folks on the Internet
put their faith in C++.
They swear that it's so powerful,
it's what God used for us.
And maybe it lets mortals dredge
their objects from the C.
But I think that explains
why only God can make a tree.
For God wrote in Lisp code
When he filled the leaves with green.
The fractal flowers and recursive roots:
The most lovely hack I've seen.
And when I ponder snowflakes,
never finding two the same,
I know God likes a language
with its own four-letter name.
Now, I've used a SUN under Unix,
so I've seen what C can hold.
I've surfed for Perls, found what Fortran's for,
Got that Java stuff down cold.
Though the chance that I'd write COBOL code
is a SNOBOL's chance in Hell.
And I basically hate hieroglyphs,
so I won't use APL.
Now, God must know all these languages,
and a few I haven't named.
But the Lord made sure, when each sparrow falls,
that its flesh will be reclaimed.
And the Lord could not count grains of sand
with a 32-bit word.
Who knows where we would go to
if Lisp weren't what he preferred?
And God wrote in Lisp code
Every creature great and small.
Don't search the disk drive for man.c,
When the listing's on the wall.
And when I watch the lightning burn
Unbelievers to a crisp,
I know God had six days to work,
So he wrote it all in Lisp.
Yes, God had a deadline.
So he wrote it all in Lisp.
Once I got the code sequences for this right on my AArch64 code generator, I don't have to ever figure it out again!
It is all a matter of having high quality macro assemblers, and people that actually care to write structured documented code in Assembly.
When they don't care, usually not even writing in C and C++ will save the kind of code they write.
I had to implement Morton ordering on this platform. The canonical C for this blows up to over 300 instructions per iteration. I unrolled the loop, used special CPU hardware and got the entire thing in under 100 instructions.
Compilers, even modern ones, are not magic and only understand CPUs popular enough to receive specific attention from compiler devs. If your CPU is unpopular, you're doing optimizations yourself.
Assembly doesn't matter to arduino script kiddies, but it's still quite important if you care at all about execution speed, binary size, resource usage.
I'd add "yet" - we runinafed that the reason new machines with similar shapes (quad core to quad core of a newer generation) doesn't immediately seem like a large a jump as it ought, in performance, is because it takes time for people other than intel to update their compilers to effectively make use of the new instructions. icc is obviously going to more quickly (in the sense of how long after the CPU is released, not `time`) generate faster executing code on new Intel hardware. But gcc will take longer to catch up.
There's a sweet spot from about 1-4 years after initial release where hardware speeds up, but toward the end of that run programs bloat and wipe all the benefits of the new instructions; leading to needing a new CPU, that isn't that much faster than the one you replaced.
Yet.
Which reminds me I need to benchmark a Linux kernel compile to see if my above supposition is correct, I have the timings from when I first bought it, as compared to a 10 year old HP 40 core machine (ryzen 5950 is 5% faster but used 1/4th the wall power.)
-> ruminated
In embedded land, if your microcontroller is unpopular, you don't get much in the way of optimization. The assembly GCC generates is frankly hot steaming trash and an intern with an hour of assembly experience can do better. This is not in any way an exaggeration.
I've run into several situations where hand-optimized assembly is tens of times faster than optimized C mangled by GCC.
I do not trust compilers anymore unless it's specifically for x86_64, and only for CPUs made this decade
I vaguely recall ada inline assembly looking like function calls, with arguments that sometimes referenced high-level-language variables)
unrelated to that, I distinguish between machine code which is binary/hex, and assembly as symbolic assembler or macro assembler, which can actually have high level macros and other niceties.
And one thing I can say for sure. I took assembly language as my second computer course, and it definitely added a lifelong context as to how machines worked, what everything translated to and whether it was fast or efficient.
A friend of mine who also had a CoCo wrote an assembler as a term project.
But there are a number of things we did that are not available or difficult in C:
- Guaranteed tail calls
- Returning multiple values without touching memory
- using the stack pointer as a general purpose pointer for writing to memory
- Changing the stack pointer to support co-routimes
- Using our own register / calling convention (e.g. setting aside a register to be available to all routines
- Unpicking the stack to reduce register setup for commonly used routines or fast longjmps
- VM "jump tables" without requiring an indirection to know where to jump to
Skipping over the bundling of instructions into code blocks, the next logical construct are functions. These have references to code and data in memory; if you want to relocate functions around in memory you introduce the concept of relocations to annotate these references and of a linker to fix them to a particular location.
But once the linker has done its job, the function is no longer relocatable, you can't move it around... or that is what someone sane might say.
If you can undo the work of the linker, you can extract relocatable functions from executables. These functions can then be reused into new executables, without decompiling them first; after all, if what you've extracted is equivalent to the original relocatable function, you can do the same things than it.
Repeat this process over the entire executable and you're stripped it for parts, ready to be put back together with the linker. Change some parts and you have the ability to modify it as if you're replacing object files, instead of binary patching it in place with all the constraints that comes with it.
Machine code is like Lego bricks, it just takes a rather unconventional point of view (and quite a bit of time to perfect the art of delinking) to realize it.
Assembly as a game, I loved playing it.
That is fun. But this one truly is enough: Turing Complete. You start with boolean logic gates and progressively work your way up to building your own processor, create your own assembly language, and use it to do things like solve mazes and more. Super duper fun
(I think you mean https://news.ycombinator.com/item?id=44184900)
oleganza•2d ago
My 23+ year experience in computer science and programming is a zebra of black-or-white moments. For the most time, things are mostly obscure, complicated, dark and daunting. Until suddenly you stumble upon a person who can explain those in simple terms, focus on important bits. You then can put this new knowledge into a well-organized hierarchy in your head and suddenly become wiser and empowered.
"Writing documentation", "talking at conferences", "chatting at a cooler", "writing to a blog" and all the other discussions from twitter to mailing lists - are all about trying to get some ideas and understanding from one head into another, so more people can get elucidated and build further.
And oh my how hard is that. We are lucky to sometimes have enlightenment through great RTFMs.