First, this has nothing to do with RAM, really. We're talking about address space here - even if you only have 16 MiB of memory, you still have the full 32 bits of address space on a 32-bit CPU.
This already answers your first question, really - at the time this was designed, real world PCs had nowhere near the full 4 GiB of memory; they were more in the range of 1-16 MiB of memory. The address space was, for all intents and purposes, free.
Now, why 0xFFFFFFF0 exactly? The CPU doesn't know how much of the BIOS there is. Some BIOSes may only take a few kilobytes, while others may take full megabytes of memory - and I'm not even getting into the various optional RAMs. The CPU must be hardwired to some address to start on - there's noöne to configure the CPU. But this is only a mapping of the address space - the address is mapped directly into the BIOS ROM chip (yes, this means you don't get access to the full 4 GiB of RAM at this point if you do have that many - but that isn't anything special, many devices require their own range in address space). On a 32-bit CPU, this address gives you full 16 bytes to do the very basic initialization - which is enough to setup your segments and, if needed, address mode (remember, x86 boots in 16-bit real mode - the address space isn't flat) and do a jump to the real boot "procedure". At this point, you don't use RAM at all - it's all just mapped ROM. In fact, RAM isn't even ready to be used at this point - that's one of the jobs of the BIOS POST! Now, you might be thinking - how does a 16-bit real mode access the address 0xFFFFFFF0? Sure, there's segments, so you have 20-bit address space, but that still isn't good enough. Well, there's a trick to it - the 12 high bits of the address are set until you execute your first long jump, giving you access to the high address space (while rejecting access to anything lower than 0xFFF00000 - until you execute a long jump).
All this are the things that are mostly hidden from programmers (not to mention users) on modern operating systems. You usually don't have any access to anything so low level - some things are already beyond salvage (you can't switch CPU modes willy-nilly), some are exclusively handled by the OS kernel.
So a nicer view comes from old-school coding on MS DOS. Another typical example of device memory being directly mapped to address space is direct access to video memory. For example, if you wanted to write text to the display fast, you wrote directly to address B800:0000
(plus offset - in 80x25 text mode, this meant (y * 80 + x) * 2
if my memory serves me right - two bytes per character, line by line). If you wanted to draw pixel-by-pixel, you used a graphics mode and the start address of A000:0000
(typically, 320x200 at 8 bits per pixel). Doing anything high-performance usually meant diving into device manuals, to figure out how to access them directly.
This survives to this day - it's just hidden. On Windows, you can see the memory addresses mapped to devices in the Device manager - just open properties of something like your network card, go to the Resources tab - all the Memory Range items are mappings from device memory to your main address space. And on 32-bit, you'll see that most of those devices are mapped above the 2 GiB (later 3 GiB) mark - again, to minimize conflicts with user-useable memory, though this is not really an issue with virtual memory (applications don't get anywhere near the real, hardware address space - they have their own virtualized chunk of memory, which might be mapped to RAM, ROM, devices or the page file, for example).
As for the stack, well, it should help to understand that by default, stack grows from the top. So if you do a push
, the new stack pointer will be at 0xFFFFFEC
- in other words, you're not trying to write to the BIOS init address :) Which of course means that the BIOS init routines can use the stack safely, before remapping it somewhere more useful. In old-school programming, before paging became the de facto default, the stack usually started on the end of RAM, and "stack overflow" happened when you started overwriting your application memory. Memory protection changed a lot of this, but in general, it maintains backwards compatibility as much as possible - note how even the most modern x86-64 CPU can still boot MS DOS 5 - or how Windows can still run many DOS applications that have no idea about paging.