/*
 * first-dos.S86  -  MGL DOS loader
 *
 * Copyright (C) 2003-2007 Gero Kuhlmann   <gero@gkminix.han.de>
 *
 *  This program 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; either version 2 of the License, or
 *  any later version.
 *
 *  This program 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 this program; if not, write to the Free Software
 *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 * $Id: first-dos.S86,v 1.8 2007/01/06 18:31:37 gkminix Exp $
 *
 *====================================================================
 */
#define DEF_HEAP_SIZE
#define DEF_STACK_SIZE

#include <common.i86>
#include <system/bios.i86>
#include <dos/general.i86>
#include <dos/exec.i86>
#include <net/ethernet.i86>
#include <net/bootp.i86>
#include "headers/runtime.i86"

.file __FILE__
.line __LINE__



/*
 *====================================================================
 *
 * Definition of heap and stack size
 */

/* Define some minimal stack */
__stack_size = 8192

/* Heap is just required to hold the BOOTP record */
__heap_size = 4096




/*
 *====================================================================
 *
 * Definitions local to this module
 */
#define READSIZE	512		/* size of one chunk of disk data */

#define RD_DATASIZE	53		/* size of DOS ramdisk info struct */
#define RD_XMSHANDLE	49		/* offset to BOOTP XMS handle */
#define RD_XMSSIZE	51		/* offset to BOOTP size */

#define MGL_SIZE	128		/* number of kB to allocate for MGL */




.line __LINE__
/*
 *====================================================================
 *
 * Define the text segment.
 */

	.text

	.extern	prcrlf			# externals from runtime library
	.extern	prnstr
	.extern	prnchr
	.extern	chkcpu
	.extern	brk
	.extern	sbrk


	.global	_start


_start:	jmp	1f

/*
 * EXE file name to use when we are unable to find a valid file
 * name in the environment. This can be caused by various debuggers.
 * This name will be filled in by the MGL compiler.
 */
exenam:	.space	14


/*
 * Initialize the data and BSS segments
 */

1:	cld
	push	ds
	mov	ax,ss			# SS is set to the data segment
	mov	ds,ax			# by the linker script
	mov	es,ax
	mov	di,offset __bss_start
	mov	cx,offset __bss_end
	sub	cx,di
	inc	cx			# clear out the BSS segment
	shr	cx,1
	xor	ax,ax
	rep	stosw
	pop	word ptr [pspseg]	# save PSP segment
	mov	[stktop],sp		# save top of stack

/*
 * Print program title and copyright information
 */

	mov	si,offset id1msg
	call	prnstr
	mov	al,0x20
	call	prnchr
	mov	si,offset id2msg
	call	prnstr
	call	prcrlf
	mov	si,offset id3msg
	call	prnstr
	call	prcrlf

/*
 * Check for correct DOS version, and that we are running on
 * a 386+ cpu.
 */

	mov	ax,0x3000
	xor	bx,bx
	int	INT_DOS			# get DOS version
	xchg	al,ah
	cmp	ax,0x0300		# has to be at least 3.0
	jae	2f
	mov	si,offset doserr
4:	call	prnstr			# print error message
	call	prcrlf
	xor	ah,ah
	int	INT_DOS			# terminate the program
	lret

2:	call	chkcpu			# check if running on correct CPU
	or	si,si
	jnz	4b

/*
 * Determine the amount of memory required for this loader program, and
 * reset the memory block size accordingly.
 */

	push	es
	mov	ax,[stktop]
	add	ax,0x000F
	shr	ax,4			# compute end segment
	mov	bx,ss
	add	bx,ax
	mov	ax,[pspseg]		# compute total size of program
	sub	bx,ax
	mov	es,ax
	inc	bx
	mov	ax,0x4A00
	int	INT_DOS			# readjust program memory block size
	pop	es
	jnc	3f
	mov	si,offset interr
	jmp	8f

/*
 * Read the BOOTP buffer either from a DOS disk file, or from the XMS
 * memory space provided by the netboot ramdisk driver.
 */

3:	call	getbtp
	or	si,si
	jnz	8f

/*
 * Read the MGL program out of this program file
 */

	call	getmgl
	or	si,si
	jnz	8f

/*
 * Call the MGL runtime module
 */

	mov	si,offset exemsg
	call	prnstr
	call	prcrlf
	mov	si,offset rtinfo
	lcall	[mglptr]		# call MGL program
	xor	al,al			# return with exit code 0
	jmp	9f

