/* SPDX-License-Identifier: GPL-2.0 */
/*
 * ACPI wakeup real mode startup stub
 */
#include <linux/linkage.h>
#include <asm/segment.h>
#include <asm/msr-index.h>
#include <asm/page_types.h>
#include <asm/pgtable_types.h>
#include <asm/processor-flags.h>
#include "realmode.h"
#include "wakeup.h"

	.code16

/* This should match the structure in wakeup.h */
	.section ".data", "aw"

	.balign	16
SYM_DATA_START(wakeup_header)
	video_mode:	.short	0	/* Video mode number */
	pmode_entry:	.long	0
	pmode_cs:	.short	__KERNEL_CS
	pmode_cr0:	.long	0	/* Saved %cr0 */
	pmode_cr3:	.long	0	/* Saved %cr3 */
	pmode_cr4:	.long	0	/* Saved %cr4 */
	pmode_efer:	.quad	0	/* Saved EFER */
	pmode_gdt:	.quad	0
	pmode_misc_en:	.quad	0	/* Saved MISC_ENABLE MSR */
	pmode_behavior:	.long	0	/* Wakeup behavior flags */
	realmode_flags:	.long	0
	real_magic:	.long	0
	signature:	.long	WAKEUP_HEADER_SIGNATURE
SYM_DATA_END(wakeup_header)

	.text
	.code16

	.balign	16
SYM_CODE_START(wakeup_start)
	cli
	cld

	LJMPW_RM(3f)
3:
	/* Apparently some dimwit BIOS programmers don't know how to
	   program a PM to RM transition, and we might end up here with
	   junk in the data segment descriptor registers.  The only way
	   to repair that is to go into PM and fix it ourselves... */
	movw	$16, %cx
	lgdtl	%cs:wakeup_gdt
	movl	%cr0, %eax
	orb	$X86_CR0_PE, %al
	movl	%eax, %cr0
	ljmpw	$8, $2f
2:
	movw	%cx, %ds
	movw	%cx, %es
	movw	%cx, %ss
	movw	%cx, %fs
	movw	%cx, %gs

	andb	$~X86_CR0_PE, %al
	movl	%eax, %cr0
	LJMPW_RM(3f)
3:
	/* Set up segments */
	movw	%cs, %ax
	movw	%ax, %ss
	movl	$rm_stack_end, %esp
	movw	%ax, %ds
	movw	%ax, %es
	movw	%ax, %fs
	movw	%ax, %gs

	lidtl	.Lwakeup_idt

	/* Clear the EFLAGS */
	pushl $0
	popfl

	/* Check header signature... */
	movl	signature, %eax
	cmpl	$WAKEUP_HEADER_SIGNATURE, %eax
	jne	bogus_real_magic

	/* Check we really have everything... */
	movl	end_signature, %eax
	cmpl	$REALMODE_END_SIGNATURE, %eax
	jne	bogus_real_magic

	/* Call the C code */
	calll	main

	/* Restore MISC_ENABLE before entering protected mode, in case
	   BIOS decided to clear XD_DISABLE during S3. */
	movl	pmode_behavior, %edi
	btl	$WAKEUP_BEHAVIOR_RESTORE_MISC_ENABLE, %edi
	jnc	1f

	movl	pmode_misc_en, %eax
	movl	pmode_misc_en + 4, %edx
	movl	$MSR_IA32_MISC_ENABLE, %ecx
	wrmsr
1:

	/* Do any other stuff... */

#ifndef CONFIG_64BIT
	/* This could also be done in C code... */
	movl	pmode_cr3, %eax
	movl	%eax, %cr3

	btl	$WAKEUP_BEHAVIOR_RESTORE_CR4, %edi
	jnc	1f
	movl	pmode_cr4, %eax
	movl	%eax, %cr4
1:
	btl	$WAKEUP_BEHAVIOR_RESTORE_EFER, %edi
	jnc	1f
	movl	pmode_efer, %eax
	movl	pmode_efer + 4, %edx
	movl	$MSR_EFER, %ecx
	wrmsr
1:

	lgdtl	pmode_gdt

	/* This really couldn't... */
	movl	pmode_entry, %eax
	movl	pmode_cr0, %ecx
	movl	%ecx, %cr0
	ljmpl	$__KERNEL_CS, $pa_startup_32
	/* -> jmp *%eax in trampoline_32.S */
#else
	jmp	trampoline_start
#endif
SYM_CODE_END(wakeup_start)

bogus_real_magic:
1:
	hlt
	jmp	1b

	.section ".rodata","a"

	/*
	 * Set up the wakeup GDT.  We set these up as Big Real Mode,
	 * that is, with limits set to 4 GB.  At least the Lenovo
	 * Thinkpad X61 is known to need this for the video BIOS
	 * initialization quirk to work; this is likely to also
	 * be the case for other laptops or integrated video devices.
	 */

	.balign	16
SYM_DATA_START(wakeup_gdt)
	.word	3*8-1		/* Self-descriptor */
	.long	pa_wakeup_gdt
	.word	0

	.word	0xffff		/* 16-bit code segment @ real_mode_base */
	.long	0x9b000000 + pa_real_mode_base
	.word	0x008f		/* big real mode */

	.word	0xffff		/* 16-bit data segment @ real_mode_base */
	.long	0x93000000 + pa_real_mode_base
	.word	0x008f		/* big real mode */
SYM_DATA_END(wakeup_gdt)

	.section ".rodata","a"
	.balign	8

	/* This is the standard real-mode IDT */
	.balign	16
SYM_DATA_START_LOCAL(.Lwakeup_idt)
	.word	0xffff		/* limit */
	.long	0		/* address */
	.word	0
SYM_DATA_END(.Lwakeup_idt)
