The segment descriptors in GDT serve several purposes at the same time. There are theoretically separate descriptors for code, data and stack segments, but you can reuse the data-segment as a stack segment usually, because the "default" data-segment type happens to have the same descriptor that the "default" stack-segment type uses.
Regardless of the type, a segment descriptor gives the starting address and the segment size. Any access through a segment register with a given descriptor loaded, will first check the logical (requested) address against the segment size-limit, and if it's within the segment bounds, the starting address is added to get the linear address.
Linear address then is what paging uses, or if no paging is enabled, it simply becomes the physical address. Since paging is portable, and easier to deal with, most systems simply use a set of "dummy" segments: the limit is such that the whole 4GB of linear addresses can be referenced (effectually there is no limit) and the base address is 0, so the logical and linear address become the same.
The other purpose the descriptors serve, is to control which ring you are executing your code in (CPL in CS) and from which ring are you allowed to access the segments (DPL in DS/SS). So even with dummies, one needs a duplicate set of descriptors so that one can have on ring0 CS, one ring0 DS for code running with the ring0 CS to use, and then one ring3 CS, and one ring3 DS for the code running in ring3 to use.
If you have paging, such a setup is all you need, because you can then futher limit access in your page tables. One of the advantages of paging is that the memory addresses allowed for your code doesn't need to continuous (there could be pages not mapped between pages mapped) and it's easy to give a program more memory (just map more pages in). This also makes it possible to organize process memory like this:
Code: Select all
CODE|DATA|BSS (heap)--------break | unmapped | STACK
When heap needs to be grown, all that needs to be done is to move the break futher, and map some of the unmapped memory. But not only can heap be grown, you can also grow stack (backwards) by mapping more pages, as long as you have some unmapped memory in between.
With segmentation that's also possible (assuming you use separate DS and SS descriptors) but a compiler like GCC which assumes that SS and DS (and CS for that matter) co-exists (addresses inside one equal addresses inside another) will not work.
I was thinking that I would add separate GDT entries to userland (for code, data, stack etc.) which couldn't address below 1MG (roughly where I think the kernel is located), which would stop the app from meddling w/ the kernel and allow it to safely return control the kernel when it is done.
Yeah, that's possible to do, but the disadvantage is that you need to convert between addresses the userland sees, and the addresses your kernel sees. I think even for single tasking system, it's easier to just use flat segments and paging. All the complications of non-continuous physical memory, memory mapped IO regions, and what not, can be deal with when you initialize the paging system, and you can then map whatever normal free page at whatever address, with whatever access is necessary, without having to worry about system details.