/*
 * Eventually print an error message and return to DOS
 */

8:	call	prnstr			# print error message
	call	prcrlf
	mov	al,0x01			# return with exit code 1
9:	mov	ah,0x4C			# if called from DOS, terminate the
	int	INT_DOS			# program
	lret



/*
 *====================================================================
 *
 * Read the BOOTP/DHCP information from a DOS file
 * Input:  None
 * Output: SI  -  Pointer to error message (zero if no error)
 * Registers changed: AX, BX, CX, DX, SI
 */
getbtp:

	call	getfilename		# get file name from command line
	or	dx,dx			# check if file name specified
	jz	2f

	mov	si,offset dflmsg	# check if file name is DEFAULT
	call	chkfname
	jnc	7f			# set default BOOTP info
	mov	si,offset pxemsg	# check if file name is PXE
	call	chkfname
	jnc	8f			# leave BOOTP info empty
	mov	si,offset xmsmsg	# check if file name is XMS
	call	chkfname
	jc	1f
	call	xmsget			# get BOOTP info from XMS
	jnc	8f
	mov	si,offset xmserr	# return with error message
	jmp	9f

1:	call	fileget			# get BOOTP info from file
	jmp	9f

2:	call	xmsget			# try to get BOOTP info from XMS
	jnc	8f

7:	mov	ax,offset btpdef	# setup default BOOTP info
	mov	word ptr [btpptr + 0],ax
	mov	word ptr [btpptr + 2],ds
	mov	word ptr [btplen],BOOTP_MIN_SIZE
8:	xor	si,si
9:	ret



/*
 *====================================================================
 *
 * Get name of BOOT/DHCP file from command line. We assume that the
 * filename is the first and only argument to the program.
 * Input:  None
 * Output: DX  -  Offset to filename within PSP (zero if no name)
 * Registers changed: AX, CX, DX, SI
 */
getfilename:

	cld
	push	ds
	xor	dx,dx			# DX gets the address of the filename
	mov	ds,[pspseg]		# put pointer to command line into DS:SI
	mov	si,_I(psp_cmdlsize)
	lodsb				# get length of command line
	movzx	cx,al
	jcxz	9f			# check if command line empty

1:	lodsb
	cmp	al,0x0D			# check for end of command line
	je	9f
	cmp	al,0x20			# loop until first non-blank found
	je	2f
	cmp	al,0x09
	jne	3f
2:	loop	1b
	jmp	9f			# no file name found

3:	mov	dx,si
	dec	dx
4:	lodsb
	cmp	al,0x0D			# check for end of command line
	je	5f
	cmp	al,0x20
	je	5f			# loop until blank or tab found
	cmp	al,0x09
	je	5f
	loop	4b
	inc	si
5:	dec	si
	mov	byte ptr [si],0		# mark end of filename string

9:	pop	ds
	ret



/*
 *====================================================================
 *
 * Compare a file name against a pattern
 * Input:  DX  -  offset to file name within PSP segment
 *         SI  -  offset to pattern
 * Output: Carry flag set if file name matches pattern
 * Registers changed: AX, SI, DI
 */
chkfname:

	cld
	push	es
	mov	es,[pspseg]
	mov	di,dx
1:	lodsb				# load next character from pattern
	scasb				# compare it with file name
	jne	8f
	or	al,al			# check if at end of string
	jz	9f
	jmp	1b

8:	stc				# set carry if strings not equal
9:	pop	es
	ret



/*
 *====================================================================
 *
 * Read the BOOTP/DHCP information from a DOS file
 * Input:  DX  -  offset to file name within PSP segment
 * Output: SI  -  offset to error message
 * Registers changed: AX, BX, CX, DX, SI
 */
fileget:

	push	es
	mov	es,[pspseg]
	call	openfile		# open file
	pop	es
	mov	si,offset opberr
	jc	9f

	xor	ax,ax			# determine pointer to disk buffer
	call	sbrk
	mov	word ptr [btpptr + 0],ax
	mov	word ptr [btpptr + 2],ds
	mov	word ptr [btplen],0
1:	mov	cx,READSIZE
	mov	ax,cx
	call	sbrk			# advance heap pointer for next read
	mov	si,offset memerr
	jc	8f
	add	word ptr [btplen],cx	# increase size of BOOTP buffer
	mov	dx,ax
	call	readfile		# read next sector from file
	mov	si,offset rdberr
	jc	8f
	cmp	ax,READSIZE
	je	1b			# continue until end of file
	xor	si,si

