Fiddly little bit-twiddling. Nobody likes it, but if you’re writing boot ROMs, device drivers, or low-level system functions, you’ve got to roll your sleeves up and do it. That’s especially true when you’re tweaking your processor’s MMU to create and enable virtual memory, paging, or protections. To make life easier, we herewith present Little Chunks o’ Code to help with that.
Most of what we’ve discussed so far applies to most any processor, but today’s code snippets are obviously x86-specific. The same virtual memory concepts may apply to ARM, MIPS, RISC-V, and other CPU families, but the code presented here sure won’t.
The first bit of code creates your page directory and two page tables. Both tables are minimal, with just a few entries apiece. It’s written as a function or subroutine that you can call from somewhere else, although you don’t have to. It also makes some assumptions about where your RAM is located, but that’s a hard-coded number that’s easy to tweak.
The first program relies on the second program, which is a little function to create new segment descriptors in the GDT. And it, in turn, relies on the third program to find an unused GDT entry it can use. All three pieces can be used standalone, or together.
; This program creates page directory and page tables and enables paging. ; No address translation is performed (i.e., identity-mapped). ; Assumptions: ; 1) Physical memory from 0x00001000 through 0x00003FFF is not already in use. ; Define some useful descriptor type values… DRO EQU 0 ;data, read-only DRW EQU 1 ;data, read/write SRO EQU 2 ;stack, read-only SRW EQU 3 ;stack, read/write CEO EQU 4 ;code, execute-only CER EQU 5 ;code, execute/read CEOC EQU 6 ;code, conforming, execute-only CERC EQU 7 ;code, conforming, execute/read page_enable PROC FAR ; Create a page directory MOV EAX, 1000h ;base address = 0x00001000 MOV EBX, 1000h ;size = 4096 bytes NOV CL, DRW ;type = read/write data segment MOV CH, 0 ;privilege = level 0 CALL make_descriptor OR EAX, EAX JL error ;failed, couldn't make descriptor MOV ES, AX ;ES gets new segment selector MOV EDI, 0 MOV EAX, 0 MOV ECX, 1024 REP STOSD ;start by zeroing out the table MOV DWORD PTR ES:, 00002003h ;1st PDE -> 1st page table MOV DWORD PTR ES:[0FFCh], 00003003h ;1024th PDE -> 2nd page table ; Create first page table... MOV EAX, 2000h ;base address = 0x00002000 MOV EBX, 1000h ;size = 4096 bytes MOV CL, DRW ;type = read/write data segment MOV CH, 0 ;privilege = level 0 CALL make_descriptor OR EAX, EAX JL error ;couldn’t make descriptor MOV ES, AX ;ES gets new segment selector MOV EDI, 0 MOV EAX, 00000003h ;first page frame address = 0 MOV ECX, 1024 table1: STOSD ;write one PTE ADD EAX, 1000h ;increment page frame address LOOP table1 ;repeat 1024 times ; Create second page table MOV EAX, 3000h ;base address = 0x00003000 MOV EBX, 1000h ;size = 1024 bytes MOV CL, DRW ;type = read/write data segment MOV CH, 0 ;privilege = level 0 CALL make_descriptor OR EAX, EAX JL error ;couldn’t make descriptor MOV ES,AX ;ES gets segment selector MOV EDI, 0 MOV EAX, 0 MOV ECX, 1008 REP STOSD ;zero first 1008 PTEs MOV EAX, 0FFFF0003h ;1009th page frame address = 0xFFFF0000 MOV ECX, 16 table2: STOSD ;write one PTE ADD EAX, 1000h ;increment page frame address LOOP table2 ;repeat 16 times ;Enable paging MOV EAX, 1000h ;physical address of page directory = 0x1000 MOV CR3, EAX ;load into Control Register 3 PUSHFD ;save EFLAGS register CLI ;disable interrupts temporarily MOV EAX, CR0 ;copy CR0 into EAX BTS EAX, 31 ;set bit 31 MOV CR0, EAX ** enable paging ** JMP flush ;flush prefetch queue flush: NOP POPFD ;restore EFLAGS RET
Listing 1: Sample program to create an x86 page directory and two page tables.
This second program creates a new segment descriptor based on the parameters you pass to it. It moves the input data around to conform to the somewhat convoluted format that x86 MMUs require. It also does one small security check, ensuring that the privilege level of the new segment it creates is not any more privileged than the program that’s calling it.
; Create a segment descriptor in the GDT. ; All information is passed in registers. ; Returns 16-bit selector in AX, or -1 if error. ; Assumptions: ; 1) Segment register DS holds selector to GDT alias as writable data segment ; 2) Base address is passed in EAX 932 bits) ; 3) Segment length (not limit) is passed in EBX (32 bits) ; 4) Segment type is passed in CL (3 bits) ; 5) Segment privilege level is passed in CH (2 bits) make_descriptor PROC FAR PUSH EDX ;save work registers PUSH EAX CALL find_first_GDT ;find the index of the first available slot MOV EDX, EAX ;EDX gets free slot, or -1 if none POP EAX OR EDX, EDX JL error ;sorry, no free GDT slots available base_address: MOV DWORD PTR DS:[EDX*8]+2, EAX ;write base address bits 23-00 ROL EAX, 8 MOV BYTE PTR DS”[EDX*8]+4, AL ;write base address bits 31-24 ROR EAX, 8 DEC EBX ;segment length becomes segment limit CMP EBX, 0FFFFFh ;is limit under 1 MB? JA page_granular ;no, go to page-granular setup byte_granular: MOV WORD PTR DS:[EDX*8]+0, BX ;write limits bits 15-00 ROL EBX, 16 MOV BYTE PTR DS:[EDX*8]+6, BL ;write limit bits 19-16 ROR EBX, 16 AND BYTE PTR DS:[EDX*8]+6, 0Fh ;drop G, D, X, U bits OR BYTE PTR DS:[EDX*8]+6, 40h ;set D bit JMP segment_type page_granular: SHR EBX, 12 ;shift limit right 12 bits MOV DWORD PTR DS:[EDX*8] +0, BX ;write limit bits 15-00 ROL EBX, 16 MOV BYTE PTR DS:[EDX*8]+6, BL ;write limit bits 19-16 ROR EBX, 16 AND BYTE PTR DS:[EDX*8]+6, 0Fh ;drop G, D, X, U bits OR BYTE PTR DS:[EDX*8]+6, 0CDh ;set G and D bits segment_type: MOV BYTE PTR DS:[EDX*8]+5, 0 ;clear Access Rights byte CMP AL, CERC JA error ; sorry, invalid segment type SHL CL, 1 ;shift ot make room for A bit OR BYTE PTR DS:[EDX*8]+5, CL ;write Type bits and A bit priv_level: MOVZX AX, CH ;AX gets requested privilege level MOV BX, WORD PTR SS:[ESP]+4 ;BX gets caller’s CPL ARPL AX, BX ;adjust AX if necessary SHL AL, 5 ;move DPL field into position OR BYTE PTR DS:[EDX*8]+5, AL ;set DPL bits OR BYTE PTR DS:[EDX*8]+5, 90h ;set Present and System bits MOV AX, DX ;AX gets GDT index SHL AX, 3 ;index * 8 = selector ARPL AX, BX ;adjust RPL field if necessary POP EDX RET error: POP EDX MOV EAX, 0 DEC EAX RET ;return -1
Listing 2: Sample program to create an x86 segment descriptor in the GDT based in input parameters.
This little guy simply searches through the Global Descriptor Table (GDT) looking for an unused slot it can use. If there are none, it returns –1, otherwise you get the index of the free slot in EAX. It needs no input parameters. As above, this assumes that you can both read and write the GDT via data segment DS.
; Return in to the first free GDT descriptor in EAX, or -1 if none. ; Assumptions: ; 1) Segment selector in DS points to an alias of the GDT as writable data. find_free_GDT: PUSH EBX ;save work registers PUSH ECX MOV BX, DS ;BX gets alias selector MOVZX EBX, BX ;extend BX to all 32 bits LSL ECX, EBX ;ECX gets alias segment limit SHR ECX, 3 ;limit / 8 = number of slots JECXZ not_found ;null length GDT; exit MOV EAX, 0 ;start at GDT(1) next: INC EAX CMP BYTE PTR DS:[EAX*8]+5, 0 ;Access Rights byte = 0? LOOPNZ next ;repeat as necessary JNZ not_found found: POP ECX POP EBX RET ;return value in EAX not_found: POP ECX POP EBX MOV EAX, 0 DEC EAX RET ;return -1 in EAX
Listing 3: Sample program to locate the first unused descriptor slot in the GDT.