/*
 * hal_boot.S - Kernel boot entry point
 *
 * Copyright (c) 2017 Maxime Villard
 * This code is inspired a lot from the NetBSD boot procedure, written by
 * Maxime Villard too. XXX copyright
 *
 * This file is part of ALMOS-MKH.
 *
 * ALMOS-MKH is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by
 * the Free Software Foundation; version 2.0 of the License.
 *
 * ALMOS-MKH is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with ALMOS-MKH.; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
 */

#define x86_ASM
#include <hal_boot.h>
#include <hal_multiboot.h>
#include <hal_register.h>
#include <hal_segmentation.h>

#define MULTIBOOT_HEADER_MAGIC  0x1BADB002
#define MULTIBOOT_HEADER_FLAGS	0x00000000
#define MULTIBOOT_INFO_MAGIC	0x2BADB002
#define CHECKSUM -(MULTIBOOT_HEADER_MAGIC + MULTIBOOT_HEADER_FLAGS)

#if L2_SLOT_KERNBASE > 0
#define TABLE_L2_ENTRIES (2 * (NKL2_KIMG_ENTRIES + 1))
#else
#define TABLE_L2_ENTRIES (NKL2_KIMG_ENTRIES + 1)
#endif

#if L3_SLOT_KERNBASE > 0
#define TABLE_L3_ENTRIES (2 * NKL3_KIMG_ENTRIES)
#else
#define TABLE_L3_ENTRIES NKL3_KIMG_ENTRIES
#endif

#define PROC0_PML4_OFF	0
#define PROC0_STK_OFF	(PROC0_PML4_OFF + 1 * PAGE_SIZE)
#define PROC0_PTP3_OFF	(PROC0_STK_OFF + STKPAGES * PAGE_SIZE)
#define PROC0_PTP2_OFF	(PROC0_PTP3_OFF + NKL4_KIMG_ENTRIES * PAGE_SIZE)
#define PROC0_PTP1_OFF	(PROC0_PTP2_OFF + TABLE_L3_ENTRIES * PAGE_SIZE)
#define TABLESIZE \
  ((NKL4_KIMG_ENTRIES + TABLE_L3_ENTRIES + TABLE_L2_ENTRIES + 1 + STKPAGES) \
    * PAGE_SIZE)

/*
 * fillkpt - Fill in a kernel page table
 *	eax = pte (page frame | control | status)
 *	ebx = page table address
 *	ecx = number of pages to map
 *
 * Each entry is 8 (PDE_SIZE) bytes long: we must set the 4 upper bytes to 0.
 */
#define fillkpt	\
	cmpl	$0,%ecx			;	/* zero-sized? */	\
	je 	2f			; \
1:	movl	$0,(PDE_SIZE-4)(%ebx)	;	/* upper 32 bits: 0 */	\
	movl	%eax,(%ebx)		;	/* store phys addr */	\
	addl	$PDE_SIZE,%ebx		;	/* next PTE/PDE */	\
	addl	$PAGE_SIZE,%eax		;	/* next phys page */	\
	loop	1b			; \
2:					;

/*
 * fillkpt_nox - Same as fillkpt, but sets the NX/XD bit.
 */
#define fillkpt_nox \
	cmpl	$0,%ecx			;	/* zero-sized? */	\
	je 	2f			; \
1:	movl	$PG_NX32,(PDE_SIZE-4)(%ebx);	/* upper 32 bits: NX */	\
	movl	%eax,(%ebx)		;	/* store phys addr */	\
	addl	$PDE_SIZE,%ebx		;	/* next PTE/PDE */	\
	addl	$PAGE_SIZE,%eax		;	/* next phys page */	\
	loop	1b			; \
2:					;

/*
 * fillkpt_blank - Fill in a kernel page table with blank entries
 *	ebx = page table address
 *	ecx = number of pages to map
 */
#define fillkpt_blank	\
	cmpl	$0,%ecx			;	/* zero-sized? */	\
	je 	2f			; \
1:	movl	$0,(PDE_SIZE-4)(%ebx)	;	/* upper 32 bits: 0 */	\
	movl	$0,(%ebx)		;	/* lower 32 bits: 0 */	\
	addl	$PDE_SIZE,%ebx		;	/* next PTE/PDE */	\
	loop	1b			; \
2:					;

/*
 * killkpt - Destroy a kernel page table (long mode)
 *	rbx = page table address
 *	rcx = number of pages to destroy
 */
#define killkpt \
1:	movq	$0,(%rbx)	; \
	addq	$PDE_SIZE,%rbx	; \
	loop	1b		;