8:	push	si
	call	closefile		# close input file
	pop	si
	or	si,si
	jz	9f			# free memory in case of error
	mov	ax,word ptr [btpptr + 0]
	call	brk
9:	ret



/*
 *====================================================================
 *
 * Read the BOOTP/DHCP information from an XMS memory block
 * Input:  None
 * Output: Carry flag set if error
 * Registers changed: EAX, BX, CX, DX, SI
 */
xmsget:

	push	es

/*
 * When running under DOS loaded by a netboot bootrom, the BOOTP
 * information is stored in an XMS memory block if the nbramdrv
 * driver has been used. First we have to check if the driver is
 * indeed installed, and then read the information block.
 */

	mov	dx,offset rddev
	mov	ax,0x3D00		# open ramdisk controlling device
	int	INT_DOS			# for reading
	jc	8f			# no ramdisk controller

	mov	bx,ax
	push	bx
	mov	dx,offset rddata
	mov	cx,RD_DATASIZE		# read data from DOS ramdisk driver
	mov	ax,0x4402
	int	INT_DOS
	pop	bx
	pushf
	push	ax
	mov	ah,0x3E			# close file handle
	int	INT_DOS
	pop	ax
	popf
	jc	8f			# check if read error

	cmp	ax,RD_DATASIZE		# check if we got enough data
	jne	8f
	mov	ax,rddata[RD_XMSHANDLE]
	or	ax,ax			# the handle has to be correct
	jz	8f
	mov	ax,rddata[RD_XMSSIZE]
	or	ax,ax			# the size has to be correct
	jz	8f

/*
 * Now try to find an XMS controller and get its function dispatcher
 * address.
 */

	mov	ax,0x4300
	int	INT_XMS			# check if XMS services are available
	cmp	al,0x80
	jne	8f
	mov	ax,0x4310
	int	INT_XMS			# get XMS entry address
	mov	word ptr [xmsadr + 0],bx
	mov	word ptr [xmsadr + 2],es

	mov	ah,0x0E
	mov	dx,rddata[RD_XMSHANDLE]
	lcall	[xmsadr]		# check if XMS handle is valid
	or	ax,ax
	jz	8f

	mov	ax,rddata[RD_XMSSIZE]
	add	ax,0x0F			# compute number of kB for
	shr	ax,4			# BOOTP information
	cmp	ax,dx			# check if XMS block is large enough
	ja	8f

/*
 * Finally copy the BOOTP information block onto the heap
 */

	mov	ax,rddata[RD_XMSHANDLE]
	mov	[xmssrc],ax		# save source handle

	mov	ax,rddata[RD_XMSSIZE]
	inc	ax			# make BOOTP size even
	and	eax,0x0000FFFE
	mov	[xmslen],eax		# save copy size
	call	sbrk			# advance heap pointer
	jc	8f			# check if we have enough memory
	mov	word ptr [xmsdst + 0],ax
	mov	word ptr [xmsdst + 2],ds

	mov	ah,0x0B
	mov	si,offset xmsemm
	lcall	[xmsadr]		# copy BOOTP information out of
	or	ax,ax			# extended memory
	jz	7f			# check if error

	mov	eax,[xmslen]
	mov	[btplen],ax		# save size of BOOTP info
	mov	eax,[xmsdst]
	mov	[btpptr],eax		# save pointer to BOOTP info
	clc
	jmp	9f

7:	mov	ax,[xmsdst + 0]		# in case of error, restore the
	call	brk			# heap pointer
8:	stc				# no BOOTP record found
9:	pop	es
	ret



/*
 *====================================================================
 *
 * Read the MGL program from the program file
 * Input:  None
 * Output: SI  -  Offset to error message
 * Registers changed: EAX, BX, CX, DX, SI, DI
 */
getmgl:

	push	es
	mov	bx,(MGL_SIZE * 1024) / 16
	mov	ah,0x48			# allocate enough memory for reading
	int	INT_DOS			# the MGL program
	mov	si,offset prgerr
	jc	9f
	mov	word ptr [mglptr + 0],0	# save pointer to MGL program
	mov	word ptr [mglptr + 2],ax
	mov	word ptr [readseg],ax
	mov	word ptr [readcount],MGL_SIZE

	call	getprogname		# get name of program file
	mov	si,offset noperr
	jc	8f
	call	openfile		# open program file
	mov	si,offset opperr
	jc	8f
	mov	eax,file_size_value
	call	seekfile		# seek to beginning of MGL program
	mov	si,offset skperr
	jc	7f

