Assumption: All the commands assume that you are located in the NalogaLongMode folder!
Important: When starting qemu choose the 4th option with no additional drivers!
For the program to work and perform it's function, that is to use more than 4G of memory, long mode is needed. Besides long mode the cpu must support 1-Gbyte physical-page translation. To check this I have written a small program, to run it please start qemu with the folowing command:
qemu-system-x86_64 -hda disk.vmdk -drive file=fat:disk:rw:"./" |
And when the VM starts enter the folowing commands:
D: |
The program should return two lines of text. The fist will indicate if long mode is supported: Long mode is supported or Long mode is not supported. And the second line will indecate if 1-Gbyte physical-page translation is supported:1G pages supported or 1G pages not supported.
For the program to work both features must be supported!
The main program is compiled to a binary file using the folowing command:
nasm -fbin Main.asm -o Main.com |
To run the program you must first start qemu. To do that please enter the folowing command into terminal:
qemu-system-x86_64 -hda disk.vmdk -drive file=fat:disk:rw:"./" -monitor stdio -m 6G |
When you get to the FreeDOS promp enter the folowing command:
D:Main.com |
The program may take a while to finish its operation. When it's finished will print out a message that it is done.
The point of the program is to enter Long mode (64-bit mode) where we can address more than 4GB of ram. Long mode is very similar to protected mode and the entry point is almost the same as well. To enter long mode you MUST follow the following steps:
The program skips the final 3 steps as the program doesnt use interrupts or the stack.
So lets start; First the program jumps to the beginning of code and sets cs to zero. After the fist jump an empty IDT is decleared for later use and some space for a small stack.
jmp 0x0000:main
ALIGN 4
IDT:
.Length dw 0
.Base dd 0
dd 0,0,0,0
stack: |
The next thing we do is clear segment registers es and ss and load the beginnig of stack into esp and address of a buffer to edi. This buffer is decleared at the end of the program in the .bss section and is later used to store Page tables.
main:
xor eax,eax
mov es,eax
mov edi,page
mov ss, ax
mov eax, stack
mov esp, eax |
To create the Page tables we must first clear the memory, to do that we just need to write a constant (0) to every memory location. This is done with the folowing code:
SwitchToLongMode:
; Zero out the 16KiB buffer.
; Since we are doing a rep stosd, count should be bytes/4.
push di ; REP STOSD alters DI.
mov ecx, 0x1000 ; Repeat 0x1000 times
xor eax, eax ; Value to be written
cld ; Clear direction flag
rep stosd
pop di |
Now we need to actualy fill the Page tables. Only one entry is needed in the PML4 page table as each entry can address 521GB of memory. The entry is made of the PDT and a few flags. The PDT address is at edi+0x1000 and present and write flags must also be set.
; Build the Page Map Level 4.
; es:di points to the Page Map Level 4 table.
lea eax, [es:di + 0x1000] ; Put the address of the Page Directory Pointer Table in to EAX.
or eax, PAGE_PRESENT | PAGE_WRITE ; Or EAX with the flags - present flag, writable flag.
mov [es:di], eax ; Store the value of EAX as the first PML4E. |
Next we will fill the PDT table to address 5GB of memory. For this we need 5 entries. They are made of the base physical address and a few flags. The bits we must set are the present bit, write and 1GB page translation so that we need to create only two page tables one with one entry and the second one with 4 etries.
; Build the Page Directory Table
push di ; Save DI for the time being.
lea di, [di + 0x1000] ; Point DI to the page table.
mov eax, PAGE_PRESENT | PAGE_WRITE | PAGE_G ; Move the flags into EAX - and point it to 0x0000.
mov [es:di], eax ; Write first entry into PDPE page table, which addresses the first 1GB of memory
add eax, 0x40000000 ; Increase start adress
add di, 8
mov [es:di], eax ; Write second entry into PDPE page table, which addresses the second GB of memory
add eax, 0x40000000 ; Increase address
add di, 8
mov [es:di], eax ; Write third entry into PDPE page table, which addresses the third GB of memory
add eax, 0x40000000 ; Increase address
add di, 8
mov [es:di], eax ; Write fourth entry into PDPE page table, which addresses the fourth GB of memory
; To address space higher than 4G we need to write a bit to a higher position, as the eax register can only hold 32-bit values
mov eax, PAGE_PRESENT | PAGE_WRITE | PAGE_G ; Reset eax to start position
mov ebx, 0x00000001 ; Set value for setting the next highest bit
mov [es:di], eax ; Write the lower half of the fifth entry
add di, 4
mov [es:di], ebx ; Write bit to the next position, fifth entry now complete.
; Now there is 5G of memory mapped
pop di ; Restore DI. |
The Page tables are now created and present in memory the next step is to disable interrupts.
; Disable IRQs
mov al, 0xFF ; Out 0xFF to 0xA1 and 0x21 to disable all IRQs.
out 0xA1, al
out 0x21, al
nop
nop |
Now that interrupts are dissabled we can load the empty IDT.
xor ax,ax
mov ds,ax
lidt [IDT] ; Load a zero length IDT so that any NMI causes a triple fault. |
There isnt much left to do, so we can start entering long mode. To do this we must first enable PAE and PGE. Load PML4 address into control register 3, set the LME bit in EFER MSR, load the GDT and finaly do a long jump to set the code segment to point into the GDT code segment.
; Enter long mode.
mov eax, 10100000b ; Set the PAE and PGE bit.
mov cr4, eax
mov edx, edi ; Point CR3 at the PML4.
mov cr3, edx
mov ecx, 0xC0000080 ; Read from the EFER MSR.
rdmsr
or eax, 0x00000100 ; Set the LME bit.
wrmsr
mov ebx, cr0 ; Activate long mode -
or ebx,0x80000001 ; - by enabling paging and protection simultaneously.
mov cr0, ebx
lgdt [GDT.Pointer] ; Load GDT.Pointer defined below.
jmp CODE_SEG:LongMode ; Load CS with 64 bit segment and flush the instruction cache |
For completnes here is the GDT table structure.
; Global Descriptor Table
GDT:
.Null:
dq 0x0000000000000000 ; Null Descriptor - should be present.
.Code:
dq 0x00209A0000000000 ; 64-bit code descriptor (exec/read).
dq 0x0000920000000000 ; 64-bit data descriptor (read/write).
ALIGN 4
dw 0 ; Padding to make the "address of the GDT" field aligned on a 4-byte boundary
.Pointer:
dw $ - GDT - 1 ; 16-bit Size (Limit) of GDT.
dd GDT |
And finaly the 64-bit code. Before we do anything we must set all of the segment register to select the GDT's data section/segment. After that we just write some value to the first 4GB of memory and print a message that we are done.
[BITS 64]
LongMode:
mov ax, DATA_SEG
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
mov rax, 0x100000 ; Move start address to 64bit register rax
mov rcx, 0x100000000 ; End address
write:
mov qword [rax], 0x2a ;write value to memory
add rax,0x8
cmp rax,rcx ;Check if we are done
jl write
; Blank out the screen to a blue color.
mov edi, 0xB8000
mov rcx, 500 ; Since we are clearing uint64_t over here, we put the count as Count/4.
mov rax, 0x1F201F201F201F20 ; Set the value to set the screen to: Blue background, white foreground, blank spaces.
rep stosq ; Clear the entire screen.
; Display "Done"
mov edi, 0x00b8000
mov rax, 0x1F651F6e1F6f1F44
mov [edi],rax
hlt |
Do not use any of the FreeDOS drivers, they will just be a hasle when trying to debug your program! You can trust me with that as I have been pulling my brains out for the last few days. At least if you do use them the first thing to check if you are having problems with loading the IDT or GDT tables is to see if paging is disabled. If the program is working in the MBR it should also work in DOS. The thing that might be going wrong is that you are writing to an address that you shouldn't be!
http://developer.amd.com/wordpress/media/2012/10/24593_APM_v21.pdf
https://en.wikibooks.org/wiki/X86_Assembly/Protected_Mode
http://wiki.osdev.org/Entering_Long_Mode_Directly
http://wiki.osdev.org/Setting_Up_Long_Mode
http://os.phil-opp.com/entering-longmode.html
http://wiki.osdev.org/GDT_Tutorial
http://wiki.osdev.org/Interrupt_Descriptor_Table