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.
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.