/* 32bit version of PG_NX */
#define PG_NX32	0x80000000

#define	RELOC(x)	((x) - KERNBASE)

	.globl	start_x86_64
	.globl	L4paddr
	.globl	iom_base

/*
 * The multiboot header
 */
	.section .boot,"ax",@progbits
multiboot_header:
	.align	4
	.long	MULTIBOOT_HEADER_MAGIC
	.long	MULTIBOOT_HEADER_FLAGS
	.long	CHECKSUM

/*
 * The variables used in the boot procedure.
 */
	.section .data
	.align	4

	.type	L4paddr, @object
	L4paddr:	.quad	0	/* paddr of L4 */

	.type	iom_base, @object
	iom_base:	.quad	0	/* virt. addr. of ISA I/O MEM */


#define GDT64_LIMIT gdt64_end-gdt64_start-1
/* Temporary gdt64, with base address in low memory */
	.type	gdt64_lo, @object
gdt64_lo:
	.word	GDT64_LIMIT
	.quad	RELOC(gdt64_start)
.align 64

/* Temporary gdt64, with base address in high memory */
	.type	gdt64_hi, @object
gdt64_hi:
	.word	GDT64_LIMIT
	.quad	gdt64_start
.align 64
#undef GDT64_LIMIT

	.type	gdt64_start, @object
gdt64_start:
	.quad 0x0000000000000000	/* always empty */
	.quad 0x00af9a000000ffff	/* kernel CS */
	.quad 0x00cf92000000ffff	/* kernel DS */
gdt64_end:

	.type	farjmp64, @object
farjmp64:
	.long	RELOC(longmode)
	.word	GDT_FIXED_SEL(GDT_KCODE_SEL, SEL_KPL)
.align 64

	/* Space for the temporary stack */
	.size	tmpstk, tmpstk - .
	.space	512
tmpstk:

	.text

start_x86_64:
	.code32

	/* Warm boot */
	movw	$0x1234,0x472
	cld

	/* Make sure it is a multiboot-compliant bootloader. */
	cmpl	$MULTIBOOT_INFO_MAGIC,%eax
	je	1f
	ret	/* what to do? */
1:

	movl	$RELOC(tmpstk),%esp

	/* Reset the PSL. */
	pushl	$PSL_MBO
	popfl

	/*
	 * Copy the various multiboot structures
	 */
	movl	%ebx,%esi			/* src */
	movl	$RELOC(mb_info),%edi		/* dst */
	movl	$MULTIBOOT_INFO_SIZE,%ecx	/* len */
	rep
	movsb	/* copy esi -> edi */

	testl	$MULTIBOOT_INFO_HAS_LOADER_NAME,MB_MI_FLAGS(%ebx)
	jz	1f
	movl	MB_MI_LOADER_NAME(%ebx),%esi	/* src */
	movl	$RELOC(mb_loader_name),%edi	/* dst */
copy_loop:
	cmpb	$0,(%esi)
	je	copy_end
	movsb	/* copy esi -> edi */
	jmp	copy_loop
copy_end:
	movsb	/* copy esi -> edi */
1:

	testl	$MULTIBOOT_INFO_HAS_MMAP,MB_MI_FLAGS(%ebx)
	jz	1f
	movl	MB_MI_MMAP_ADDR(%ebx),%esi	/* src */
	movl	$RELOC(mb_mmap),%edi		/* dst */
	movl	MB_MI_MMAP_LENGTH(%ebx),%ecx	/* len */

	rep
	movsb	/* copy esi -> edi */
1:

/*
 * There are four levels of pages in amd64: PML4 -> PDP -> PD -> PT. They will
 * be referred to as: L4 -> L3 -> L2 -> L1.
 *
 * Virtual address space of the kernel:
 * +---------------+------+-----------------------------------+-------------+
 * | TEXT + RODATA | DATA | L4 -> PROC0 STK -> L3 -> L2 -> L1 | ISA I/O MEM |
 * +---------------+------+-----------------------------------+-------------+
 *                       (1)                                 (2)
 *
 * PROC0 STK is obviously not linked as a page level. It just happens to be
 * caught between L4 and L3.
 *
 * (PROC0 STK + L4 + L3 + L2 + L1) is later referred to as BOOTSTRAP TABLES.
 *
 * ISA I/O MEM has no physical page allocated here, just virtual addresses.
 *
 * Important note: the kernel segments are properly 4k-aligned
 * (see kernel_x86.ld), so there's no need to enforce alignment.
 */

	/* Find end of kernel image; brings us on (1). */
	movl	$RELOC(__kernel_end),%edi

	/* Align up for BOOTSTRAP TABLES. */
	movl	%edi,%esi
	addl	$PGOFSET,%esi
	andl	$~PGOFSET,%esi

	/* We are on the BOOTSTRAP TABLES. Save L4's physical address. */
	movl	$RELOC(L4paddr),%ebp
	movl	%esi,(%ebp)
	movl	$0,4(%ebp)

	/* Now, zero out the BOOTSTRAP TABLES (before filling them in). */
	movl	%esi,%edi
	xorl	%eax,%eax
	cld
	movl	$TABLESIZE,%ecx
	shrl	$2,%ecx
	rep
	stosl				/* copy eax -> edi */