1:	mov	es,[readseg]		# get current read segment
	xor	dx,dx
	mov	cx,1024
	call	readfile		# read next 1kB from file
	mov	si,offset rdperr
	jc	7f
	cmp	ax,1024			# check if at end of file
	jb	2f
	shr	ax,4
	add	word ptr [readseg],ax
	dec	word ptr [readcount]
	jnz	1b			# continue with next 1kB
	mov	si,offset prgerr	# check if MGL program is too
	jmp	7f			# large for our buffer

2:	mov	es,word ptr [mglptr + 2]
	mov	di,es:rtdata_id[RT_DATAOFS]
	mov	si,offset rtid
	mov	cx,offset rtidend
	sub	cx,si			# check if ID string in MGL
	repe	cmpsb			# program is correct
	mov	si,offset inverr
	jne	7f
	xor	si,si

7:	push	si
	call	closefile		# close file
	pop	si
	or	si,si
	jz	9f
8:	mov	es,word ptr [mglptr + 2]
	mov	ah,0x49			# release memory in case of error
	int	INT_DOS
9:	pop	es
	ret



/*
 *====================================================================
 *
 * Get the name of program .exe file
 * Input:  None
 * Output: ES:DX  -  Pointer to file name
 *         Carry flag set if error
 * Registers changed: AX, CX, DX, DI, ES
 */
getprogname:

	cld
	mov	es,[pspseg]
	mov	es,es:[psp_env]		# get segment of program environment
	xor	di,di
1:	xor	al,al
	mov	cx,0xFFFF		# scan to end of environment string
	repne	scasb
	jne	7f			# error if no end found
	mov	al,es:[di]		# check if at end of environment
	or	al,al
	jnz	1b			# continue with next string

	mov	ax,es:[di + 1]
	or	ax,ax			# check if string count not zero
	jz	7f
	cmp	ax,256			# check for unreasonably long string
	jae	7f
	lea	dx,[di + 3]		# determine address of program file
	add	di,ax			# name
	cmp	byte ptr [di + 3],0	# check if name end with a zero
	jne	7f
	clc
	jmp	9f

7:	mov	al,cs:[exenam]		# check if a default EXE file name
	or	al,al			# has been filled in
	jz	8f
	mov	ax,cs
	mov	es,ax			# set default EXE file name
	mov	dx,offset exenam
	clc
	jmp	9f

8:	stc
9:	ret



/*
 *====================================================================
 *
 * Open a DOS file
 * Input:  ES:DX  -  Pointer to filename
 * Output: Carry flag set if error
 * Registers changed: AX
 */
openfile:

	push	si
	mov	si,offset lodmsg
	call	prnstr
	push	ds
	mov	ax,es
	mov	ds,ax
	mov	si,dx			# print name of file to open
	call	prnstr
	mov	ax,0x3D00		# open file using DOS
	int	INT_DOS
	pop	ds
	jc	9f
	mov	[handle],ax		# save file handle
9:	pushf
	call	prcrlf
	popf
	pop	si
	ret



/*
 *====================================================================
 *
 * Read one sector from input file
 * Input:  ES:DX  -  Pointer to destination buffer
 *         CX     -  Size of destination buffer
 * Output: AX     -  Number of bytes actually read
 *         Carry flag set if error
 * Registers changed: AX, BX, CX, DX
 */
readfile:

	mov	bx,[handle]		# get file handle into BX
	or	bx,bx			# check if file opened correctly
	jz	8f

	push	ds
	mov	ax,es
	mov	ds,ax
	mov	ax,0x3F00
	int	INT_DOS			# call DOS to read next sector
	pop	ds
	jmp	9f

8:	stc				# return with error
9:	ret



/*
 *====================================================================
 *
 * Seek within file
 * Input:  EAX  -  File position from the beginning
 * Output: Carry flag set if error
 * Registers changed: EAX, BX, CX, DX
 */
seekfile:

	mov	bx,[handle]		# get file handle into BX
	or	bx,bx			# check if file opened correctly
	jz	8f

	mov	dx,ax
	shr	eax,16			# put 32-bit file offset into CX:DX
	mov	cx,ax
	mov	ax,0x4200
	int	INT_DOS			# call DOS to position within file
	jmp	9f

8:	stc
9:	ret



