I tossed it in BOCHS and one observation I had was that
the GDT is defined with Data Segment that is limited to the first 64MB. That is done in this code:
Code: Select all
GlobalDescriptorTable::GlobalDescriptorTable()
: nullSegmentSelector(0, 0, 0),
unusedSegmentSelector(0, 0, 0),
codeSegmentSelector(0, 64*1024*1024, 0x9A),
dataSegmentSelector(0, 64*1024*1024, 0x92)
It appears that the Frame Buffer is beyond the 64MB. What happens if you modify the dataSegmentSelector so its range is the entire 4gb address space with:
Code: Select all
GlobalDescriptorTable::GlobalDescriptorTable()
: nullSegmentSelector(0, 0, 0),
unusedSegmentSelector(0, 0, 0),
codeSegmentSelector(0, 64*1024*1024, 0x9A),
dataSegmentSelector(0, 0xFFFFFFFF, 0x92)
As for why your code works if you place it before interrupt activation? It is because the author of the tutorial has done something incorrectly. It seems he decided to model his GDT after the one GRUB uses rather than do the correct thing. He shouldn't be relying on CS, DS, ES, FS, GS having the values he thinks GRUB is using (GRUB can change the GDT it uses, so there are no guarantees). After he does his LGDT instruction he doesn't reload these segment registers from his own GDT. He's still using GRUB's base, limits, and access rights through the cached descriptors. GRUB's GDT per the spec creates code and data selectors for the entire 4gb address space. So in your code that works (when placed before the interrupt activation) you are using 4gb flat memory descriptors that allow the framebuffer address to be accessible since it is within the 4gb address space.
Note: the framebuffer address is usually an address >64MB
When you move your rendering code after interrupt activation your interrupt handler will fire at some point while trying to render your bitmap (likely before anything on screen gets updated). During the first interrupt you push all the segment registers (ES, FS, DS, GS) and then restore them with POPs. The problem here is the first time your interrupt handler exits the POP %DS will reload %DS with the descriptor that the OS author created in his GDT. That will limit the address space to 64MB. When the interrupt returns at some point you will access the frame buffer which is now outside the segment limits of DS and it will cause a fault. The end result is likely nothing from your bitmap will ever be displayed.
Note: some virtual environment may check the limits before accessing memory and some do not. You may find despite the 64MB limits in your OS, some virtual machines will blindly allow you to exceed the limits. This speeds up emulators as an extra limit check doesn't have to be done on each memory access. QEMU may work if run without the
-enable-kvm and fail with it. Bochs will always check the memory limits so will fail and warn on the console.
I would say that the code that calls LGDT should properly handle setting the CS,DS,ES,FS,GS segment registers explicitly after issuing LGDT. Why he limited the data segment to 64MB is something you'd have to ask him. I looked at his tutorial a couple years ago and saw only the first few videos. He may have an explanation as to why he set a 64MB limit. I'd say use the full 4gb address space as shown in the code change at the beginning of this answer.
A fix to the LGDT concerns I raised could be just to create an assembly routine that sets the data selectors (including the stack) and code segment selector after issuing an LGDT instruction. Add this to one of the assembly files like interruptstubs.s (or create a new assembly file):
Code: Select all
.global load_gdt
load_gdt:
mov 4(%esp), %edx # EDX is 1st argument - GDT record pointer
mov 8(%esp), %eax # EAX is 2nd argument - Data Selector
lgdt (%edx) # Load GDT Register with GDT record at pointer passed as 1st argument
mov %eax, %ds # Reload all the data descriptors with Data selector (2nd argument)
mov %eax, %es
mov %eax, %gs
mov %eax, %fs
mov %eax, %ss
pushl 12(%esp) # Create FAR pointer on stack using Code selector (3rd argument)
pushl $.setcs # Offset of FAR JMP will be setcs label below
ljmp *(%esp) # Do the FAR JMP to next instruction to set CS with Code selector, and
# set the EIP (instruction pointer) to offset of setcs
.setcs:
add $8, %esp # Restore stack (remove 2 DWORD values we put on stack to create FAR Pointer)
ret
Then modify gdt.cpp to be something like:
Code: Select all
extern "C" void load_gdt(uint8_t *gdt_ptr, uint32_t data_sel, uint32_t code_sel);
GlobalDescriptorTable::GlobalDescriptorTable()
: nullSegmentSelector(0, 0, 0),
unusedSegmentSelector(0, 0, 0),
codeSegmentSelector(0, 64*1024*1024, 0x9A),
dataSegmentSelector(0, 0xFFFFFFFF, 0x92)
{
uint32_t i[2];
i[1] = (uint32_t)this;
i[0] = sizeof(GlobalDescriptorTable) << 16;
load_gdt((((uint8_t *) i)+2), DataSegmentSelector(), CodeSegmentSelector());
}
If you are intent on using inline assembly it is possible to convert the external assembly function. This should work:
Code: Select all
GlobalDescriptorTable::GlobalDescriptorTable()
: nullSegmentSelector(0, 0, 0),
unusedSegmentSelector(0, 0, 0),
codeSegmentSelector(0, 64*1024*1024, 0x9A),
dataSegmentSelector(0, 0xFFFFFFFF, 0x92)
{
uint32_t i[2];
i[1] = (uint32_t)this;
i[0] = sizeof(GlobalDescriptorTable) << 16;
asm volatile("lgdt %[gdtr]\n\t"
"mov %[datasel], %%ds\n\t"
"mov %[datasel], %%es\n\t"
"mov %[datasel], %%gs\n\t"
"mov %[datasel], %%fs\n\t"
"mov %[datasel], %%ss\n\t"
"pushl %[codesel]\n\t"
"pushl $1f\n\t"
"ljmp *(%%esp)\n"
"1:\n\t"
"add $8, %%esp"
:
:[gdtr]"m"(*(((uint8_t *) i)+2)),
[codesel]"g"((uint32_t)CodeSegmentSelector()),
[datasel]"r"((uint32_t)DataSegmentSelector())
: "memory");
}
This ensures that the selectors are reloaded based on the descriptors in your own GDT and not using GRUB's GDT. You can now eliminate the unusedSegmentSelector variable altogether. I don't think the original author of the tutorial understood how to properly reload all the segment registers (including CS).