/*
 * Build the page tables and levels. We go from L1 to L4, and link the levels
 * together. Note: RELOC computes &addr - KERNBASE in 32 bits; the value can't
 * be > 4G, or we can't deal with it anyway, since we are in 32bit mode.
 */
	/*
	 * Build L1.
	 */
	leal	(PROC0_PTP1_OFF)(%esi),%ebx

	/* Skip the area below the kernel text. */
	movl	$(KERNTEXTOFF_LO - KERNBASE_LO),%ecx
	shrl	$PGSHIFT,%ecx
	fillkpt_blank

	/* Map the kernel code RX. */
	movl	$(KERNTEXTOFF_LO - KERNBASE_LO),%eax	/* start of TEXT */
	movl	$RELOC(__kernel_data_start),%ecx
	subl	%eax,%ecx
	shrl	$PGSHIFT,%ecx
	orl	$(PG_V|PG_KR|PG_G),%eax
	fillkpt

	/* Map the kernel data RW. */
	movl	$RELOC(__kernel_data_start),%eax
	movl	$RELOC(__kernel_end),%ecx
	subl	%eax,%ecx
	shrl	$PGSHIFT,%ecx
	orl	$(PG_V|PG_KW|PG_G),%eax
	fillkpt_nox

	/* Map the BOOTSTRAP TABLES RW. */
	movl	$RELOC(__kernel_end),%eax		/* start of BOOTSTRAP TABLES */
	movl	$TABLESIZE,%ecx		/* length of BOOTSTRAP TABLES */
	shrl	$PGSHIFT,%ecx
	orl	$(PG_V|PG_KW|PG_G),%eax
	fillkpt_nox

	/* We are on (2). Map ISA I/O MEM RW. */
	movl	$IOM_BEGIN,%eax
	movl	$IOM_SIZE,%ecx	/* size of ISA I/O MEM */
	shrl	$PGSHIFT,%ecx
	orl	$(PG_V|PG_KW|PG_G),%eax
	fillkpt_nox

	/*
	 * Build L2. Linked to L1.
	 */
	leal	(PROC0_PTP2_OFF)(%esi),%ebx
	leal	(PROC0_PTP1_OFF)(%esi),%eax
	orl	$(PG_V|PG_KW),%eax
	movl	$(NKL2_KIMG_ENTRIES+1),%ecx
	fillkpt

#if L2_SLOT_KERNBASE > 0
	/* If needed, set up level 2 entries for actual kernel mapping */
	leal	(PROC0_PTP2_OFF + L2_SLOT_KERNBASE * PDE_SIZE)(%esi),%ebx
	leal	(PROC0_PTP1_OFF)(%esi),%eax
	orl	$(PG_V|PG_KW),%eax
	movl	$(NKL2_KIMG_ENTRIES+1),%ecx
	fillkpt
#endif

	/*
	 * Build L3. Linked to L2.
	 */
	leal	(PROC0_PTP3_OFF)(%esi),%ebx
	leal	(PROC0_PTP2_OFF)(%esi),%eax
	orl	$(PG_V|PG_KW),%eax
	movl	$NKL3_KIMG_ENTRIES,%ecx
	fillkpt

#if L3_SLOT_KERNBASE > 0
	/* If needed, set up level 3 entries for actual kernel mapping */
	leal	(PROC0_PTP3_OFF + L3_SLOT_KERNBASE * PDE_SIZE)(%esi),%ebx
	leal	(PROC0_PTP2_OFF)(%esi),%eax
	orl	$(PG_V|PG_KW),%eax
	movl	$NKL3_KIMG_ENTRIES,%ecx
	fillkpt