/*
 *====================================================================
 *
 * Close BOOTP/DHCP file
 * Input:  None
 * Output: None
 * Registers changed: AX, BX
 */
closefile:

	mov	bx,[handle]
	or	bx,bx			# check that the file handle is
	jz	9f			# valid

	mov	ax,0x3E00
	int	INT_DOS			# call DOS to close the file
	mov	word ptr [handle],0
9:	ret




/*
 *====================================================================
 *
 * Read-only Data area
 */
	_rodata


/* Copyright message */
id1msg:	.asciz	"DOS MGL loader"
id2msg:	.asciz	VERSION
id3msg:	.asciz	COPYRIGHT


/* ID string for MGL runtime module */
rtid:	.ascii	RT_IDSTRING
rtidend:
	.byte	0


/* Name of DOS ramdisk character device */
rddev:	.asciz	"RAMCNTR0"


/* Special BOOTP "file" names */
xmsmsg:	.asciz	"xms"
pxemsg:	.asciz	"pxe"
dflmsg:	.asciz	"default"


/* General runtime messages */
lodmsg:	.asciz	"Loading "
exemsg:	.asciz	"Executing MGL program"


/* Error messages */
doserr:	.asciz	"Invalid DOS version"
interr:	.asciz	"Internal memory error"
memerr:	.asciz	"Not enough memory for BOOTP/DHCP file"
xmserr:	.asciz	"Unable to read BOOTP/DHCP info from XMS"
opberr:	.asciz	"Unable to open BOOTP/DHCP file"
rdberr:	.asciz	"Unable to read from BOOTP/DHCP file"
noperr:	.asciz	"Unable to determine program file name"
opperr:	.asciz	"Unable to open program file"
skperr:	.asciz	"Unable to find MGL program in executable"
rdperr:	.asciz	"Unable to read from program file"
prgerr:	.asciz	"Not enough memory for MGL program"
inverr:	.asciz	"Invalid MGL program"




/*
 *====================================================================
 *
 * Data area
 */
	.data


/* Structure used to pass various parameters to the runtime module */
rtinfo:	.word	0			# no flags set
btplen:	.word	0			# size of BOOTP record
btpptr:	.long	0			# far pointer to BOOTP record
	.long	0			# pointer to !PXE or PXENV+ structure


/* XMS EMM structure */
xmsemm:
xmslen:	.long	0			# number of bytes to move
xmssrc:	.word	0			# source handle
	.long	0			# source offset
	.word	0			# destination handle
xmsdst:	.long	0			# destination offset


/* Default BOOTP/DHCP record */
btpdef:	.byte	BOOTP_OP_REPLY		# indicate reply packet
	.byte	1			# hardware type
	.byte	ETH_ALEN		# ethernet address length
	.byte	0			# hop count
	.long	0			# transaction ID
	.word	0			# time since client booted
	.word	0			# flags
	.byte	127,0,0,1		# client IP address
	.byte	127,0,0,1		# your (client) IP address
	.byte	127,0,0,2		# server IP address
	.byte	0,0,0,0			# gateway IP address
	.fill	BOOTP_CHADDR_SIZE	# client hardware address
	.ascii	"server"		# server host name
	.fill	BOOTP_SNAME_SIZE - 6
	.fill	BOOTP_FILE_SIZE		# boot file name
	.long	BOOTP_VM_RFC1048	# options header
	.byte	VEND_RFC_MSGTYPE	# DHCP message type
	.byte	1			# length of option
	.byte	VEND_DHCP_ACK		# ACK message type
	.byte	VEND_RFC_HNAME		# client host name
	.byte	7			# length of option
	.asciz	"client"		# default client name
	.byte	VEND_RFC_END		# end of vendor options
	.space	BOOTP_MIN_SIZE + btpdef - .



/*
 *====================================================================
 *
 * BSS segment
 */
	.bss

	.global	stktop			# required for i386 library

	.lcomm	stktop,2		# pointer to top of stack
	.lcomm	mglptr,4		# pointer to MGL program
	.lcomm	pspseg,2		# DOS PSP segment
	.lcomm	handle,2		# BOOTP/DHCP file handle
	.lcomm	readseg,2		# current read segment
	.lcomm	readcount,2		# current read counter
	.lcomm	xmsadr,4		# XMS handler address
	.lcomm	rddata,RD_DATASIZE	# DOS ramdisk data space




/*
 *====================================================================
 */
	.end

