For convenience, many architectures have a single assembly instruction for taking one register (called the "stack pointer"), adjusting it by a word, and moving a register to the address at that word, and a corresponding instruction to move data back to a register and adjust it in the other direction. That's the extent of the abstraction. You can implement it with two instructions instead of one, and then you get as many stack pointers as you want. Zero, if you want.
There is no "heap" on modern OSes. There used to be a thing called the program break, beyond which was the heap. It's almost meaningless now. You ask for an area of virtual memory via mmap or equivalent; it is now one of several heaps you have.
And you can pass around pointers from all your stacks and all your heaps in the same ways, as long as they remain valid.
You need to understand this in order to understand how thread stacks work (you typically allocate them via mmap or from "the heap"); how sigaltstack works and why you need it to catch SIGSEGV from stack oveflows; how segmented stacks (as previously used in Go and Rust) work; how Stackless Python works; how to share data structures between processes; etc.
Since this is a distinction that is very important both for performance in high-level languages and for correctness in C, and one C forces you to think about and makes plain without having to reason through escape analysis and closures, I think the previous comment's point is well taken.
Consider, say, shadow stacks- local variables might end up living on a totally separate "stack" from the one "call" pushes onto and "ret" pops from, and one which the ISA has no idea about.
PUSH/POP are more obviously "hey store your locals on the stack, kids", but on, say, x64, how often do you actually see a push instead of bp/sp-relative addressing? Most compilers seem to be much happier representing each stack frame as "a bunch of slots, some for things where the address is taken so it needs to be in memory, and others where I spill things into as needed".
On ARMv7 (i don't know enough about v8 / AARCH64), "push"/"pop" are just stores/loads with postdecrement / postincrement.
Is this lawyer-y? Sure. But I think it's still correct, and this being HN...
It's much more optimal to use a series of push/pops for smaller operations like saving registers before a call than to manually adjust and store onto the stack.
While technically this is still incrementing/decrementing a register and storing, the amount of isa/hardware support for such things clearly demonstrates that the x86 isa and modern x86 hardware gives special treatment to the stack.
http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1256.pdf
There are no occurrences of the words "stack" or "heap" in this document.
What the spec actually discusses is "storage durations". Now, in many cases you can say, well, "automatic storage duration" means it's on the stack, but that's not something C has any opinions about.
If you want to know about the stack and the heap, saying "learn C, and then learn how these abstract C concepts map onto the stack and the heap" might not actually be the best way to figure this stuff out.
What actually ends up happening, in my experience, is that to figure C out you have to get a decent mental model of how the stack and heap work, and then, from that, say "automatic storage duration and malloc/free are basically just the stack and the heap".
I learned calculus as a kid because I needed it for an online electronics class I was taking. But I'm not going to recommend that folks take electronics classes so they learn calculus! (that said- having a motivation to learn something, having an application in mind, a problem you want to solve... certainly seems to make learning easier.)
[1] https://arcanesentiment.blogspot.com/2014/12/customary-seman...
Oddly you seem to recognize this, so I’m not sure what your point is.
's funny, I am certainly aware of having, at some point, at least skimmed over the C99 standard encountered the concept of a storage duration. But, aside from that brief few hours, virtually the entirety of my C-knowing career has been spent thinking and talking in terms of stacks and heaps.
Meanwhile, in my .NET career, I really did (and, I guess, still do) feel like it was important to keep track of how .NET's distinction is between reference types and value types, and whether or not they were stack or heap allocated was an implementation detail.
The semantics of automatic storage necessarily imply #1, but they do not require #2. Indeed, there are widely used implementations which implement #1 using linked lists (e.g. IBM mainframes, and GCC's split stacks or clang's segmented stacks).
Similarly, function recursion semantics necessarily imply #1 for restoring execution flow, but not #2. In addition to the examples above, so-called shadow stacks are on the horizon which maintain two separate stacks, one for function return addresses and another for data.[1] In the near future a single contiguous stack may be atypical.
[1] Some variations might mix data and return addresses if the compiler can prove safe object access. Or the shadow stack may simply contain checksums or other auxiliary information that can be optionally used, preserving ABI compatibility.
I'm not even sure the C memory model depends on the code/stack/heap separation.
Modern languages solve problems created developing in legacy languages (primarily C). The issue is that knowing a solution without knowing the prior problem which the solution addresses, doesn't really lend itself to clarity.
Point is that out of context, a lot of the things modern languages provide seem superfluous. Why have objects? Why have templates, closures, or lambdas? For a novice programmer, these are many answers for questions yet to be asked.
When you come from a heavy C background and you encounter something like templates, you know EXACTLY what this is for and wish you had it years ago.
I'm as hardcore as they get C programmer and I'm having a ball with C# for this very reason.
Yet we often see passionate arguments in its favor, as for instance from Sústrik (http://250bpm.com/blog:56) and Tatham (https://www.chiark.greenend.org.uk/~sgtatham/mp/) and a person on the internet who recommends replacing LAPACK with preprocessor macros (http://wordsandbuttons.online/outperforming_lapack_with_c_me...). Would you care to comment on its enduring popularity?
C does have a lot of it, and it can be anywhere. In many languages, you can cause undefined behavior through its FFI. Some languages, like Rust, have UB, but only in well-defined places (a module containing unsafe code, in its case).
I do take your point about Rust, but I'd see that as deriving from LLVM's undefined behaviour which in turn descends from C; I'm not aware of any pre-C languages having C-style exploding undefined behaviour or of any subsequent languages inventing it independently.
That is fair, however, I don't think it's inherently due to C. Yes, most languages use the C ABI for FFI, but that doesn't mean they have to; it's a more general problem when combining two systems, they cannot track statically the guarantees of the other system.
With Rust, it has nothing to do with LLVM; it has to do with the fact that we cannot track things, that's why it's unsafe! Even if an implementation of the language does not use LLVM, we will still have UB.
I can see that any implementation of unsafe Rust would always have assembly-like unsafeness (e.g. reading an arbitrary memory address might result in an arbitrary value, or segfault). But I don't see why you would need C-style "arbitrary lines before and after the line that will not execute, reads from unrelated memory addresses will return arbitrary values" UB?
The reason to have it is the exact same reason that the distinction between "implementation defined" and "undefined" exists in the first place: UB allows compilers to assume that it will never happen, and optimized based on it. That is a useful property, but it's a tradeoff, of course.
> I don't see why you would need C-style "arbitrary lines before and after the line that will not execute, reads from unrelated memory addresses will return arbitrary values" UB
This reason this happens is because we don't compile things down to obvious assembly, they get optimized. Each of those optimizations requires assumptions to be made about the the code. If you break those assumptions, then the optimizations can result in arbitrary results happening. Those assumptions determine what is and isn't UB.
Most languages just don't give the programmer any way to break those assumptions, but languages like C and Rust do. Thus, Rust will always have this 'problem' because it can/will make even more aggressive optimizations then C will, meaning badly written `unsafe` code will have arbitrary behavior and results from the optimizer if the compiler doesn't understand what you're doing.
FFI isn't into "C", it's into your operating system's binary format. And since no two systems behave the same, it's UB however you look at it.
Most people call that format "the C ABI," and "FFI into C" is short for "FFI via the C ABI."
> And since no two systems behave the same, it's UB however you look at it.
That's not what UB means. It would be implementation defined, not undefined.
The reason that a spec is taking so long is that we're interested in making an ironclad, really good spec. Formal methods take time. C and C++ did not have specs for much, much longer than the three years that Rust has existed. We'll get there.
You are right that we are far, but that’s because it’s a monumental task, and it’s fundamentally unfair to expect equivalence from a young language. It is fair to say that it is a drawback.
It doesn't have to be a monumental task. Rust is simply too big, and getting bigger.
Specs are always a monumental task. The first C spec took six years to make in the first place!
If 99% of what that person does is gluing together APIs and software modules, and they can see their memory usages are well within range, why does it matter?
if you're just going to move couches around and put down throw pillows, who cares about a solid foundation of design fundamentals and architectural psychology.
Without being able to refer to fundamental structures and concepts, I usually just exclusively teach students non-mutable patterns (e.g. list.sorted vs list.sort), while pushing them to become much proactive at inspecting and debugging.
(I kept this post vague but can offer concrete examples of you'd like)
Go does that too. C teaches manual memory allocation, de-allocation and pointer arithmetic as well.
And C pretends there's a distinction between the stack & heap that doesn't actually exist. There is no significant difference there.
char *dupstr(const char *src) {
char new[1024];
strlcpy(new, src, 1024);
return new;
}In fact C teaches a model of a semi-standard virtual architecture - loosely based on the DEC PDP7 and/or PDP11 - which is long gone from real hardware.
Real hardware today has multiple abstraction layers under the assembly code, and all but the top layer is inaccessible.
So there's no single definitive model of "How computers work."
They work at whatever level of abstraction you need them to work. You should definitely be familiar with a good selection of levels - and unless you're doing chip design, they're all equally real.
There are significant performance and strategy differences between the stack and the heap. On the stack, allocation is cheap, deallocation is free and automatic, fragmentation is impossible, the resource is limited, and the lifetime is lexically scoped.
On the heap, allocation might be cheap or it might be expensive, deallocation might be cheap or it might be expensive, fragmentation is a risk, the resource is 'unlimited', and the lifetime is unscoped.
Your statement is equivalent to saying "there's no difference between pointers and integers" - technically, they are both just numbers that live in registers or somewhere in memory. In reality, that approach will not get you far in computer science.
By all means it's certainly a valuable abstraction, one that most high-level languages support. But that's like how functions are a valuable abstraction, or objects, or key-value stores, or Berkeley sockets. Learning those abstractions is absolutely important and also completely irrelevant to understanding "the computer".
(As Dijkstra once said, computer science is no more about computers than astronomy is about telescopes. Learning C is valuable for computer science, but that doesn't mean it gives you a deep understanding of the computer itself.)
In other words it might behave just like "the stack"; the differences between different kinds of "heaps" are as large or larger than the difference between "the stack" and "the heap".
> fragmentation is a risk, the resource is 'unlimited', and the lifetime is unscoped.
None of these is true in all implementations and circumstances.
Understanding the details of memory management performance is important for particular kinds of programming. But learning C's version of "the stack" and "the heap" will not help you with that.
True enough; however sometimes access to "the heap" (in C terms) will use those instructions, and sometimes access to "the stack" will not. Learning one or two assembly languages is well worth doing, since they offer a coherent abstraction that is genuinely relevant to the implementation of higher-level languages. Not so C.
No, because in reality those are handled by different computational units. Integers are handled by the ALU, and pointers are handled by the loader. They are distinct things to the CPU.
Stack & heap have no such distinction. There isn't even a heap in the first place. There's as many heaps of as many sizes as you want, as the "heap" concept is an abstraction over memory (strictly speaking over virtual address space - another concept C won't teach you, yet is very important for things like mmap). It's not a tangible thing to the computer.
Same with the stack. It's why green-threads work, because the stack is simply an abstraction over memory.
I strongly disagree. Yes some execution units are more let's say "dedicated" to pointers than other, and obviously ultimately you will dereference your pointers, so you will load/store, but compilers happily emit lea to do e.g. Ax9+B and in the other direction, add or sub on pointers. Some ISA even have almost no pointer "oriented" register (and even x64 has very few)
Although, it sounds like you're agreeing with me that there is in fact a difference between the stack and the heap because to your point one exists supported at the hardware level with instructions and registers, and one doesn't exist or can exist many times over. Hence, different.