#endif

	/*
	 * Build L4 for identity mapping. Linked to L3.
	 */
	leal	(PROC0_PML4_OFF)(%esi),%ebx
	leal	(PROC0_PTP3_OFF)(%esi),%eax
	orl	$(PG_V|PG_KW),%eax
	movl	$NKL4_KIMG_ENTRIES,%ecx
	fillkpt

	/* Set up L4 entries for actual kernel mapping */
	leal	(PROC0_PML4_OFF + L4_SLOT_KERNBASE * PDE_SIZE)(%esi),%ebx
	leal	(PROC0_PTP3_OFF)(%esi),%eax
	orl	$(PG_V|PG_KW),%eax
	movl	$NKL4_KIMG_ENTRIES,%ecx
	fillkpt

	/* Install recursive top level PDE (one entry) */
	leal	(PROC0_PML4_OFF + PDIR_SLOT_PTE * PDE_SIZE)(%esi),%ebx
	leal	(PROC0_PML4_OFF)(%esi),%eax
	orl	$(PG_V|PG_KW),%eax
	movl	$1,%ecx
	fillkpt_nox

	/*
	 * Startup checklist:
	 * 1. Enable PAE (and SSE while here).
	 */
	movl	%cr4,%eax
	orl	$(CR4_PAE|CR4_OSFXSR|CR4_OSXMMEXCPT|CR4_PGE),%eax
	movl	%eax,%cr4

	/*
	 * 2. Set Long Mode Enable in EFER. Also enable the syscall extensions,
	 *    and NOX.
	 */
	movl	$MSR_EFER,%ecx
	rdmsr
	xorl	%eax,%eax	/* XXX */
	orl	$(EFER_LME|EFER_SCE|EFER_NXE),%eax
	wrmsr

	/*
	 * 3. Load %cr3 with pointer to PML4.
	 */
	movl	%esi,%eax
	movl	%eax,%cr3

	/*
	 * 4. Enable paging and the rest of it.
	 */
	movl	%cr0,%eax
	orl	$(CR0_PE|CR0_PG|CR0_NE|CR0_TS|CR0_MP|CR0_WP|CR0_AM),%eax
	movl	%eax,%cr0
	jmp	compat
compat:

	/*
	 * 5. Not quite done yet, we're now in a compatibility segment, in
	 *    legacy mode. We must jump to a long mode segment. Need to set up
	 *    a temporary GDT with a long mode segment in it to do that.
	 */
	movl	$RELOC(gdt64_lo),%eax
	lgdt	(%eax)
	movl	$RELOC(farjmp64),%eax
	ljmp	*(%eax)

	.code64
longmode:
	/*
	 * 6. Finally, we're in long mode. However, we're still in the identity
	 *    mapped area (could not jump out of that earlier because it would
	 *    have been a > 32bit jump). We can do that now, so here we go.
	 */
	movabsq	$longmode_hi,%rax
	jmp	*%rax

longmode_hi:
	/*
	 * We left the identity mapped area. Base address of
	 * the temporary gdt64 should now be in high memory.
	 */
	movq	$RELOC(gdt64_hi),%rax
	lgdt	(%rax)

	/*
	 * We have arrived. There's no need anymore for the identity mapping in
	 * low memory, remove it.
	 */
	movq	$KERNBASE,%r8

#if L2_SLOT_KERNBASE > 0
	movq	$(NKL2_KIMG_ENTRIES+1),%rcx
	leaq	(PROC0_PTP2_OFF)(%rsi),%rbx	/* old, phys address */
	addq	%r8,%rbx			/* new, virt address */
	killkpt
#endif

#if L3_SLOT_KERNBASE > 0
	movq	$NKL3_KIMG_ENTRIES,%rcx
	leaq	(PROC0_PTP3_OFF)(%rsi),%rbx	/* old, phys address */
	addq	%r8,%rbx			/* new, virt address */
	killkpt
#endif

	movq	$NKL4_KIMG_ENTRIES,%rcx
	leaq	(PROC0_PML4_OFF)(%rsi),%rbx	/* old, phys address of PML4 */
	addq	%r8,%rbx			/* new, virt address of PML4 */
	killkpt

	/* Save the virtual address of ISA I/O MEM. */
	movq	$(TABLESIZE+KERNBASE),%rdx
	addq	%rsi,%rdx
	movq	%rdx,iom_base(%rip)

	/* Set up bootstrap stack. */
	leaq	(PROC0_STK_OFF)(%rsi),%rax
	addq	%r8,%rax
	leaq	(STKSIZE)(%rax),%rsp
	xorq	%rbp,%rbp			/* mark end of frames */

	xorw	%ax,%ax
	movw	%ax,%gs
	movw	%ax,%fs

	/* The first physical page available. */
	leaq	(TABLESIZE)(%rsi),%rdi

	call	init_x86_64

