Page 1 of 2
Trying to set up a stack segment with limits and failing
Posted: Sun Mar 02, 2025 11:51 am
by sboydlns
Up until now I have been using the GDT settings from the tutorials. That is one massive 4GB address space for the code and data segments. Now I am trying to set up a segment for the stack with some hard limits. There is clearly something that I am not understanding about all of this since I can't seem to get it to work. I'm not getting exceptions but memory is being corrupted and I can't figure out why.
My stack segment looks like this:
Code: Select all
gdt_stack: # limit 3e00 - 07dff
.word 0x3fff # segment limit 0-15
.word 0x3e00 # base 0-15
.byte 0x0 # base 15-23
.byte 0x92 # present, priv 0, data segment, writable
.byte 0x40 # byte granularity, 32-bit, limit 16-19
.byte 0 # base 24-31
I am setting %SS to 0x18 (which is the offset into my GDT where this segment descriptor is located) and I am loading %ESP with 0x3ff0. According to my understanding this should have the effect of locating my stack at 0x7df0 (0x3e00 + 0x3ff0). Yet this doesn't seem to be happening. As I said earlier, memory is being corrupted.
Setting %SS to the offset to my data segment and setting %ESP to 0x7df0 works as expected.
I am puzzled. Any help is appreciated.
TIA
Re: Trying to set up a stack segment with limits and failing
Posted: Sun Mar 02, 2025 2:35 pm
by iansjack
Without seeing your code it’s not easy to guess what you are doing wrong. Do you have an on-line repository?
Re: Trying to set up a stack segment with limits and failing
Posted: Sun Mar 02, 2025 3:16 pm
by MichaelPetch
An observation: The 32-bit and 64-bit GCC compilers emit code that assumes that the base of SS, DS, ES, and CS are all 0. Non zero base will cause problems for emitted C code especially if you are passing the address of a stack based variable around. I recall from your previous posts you are using GCC/clang.
QEMU's software emulator doesn't do segment limit checks (for performance) so if you do have an ESP that falls outside the stack segment limits you won't get an #SS exception. Running with `--enable-kvm` may produce those #SS exceptions.
Re: Trying to set up a stack segment with limits and failing
Posted: Sun Mar 02, 2025 3:19 pm
by iansjack
I was assuming the code was assembly as the small fraction quoted is. But, as you say, if it’s C or C++ you have to assume a flat memory model.
Re: Trying to set up a stack segment with limits and failing
Posted: Sun Mar 02, 2025 3:28 pm
by sboydlns
iansjack wrote: ↑Sun Mar 02, 2025 2:35 pm
Without seeing your code it’s not easy to guess what you are doing wrong. Do you have an on-line repository?
Unfortunately, I don't.
After a bit more poking around with the debugger I have confirmed that my stack is in the place where I expect it to be. It almost seems as though my .data section has been moved as well. So it's not so much a data corruption problem as it is the data isn't where my C code expects to find it.
May some code snippets will help.
GDT definition
Code: Select all
.align 8
gdt:
.long 0 # system reserved entry
.long 0
gdt_code: # limit 0 - ffffffff
.word 0xffff # segment limit 0-15
.word 0x0 # base 0-15
.byte 0x0 # base 15-23
.byte 0x9a # present, priv 0, code segment, readable
.byte 0xcf # 4k blocks, 32-bit, limit 16-19
.byte 0 # base 24-31
gdt_data: # limit 0 - ffffffff
.word 0xffff # segment limit 0-15
.word 0x0 # base 0-15
.byte 0x0 # base 15-23
.byte 0x92 # present, priv 0, data segment, writable
.byte 0xcf # 4k blocks, 32-bit, limit 16-19
.byte 0 # base 24-31
gdt_stack: # limit 3e00 - 07dff
.word 0x3fff # segment limit 0-15
.word 0x3e00 # base 0-15
.byte 0x0 # base 15-23
.byte 0x92 # present, priv 0, data segment, writable
.byte 0x40 # byte granularity, 32-bit, limit 16-19
.byte 0 # base 24-31
gdt_end:
.equ gdt_len, gdt_end - gdt
.equ CODE_SEG, gdt_code - gdt
.equ DATA_SEG, gdt_data - gdt
.equ STACK_SEG, gdt_stack - gdt
Set up 32 bit mode
Code: Select all
# Copy initial GDT to low memory
cld # set movs to increment
movw $gdt,%si # move from our copy of GDT
movw $GDT,%di # to the one in low memory
movw $gdt_len,%cx # for the length of our copy
rep
movsb
# Enable 32-bit protected mode
cli # disable interrupts until we are ready
movw $GDT_LEN,GDT_REG # init the GDT register with the length
movl $GDT,GDT_REG+2 # & the address
lgdt GDT_REG # load the global descriptor table
movl %cr0,%eax # set protected mode
orl $0x01,%eax
movl %eax,%cr0
jmp $CODE_SEG,$start # force CS to use entry from GDT
.code32
# From this point on, we are in 32-bit protected mode.
start:
movw $DATA_SEG,%ax # set data segment registers
movw %ax,%ds
movw %ax,%es
movw %ax,%fs
movw %ax,%gs
# Set up a stack
#
# GDT stack segment is configured to support a 16K kernel
# stack occupying 0x3e00 - 0x7dff
movw $STACK_SEG,%ax
movw %ax,%ss
movl $0x3ff0,%esp # set bottom of stack to end of boot area
Using the debugger I have verified that DS contains 0x10 and SS contains 0x18. Changing the offset on the segment description that I am using for the stack causes everything to start working again. Hmmmm!
Re: Trying to set up a stack segment with limits and failing
Posted: Sun Mar 02, 2025 3:48 pm
by sboydlns
MichaelPetch wrote: ↑Sun Mar 02, 2025 3:16 pm
An observation: The 32-bit and 64-bit GCC compilers emit code that assumes that the base of SS, DS, ES, and CS are all 0. Non zero base will cause problems for emitted C code especially if you are passing the address of a stack based variable around. I recall from your previous posts you are using GCC/clang.
That would explain it. I guess I naively assumed that using the GDT would provide a convenient way to relocate code without have to do anything except load it at the new address and modify the base in the segment descriptor.
This raises a question that isn't relevant at this point but will become so. If C assumes that segment bases are zero then how does one move .data sections around in memory if the heap needs to be expanded or whatever? I always assumed you would move the section and modify the GDT. But I'm guessing that is not the case. Possibly that has something to do with paging? Which I haven't really looked at yet.
Re: Trying to set up a stack segment with limits and failing
Posted: Sun Mar 02, 2025 4:42 pm
by Demindiro
sboydlns wrote: ↑Sun Mar 02, 2025 3:48 pm
This raises a question that isn't relevant at this point but will become so. If C assumes that segment bases are zero then how does one move .data sections around in memory if the heap needs to be expanded or whatever? I always assumed you would move the section and modify the GDT. But I'm guessing that is not the case. Possibly that has something to do with paging? Which I haven't really looked at yet.
The heap and .data sections are unrelated.
.data is loaded when the program is loaded (by the kernel or some helper program) and remains at a fixed place (after relocations are applied, if necessary). Generally, the program is responsible for providing its own heap. Usually libc or whatever equivalent you use is responsible for initializing the allocator.
The user-space memory allocator shouldn't have to care where exactly memory is allocated. It just needs to be able to get memory from the kernel (who in turn should know which regions are unallocated). Paging is useful here since you can leave unused ranges unmapped to save memory.
The kernel of course needs to know what is actually RAM and usable, but again that's unrelated to .data, the allocator shouldn't touch that.
Re: Trying to set up a stack segment with limits and failing
Posted: Mon Mar 03, 2025 1:51 am
by rdos
The simple answer is that you cannot use segmentation if you use GCC or any other "mainstream" compiler that assumes a flat memory model.
I use OpenWatcom, which was designed when flat memory model was not the standard, and it supports both 16-bit and 32-bit segmentation.
Re: Trying to set up a stack segment with limits and failing
Posted: Mon Mar 03, 2025 2:34 am
by iansjack
Forget segmentation and use paging instead. You'll need it if you ever want to support 64-bit operation.
Re: Trying to set up a stack segment with limits and failing
Posted: Mon Mar 03, 2025 7:36 am
by sboydlns
First of all, thank you all for your feed back.
Let me start out by saying that I am an old mainframe programmer so I have lots of ideas about how an operating system SHOULD work that may or may not be portable to x86. Also x86 architecture is, at least at the operating system level, still new to me. This whole foray into x86 operating systems is just something to keep me occupied until spring gets here and I can get outside and do the things that really matter. So, I'm trying to keep this all as simple as possible.
I was thinking of a real memory OS with no paging. Because, to an old mainframer, 4GB of memory is unimaginably large and I can't see myself ever running out. Feel free to cast whatever stones you like here.
Having said all that, I've been thinking about the things you all said and some things come to mind.
1) It was said that since GCC uses a flat memory model it expects all segment base addresses to be zero. If that is true, then doesn't the GDT become more-or-less useless. If every task needs a flat zero thru 4GB memory space then the GDT serves no purpose. It doesn't even offer any separation of OS and user memory spaces.
2) About the segment base addresses needing to be zero. Wouldn't it be more accurate to say that all base addresses for a GCC process need to be the same, not necessarily zero. If this were true, then the OS could have a flat zero thru 4GB address space while each user process could have a flat address space of something less than 4GB starting at the address where the process was loaded. Or am I still missing something important?
3) Given the limitations of the GCC flat address constraint, it isn't difficult to see why writing a secure OS on x86 seems to be so difficult. There is no separation of code, data and stack spaces.
Re: Trying to set up a stack segment with limits and failing
Posted: Mon Mar 03, 2025 8:08 am
by Demindiro
sboydlns wrote: ↑Mon Mar 03, 2025 7:36 am
First of all, thank you all for your feed back.
Let me start out by saying that I am an old mainframe programmer so I have lots of ideas about how an operating system SHOULD work that may or may not be portable to x86. Also x86 architecture is, at least at the operating system level, still new to me. This whole foray into x86 operating systems is just something to keep me occupied until spring gets here and I can get outside and do the things that really matter. So, I'm trying to keep this all as simple as possible.
I was thinking of a real memory OS with no paging. Because, to an old mainframer, 4GB of memory is unimaginably large and I can't see myself ever running out. Feel free to cast whatever stones you like here.
Whether 4GB is a lot depends on what you're doing. You certainly can fit a fully functional OS and plenty more with that space, but having more is useful when processing a lot of data. Swapping from/to disk is certainly an option, but might be slow for many random reads/writes.
Having said all that, I've been thinking about the things you all said and some things come to mind.
1) It was said that since GCC uses a flat memory model it expects all segment base addresses to be zero. If that is true, then doesn't the GDT become more-or-less useless. If every task needs a flat zero thru 4GB memory space then the GDT serves no purpose. It doesn't even offer any separation of OS and user memory spaces.
2) About the segment base addresses needing to be zero. Wouldn't it be more accurate to say that all base addresses for a GCC process need to be the same, not necessarily zero. If this were true, then the OS could have a flat zero thru 4GB address space while each user process could have a flat address space of something less than 4GB starting at the address where the process was loaded. Or am I still missing something important?
Minor caveat: hardware DMA doesn't care about segments. Though it doesn't care about paging either.
And yes, the GDT is merely an artefact, especially in 64-bit mode where it is pretty much ignored.
3) Given the limitations of the GCC flat address constraint, it isn't difficult to see why writing a secure OS on x86 seems to be so difficult. There is no separation of code, data and stack spaces.
There is: paging supports RWX bits as well as some more bits for kernel/userspace separation.
The difficulty is more so with outright hardware bugs like Meltdown and overcomplicated and/or unsuitable security models.
Re: Trying to set up a stack segment with limits and failing
Posted: Mon Mar 03, 2025 8:12 am
by iansjack
Paging is not about shortage of memory. It is not the same as paging memory to disk.
Paging provides very good protection of memory areas, efficient use of physical RAM, and provides a large address space (essentially infinite when PAE is used), however much real memory you have. And it makes life easier to be able to load all user programs at the same address.
If you ever move on to 64-bit operation (and, IMO, 32-bit is pretty much dead nowadays) then you will have to use paging and you will have to use a flat address space. Why not start now?
You are correct that a GDT is a requirement that is essentially useless nowadays. That's the price of backwards compatibility.
Re: Trying to set up a stack segment with limits and failing
Posted: Mon Mar 03, 2025 8:17 am
by sboydlns
iansjack wrote: ↑Mon Mar 03, 2025 8:12 am
Paging is not about shortage of memory. It is not the same as paging memory to disk.
See, this is where my old mainframe brain is causing problems. To me, paging = swapping. Guess I'm going to have to read that section of the manual after all. Sigh!
Re: Trying to set up a stack segment with limits and failing
Posted: Mon Mar 03, 2025 9:11 am
by iansjack
It is confusing and many people think that paging means "paging to disk". It's actually a matter of "paging memory", i.e. dividing the logical memory address space into pages (by default 4K in size but other sizes are available) that are mapped to (usually at a different physical address) a page of physical memory. Various protections can be applied to the page, and each process can have it's own page table so it sees a different address space.
I the processor tries to access a page that is not mapped, or not present, it triggers a page fault exception which the OS can respond to. For example, in the case of a stack, you don't know in advance how big you want it to be. So you can allocate a few pages followed by a guard page, which is not present. If the stack wants to grow beyond it's allotted size the OS can allocate another page, set up a new guard page, and continue running the program. As far as the program is concerned nothing has happened. With the large address space provided by PAE you cvan set the stack at an address that is never going to conflict with any data area. And because you can run all user programs at the same (logical) address they can't access another process's memory (unless you want that to happen by sharing a page) and you can place them so that there is no possibility of them conflicting with other memory areas.
Paging is really cool. It's worth taking the time to understand it. There's quite a good article here:
https://os.phil-opp.com/paging-introduction/ (it's written with Rust in mind, but still applies) that might be worth a look rather than trying to understand it just from the Intel manuals.
Re: Trying to set up a stack segment with limits and failing
Posted: Tue Mar 04, 2025 2:02 am
by rdos
iansjack wrote: ↑Mon Mar 03, 2025 8:12 am
Paging is not about shortage of memory. It is not the same as paging memory to disk.
Paging provides very good protection of memory areas, efficient use of physical RAM, and provides a large address space (essentially infinite when PAE is used), however much real memory you have. And it makes life easier to be able to load all user programs at the same address.
Paging and the flat memory model provide NO PROTECTION AT ALL within an application, or between device drivers in a kernel.
Also, you are mistaken if you think a 32-bit OS will either use paging or segmentation. It's perfectly possible to use both, as they solve different problems. Paging solves physical memory management issues, while segmentation solve protection issues.
iansjack wrote: ↑Mon Mar 03, 2025 8:12 am
If you ever move on to 64-bit operation (and, IMO, 32-bit is pretty much dead nowadays) then you will have to use paging and you will have to use a flat address space. Why not start now?
There is a "segmentation feature" in x86_64 (long mode) that few people seem to use, and which GCCs linker cannot handle properly. Since long mode is essentially a 64-bit capable mode that uses 32-bit offsets, you can isolate your kernel device drivers by placing them in different 4G segments, and thereby providing some rudimentary protection. However, mainstream OSes still link flat binaries where drivers are adjacent in linear memory, and therefore are essentially unprotected.
iansjack wrote: ↑Mon Mar 03, 2025 8:12 am
You are correct that a GDT is a requirement that is essentially useless nowadays. That's the price of backwards compatibility.
I make heavy use of both the GDT and LDT. They are not useless for me.