In parallel to this low-level thread of learning, you have to learn other high-level languages, of course.
In parallel to this low-level thread of learning, you have to learn other high-level languages, of course.
This is true in x86, ARM, RISC-V, and just about any architecture I can think of, but it is not quite true in C, even when it is implemented on top of a 2's complement architecture. Signed integer overflow is undefined, you have to explicitly convert to unsigned before you do any overflow. Only then do you get the implementation defined behaviour your sought, instead of the usual nasal demons.
Also, assembly doesn't mean much with out of order cores. Data dependencies are likely more important than the generated assembly.
100% agree that this is a dangerous falsehood. I would add the word "always" in your sentence to clarify its falsehood.
But there is an even more dangerous, and more insiduous falsehood: "integers are encoded in some mysterious way that is useless to know".
It's already true in just about every system in the world today, and people with extremely obscure hardware can just hack a workaround into their compiler (or straight up ignore the standard because they're just a handful of people anyway).
I get that back in the day the K&R didn't want to impose on the hardware vendors and left the standard vague where they had different ideas, but that doesn't mean you can't evolve the standard as the hardware evolves over the decades.
Something similar happened to me in a general engineering curriculum where our introduction to programming was in C on some old unix machines that were already already very obsolete (as of end of 90s), with no more IDE or debugger than the equivalent of notepad and debug by printf. I thought it was interesting but too tedious for my taste (I had never heard of IDEs and debuggers).
It's only years later that I went back to programming, from higher level languages, and progressively making my way into more sophisticated / lower level languages.
You don't teach kids math by starting with topology. Most 1st year university students will have never programmed in their life (particularly if all they dealt in their life was smartphones and tablets).
IMHO that's a major part of the problem. Somehow Asm turned into something horrible to avoid, instead of being the crucial understanding that makes everything else fall into place.
I have an engineering textbook from the mid 60s. There is an entire chapter on how computers work and how to program them (in Asm!) to solve numerical problems. The audience of this book was not computer scientists/specialists, but just engineers in general.
Two decades later, computer magazines --- not "developer-oriented", just "user" or perhaps "power user" --- had Asm listings of simple (sub-512-byte binary) and useful utilities for readers to manually enter, use, and modify.
Another two decades later, and the programmers today barely understand binary nor Asm, despite the concepts being so ridiculously simple. A computer is a machine that executes instructions step-by-step, and nothing evokes than more clearly than an Asm listing. An HLL with all its nested structure doesn't convey the same "it's just a list of instructions" to the beginner.
I thought it was interesting but too tedious for my taste
"Genius is 1% inspiration and 99% perspiration."
You don't teach kids math by starting with topology. Most 1st year university students will have never programmed in their life (particularly if all they dealt in their life was smartphones and tablets)
Starting with Asm is like learning arithmetic. Starting with a HLL is like teaching calculus to students who don't know arithmetic by giving them calculators.
I've worked with a lot of both "new-school" and "old-school" developers over the years, and there's a large contrast between them: the former group is heavily reliant on tooling, tends to make many localised changes to "fix" things without considering the whole picture (causing more problems later), and get stuck easily when debugging. The latter tend to adopt a more methodical approach to problem-solving, and while they don't appear to be doing as much interaction with the computer, are overall more productive.
Python -> A restricted C subset ; standard ML -> asm
Teaching fun and power first is good for engagement. Teaching how to think and structure is good for later asm experience so people have an existing concept of how to turn problems into solutions in code.
The implicit context is that we are talking about fields that need to understand programming at a low level: Computer Science, Computer engineering, Software Engineering, Electrical engineering.
There is no point in a Chemical Engineer programming in asm. Just throw some python and R at him so he understands the basic concepts.
But for the fields that need to understand computers at a low level, I would expect enthusiasm from the student to understand how it all works.
What? That's completely untypical for me - I would expect that in a university with decent admission competition (don't know how these things work in US exactly) students that get in know at least a couple of high-level programming languages in addition to taking part in programming competition and general geekiness like brainfuck.
Universities with decent admission competition value well-rounded, high-achieving students. Such students are involved in a lot of co-curricular activities, may take many subjects, and have broad interests. It's perfectly reasonable to expect that they simply never had the time to learn multiple programming languages and compete in programming competitions, in addition to whatever else they do.
And many universities do follow books like "From NAND 2 Tetris".
But to understand semiconductors you first need to understand quantum mechanics. And to understand QM you first need to understand classical physics.
Oops, I think we just ran out of time for a masters program. ;)
More seriously, it is possible to fit quite a lot of this stuff into an undergrad program - my Cambridge degree included logic gates (including drawing silicon layout with coloured pens), computer architecture (ARM assembler), a lab on FPGAs, Java, Standard ML (for type inference and lambda calculus), and had plenty of time left over for higher level topics.
Current syllabus: https://www.cl.cam.ac.uk/teaching/1819/cst.pdf (one term = 8 weeks)
That is a horrible, horrible idea. It gives people the misrepresentation that C is really "just" a thin veneer over assembly language, which it most certainly has not been for at least 20 years. Moreover, it feeds the cult of C, the idea that C is the only language capable of being close to the hardware and that other languages or attempts to build system-level languages need not apply.
C has lots of flaws and faults in the language, but we're not going to be able to make any progress in fixing the language, or more likely replacing it with a better language, if we continue acting as if it is the only way to get low-level access to the hardware.
C is a simple language that obviously maps to a programmer's idea of the assembly they'd write.
It biases towards lots of fixed-size stack frames and fixed-layout structs. Writing C code really does feel very close to the "bounded turing machine" metaphor that the CPU gives us.
Other languages have their advantages, but C's proximity to assembler, with all the faults included, really is a great pedagogical tool.
It's not. That's my point. You don't need undefined behavior to get to that point, just consider what assembly you get from this code:
unsigned prog_a(unsigned a) { return a * 2; }
unsigned prog_b(unsigned a) { return a / 3; }
What do you think you're going to get for that code? The "obvious mapping" principle for both codes suggest you're going to get a multiply and a division instruction, respectively. On x86, however, what you get is this: prog_a:
leal (%rdi, %rdi), %eax
ret
prog_b:
movl %edi, %eax
movl $-1431655765, %edx
mull %edx
shrl %edx
movl %edx, %eax
ret
In other words, the first program turned into a load-effective address instruction, and the latter turned into a multiply-hi instruction. Even something as simple as basic arithmetic gets turned into weird instructions, and which ones get this treatment is essentially a game of roulette based on compiler optimizations and particularly heuristics. We're not even bringing undefined behavior into play here, which is usually the major objection to C-obviously-maps-to-assembly; it's falling flat enough without it.The primary advantage C brings, what people are really touting when they tout its proximity to assembly, isn't that it's close to assembly but that it has a well-known, stable ABI for that mapping. Of course, it turns out that said ABI is actually a bad idea if you want performant modern code on most architectures (the infamous array-of-struct versus struct-of-array problem).
People saying "but, but" with various optimization hacks and "well actually" stuff about microcode are missing the main point -- forest vs trees.
I believe Rust has the same property, but only a tiny fraction as much software is written in it. Ditto for Zig and Nim. C++ has all of the paradigm-breakers mentioned above, Go has its own hidden magic mostly related to goroutines, Java doesn't even try, and anything functional doesn't even occupy the same conceptual space.
As a mental exercise, try taking any random piece of generated assembler code and map it back to the corresponding higher-level-language code. I can usually do this pretty easily for C code, though switch/case statements present some challenges and heavy use of macros in any language can defeat the effort. Even calls to compiler-internal functions (e.g. memcpy-like functions for structure assignment) will be easy to interpret. Again, you can probably do the same for Rust/Nim/Zig. Attempting the same for C++ or Go will lead to all sorts of random places other than the original source line, and for just about anything else good luck even finding that generated assembler in the first place.
When you can map easily back and forth between the language and CPU instructions, you're as close to the hardware as you're going to get. This is something few programmers - even in kernels and such - actually need, but C offers it and most other languages don't.
If you think this is true, try building a native assembly to C decompiler. There are a lot of warts that you realize C gives you absolutely no way to describe. For example, C has no concept of unwinding, which both C++ and Rust do. (Admittedly, much of unwinding is handled by library functions, but one piece of the unwinding puzzle is do-something-in-this-function-when-unwinding, which can't be handled by library functions). There's no support for SIMD vector units in C. And getting floating point edge case support is tricky: there's a difference between a * b + c and fma(a, b, c), and that's before you get into trickiness like floating point exceptions, rounding mode, and signaling NaN.
The C language only has an easy bijective mapping to the actual CPU assembly if you focus on the easy scalar integer, general-purpose subset of both. Throw in floating point support, control flow [1], and vectors, and the mapping is much more difficult. Trying to support the actual hardware memory model basically kills the ability to map CPU assembly back to C/C++ [2]. The mapping just isn't as good as most people think it is, not if you actually care to start exploring the space of what can be done.
[1] I didn't mention this earlier, but try taking the address of blocks (this is a GNU extension, not standard C) while not doing anything else with them and see how well-preserved they are even at -O0. Spoiler alert: they're not.
[2] Virtually all of the effort to actually make hardware features invented in the past 20 years available in standard programming language is concentrated in the C++ working group, not the C working group.
Of course, the #1 biggest problem of C is that C's semantics are not what people think they are. Most notably, pointers are not integers in the C abstract machine, nor can you really enforce such a mapping without prohibiting nearly all optimizations [1]. And when you teach people to understand how processors work via C, you start to claim that C's semantics are processor semantics, which has not been the case for a very long time.
[1] The underlying issue here is that the model you need for any sane optimization is that if you tell nobody that this box of data exists, then there is there no way for anyone else to refer to it. This basic fact opens up the ability to do things like move variables from stack allocations to registers, eliminate dead stores, etc. It also implies that you need some sort of pointer provenance in the semantic model to track who actually has the ability to refer to something. In practice, a semantic model for C needs to carry pointer provenance through integers as well, but that really means that C's integers also aren't mathematical integers (modulo some power of 2).
if pointers are not integers how can you iterate through an arbitrary array using char* ?
In C, however, pointers are not integers. I've not fully built any kind of operational semantics for C, so I can't say what the full list of features you need to track for pointers is for sure, but I can take a stab at how you could describe it. A pointer consists of a tag of the object being stored in the object model (which needs to tell you what the current dynamic type of the object is and its size and alignment in addition to the actual memory contents of the object) as well as an offset of where it points inside that object. Iterating through an array would merely be incrementing (or decrementing, as the case may be) the offset field of the pointer, subject of course to the well-definedness characteristics that you can't exceed one past the end of the object or go before the beginning of the object.
At compile time, the compiler knows the type of a pointer, so it can interpret struct_ptr->field and do the proper arithmetic (field is 20 bytes offset into struct).
But when running, the pointers are just int64s on the stack. Check stdint.h. It's all right there.
Anyone writing C 'gets' roughly what's happening and knows the shape of their structs in memory.
Programming without knowing anything about assembler is like driving without having ever opened the hood of your car and changed some oil. Of course, if you are a professional driver you will have specialist mechanicians that take care of all of it, but still it would be ridiculous if you could not do a few simple things.
You are so wrong. It is not valueable at all. If you are able to do this, then you're just capable of signaling that you have been exposed to assembly and familiar with the very basics of it. But how is it any useful? If you could find some intricate bug, or optimize a program in a considerable way based on insight you gained from reading it's assembly code that would have been very valueable indeed.
Plus dialects like Object Pascal and Free Pascal provide a couple of niceties as well.