;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; ;; ;; This file is part of 'M', a MIPS system emulator. ;; ;; ;; ;; (C) 2019 Stanislav Datskovskiy ( www.loper-os.org ) ;; ;; http://wot.deedbot.org/17215D118B7239507FAFED98B98228A001ABFFC7.html ;; ;; ;; ;; You do not have, nor can you ever acquire the right to use, copy or ;; ;; distribute this software ; Should you use this software for any purpose, ;; ;; or copy and distribute it to anyone or in any manner, you are breaking ;; ;; the laws of whatever soi-disant jurisdiction, and you promise to ;; ;; continue doing so for the indefinite future. In any case, please ;; ;; always : read and understand any software ; verify any PGP signatures ;; ;; that you use - for any purpose. ;; ;; ;; ;; See also http://trilema.com/2015/a-new-software-licensing-paradigm . ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;----------------------------------------------------------------------------- ;; State ;----------------------------------------------------------------------------- section .bss M resb 8 ; Addr mmap'd M (message) buffer Mbytes resb 8 ; Length of image (bytes) RAMbytes resb 8 ; Total size of RAM (bytes) ;----------------------------------------------------------------------------- section .text ;----------------------------------------------------------------------------- ; Jump if register value resides in given range ;----------------------------------------------------------------------------- %macro JMP_If_In 4 ; %1: register, %2: r.bottom, %3: r.top, %4: dest. cmp %1, %3 ; Compare register to TOP of range ja %%nope ; If above top -- skip. cmp %1, %2 ; Compare register to BOTTOM of range jb %%nope ; If below bottom -- skip. jmp %4 ; ... Else, register is in the range, and jump. %%nope %endmacro ;----------------------------------------------------------------------------- ;----------------------------------------------------------------------------- ; Allocate RAM: ;----------------------------------------------------------------------------- _ram_allocate: ;; Allocate memory for sim RAM: mov rax, SYS_MMAP xor r9, r9 ; offset=0 xor r8, r8 mov r10, MAP_PRIVATE | MAP_ANONYMOUS mov rdx, PROT_READ | PROT_WRITE mov rsi, [RAMbytes] ; # of bytes to allocate xor rdi, rdi ; os will give addr syscall ; invoke mmap ;; Verify whether we actually got our memory: cmp rax, 0 jg _mmap_ok ;; ... if not: EGGOG "Could not allocate memory for sim RAM!" _mmap_ok: ;; we got the memory; ret ;----------------------------------------------------------------------------- ;----------------------------------------------------------------------------- ; Free RAM: ;----------------------------------------------------------------------------- _ram_deallocate: mov rax, SYS_MUNMAP mov rsi, [RAMbytes] mov rdi, [M] syscall ; munmap(M, Mbytes) ret ;----------------------------------------------------------------------------- ;----------------------------------------------------------------------------- ; Init RAM: ;----------------------------------------------------------------------------- _ram_init: mov RAM_Floor, [M] ; Set 'floor' of RAM mov RAM_Ceiling, RAM_Floor ; 'ceiling' add RAM_Ceiling, [RAMbytes]; ... size of the mmap ;; TODO: test for unaligned floor or ceiling ret ;----------------------------------------------------------------------------- ;----------------------------------------------------------------------------- ; Translate_Address : virtual address in eax; output (physical addr) in eax ;----------------------------------------------------------------------------- align GRAIN, db 0x90 _Virt_To_Phys: bt eax, 31 ; CF := 31st (top) bit of vAddr jc _Above_7FFFFFFF ; If 31st bit = 1, kseg 0/1/2; else: ;; 0x00000000 <= vAddr <= 0x7FFFFFFF (kuseg) : bt CP0_Status, CP0St_ERL ; CF := CP0St_ERL Flag jnc _TLB_Lookup ; If ERL = 0: TLB Lookup required; else: jmp _No_Tlb_Lookup ; pAddr is equal to vAddr, return. _Above_7FFFFFFF: bt eax, 30 ; CF := 30th (2nd from top) bt of vAddr jc _Above_BFFFFFFF ; If 30th bit = 1 : kseg2; else: ;; 0x80000000 <= vAddr <= 0x9FFFFFFF (kseg0) : ;; 0xA0000000 <= vAddr <= 0xBFFFFFFF (kseg1) : and eax, 0x1FFFFFFF ; kseg0 and kseg1: clear top 3 bits, jmp _No_Tlb_Lookup ; i.e. pAddr := bottom 29 bts of vAddr. _Above_BFFFFFFF: ;; 0xC0000000 <= vAddr <= 0xFFFFFFFF (kseg2) : bt CP0_Status, CP0St_UM ; CF := CP0St_UM Flag jnc _TLB_Lookup ; If UM = 0: K. Mode, so do TLB; else: test CP0_Status, (1 << CP0St_EXL) | (1 << CP0St_ERL) ; EXL or ERL jnz _TLB_Lookup ; If EXL && ERL, K. Mode, do TLB ;; Else: vAddr is in kseg2, but we are NOT in Kernel Mode: Flg_Get IsWriting ; Is Writing? jc _V2P_Eggog_Wr ; If so, we want to set AdES; _V2P_Eggog_Rd: ; ... otherwise, set AdEL. SetEXC EXC_AdEL ; Fetch address error. jmp _V2P_Eggog_Fin ; Proceed to abort. _V2P_Eggog_Wr: SetEXC EXC_AdES ; Store address error. _V2P_Eggog_Fin: ;; Will go into exception handler instead of back to _Virt_xxx etc add rsp, 16 ; squelch return to _Virt_xxx and its caller push _Handle_Exception ; 'return' directly to exc handler. _No_Tlb_Lookup: Flg_Off IsWriting ; Consume 'is writing' flag. ret ; Done. _TLB_Lookup: ; TLB Lookup Required: ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Save ebx, ecx, edx, AUX, to xmm ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; movd xmm0, ebx movd xmm1, ecx movd xmm2, edx movd xmm3, AUX ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; Flg_Off ExcWasTLBNoMatch ; Clear the ExcWasTLBNoMatch Flag ;; Get the active ASID: mov edx, Sr(CP0_EntryHi) ; edx := CP0_EntryHi and edx, 0xFF ; edx := edx & 0xFF (get current ASID) ;; For each slot in table (0 .. 15), attempt lookup xor AUX, AUX ; Start with the 0-th entry in table _Lookup_TLB_E: mov ecx, eax ; ecx := eax (vAddr) and ecx, 0xFFFFF000 ; ecx := ecx & 0xFFFFF000 shr ecx, 13 ; ecx := ecx >> 13 (get vAddr's Tag) mov ebx, TLB_E(AUX) ; ebx := current TLB entry and ebx, TLB_VPN2_Mask ; get VPN2 of this entry cmp ebx, ecx ; cmp(entry.VPN2, vAddr.tag) jne _Lookup_TLB_E_Not_Here ; if entry.VPN2 != vAddr.tag: no match mov ebx, TLB_E(AUX) ; ebx := current TLB entry bt ebx, TLB_G ; is entry.G = 1? jc _Lookup_TLB_E_Match ; then match. shr ebx, TLB_ASID_Shift ; ebx := ebx >> TLB_ASID_Shift and ebx, TLB_ASID_Mask ; ebx := entry.ASID cmp ebx, edx ; entry.ASID = current ASID ? jne _Lookup_TLB_E_Not_Here ; if neither G=1 nor ASID match. mov ebx, TLB_E(AUX) ; ebx := current TLB entry _Lookup_TLB_E_Match: ; TLB Match: bt eax, 12 ; Test odd/even junior bit jc _Lookup_TLB_E_Match_Odd ; If odd: test V1, D1 _Lookup_TLB_E_Match_Even: ; If even: test V0, D0 lea ecx, TLB_PFN_E(AUX) ; prepare to load even PFN entry bt ebx, TLB_V0 ; Is entry.V0=1 ? jnc _Lookup_TLB_E_Invalid ; If not, TLBRET_INVALID bt ebx, TLB_D0 ; Is entry.D0=1 ? jc _Lookup_TLB_E_Match_Yes ; If entry.D0=1, then Match Yes jmp _Lookup_TLB_E_Match_Wr ; else, go to 'is writing?' _Lookup_TLB_E_Match_Odd: ; Odd bit: lea ecx, TLB_PFN_O(AUX) ; prepare to load odd PFN entry bt ebx, TLB_V1 ; Is entry.V1=1 ? jnc _Lookup_TLB_E_Invalid ; If not, TLBRET_INVALID bt ebx, TLB_D1 ; Is entry.D1=1 ? jc _Lookup_TLB_E_Match_Yes ; If entry.D1=1, then Match Yes _Lookup_TLB_E_Match_Wr: Flg_Get IsWriting ; Is Writing? jnc _Lookup_TLB_E_Match_Yes ; If not writing, go to Match Yes _Lookup_TLB_E_Dirty: ; ... else, Dirty: SetEXC EXC_Mod ; Set the EXC_Mod Exception jmp _Lookup_TLB_E_WriteExtr ; Write the 'extra data' and finish. _Lookup_TLB_E_Match_Yes: ; This is the 'success' case mov ebx, dword [ecx] ; Actually load the current PFN entry and eax, 0xFFF ; vAddr := vAddr & 0xFFF or eax, ebx ; vAddr := vAddr | entry.PFN[lowbit] jmp _Lookup_TLB_Done ; vAddr is now correct pAddr, done. _Lookup_TLB_E_Not_Here: ; try next one in the table, if any inc AUX ; index := index + 1 cmp AUX, TLB_ENTRIES_COUNT ; see if still in range 0 .. n-1 jb _Lookup_TLB_E ; if in range, go to next entry ;; ... else: Flg_On ExcWasTLBNoMatch ; Set the ExcWasTLBNoMatch Flag ;; ... now drop down into 'invalid' : _Lookup_TLB_E_Invalid: Flg_Get IsWriting ; Was Writing? jc _Lookup_TLB_E_Invalid_W ; If so, we want to set EXC_TLBS _Lookup_TLB_E_Invalid_R: ; Otherwise, set EXC_TLBL exception SetEXC EXC_TLBL ; Set the EXC_TLBL Exception jmp _Lookup_TLB_E_WriteExtr ; Write the 'extra data' and finish. _Lookup_TLB_E_Invalid_W: SetEXC EXC_TLBS ; Set the EXC_TLBS Exception ;; then drop down to 'write extra' : _Lookup_TLB_E_WriteExtr: ; Write the 'extra data' and finish mov Sr(CP0_BadVAddr), eax ; CP0_BadVAddr := vAddr mov ecx, eax ; ecx := vAddr mov ebx, Sr(CP0_Context) ; ebx := CP0_Context and ebx, ~0x007FFFFF ; ebx := ebx & ~0x007FFFFF shr ecx, 9 ; ecx := ecx >> 9 and ecx, 0x007FFFF0 ; ecx := ecx & 0x007FFFF0 or ebx, ecx ; ebx := ebx | ecx mov Sr(CP0_Context), ebx ; CP0_Context := ebx mov ecx, eax ; ecx := vAddr mov ebx, Sr(CP0_EntryHi) ; ebx := CP0_EntryHi and ebx, 0xFF ; ebx := ebx & 0xFF and ecx, 0xFFFFE000 ; ecx := ecx & 0xFFFFE000 or ebx, ecx ; ebx := ebx | ecx mov Sr(CP0_EntryHi), ebx ; CP0_EntryHi := ebx ;; Will go into exception handler instead of back to _Virt_xxx etc add rsp, 16 ; squelch return to _Virt_xxx and its caller push _Handle_Exception ; 'return' directly to exc handler. ;; and drop into 'done' : _Lookup_TLB_Done: Flg_Off IsWriting ; Consume 'is writing' flag. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Restore ebx, ecx, edx, AUX, from xmm ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; movd ebx, xmm0 movd ecx, xmm1 movd edx, xmm2 movd AUX, xmm3 ret ;----------------------------------------------------------------------------- ;----------------------------------------------------------------------------- ; ReadWord - Source Address (Virtual) is EAX; Resulting Word Read is EAX. ;----------------------------------------------------------------------------- ; TODO: 1) is the 'unaligned' case in fact physically possible? ; 2) would go faster if used tmp instead of self-clobbering rax ? align GRAIN, db 0x90 _Virt_Read_Word: call _Virt_To_Phys ; Transform vAddr to pAddr test eax, 0x3 ; Are any of the bottom 2 bits set? jnz _V_Rd_Word_Unaligned ; If yes, go to eggog. Else: ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; If pAddr is in Memory-Mapped Device space: JMP_If_In eax, MMIO_BASE, MMIO_TOP, _Phys_Device_Read_Word ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; add rax, RAM_Floor ; Calculate the target PC RAM addr cmp rax, RAM_Ceiling ; compare the above with RAM ceiling jnb _V_Rd_Word_Hit_Ceiling ; if not below: RAM ceiling eggog! ;; Address is valid, so load the word: mov eax, dword [rax] ; eax := dword [rax] ret ; Done, return with read result in eax. _V_Rd_Word_Unaligned: ;; TODO: print address ACHTUNG "Virt_Read_Word: Unaligned Physical Address!" jmp _V_Rd_Word_Eggog ; Go to eggogology _V_Rd_Word_Hit_Ceiling: ;; TODO: print address ACHTUNG "Virt_Read_Word: Hit RAM Ceiling!" _V_Rd_Word_Eggog: SetEXC EXC_AdEL ; Fetch address error. ;; Will go into exception handler instead of back to caller add rsp, 8 ; squelch return to original caller push _Handle_Exception ; 'return' directly to exc handler. ret ; Go there. ;----------------------------------------------------------------------------- ;----------------------------------------------------------------------------- ; WriteWord - Destination Address (Virtual) is EAX; Word to be Written is TMP. ;----------------------------------------------------------------------------- ; TODO: 1) is the 'unaligned' case in fact physically possible? ; 2) would go faster if used tmp instead of self-clobbering rax ? ; 3) do we need to explicitly zero-extend rax here? align GRAIN, db 0x90 _Virt_Write_Word: Flg_On IsWriting ; Tell the translator that we're writing call _Virt_To_Phys ; Transform vAddr to pAddr test eax, 0x3 ; Are any of the bottom 2 bits set? jnz _V_Wr_Word_Unaligned ; If yes, go to eggog. Else: ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; If pAddr is in Memory-Mapped Device space: JMP_If_In eax, MMIO_BASE, MMIO_TOP, _Phys_Device_Write_Word ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; add rax, RAM_Floor ; Calculate the target PC RAM addr cmp rax, RAM_Ceiling ; compare the above with RAM ceiling jnb _V_Wr_Word_Hit_Ceiling ; if not below: RAM ceiling eggog! ;; Address is valid, so write the word: mov dword [rax], TMP ; dword [rax] := TMP ret ; Done. _V_Wr_Word_Unaligned: ;; TODO: print address ACHTUNG "Virt_Write_Word: Unaligned Physical Address!" jmp _V_Wr_Word_Eggog _V_Wr_Word_Hit_Ceiling: ;; TODO: print address ACHTUNG "Virt_Write_Word: Hit RAM Ceiling!" _V_Wr_Word_Eggog: SetEXC EXC_AdES ; Store address error. ;; Will go into exception handler instead of back to caller add rsp, 8 ; squelch return to original caller push _Handle_Exception ; 'return' directly to exc handler. ret ; Go there. ;----------------------------------------------------------------------------- ;----------------------------------------------------------------------------- ; ReadByte - Source Address (Virtual) is EAX; Resulting Byte Read is EAX. ;----------------------------------------------------------------------------- align GRAIN, db 0x90 _Virt_Read_Byte: call _Virt_To_Phys ; Transform vAddr to pAddr ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; If pAddr is in Memory-Mapped Device space: JMP_If_In eax, MMIO_BASE, MMIO_TOP, _Phys_Device_Read_Byte ;;;;;;;;;;;;;;;;;; ENDIANISM ;;;;;;;;;;;;;;;;;; %ifdef LITTLE_ENDIAN ;; little -- do nothing %else ;; big -- flip : xor eax, 0x3 ; Flip the 3 unalignment bits. %endif ;;;;;;;;;;;;;;;;;; ENDIANISM ;;;;;;;;;;;;;;;;;; add rax, RAM_Floor ; Calculate the target PC RAM addr cmp rax, RAM_Ceiling ; compare the above with RAM ceiling jnb _V_Rd_Byte_Hit_Ceiling ; if not below: RAM ceiling eggog! ;; Address is valid, so proceed to load: movzx ax, byte [rax] ; ax := byte [rax] and eax, 0xFF ; keep only bottom byte ret ; Done, return with read result in eax. _V_Rd_Byte_Hit_Ceiling: ACHTUNG "Virt_Read_Byte: Hit RAM Ceiling!" ;; TODO: print address SetEXC EXC_AdEL ; Fetch address error. ;; Will go into exception handler instead of back to caller add rsp, 8 ; squelch return to original caller push _Handle_Exception ; 'return' directly to exc handler. ret ; Go there. ;----------------------------------------------------------------------------- ;----------------------------------------------------------------------------- ; WriteByte - Destination Address (Virtual) is EAX; Byte to be Written is DL. ;----------------------------------------------------------------------------- align GRAIN, db 0x90 _Virt_Write_Byte: Flg_On IsWriting ; Tell the translator that we're writing call _Virt_To_Phys ; Transform vAddr to pAddr ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; If pAddr is in Memory-Mapped Device space: JMP_If_In eax, MMIO_BASE, MMIO_TOP, _Phys_Device_Write_Byte ;;;;;;;;;;;;;;;;;; ENDIANISM ;;;;;;;;;;;;;;;;;; %ifdef LITTLE_ENDIAN ;; little -- do nothing %else ;; big -- flip : xor eax, 0x3 ; Flip the 3 unalignment bits. %endif ;;;;;;;;;;;;;;;;;; ENDIANISM ;;;;;;;;;;;;;;;;;; add rax, RAM_Floor ; Calculate the target PC RAM addr cmp rax, RAM_Ceiling ; compare the above with RAM ceiling jnb _V_Wr_Byte_Hit_Ceiling ; if not below: RAM ceiling eggog! ;; Address is valid, so write the byte: mov byte [rax], DL ; dword [rax] := TMP ret ; Done. _V_Wr_Byte_Hit_Ceiling: ACHTUNG "Virt_Write_Byte: Hit RAM Ceiling!" ;; TODO: print address SetEXC EXC_AdES ; Store address error. ;; Will go into exception handler instead of back to caller add rsp, 8 ; squelch return to original caller push _Handle_Exception ; 'return' directly to exc handler. ret ; Go there. ;----------------------------------------------------------------------------- ;----------------------------------------------------------------------------- ; Write TLB Entry at the given index. Index (0 .. 15) is in AUX. ; Kills eax, ebx, ecx, edx. ;----------------------------------------------------------------------------- _write_tlb_entry: mov edx, Sr(CP0_EntryHi) ; edx := CP0_EntryHi mov ecx, edx ; ecx := edx shr ecx, 13 ; ecx := ecx >> 13 to get VPN2 and edx, TLB_ASID_Mask ; edx := edx & 0xFF to get ASID shl edx, TLB_ASID_Shift ; edx := edx << 19 to put ASID in place or ecx, edx ; now we have VPN2 and ASID in ecx ;; done with edx, can reuse mov edx, Sr(CP0_EntryLo0) ; edx := CP0_EntryLo0 mov ebx, Sr(CP0_EntryLo1) ; ebx := CP0_EntryLo1 ;; get G: mov eax, edx ; eax := CP0_EntryLo0 and eax, ebx ; eax := eax & CP0_EntryLo1 and eax, 0x1 ; eax := eax & 1 to get G shl eax, TLB_G ; move G bit into position or ecx, eax ; ecx := ecx | eax to put in G bit ;; now ecx contains VPN2, ASID, G ;; Get V0 from CP0_EntryLo0 and put in ecx where belongs: mov eax, edx ; eax := CP0_EntryLo0 and eax, 0x2 ; eax := eax & 0x2 to get V0 bit shl eax, (TLB_V0 - 1) ; put V0 bit in position or ecx, eax ; ecx := ecx | eax to put in V0 bit ;; Get D0 from CP0_EntryLo0 and put in ecx where belongs: mov eax, edx ; eax := CP0_EntryLo0 and eax, 0x4 ; eax := eax & 0x4 to get D0 bit shl eax, (TLB_D0 - 2) ; put D0 bit in position or ecx, eax ; ecx := ecx | eax to put in D0 bit ;; Get V1 from CP0_EntryLo1 and put in ecx where belongs: mov eax, ebx ; eax := CP0_EntryLo1 and eax, 0x2 ; eax := eax & 0x2 to get V1 bit shl eax, (TLB_V1 - 1) ; put V1 bit in position or ecx, eax ; ecx := ecx | eax to put in V1 bit ;; Get D1 from CP0_EntryLo1 and put in ecx where belongs: mov eax, ebx ; eax := CP0_EntryLo1 and eax, 0x4 ; eax := eax & 0x4 to get D1 bit shl eax, (TLB_D1 - 2) ; put D1 bit in position or ecx, eax ; ecx := ecx | eax to put in D1 bit ;; Write the TLB entry to the given index (in AUX) : and AUX, 0xF ; Index of TLB entry is bottom 4 bits mov TLB_E(AUX), ecx ; Write TLB entry in ecx to n-th slot. ;; Transform CP0_EntryLo0 (edx) into PFN0: shr edx, 6 and edx, 0xFFFFF shl edx, 12 ;; Transform CP0_EntryLo1 (ebx) into PFN1: shr ebx, 6 and ebx, 0xFFFFF shl ebx, 12 ;; Store PFN: mov TLB_PFN_E(AUX), edx ; Store PFN0 mov TLB_PFN_O(AUX), ebx ; Store PFN1 ret ; Done. ;-----------------------------------------------------------------------------