Hi,
Basically, the x86 has 2 methods you can use to separate program spaces: segmentation and paging.
It looks like segmentation support will soon become 'legacy', as long mode cannot use a segmented memory model. I have never written a segmented OS, but all the reading I have done indicates that it is a pain in the proverbial. This may be because if you are using task-descriptors (which can only appear in the GDT, not LDT's), you are limited as to how many tasks you can run at the same time (you can only have 8192 GDT entries). If you think that you have to have LDT entries, with the LDT containing different CS and DS descriptors, you can see that the whole thing could quickly become muddled.
So, if you assume that segmentation is more hassle than it is worth, that leaves us with paging.
In this system, virtual memory is mapped to physical memory using a page directory and page tables. If memory does not appear in the current page directory, it does not exist (as far as the current application is concerned). This means, you can link all your user-mode programs to 0x100000 (say).
For each program, you load a different page directory. As far as your program is concerned, it is running at 0x100000. In reality, the actual binary could be anywhere in physical RAM (often, the physical pages are not even contiguous). This allows you to have, for example, 100 different processes running at 0x100000. As long as the correct page directoy is loaded at the correct time for the appropriate process, your user programs will all be happy.
Another advantage of paging, is that you can have your kernel mapped to 0xC0000000, for example, in each process space. When your user program calls a kernel function, you do not then have to switch memory spaces to run that kernel code. Paging also allows you to easily share memory between processes (just map a physical page in to 2 process spaces).
I hope this clarifies things a little - sorry to ramble
Cheers,
Adam