Repository: Mellvik/TLVC
Pull request #165: [kernel] Adds LOADALL based XMS block move for 286 systems
Author: login: Mellvik
Created: 2025-05-02T15:18:30Z | Merged: 2025-05-07T11:04:06Z

This PR adds support for XMS access by means of the LOADALL instruction on 286 based systems, which adds the missing pice from the TLVC XMS support package. XMS access has been available for 286 systems for quite some time, but only via the BIOS xms block move call, which 1) is a tie-in to the system BIOS, 2) is relatively slow since the processor has to be reset after every transaction and 3) the BIOS call turns off the A20 line when it finishes, which creates problems when the kernel is loaded in HMA.
The latter has been fixed by literally turning off all interrupts on the system until we've had the chance to turn the A20 line back on, and the solution has turned out to work reliably, but it's still sort of a hack and the LOADALL solution is cleaner and faster. 

This implementation is based on the work @drachen6jp did for ELKS in April 2025. Located in the same file as the BIOS INT15 block_move code (`lib/bios15.S`), the comments in the source explains the basics. Playing with LOADALL has been an educating experience: Even if there are quite a few good sources on the subject available on the net, the real learning comes with doing it In practice, making the mistakes etc. Here are a few takeaways from the process, some of them obvious, but they don't necessarily become obvious until staring them in the face.
- Realoading every single registers in the processor, which is what LOADALL does, sounds daunting - as in 'is it possibly to get it right?'. I practice it isn't - for the simple reason that we're in real mode. Many of the structures and registers loaded my LOADALL apply only to protected mode and can be ignored (left as zeros). Even registers like the MSW and FLAGS are conveniently left as zeros.
- LOADALL provides access to the fill 16M address space of the 286, but is not a 'mode' or a 'state' like 'unreal mode'. It's more like a switch we turn on and off per transaction. 
- Typically what we need is to transfer between low (sub-1M) memory and XMS memory. So we set up the Data Segment and the Extra Segment as the source and destination, just like we always do in real mode. Except in this case we put the source and destination addresses into the processor's segment register caches instead of the segment registers. That's all there is to it. The rest is 'administrative overhead' to do it.
- When the LOADALL data structure has been prepared and the LOADALL instruction executed, the segment registers don't matter. They're not used until they are changed. At that time the new value overwrites whatever was in the cache (obviously) and the upper four bits which we got access to via the LOADALL 'trick' go back to zero because we're in real mode. IOW - what we put into the segment registers in the LOADALL data structure is ignored, it's what we put into the segment register caches that count.
- Unlike the 386+ processor's unreal mode, LOADALL doesn't provide access to more than 64k per 'update'. That is, we put the source and destination addresses into the DS and ES caches and we can move a maximum of 64k bytes simply because that's the size of the SI and DI registers. Not a problem, we never transfer more than 1k per transaction, but important for understanding the picture.
- As implemented, the LOADALL based memory move must be protected against interrupts. Not a problem, a 1k transfers is very fast even with the administrative overhead of setting up the structure. The reason is that an interrupt handler will have to change several of the critical registers and the interrupted transfer will likely crash the machine if restarted.
- The current implementation covers both byte and word transfers. `fmemset` support is planned. 

The LOADALL based XMS access method is enabled by setting `xms=on` in bootopts, just like unreal mode. When the system boots and finds itself running on a 286, it sets the internal  `xms_mode` setting to `XMS_LOADALL`. This way we avoid adding yet an other option to bootopts, and the INT15 based method is still available. For now, LOADALL support is encapsulated by `CONFIG_FS_XMS_LOADALL`. The INT15 method add-on, the interrupt disable hack mentioned above, is similarly enabled by `CONFIG_FS_XMS_INT15DI`. 
It is not obvious that the INT15 method is meaningful in the long run, given its disadvantages compared to LOADALL, but it does have the advantage of running on all systems 286 and up.

This LOADALL implementation is still experimental, and needs more testing. Lots of cleanup is needed, and the modified low memory allocation scheme in `config.h` will be reworked to accommodate various CONFIG settings like before. Actually, OPTSEG could remain at 0x50-0x90 - its use is long over when the 0x800-0x87F is needed for LOADALL.

Further, once initialized, the LOADALL datablock is very stable from one call to the next. This can be taken advantage of by (say) putting a signature word at the high end of the area at time of initialization and after that - if the signature is found - simply do minimalist updates.



Diff:
diff --git a/tlvc/arch/i86/boot/setup.S b/tlvc/arch/i86/boot/setup.S
index b79e7052..bd656dfe 100644
--- a/tlvc/arch/i86/boot/setup.S
+++ b/tlvc/arch/i86/boot/setup.S
@@ -1117,11 +1117,10 @@ pc98_int1b:
 
 	jnc	1f
 0:	mov	$'F',%al
-	call	putc
 	jmp	2f
 1:	mov	$' ',%al
-	call	putc
-2:	pop	%es
+2:	call	putc
+	pop	%es
 	pop	%ds
 	ret
 
diff --git a/tlvc/arch/i86/lib/bios15.S b/tlvc/arch/i86/lib/bios15.S
index 5b571e09..f9e82994 100644
--- a/tlvc/arch/i86/lib/bios15.S
+++ b/tlvc/arch/i86/lib/bios15.S
@@ -14,7 +14,9 @@
 #	and uses a int15_fmemcpy wrapper above it to prefill the passed GDT table.
 #
 
-	.arch	i8086, nojumps
+#include <linuxmt/config.h>
+
+	.arch	i286, nojumps
 	.code16
 	.section .fartext.far
 
@@ -34,7 +36,6 @@
 # in 'many k bytes'. 1K (one disk block) per transfer seems to work 
 # fine. May also be system speed dependent. Tested on 12.5MHz 286
 #
-#define CONFIG_FS_XMS_INT15DI
 block_move:
 	push	%es
 	push	%si
@@ -84,40 +85,3 @@ block_move:
 	pop	%es
 	lret
 
-#if UNUSED
-#
-# Test functions
-#
-int0Dh:	push	%bp
-	push	%ax
-	mov	%sp,%bp
-	mov	4(%bp),%ax
-	inc	%ax
-	//inc	%ax
-	mov	%ax,4(%bp)
-	pop	%ax
-	pop	%bp
-	iret
-
-setgpf:	push	%ds
-	xor	%ax,%ax
-	mov	%ax,%ds
-	mov	$0x0D*4,%bx
-	mov	$int0Dh,%ax
-	mov	%ax,(%bx)
-	add	$2,%bx
-	push	%cs
-	pop	%ax
-	mov	%ax,(%bx)
-	pop	%ds
-	ret
-
-	.global	get_extmem
-get_extmem:
-	mov	$0x88,%ah
-	int	$0x15
-	jc	1f
-	ret
-1:	mov	$-1,%ax
-	ret
-#endif
diff --git a/tlvc/arch/i86/lib/fmemory.S b/tlvc/arch/i86/lib/fmemory.S
index e43a281e..32364a9e 100644
--- a/tlvc/arch/i86/lib/fmemory.S
+++ b/tlvc/arch/i86/lib/fmemory.S
@@ -3,6 +3,8 @@
 // segment parameter after offset to allow LDS/LES from the stack
 // assume DS=SS, save ES, for GCC-IA16
 
+#include <linuxmt/config.h>
+
 #define ARG0	2
 #define ARG1	4
 #define ARG2	6
@@ -159,3 +161,162 @@ fmemcmpw_exit:
 	mov    %dx,%ds
 	mov    %bx,%es
 	ret
+
+#ifdef CONFIG_FS_XMS_LOADALL
+
+	.arch	i286, nojumps
+
+	.section .fartext.far
+	.global loadall_block_move
+
+# XMS block move for 286 processors using the LOADALL instruction.
+# Based on code from @drachen6jp for ELKS
+#
+# int FARPROC loadall_block_move(addr_t src, addr_t dst, size_t bytes)
+#
+# Prepare a loadall register block which changes the DS cache entry to
+# point to the source address of the move, while the ES cache entry 
+# points to the destination address. Either can be anywhere in the 16M
+# address space. Filling the other two segment register caches (CS, SS)
+# is also required since this is what the processor will be running on
+# after the LOADALL instruction has been executed. 
+# The segment caches in the processor will be individually reset to 
+# 'normal' real mode values (high 4 bits clear) whenever the 
+# corresponding register is changed by the running code. 
+# Since this method copies the current SS and CS values into the corresponding
+# caches, resetting them after is not critical. What's needed is
+# that the DS and ES registers get updated (restored) before we exit.
+#
+# The return value is usually ignored, but may be useful for debugging.
+
+loadall_block_move:
+
+	push	%es
+	push	%si
+	push	%di
+	push	%bp
+	mov	%sp,%bp
+
+	xor	%di,%di
+
+	cld
+	mov	$0x80,%cx
+	mov	%cx,%es		# ES=0080h segment
+	mov	$0x33,%cx	# 102 bytes
+	xor	%ax,%ax
+	cli
+	rep
+	stosw
+
+	xor	%di,%di
+	dec	%ax		# limit is always 0xffff
+	mov	%ax,%es:(0x3a)	# ES limit
+	mov	%ax,%es:(0x4c)	# DS limit
+	mov	%ax,%es:(0x40)	# CS limit
+	mov	%ax,%es:(0x46)	# SS limit
+	mov	%al,%es:(0x5f)	# IDT - important
+
+	# if highword(src) = 0xffff, do memset 
+# Prepare src (DS)
+	xor	%bx,%bx
+	mov	12(%bp),%ax	#src addr low16
+	mov	%ax,%es:(0x48)	#new DS low16
+	mov	14(%bp),%ax	#src addr high8
+	cmp	$0xffff,%ax
+	jnz	1f
+	mov	$1,%bl		# memset flag
+	mov	%bl,%al		# make the high byte inoccuous
+				# not used when doing a memset
+1:	mov	$0x93,%ah	# access byte
+	mov	%ax,%es:(0x4a)	# new DS high8 & access byte
+	
+# Prepare dst (ES)
+	mov	16(%bp),%ax	#dst addr low16
+	mov	%ax,%es:(0x36)	#new ES low16
+	mov	18(%bp),%ax	#dst addr high8 
+	mov	$0x93,%ah	# & access byte
+	mov	%ax,%es:(0x38)	#new ES high8 & access byte
+
+# code segment cache entry
+	mov	%cs,%ax
+	mov	$12,%cl
+	shr	%cl,%ax
+	mov	%al,%es:(0x3e)	#CS high8
+	mov	%cs,%ax
+	mov	$4,%cl
+	shl	%cl,%ax
+	mov	%ax,%es:(0x3c)	#CS low16
+	mov	$0x9a93,%ax
+	mov	%ah,%es:(0x3f)	#cs access byte
+	mov	%al,%es:(0x45)	#ss access byte
+
+# stack segment cache entry
+	mov	%ss,%ax
+	mov	$12,%cl
+	shr	%cl,%ax
+	mov	%al,%es:(0x44)	#SS high8
+	mov	%ss,%ax
+	mov	$4,%cl
+	shl	%cl,%ax
+	mov	%ax,%es:(0x42)	#SS low16
+
+	mov	20(%bp),%cx	# byte count
+
+	push	%ds
+	push	%ss		# needed for seg reg cache reset
+	push	%cs		# after the transfer
+
+	cmp	$1,%bl		# check if memset
+	jnz	1f
+	mov	12(%bp),%ax		# get filler value
+	movw	$memset,%es:(0x1a)	# run memset
+	jmp	2f
+1:	movw	$copyblk,%es:(0x1a)	# run block transfer
+
+2:	mov	%ds,%es:(0x1e)
+	mov	%ss,%es:(0x20)
+	mov	%cs,%es:(0x22)
+	mov	%es,%es:(0x24)
+	mov	%sp,%es:(0x2c)
+	//mov	%di,%es:(0x26)	# conveniently left at zero
+	//mov	%si,%es:(0x28)	# conveniently left at zero
+	//mov	%bp,%es:(0x2a)
+	//mov	%bx,%es:(0x2e)
+	//mov	%dx,%es:(0x30)
+	mov	%cx,%es:(0x32)	# counter
+	mov	%ax,%es:(0x34)	# needed with memset
+exe:
+	.byte 0x0f,0x05		#loadall
+
+modechange2:
+	retf		# use the push %cs above
+memset:
+	shr	$1,%cx
+	rep
+	stosw
+	rcl	$1,%cx
+	rep
+	stosb
+	jmp	done
+copyblk:
+	shr	$1,%cx	# make words
+	rep
+	movsw		# do the block move
+	rcl	$1,%cx
+	rep
+	movsb		# get the final byte if any
+
+done:	call	modechange2	#reset CS
+	pop	%ax		#reset SS
+	mov	%ax,%ss
+	pop	%ax		#reset DS
+	mov	%ax,%ds
+	sti
+
+	pop	%bp
+	pop	%di
+	pop	%si
+	pop	%es
+	xor	%ax,%ax		#all success
+	lret
+#endif
diff --git a/tlvc/arch/i86/mm/xms.c b/tlvc/arch/i86/mm/xms.c
index 3480d56c..aca73255 100644
--- a/tlvc/arch/i86/mm/xms.c
+++ b/tlvc/arch/i86/mm/xms.c
@@ -54,8 +54,13 @@ void xms_init(void)
 		printk("not available");
 		return;
 	}
+
 	if (arch_cpu == 6) 
+#ifndef CONFIG_FS_XMS_LOADALL
 		xms_mode = XMS_INT15;	/* Force INT15 if 286 */
+#else
+		xms_mode = XMS_LOADALL;
+#endif
 	if (xms_mode == XMS_UNREAL) {
 		if (!check_unreal_mode()) {
 		    printk("disabled, unreal mode requires 386");
@@ -98,6 +103,8 @@ void xms_init(void)
 
 	if (xms_mode == XMS_INT15)
 		printk("using int 15/1F");
+	else if (xms_mode == XMS_LOADALL)
+		printk("using 286/LOADALL");
 	else 
 		printk("using unreal mode");
 	//enable_a20_gate();	/*DEBUG - makes no difference */
@@ -130,15 +137,28 @@ void xms_fmemcpyw(void *dst_off, ramdesc_t dst_seg, void *src_off, ramdesc_t src
 	int	need_xms_src = src_seg >> 16;
 	int	need_xms_dst = dst_seg >> 16;
 
-	//printk("FM: %c %u;", need_xms_dst?'W':'R', need_xms_dst?
-		//((word_t)((dst_seg>>10)-xms_start)) : ((word_t)((src_seg>>10)-xms_start)));
 	if (need_xms_src || need_xms_dst) {
 		if (!xms_avail) panic("xms_fmemcpyw");
 		if (!need_xms_src) src_seg <<= 4;
 		if (!need_xms_dst) dst_seg <<= 4;
-
+		//printk("FM: S:%lx/%x -> D:%lx/%x C:%d\n", src_seg, src_off, dst_seg, dst_off, count);  
 		if (xms_mode == XMS_UNREAL)
 		    linear32_fmemcpyw(dst_off, dst_seg, src_off, src_seg, count);
+#ifdef CONFIG_FS_XMS_LOADALL
+		else if (xms_mode == XMS_LOADALL) {
+		    src_seg += (word_t)src_off;
+		    dst_seg += (word_t)dst_off;
+#ifdef LOADALL_DEBUG
+		    if (loadall_block_move(src_seg, dst_seg, count<<1)) {
+			xms_mode = XMS_INT15;
+		    	int15_fmemcpyw(dst_off, dst_seg, src_off, src_seg, count);
+			printk("DEBUG: switching to INT15\n");
+		    }
+#else
+		    loadall_block_move(src_seg, dst_seg, count<<1);
+		}
+#endif /* LOADALL_DEBUG */
+#endif /* CONFIG_FS_XMS_LOADALL */
 		else
 		    int15_fmemcpyw(dst_off, dst_seg, src_off, src_seg, count);
 		return;
@@ -160,6 +180,10 @@ void xms_fmemcpyb(void *dst_off, ramdesc_t dst_seg, void *src_off, ramdesc_t src
 
 		if (xms_mode == XMS_UNREAL)
 		    linear32_fmemcpyb(dst_off, dst_seg, src_off, src_seg, count);
+#ifdef CONFIG_FS_XMS_LOADALL
+		else if (xms_mode == XMS_LOADALL)
+		    loadall_block_move(src_seg+(word_t)src_off, dst_seg+(word_t)dst_off, count);
+#endif
 		else {
 		/* lots of extra work on odd transfers because INT 15 block moves words only */
 		    size_t wc = count >> 1;
@@ -191,7 +215,7 @@ void xms_fmemcpyb(void *dst_off, ramdesc_t dst_seg, void *src_off, ramdesc_t src
 	fmemcpyb(dst_off, (seg_t)dst_seg, src_off, (seg_t)src_seg, count);
 }
 
-/* memset XMS or far memory, INT 15 not yet supported */
+/* memset XMS of far memory, INT 15 not supported */
 void xms_fmemset(void *dst_off, ramdesc_t dst_seg, byte_t val, size_t count)
 {
 	int	need_xms_dst = dst_seg >> 16;
@@ -199,8 +223,16 @@ void xms_fmemset(void *dst_off, ramdesc_t dst_seg, byte_t val, size_t count)
 	if (need_xms_dst) {
 		if (!xms_size) panic("xms_fmemset");
 
-		if (xms_mode == XMS_INT15) panic("xms_fmemset int15");
-		linear32_fmemset(dst_off, dst_seg, val, count);
+		if (xms_mode == XMS_INT15) panic("xms_fmemset w/o unreal");
+		if (xms_mode == XMS_UNREAL)
+		    linear32_fmemset(dst_off, dst_seg, val, count);
+#ifdef CONFIG_FS_XMS_LOADALL
+		else {
+		    //printk("memset %lx,%x,%d; ", dst_seg+(word_t)dst_off, val&0xff, count);
+		    //loadall_fmemset(dst_off, dst_seg, 0, count);
+		    loadall_block_move(0xffff0000L, dst_seg+(word_t)dst_off, count);
+		}
+#endif
 		return;
 	}
 	fmemsetb(dst_off, (seg_t)dst_seg, val, count);
@@ -268,6 +300,7 @@ void int15_fmemcpyw(void *dst_off, addr_t dst_seg, void *src_off, addr_t src_seg
 	dst_seg += (word_t)dst_off;
 
 	gp = &gdt_table[2];		/* source descriptor*/
+	clr_irq();			/* protect gdt_tabe against reentry */
 	gp->limit_15_0 = 0xffff;
 	gp->base_15_0 = (word_t)src_seg;
 	gp->base_23_16 = src_seg >> 16;
@@ -285,6 +318,7 @@ void int15_fmemcpyw(void *dst_off, addr_t dst_seg, void *src_off, addr_t src_seg
 	//gp->flags_limit_19_16 = 0xCF;	/* page-granular, 32-bit, limit=4GB */
 	gp->base_31_24 = dst_seg >> 24;
 	block_move(gdt_table, count);
+	set_irq();
 }
 
 #endif /* CONFIG_FS_XMS_BUFFER */
diff --git a/tlvc/fs/buffer.c b/tlvc/fs/buffer.c
index 554300e2..2e5ee436 100644
--- a/tlvc/fs/buffer.c
+++ b/tlvc/fs/buffer.c
@@ -670,7 +670,7 @@ void zero_buffer(struct buffer_head *bh, size_t offset, int count)
 #define FORCEMAP 0
 #endif
     /* xms int15 doesn't support a memset function, so map into L1 */
-    if (FORCEMAP || bh->b_data || xms_mode == XMS_INT15) {
+    if (FORCEMAP || bh->b_data || xms_mode == XMS_INT15) {	// DANGEROUS!!!
         map_buffer(bh);
         memset(bh->b_data + offset, 0, count);
         unmap_buffer(bh);
diff --git a/tlvc/fs/config.in b/tlvc/fs/config.in
index e7dcce98..8877d81f 100644
--- a/tlvc/fs/config.in
+++ b/tlvc/fs/config.in
@@ -40,6 +40,7 @@ mainmenu_option next_comment
 	bool 'Use XMS memory'                  CONFIG_FS_XMS       n
 	if [ "$CONFIG_FS_XMS" == "y" ]; then
 	    bool 'Disable interrupts in XMS block_move' CONFIG_FS_XMS_INT15DI n
+	    bool 'Enable 286 LOADALL for XMS access'    CONFIG_FS_XMS_LOADALL n
 	    bool 'XMS buffer cache'            CONFIG_FS_XMS_BUFFER       n
 	    if [ "$CONFIG_FS_XMS_BUFFER" == "y" ]; then
 	        int 'Number of XMS buffers'        CONFIG_FS_NR_XMS_BUFFERS   256
diff --git a/tlvc/include/linuxmt/config.h b/tlvc/include/linuxmt/config.h
index 73f995f6..f6a85980 100644
--- a/tlvc/include/linuxmt/config.h
+++ b/tlvc/include/linuxmt/config.h
@@ -149,7 +149,7 @@
  | Floppy bounce   |  1k
  | bootopts buffer | 
  +-----------------+ 0x50 DEF_OPTSEG FD_BOUNCESEG
- | IRQ vectors/BDA |
+ | IRQ vectors/BDA |	  If XMS_LOADALL, this segment is expanded to 0x90
  +-----------------+ 000
 
 */
@@ -167,11 +167,22 @@
 #define HAS_FDCACHE 0
 #endif
 
+// NOTE: for accomodating the LOADALL 0x80:0 seg (0x80:0-0x87:0),
+// leave OPTSEG in place (its use is over before LOADALL needs it)
+// and move REL_INITSEG above LOADALL (0x87 or 0x90)
+
+#define LOADALL_SEG	0x80		/* 286 only, 0x80-0x87 */
+#ifdef CONFIG_FS_XMS_LOADALL
+#define DEF_OPTSEG	0x90		/* release 0x50-0x7f in init */
+#define UNUSED_SEG	0x50		/* too small to release */
+					/* maybe use for SETUP_DATA */
+#else
 #define DEF_OPTSEG	0x50
+#endif
 #define OPTSEGSZ	0x400		/* max size of /bootopts file */
 #define FD_BOUNCESEG	DEF_OPTSEG	/* IRQ vectors/ BDA below this */
 #define DMASEG		FD_BOUNCESEG	/* used by bioshd driver only */
-#define REL_INITSEG	0x90
+#define REL_INITSEG	(FD_BOUNCESEG+0x40)
 #define SETUP_DATA	REL_INITSEG	/* 0x200 bytes setup data */
 #define FD_CACHESEG	SETUP_DATA	/* overlapping if present */
 #define XD_BOUNCESEG	(FD_CACHESEG + 0x20 + \
diff --git a/tlvc/include/linuxmt/memory.h b/tlvc/include/linuxmt/memory.h
index 1563e0fd..50c1b3b3 100644
--- a/tlvc/include/linuxmt/memory.h
+++ b/tlvc/include/linuxmt/memory.h
@@ -39,12 +39,14 @@ void enable_unreal_mode(void);	/* requires 386+ CPU to call */
 int FARPROC enable_a20_gate(void);	/* returns 0 on fail */
 struct gdt_table;
 int FARPROC block_move(struct gdt_table *gdtp, size_t words); /* use INT 15/1F */
+int FARPROC loadall_block_move(addr_t ssrc, addr_t dst, size_t words); /* use loadall */
 
 /* XMS memory management */
 /* possible values for xms_mode */
 #define XMS_DISABLED	0	/* Off */
 #define XMS_UNREAL	1	/* using unreal mode and linear32_fmemcpy for block moves */
 #define XMS_INT15	2	/* using BIOS INT 15 block move */
+#define XMS_LOADALL	3	/* Using LOADALL on 286 system */
 extern int xms_mode;
 
 #ifdef CONFIG_FS_XMS_BUFFER


Reviews:
user: login: ghaerr
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-03T08:06:30Z
user: login: ghaerr
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-03T08:15:56Z
user: login: Mellvik
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-03T08:25:32Z
user: login: ghaerr
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-03T08:26:20Z
user: login: Mellvik
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-03T08:29:42Z
user: login: ghaerr
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-03T08:33:29Z
user: login: ghaerr
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-03T08:35:22Z
user: login: Mellvik
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-03T09:23:32Z
user: login: Mellvik
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-03T13:16:16Z
user: login: ghaerr
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-03T13:42:07Z
user: login: ghaerr
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-03T19:58:16Z
user: login: ghaerr
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-03T20:23:05Z
user: login: Mellvik
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-04T07:29:38Z
user: login: Mellvik
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-04T07:39:51Z
user: login: Mellvik
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-04T17:41:16Z

Review comments:
diff_hunk: @@ -84,6 +86,161 @@ block_move:
 	pop	%es
 	lret
 
+
+#ifdef CONFIG_FS_XMS_LOADALL
+#  EXPERIMENTAL -- based on code from @drachen6jp for ELKS
+# int FARPROC loadall_block_move(addr_t src, addr_t dst, size_t bytes)
+#
+# Prepare a loadall register block which changes the DS cache entry to
+# point to the source address of the move, while the ES cache entry 
+# points to the destination address. Either can be anywhere in the 16M
+# address space. Filling the other two segment register caches (CS, SS)
+# is also required since this is what the processor will be running on
+# after the LOADALL instruction has been executed. 
+# The caches in the processor will be individually reset to 'normal' real
+# mode values (high 4 bits clear) whenever the corresponding register is
+# changed in the code. 
+# Since this metnhod copies the current SS and CS values into the corresponding
+# caches, resetting them after is not critical. What's needed is
+# that the DS and ES registers get updated (restored) before we exit.
+#
+# The return value is usually ignored, but may be useful for debugging.
+
+loadall_block_move:
+
+	push	%es
+	push	%si
+	push	%di
+	push	%bp
+	mov	%sp,%bp
+
+	//xor	%si,%si
+	xor	%di,%di
+
+	cld
+	mov	$0x80,%cx
+	mov	%cx,%es		#ES=0080h segment
+	//mov	$0x80,%cx
+	xor	%ax,%ax
+	rep
+	stosw			#800h-880h clear
+
+	//xor	%di,%di
+	dec	%ax		# limit is always 0xffff
+	mov	%ax,%es:(0x3a)	# ES limit
+	mov	%ax,%es:(0x4c)	# DS limit
+	mov	%ax,%es:(0x40)	# CS limit
+	mov	%ax,%es:(0x46)	# SS limit
path: tlvc/arch/i86/lib/bios15.S
user: login: ghaerr
body: This is probably a very good idea to set the limits at FFFF here, even though the upper level C code currently already does that. My reason is that it appears that, when the cache registers are reset by reloading the segment registers after LOADALL, only the cache base, not the cache limit, is reset. [Collin's article](https://www.rcollins.org/articles/loadall/tspec_a3_doc.html) gets into this and details that the Intel documentation is incorrect, with the exception of the CS register. Apparently the cache access bytes are also not reset with segment loads. All this likely sounds very familiar, since the same internal operation is how unreal mode works. It seems it was all designed starting with the 80286. You have a unique capability of testing whether the cache limits stay active on the 286 after segment reloading - not sure I would trust an emulator on that.

The real difference between the upper level and this particular code is that the upper level xms.c GDT will work for both 286 and 386, whereas this code is 286 only (although I think it was possibly also (re)written from 386 LOADALL, more on that later). Of course, the 386-only GDT fields aren't copied into the LOADALL memory by this routine. I haven't yet determined the best course of action regarding INT 15 GDT/LOADALL compability, but am likely to keep both methods operational for testing.

I do have some concern about the statement below setting the IDT length to FF00, rather than 03FF which is the actual valid length. I've found no documentation on this, and IDT isn't actually used in real mode anyways. I think it would be interesting if you commented line 134 out entirely to see whether it still worked.

Unfortunately, I'm currently unable to test any LOADALL code since my version of QEMU won't emulate a 286 and DosBox-X hasn't yet added 0F05 opcode. PCem has LOADALL implemented and apparently works well for PC-98. I can't test PC-98 since the ROMs aren't legal downloads on the internet from Japan.
created_at: 2025-05-03T08:06:30Z
author_association: NONE
diff_hunk: @@ -84,6 +86,161 @@ block_move:
 	pop	%es
 	lret
 
+
+#ifdef CONFIG_FS_XMS_LOADALL
+#  EXPERIMENTAL -- based on code from @drachen6jp for ELKS
+# int FARPROC loadall_block_move(addr_t src, addr_t dst, size_t bytes)
+#
+# Prepare a loadall register block which changes the DS cache entry to
+# point to the source address of the move, while the ES cache entry 
+# points to the destination address. Either can be anywhere in the 16M
+# address space. Filling the other two segment register caches (CS, SS)
+# is also required since this is what the processor will be running on
+# after the LOADALL instruction has been executed. 
+# The caches in the processor will be individually reset to 'normal' real
+# mode values (high 4 bits clear) whenever the corresponding register is
+# changed in the code. 
+# Since this metnhod copies the current SS and CS values into the corresponding
+# caches, resetting them after is not critical. What's needed is
+# that the DS and ES registers get updated (restored) before we exit.
+#
+# The return value is usually ignored, but may be useful for debugging.
+
+loadall_block_move:
+
+	push	%es
+	push	%si
+	push	%di
+	push	%bp
+	mov	%sp,%bp
+
+	//xor	%si,%si
+	xor	%di,%di
+
+	cld
+	mov	$0x80,%cx
+	mov	%cx,%es		#ES=0080h segment
+	//mov	$0x80,%cx
+	xor	%ax,%ax
+	rep
+	stosw			#800h-880h clear
+
+	//xor	%di,%di
+	dec	%ax		# limit is always 0xffff
+	mov	%ax,%es:(0x3a)	# ES limit
+	mov	%ax,%es:(0x4c)	# DS limit
+	mov	%ax,%es:(0x40)	# CS limit
+	mov	%ax,%es:(0x46)	# SS limit
+	mov	%al,%es:(0x5f)	# IDT - important
+
+#src DS
+	mov	12(%bp),%ax	#src addr low16
+	mov	%ax,%es:(0x48)	#new DS low16
+	mov	14(%bp),%ax	#src addr high8
+	mov	$0x93,%ah	# & access byte
+	mov	%ax,%es:(0x4a)	#new DS high8 & access byte
+	
+#dst ES
+	mov	16(%bp),%ax	#dst addr low16
+	mov	%ax,%es:(0x36)	#new ES low16
+	mov	18(%bp),%ax	#dst addr high8 
+	mov	$0x93,%ah	# & access byte
+	mov	%ax,%es:(0x38)	#new ES high8 & access byte
+
+# code segment cache entry
+	cli
+	mov	%cs,%ax
+	mov	$12,%cl
+	shr	%cl,%ax
+	mov	%al,%es:(0x3e)	#CS high8
+	mov	%cs,%ax
+	mov	$4,%cl
+	shl	%cl,%ax
+	mov	%ax,%es:(0x3c)	#CS low16
+	mov	$0x9a93,%ax
+	mov	%ah,%es:(0x3f)	#cs access byte
+	mov	%al,%es:(0x45)	#ss access byte
+
+# stack segment cache entry
+	mov	%ss,%ax
+	mov	$12,%cl
+	shr	%cl,%ax
+	mov	%al,%es:(0x44)	#SS high8
+	mov	%ss,%ax
+	mov	$4,%cl
+	shl	%cl,%ax
+	mov	%ax,%es:(0x42)	#SS low16
+
+	mov	20(%bp),%cx	# byte count
+
+	push	%ds
+	push	%ss		# needed for seg reg cache reset
+	push	%cs		# after the transfer
+
+	//push	%cs
+	//call	modechange2
+	//mov	%sp,%bp
+	//mov	-6(%bp),%ax
+	//add	$2,%ax
+	//mov	%ax,%es:(0x1a)	#new IP
+	movw	$modechange,%es:(0x1a)	# where to start after loadall
+	//mov	%ax,%bx
path: tlvc/arch/i86/lib/bios15.S
user: login: ghaerr
body: Wow, nice simplification here. Previously, this is the bit of code that seems like it was either 1) written to execute without ASM source, since the new IP is calculated dynamically, where as in ASM it's so simply specified, and/or 2) this routine was originally written for 386 LOADALL, where the 386 CPU may have more requirements to make it work (see original code comments regarding 32-bit CPU). Admittedly, using push CS/call near/RETF seems easier than coding a LJMP with a static CS operand.

A potential big problem with the previous code pushing IP onto [SP-6] is that even with interrupts off, a platform NMI could potentially overwrite that value. Even though it seems we've both figured to leave NMI alone in the OS, that doesn't prohibit one from occurring that the BIOS handles internally - overwrite the stack in the previous case.
created_at: 2025-05-03T08:15:56Z
author_association: NONE
diff_hunk: @@ -84,6 +86,161 @@ block_move:
 	pop	%es
 	lret
 
+
+#ifdef CONFIG_FS_XMS_LOADALL
+#  EXPERIMENTAL -- based on code from @drachen6jp for ELKS
+# int FARPROC loadall_block_move(addr_t src, addr_t dst, size_t bytes)
+#
+# Prepare a loadall register block which changes the DS cache entry to
+# point to the source address of the move, while the ES cache entry 
+# points to the destination address. Either can be anywhere in the 16M
+# address space. Filling the other two segment register caches (CS, SS)
+# is also required since this is what the processor will be running on
+# after the LOADALL instruction has been executed. 
+# The caches in the processor will be individually reset to 'normal' real
+# mode values (high 4 bits clear) whenever the corresponding register is
+# changed in the code. 
+# Since this metnhod copies the current SS and CS values into the corresponding
+# caches, resetting them after is not critical. What's needed is
+# that the DS and ES registers get updated (restored) before we exit.
+#
+# The return value is usually ignored, but may be useful for debugging.
+
+loadall_block_move:
+
+	push	%es
+	push	%si
+	push	%di
+	push	%bp
+	mov	%sp,%bp
+
+	//xor	%si,%si
+	xor	%di,%di
+
+	cld
+	mov	$0x80,%cx
+	mov	%cx,%es		#ES=0080h segment
+	//mov	$0x80,%cx
+	xor	%ax,%ax
+	rep
+	stosw			#800h-880h clear
+
+	//xor	%di,%di
+	dec	%ax		# limit is always 0xffff
+	mov	%ax,%es:(0x3a)	# ES limit
+	mov	%ax,%es:(0x4c)	# DS limit
+	mov	%ax,%es:(0x40)	# CS limit
+	mov	%ax,%es:(0x46)	# SS limit
path: tlvc/arch/i86/lib/bios15.S
user: login: Mellvik
body: Thanks for reviewing the code, @ghaerr. I'm in the middle of cleaning up right now, removing commented-out code etc.

> This is probably a very good idea to set the limits at FFFF here, even though the upper level C code currently already does that. 

I'm not following you on this one. How can the upper level C code get access to the processor segment registers?

> My reason is that it appears that, when the cache registers are reset by reloading the segment registers after LOADALL, only the cache base, not the cache limit, is reset. [Collin's article](https://www.rcollins.org/articles/loadall/tspec_a3_doc.html) gets into this and details that the Intel documentation is incorrect, with the exception of the CS register. Apparently the cache access bytes are also not reset with segment loads. All this likely sounds very familiar, since the same internal operation is how unreal mode works. It seems it was all designed starting with the 80286. You have a unique capability of testing whether the cache limits stay active on the 286 after segment reloading - not sure I would trust an emulator on that.

Agreed. My thinking is that when the segment caches get reset (by a register change) to 'real mode defaults', whatever the invisible fields are set to doesn't matter, it just has to make sure the processor behaves as expected in real mode - and we never see it anyway (unless we do a StoreALL, but it's really hard to get to the memory of a stopped processor). You may be seeing something I don't see though.

> 
> The real difference between the upper level and this particular code is that the upper level xms.c GDT will work for both 286 and 386, whereas this code is 286 only (although I think it was possibly also (re)written from 386 LOADALL, more on that later). Of course, the 386-only GDT fields aren't copied into the LOADALL memory by this routine. I haven't yet determined the best course of action regarding INT 15 GDT/LOADALL compability, but am likely to keep both methods operational for testing.

I'm not using the GDTs at all, so it becomes kind of moot, don't you think?

> 
> I do have some concern about the statement below setting the IDT length to FF00, rather than 03FF which is the actual valid length. I've found no documentation on this, and IDT isn't actually used in real mode anyways. I think it would be interesting if you commented line 134 out entirely to see whether it still worked.

I kept this (from @drachen6jp's code) w/o challenging yet. Three is a reference about this in Collins' code, but I haven't looked closer at it. Both Collins and other sources focus on much larger ambitions for using LOADALL than our simple case (like running code in XMS space), and much of their concern is less relevant here.

> 
> Unfortunately, I'm currently unable to test any LOADALL code since my version of QEMU won't emulate a 286 and DosBox-X hasn't yet added 0F05 opcode. PCem has LOADALL implemented and apparently works well for PC-98. I can't test PC-98 since the ROMs aren't legal downloads on the internet from Japan.

Well, what can I say? Just keep the discussion going, I'll test it at the best of my capability. This is both educating and interesting. I did have a setback just after I got the original code from @drachen6jp going, the PS on the 286 conked out. Obviously, it was a temporary one.


created_at: 2025-05-03T08:25:32Z
author_association: OWNER
diff_hunk: @@ -84,6 +86,161 @@ block_move:
 	pop	%es
 	lret
 
+
+#ifdef CONFIG_FS_XMS_LOADALL
+#  EXPERIMENTAL -- based on code from @drachen6jp for ELKS
+# int FARPROC loadall_block_move(addr_t src, addr_t dst, size_t bytes)
+#
+# Prepare a loadall register block which changes the DS cache entry to
+# point to the source address of the move, while the ES cache entry 
+# points to the destination address. Either can be anywhere in the 16M
+# address space. Filling the other two segment register caches (CS, SS)
+# is also required since this is what the processor will be running on
+# after the LOADALL instruction has been executed. 
+# The caches in the processor will be individually reset to 'normal' real
+# mode values (high 4 bits clear) whenever the corresponding register is
+# changed in the code. 
+# Since this metnhod copies the current SS and CS values into the corresponding
+# caches, resetting them after is not critical. What's needed is
+# that the DS and ES registers get updated (restored) before we exit.
+#
+# The return value is usually ignored, but may be useful for debugging.
+
+loadall_block_move:
+
+	push	%es
+	push	%si
+	push	%di
+	push	%bp
+	mov	%sp,%bp
+
+	//xor	%si,%si
+	xor	%di,%di
+
+	cld
+	mov	$0x80,%cx
+	mov	%cx,%es		#ES=0080h segment
+	//mov	$0x80,%cx
+	xor	%ax,%ax
+	rep
+	stosw			#800h-880h clear
+
+	//xor	%di,%di
+	dec	%ax		# limit is always 0xffff
+	mov	%ax,%es:(0x3a)	# ES limit
+	mov	%ax,%es:(0x4c)	# DS limit
+	mov	%ax,%es:(0x40)	# CS limit
+	mov	%ax,%es:(0x46)	# SS limit
+	mov	%al,%es:(0x5f)	# IDT - important
+
+#src DS
+	mov	12(%bp),%ax	#src addr low16
+	mov	%ax,%es:(0x48)	#new DS low16
+	mov	14(%bp),%ax	#src addr high8
+	mov	$0x93,%ah	# & access byte
+	mov	%ax,%es:(0x4a)	#new DS high8 & access byte
+	
+#dst ES
+	mov	16(%bp),%ax	#dst addr low16
+	mov	%ax,%es:(0x36)	#new ES low16
+	mov	18(%bp),%ax	#dst addr high8 
+	mov	$0x93,%ah	# & access byte
+	mov	%ax,%es:(0x38)	#new ES high8 & access byte
+
+# code segment cache entry
+	cli
+	mov	%cs,%ax
+	mov	$12,%cl
+	shr	%cl,%ax
+	mov	%al,%es:(0x3e)	#CS high8
+	mov	%cs,%ax
+	mov	$4,%cl
+	shl	%cl,%ax
+	mov	%ax,%es:(0x3c)	#CS low16
+	mov	$0x9a93,%ax
+	mov	%ah,%es:(0x3f)	#cs access byte
+	mov	%al,%es:(0x45)	#ss access byte
+
+# stack segment cache entry
+	mov	%ss,%ax
+	mov	$12,%cl
+	shr	%cl,%ax
+	mov	%al,%es:(0x44)	#SS high8
+	mov	%ss,%ax
+	mov	$4,%cl
+	shl	%cl,%ax
+	mov	%ax,%es:(0x42)	#SS low16
+
+	mov	20(%bp),%cx	# byte count
+
+	push	%ds
+	push	%ss		# needed for seg reg cache reset
+	push	%cs		# after the transfer
+
+	//push	%cs
+	//call	modechange2
+	//mov	%sp,%bp
+	//mov	-6(%bp),%ax
+	//add	$2,%ax
+	//mov	%ax,%es:(0x1a)	#new IP
+	movw	$modechange,%es:(0x1a)	# where to start after loadall
+	//mov	%ax,%bx
+	//sub	$7,%bx
+	//mov	$0x050f,%ax		#loadall
+	//mov	%ax,%cs:(%bx)
+	//pushf			# no flags needed, zero is OK (IF == off)
+	//pop	%ax
+	//mov	%ax,%es:(0x18)
+	//smsw	%es:(0x06)	# not useful
+
+	mov	%ds,%es:(0x1e)
+	mov	%ss,%es:(0x20)
+	mov	%cs,%es:(0x22)
+	mov	%es,%es:(0x24)
+	//mov	%di,%es:(0x26)	# conveniently left at zero
+	//mov	%si,%es:(0x28)	# conveniently left at zero
+	//mov	%bp,%es:(0x2a)
+	mov	%sp,%es:(0x2c)
+	mov	%bx,%es:(0x2e)
+	mov	%dx,%es:(0x30)
+	mov	%cx,%es:(0x32)
+	//mov	%ax,%es:(0x34)
+exe:
+	.byte 0x0f	#loadall
+	.byte 0x05
+modechange2:
+	//call	modechange3
+	retf		# use the push %cs above
+modechange3:
+	//ret
+modechange:
+	shr	$1,%cx	# make words
+	rep
+	movsw		# do the block move
+	rcl	$1,%cx
+	rep
+	movsb		# get the final byte if any
+
+	call	modechange2	#reset CS
+	pop	%ax		#reset SS
+	mov	%ax,%ss
+	pop	%ax		#reset DS
+	mov	%ax,%ds
path: tlvc/arch/i86/lib/bios15.S
user: login: ghaerr
body: A single POP DS here is simpler. And I think a POP SS would work above also.

In ELKS, I required ES cache base to be reset before STI, just in the odd case that an interrupt handler might execute and make assumptions about the ES descriptor cache base register. This was accomplished via PUSH ES/POP ES.

I have questions about whether an SS descriptor cache base register reset is also needed - as the SS base was set to the original SS base value. Certainly DS and ES base may differ, but not SS. CS is treated specially according to Collins and the CPU applies a standard access byte upon CS reload. So there are some unanswered questions and/or further optimizations that could be tested, I suppose. OTOH, since the routine already requires a long REP MOVS, a few extra instructions just in case is probably good, given the competing "documentation" out there.
created_at: 2025-05-03T08:26:20Z
author_association: NONE
diff_hunk: @@ -84,6 +86,161 @@ block_move:
 	pop	%es
 	lret
 
+
+#ifdef CONFIG_FS_XMS_LOADALL
+#  EXPERIMENTAL -- based on code from @drachen6jp for ELKS
+# int FARPROC loadall_block_move(addr_t src, addr_t dst, size_t bytes)
+#
+# Prepare a loadall register block which changes the DS cache entry to
+# point to the source address of the move, while the ES cache entry 
+# points to the destination address. Either can be anywhere in the 16M
+# address space. Filling the other two segment register caches (CS, SS)
+# is also required since this is what the processor will be running on
+# after the LOADALL instruction has been executed. 
+# The caches in the processor will be individually reset to 'normal' real
+# mode values (high 4 bits clear) whenever the corresponding register is
+# changed in the code. 
+# Since this metnhod copies the current SS and CS values into the corresponding
+# caches, resetting them after is not critical. What's needed is
+# that the DS and ES registers get updated (restored) before we exit.
+#
+# The return value is usually ignored, but may be useful for debugging.
+
+loadall_block_move:
+
+	push	%es
+	push	%si
+	push	%di
+	push	%bp
+	mov	%sp,%bp
+
+	//xor	%si,%si
+	xor	%di,%di
+
+	cld
+	mov	$0x80,%cx
+	mov	%cx,%es		#ES=0080h segment
+	//mov	$0x80,%cx
+	xor	%ax,%ax
+	rep
+	stosw			#800h-880h clear
+
+	//xor	%di,%di
+	dec	%ax		# limit is always 0xffff
+	mov	%ax,%es:(0x3a)	# ES limit
+	mov	%ax,%es:(0x4c)	# DS limit
+	mov	%ax,%es:(0x40)	# CS limit
+	mov	%ax,%es:(0x46)	# SS limit
+	mov	%al,%es:(0x5f)	# IDT - important
+
+#src DS
+	mov	12(%bp),%ax	#src addr low16
+	mov	%ax,%es:(0x48)	#new DS low16
+	mov	14(%bp),%ax	#src addr high8
+	mov	$0x93,%ah	# & access byte
+	mov	%ax,%es:(0x4a)	#new DS high8 & access byte
+	
+#dst ES
+	mov	16(%bp),%ax	#dst addr low16
+	mov	%ax,%es:(0x36)	#new ES low16
+	mov	18(%bp),%ax	#dst addr high8 
+	mov	$0x93,%ah	# & access byte
+	mov	%ax,%es:(0x38)	#new ES high8 & access byte
+
+# code segment cache entry
+	cli
+	mov	%cs,%ax
+	mov	$12,%cl
+	shr	%cl,%ax
+	mov	%al,%es:(0x3e)	#CS high8
+	mov	%cs,%ax
+	mov	$4,%cl
+	shl	%cl,%ax
+	mov	%ax,%es:(0x3c)	#CS low16
+	mov	$0x9a93,%ax
+	mov	%ah,%es:(0x3f)	#cs access byte
+	mov	%al,%es:(0x45)	#ss access byte
+
+# stack segment cache entry
+	mov	%ss,%ax
+	mov	$12,%cl
+	shr	%cl,%ax
+	mov	%al,%es:(0x44)	#SS high8
+	mov	%ss,%ax
+	mov	$4,%cl
+	shl	%cl,%ax
+	mov	%ax,%es:(0x42)	#SS low16
+
+	mov	20(%bp),%cx	# byte count
+
+	push	%ds
+	push	%ss		# needed for seg reg cache reset
+	push	%cs		# after the transfer
+
+	//push	%cs
+	//call	modechange2
+	//mov	%sp,%bp
+	//mov	-6(%bp),%ax
+	//add	$2,%ax
+	//mov	%ax,%es:(0x1a)	#new IP
+	movw	$modechange,%es:(0x1a)	# where to start after loadall
+	//mov	%ax,%bx
path: tlvc/arch/i86/lib/bios15.S
user: login: Mellvik
body: > 
> A potential big problem with the previous code pushing IP onto [SP-6] is that even with interrupts off, a platform NMI could potentially overwrite that value. Even though it seems we've both figured to leave NMI alone in the OS, that doesn't prohibit one from occurring that the BIOS handles internally - overwrite the stack in the previous case.

I've been considering this, and found that it's very unlikely that a NMI will happen and the system will run at all afterwards. In most cases the NMI is a fatal error like a memory error or other HW related error. Or a watchdog timer which kicks in after a certain period of inactivity to restart the system (typical industrial uses). It's easy enough to add NMI protection if it turns out to be needed.


created_at: 2025-05-03T08:29:41Z
author_association: OWNER
diff_hunk: @@ -84,6 +86,161 @@ block_move:
 	pop	%es
 	lret
 
+
+#ifdef CONFIG_FS_XMS_LOADALL
+#  EXPERIMENTAL -- based on code from @drachen6jp for ELKS
+# int FARPROC loadall_block_move(addr_t src, addr_t dst, size_t bytes)
+#
+# Prepare a loadall register block which changes the DS cache entry to
+# point to the source address of the move, while the ES cache entry 
+# points to the destination address. Either can be anywhere in the 16M
+# address space. Filling the other two segment register caches (CS, SS)
+# is also required since this is what the processor will be running on
+# after the LOADALL instruction has been executed. 
+# The caches in the processor will be individually reset to 'normal' real
+# mode values (high 4 bits clear) whenever the corresponding register is
+# changed in the code. 
+# Since this metnhod copies the current SS and CS values into the corresponding
+# caches, resetting them after is not critical. What's needed is
+# that the DS and ES registers get updated (restored) before we exit.
+#
+# The return value is usually ignored, but may be useful for debugging.
+
+loadall_block_move:
+
+	push	%es
+	push	%si
+	push	%di
+	push	%bp
+	mov	%sp,%bp
+
+	//xor	%si,%si
+	xor	%di,%di
+
+	cld
+	mov	$0x80,%cx
+	mov	%cx,%es		#ES=0080h segment
+	//mov	$0x80,%cx
+	xor	%ax,%ax
+	rep
+	stosw			#800h-880h clear
+
+	//xor	%di,%di
+	dec	%ax		# limit is always 0xffff
+	mov	%ax,%es:(0x3a)	# ES limit
+	mov	%ax,%es:(0x4c)	# DS limit
+	mov	%ax,%es:(0x40)	# CS limit
+	mov	%ax,%es:(0x46)	# SS limit
path: tlvc/arch/i86/lib/bios15.S
user: login: ghaerr
body: > I'm not following you on this one. How can the upper level C code get access to the processor segment registers?

It doesn't - I was talking about the GDT table loading performed by int15_fmemcpyw, the routine that originally called block_move and loadall_block_move. You may have modified that in your system.

> I'm not using the GDTs at all, so it becomes kind of moot, don't you think?

My code still sets the base address via GDT. Yes, if you're skipping all that now, it doesn't matter. I may use the same code path since it has to be there for INT 15 anyways.

> My thinking is that when the segment caches get reset (by a register change) to 'real mode defaults', whatever the invisible fields are set to doesn't matter, 

To be precise, there are no "invisible fields". There are the 24-bit base, 16-bit limit, and 8-byte access byte; three words total. What I was saying is that the segment limit and access byte are not reset, even using a segment register load. That's what Collins is talking about - only the base register is reset. Thus, without explicitly forcing the "new" limits to be FFFF, there is the possibility of not having real mode defaults after segment register reloading - only another LOADALL could change that!
created_at: 2025-05-03T08:33:28Z
author_association: NONE
diff_hunk: @@ -84,6 +86,161 @@ block_move:
 	pop	%es
 	lret
 
+
+#ifdef CONFIG_FS_XMS_LOADALL
+#  EXPERIMENTAL -- based on code from @drachen6jp for ELKS
+# int FARPROC loadall_block_move(addr_t src, addr_t dst, size_t bytes)
+#
+# Prepare a loadall register block which changes the DS cache entry to
+# point to the source address of the move, while the ES cache entry 
+# points to the destination address. Either can be anywhere in the 16M
+# address space. Filling the other two segment register caches (CS, SS)
+# is also required since this is what the processor will be running on
+# after the LOADALL instruction has been executed. 
+# The caches in the processor will be individually reset to 'normal' real
+# mode values (high 4 bits clear) whenever the corresponding register is
+# changed in the code. 
+# Since this metnhod copies the current SS and CS values into the corresponding
+# caches, resetting them after is not critical. What's needed is
+# that the DS and ES registers get updated (restored) before we exit.
+#
+# The return value is usually ignored, but may be useful for debugging.
+
+loadall_block_move:
+
+	push	%es
+	push	%si
+	push	%di
+	push	%bp
+	mov	%sp,%bp
+
+	//xor	%si,%si
+	xor	%di,%di
+
+	cld
+	mov	$0x80,%cx
+	mov	%cx,%es		#ES=0080h segment
+	//mov	$0x80,%cx
+	xor	%ax,%ax
+	rep
+	stosw			#800h-880h clear
+
+	//xor	%di,%di
+	dec	%ax		# limit is always 0xffff
+	mov	%ax,%es:(0x3a)	# ES limit
+	mov	%ax,%es:(0x4c)	# DS limit
+	mov	%ax,%es:(0x40)	# CS limit
+	mov	%ax,%es:(0x46)	# SS limit
+	mov	%al,%es:(0x5f)	# IDT - important
+
+#src DS
+	mov	12(%bp),%ax	#src addr low16
+	mov	%ax,%es:(0x48)	#new DS low16
+	mov	14(%bp),%ax	#src addr high8
+	mov	$0x93,%ah	# & access byte
+	mov	%ax,%es:(0x4a)	#new DS high8 & access byte
+	
+#dst ES
+	mov	16(%bp),%ax	#dst addr low16
+	mov	%ax,%es:(0x36)	#new ES low16
+	mov	18(%bp),%ax	#dst addr high8 
+	mov	$0x93,%ah	# & access byte
+	mov	%ax,%es:(0x38)	#new ES high8 & access byte
+
+# code segment cache entry
+	cli
+	mov	%cs,%ax
+	mov	$12,%cl
+	shr	%cl,%ax
+	mov	%al,%es:(0x3e)	#CS high8
+	mov	%cs,%ax
+	mov	$4,%cl
+	shl	%cl,%ax
+	mov	%ax,%es:(0x3c)	#CS low16
+	mov	$0x9a93,%ax
+	mov	%ah,%es:(0x3f)	#cs access byte
+	mov	%al,%es:(0x45)	#ss access byte
+
+# stack segment cache entry
+	mov	%ss,%ax
+	mov	$12,%cl
+	shr	%cl,%ax
+	mov	%al,%es:(0x44)	#SS high8
+	mov	%ss,%ax
+	mov	$4,%cl
+	shl	%cl,%ax
+	mov	%ax,%es:(0x42)	#SS low16
+
+	mov	20(%bp),%cx	# byte count
+
+	push	%ds
+	push	%ss		# needed for seg reg cache reset
+	push	%cs		# after the transfer
+
+	//push	%cs
+	//call	modechange2
+	//mov	%sp,%bp
+	//mov	-6(%bp),%ax
+	//add	$2,%ax
+	//mov	%ax,%es:(0x1a)	#new IP
+	movw	$modechange,%es:(0x1a)	# where to start after loadall
+	//mov	%ax,%bx
path: tlvc/arch/i86/lib/bios15.S
user: login: ghaerr
body: > and found that it's very unlikely that a NMI will happen and the system will run at all afterwards. 

Agreed, I'm taking the same approach with no NMI interaction, assuming the system error would likely be fatal anyways.
created_at: 2025-05-03T08:35:21Z
author_association: NONE
diff_hunk: @@ -84,6 +86,161 @@ block_move:
 	pop	%es
 	lret
 
+
+#ifdef CONFIG_FS_XMS_LOADALL
+#  EXPERIMENTAL -- based on code from @drachen6jp for ELKS
+# int FARPROC loadall_block_move(addr_t src, addr_t dst, size_t bytes)
+#
+# Prepare a loadall register block which changes the DS cache entry to
+# point to the source address of the move, while the ES cache entry 
+# points to the destination address. Either can be anywhere in the 16M
+# address space. Filling the other two segment register caches (CS, SS)
+# is also required since this is what the processor will be running on
+# after the LOADALL instruction has been executed. 
+# The caches in the processor will be individually reset to 'normal' real
+# mode values (high 4 bits clear) whenever the corresponding register is
+# changed in the code. 
+# Since this metnhod copies the current SS and CS values into the corresponding
+# caches, resetting them after is not critical. What's needed is
+# that the DS and ES registers get updated (restored) before we exit.
+#
+# The return value is usually ignored, but may be useful for debugging.
+
+loadall_block_move:
+
+	push	%es
+	push	%si
+	push	%di
+	push	%bp
+	mov	%sp,%bp
+
+	//xor	%si,%si
+	xor	%di,%di
+
+	cld
+	mov	$0x80,%cx
+	mov	%cx,%es		#ES=0080h segment
+	//mov	$0x80,%cx
+	xor	%ax,%ax
+	rep
+	stosw			#800h-880h clear
+
+	//xor	%di,%di
+	dec	%ax		# limit is always 0xffff
+	mov	%ax,%es:(0x3a)	# ES limit
+	mov	%ax,%es:(0x4c)	# DS limit
+	mov	%ax,%es:(0x40)	# CS limit
+	mov	%ax,%es:(0x46)	# SS limit
+	mov	%al,%es:(0x5f)	# IDT - important
+
+#src DS
+	mov	12(%bp),%ax	#src addr low16
+	mov	%ax,%es:(0x48)	#new DS low16
+	mov	14(%bp),%ax	#src addr high8
+	mov	$0x93,%ah	# & access byte
+	mov	%ax,%es:(0x4a)	#new DS high8 & access byte
+	
+#dst ES
+	mov	16(%bp),%ax	#dst addr low16
+	mov	%ax,%es:(0x36)	#new ES low16
+	mov	18(%bp),%ax	#dst addr high8 
+	mov	$0x93,%ah	# & access byte
+	mov	%ax,%es:(0x38)	#new ES high8 & access byte
+
+# code segment cache entry
+	cli
+	mov	%cs,%ax
+	mov	$12,%cl
+	shr	%cl,%ax
+	mov	%al,%es:(0x3e)	#CS high8
+	mov	%cs,%ax
+	mov	$4,%cl
+	shl	%cl,%ax
+	mov	%ax,%es:(0x3c)	#CS low16
+	mov	$0x9a93,%ax
+	mov	%ah,%es:(0x3f)	#cs access byte
+	mov	%al,%es:(0x45)	#ss access byte
+
+# stack segment cache entry
+	mov	%ss,%ax
+	mov	$12,%cl
+	shr	%cl,%ax
+	mov	%al,%es:(0x44)	#SS high8
+	mov	%ss,%ax
+	mov	$4,%cl
+	shl	%cl,%ax
+	mov	%ax,%es:(0x42)	#SS low16
+
+	mov	20(%bp),%cx	# byte count
+
+	push	%ds
+	push	%ss		# needed for seg reg cache reset
+	push	%cs		# after the transfer
+
+	//push	%cs
+	//call	modechange2
+	//mov	%sp,%bp
+	//mov	-6(%bp),%ax
+	//add	$2,%ax
+	//mov	%ax,%es:(0x1a)	#new IP
+	movw	$modechange,%es:(0x1a)	# where to start after loadall
+	//mov	%ax,%bx
+	//sub	$7,%bx
+	//mov	$0x050f,%ax		#loadall
+	//mov	%ax,%cs:(%bx)
+	//pushf			# no flags needed, zero is OK (IF == off)
+	//pop	%ax
+	//mov	%ax,%es:(0x18)
+	//smsw	%es:(0x06)	# not useful
+
+	mov	%ds,%es:(0x1e)
+	mov	%ss,%es:(0x20)
+	mov	%cs,%es:(0x22)
+	mov	%es,%es:(0x24)
+	//mov	%di,%es:(0x26)	# conveniently left at zero
+	//mov	%si,%es:(0x28)	# conveniently left at zero
+	//mov	%bp,%es:(0x2a)
+	mov	%sp,%es:(0x2c)
+	mov	%bx,%es:(0x2e)
+	mov	%dx,%es:(0x30)
+	mov	%cx,%es:(0x32)
+	//mov	%ax,%es:(0x34)
+exe:
+	.byte 0x0f	#loadall
+	.byte 0x05
+modechange2:
+	//call	modechange3
+	retf		# use the push %cs above
+modechange3:
+	//ret
+modechange:
+	shr	$1,%cx	# make words
+	rep
+	movsw		# do the block move
+	rcl	$1,%cx
+	rep
+	movsb		# get the final byte if any
+
+	call	modechange2	#reset CS
+	pop	%ax		#reset SS
+	mov	%ax,%ss
+	pop	%ax		#reset DS
+	mov	%ax,%ds
path: tlvc/arch/i86/lib/bios15.S
user: login: Mellvik
body: > A single POP DS here is simpler. And I think a POP SS would work above also.
> 

An oversight! Thanks.

> In ELKS, I required ES cache base to be reset before STI, just in the odd case that an interrupt handler might execute and make assumptions about the ES descriptor cache base register. This was accomplished via PUSH ES/POP ES.

Are you saying that an interrupt handler may actually make assumptions about what's in the ES upon entry?

> 
> I have questions about whether an SS descriptor cache base register reset is also needed - as the SS base was set to the original SS base value.  Certainly DS and ES base may differ, but not SS. CS is treated specially according to Collins and the CPU applies a standard access byte upon CS reload. So there are some unanswered questions and/or further optimizations that could be tested, I suppose. OTOH, since the routine already requires a long REP MOVS, a few extra instructions just in case is probably good, given the competing "documentation" out there.

I don't think it's needed, it's on my list to verify that.

Thanks!


created_at: 2025-05-03T09:23:32Z
author_association: OWNER
diff_hunk: @@ -84,6 +86,161 @@ block_move:
 	pop	%es
 	lret
 
+
+#ifdef CONFIG_FS_XMS_LOADALL
+#  EXPERIMENTAL -- based on code from @drachen6jp for ELKS
+# int FARPROC loadall_block_move(addr_t src, addr_t dst, size_t bytes)
+#
+# Prepare a loadall register block which changes the DS cache entry to
+# point to the source address of the move, while the ES cache entry 
+# points to the destination address. Either can be anywhere in the 16M
+# address space. Filling the other two segment register caches (CS, SS)
+# is also required since this is what the processor will be running on
+# after the LOADALL instruction has been executed. 
+# The caches in the processor will be individually reset to 'normal' real
+# mode values (high 4 bits clear) whenever the corresponding register is
+# changed in the code. 
+# Since this metnhod copies the current SS and CS values into the corresponding
+# caches, resetting them after is not critical. What's needed is
+# that the DS and ES registers get updated (restored) before we exit.
+#
+# The return value is usually ignored, but may be useful for debugging.
+
+loadall_block_move:
+
+	push	%es
+	push	%si
+	push	%di
+	push	%bp
+	mov	%sp,%bp
+
+	//xor	%si,%si
+	xor	%di,%di
+
+	cld
+	mov	$0x80,%cx
+	mov	%cx,%es		#ES=0080h segment
+	//mov	$0x80,%cx
+	xor	%ax,%ax
+	rep
+	stosw			#800h-880h clear
+
+	//xor	%di,%di
+	dec	%ax		# limit is always 0xffff
+	mov	%ax,%es:(0x3a)	# ES limit
+	mov	%ax,%es:(0x4c)	# DS limit
+	mov	%ax,%es:(0x40)	# CS limit
+	mov	%ax,%es:(0x46)	# SS limit
path: tlvc/arch/i86/lib/bios15.S
user: login: Mellvik
body: > 
> > I'm not using the GDTs at all, so it becomes kind of moot, don't you think?
> 
> My code still sets the base address via GDT. Yes, if you're skipping all that now, it doesn't matter. I may use the same code path since it has to be there for INT 15 anyways.

I found that since only the src and dst addresses are used, setting up the the GDT was a waste of time, so the `loadall_block_move` is now taking a long src, long dst, and a byte count as parameters, and covers both byte and word moves.

> 
> > My thinking is that when the segment caches get reset (by a register change) to 'real mode defaults', whatever the invisible fields are set to doesn't matter,
> 
> To be precise, there are no "invisible fields". There are the 24-bit base, 16-bit limit, and 8-byte access byte; three words total. 

These fields are invisible from real mode except inside the LOADALL parameter block, thus the designation. Since we're explicitly setting the limit to max all over, we should be safe. 


created_at: 2025-05-03T13:16:16Z
author_association: OWNER
diff_hunk: @@ -84,6 +86,161 @@ block_move:
 	pop	%es
 	lret
 
+
+#ifdef CONFIG_FS_XMS_LOADALL
+#  EXPERIMENTAL -- based on code from @drachen6jp for ELKS
+# int FARPROC loadall_block_move(addr_t src, addr_t dst, size_t bytes)
+#
+# Prepare a loadall register block which changes the DS cache entry to
+# point to the source address of the move, while the ES cache entry 
+# points to the destination address. Either can be anywhere in the 16M
+# address space. Filling the other two segment register caches (CS, SS)
+# is also required since this is what the processor will be running on
+# after the LOADALL instruction has been executed. 
+# The caches in the processor will be individually reset to 'normal' real
+# mode values (high 4 bits clear) whenever the corresponding register is
+# changed in the code. 
+# Since this metnhod copies the current SS and CS values into the corresponding
+# caches, resetting them after is not critical. What's needed is
+# that the DS and ES registers get updated (restored) before we exit.
+#
+# The return value is usually ignored, but may be useful for debugging.
+
+loadall_block_move:
+
+	push	%es
+	push	%si
+	push	%di
+	push	%bp
+	mov	%sp,%bp
+
+	//xor	%si,%si
+	xor	%di,%di
+
+	cld
+	mov	$0x80,%cx
+	mov	%cx,%es		#ES=0080h segment
+	//mov	$0x80,%cx
+	xor	%ax,%ax
+	rep
+	stosw			#800h-880h clear
+
+	//xor	%di,%di
+	dec	%ax		# limit is always 0xffff
+	mov	%ax,%es:(0x3a)	# ES limit
+	mov	%ax,%es:(0x4c)	# DS limit
+	mov	%ax,%es:(0x40)	# CS limit
+	mov	%ax,%es:(0x46)	# SS limit
+	mov	%al,%es:(0x5f)	# IDT - important
+
+#src DS
+	mov	12(%bp),%ax	#src addr low16
+	mov	%ax,%es:(0x48)	#new DS low16
+	mov	14(%bp),%ax	#src addr high8
+	mov	$0x93,%ah	# & access byte
+	mov	%ax,%es:(0x4a)	#new DS high8 & access byte
+	
+#dst ES
+	mov	16(%bp),%ax	#dst addr low16
+	mov	%ax,%es:(0x36)	#new ES low16
+	mov	18(%bp),%ax	#dst addr high8 
+	mov	$0x93,%ah	# & access byte
+	mov	%ax,%es:(0x38)	#new ES high8 & access byte
+
+# code segment cache entry
+	cli
+	mov	%cs,%ax
+	mov	$12,%cl
+	shr	%cl,%ax
+	mov	%al,%es:(0x3e)	#CS high8
+	mov	%cs,%ax
+	mov	$4,%cl
+	shl	%cl,%ax
+	mov	%ax,%es:(0x3c)	#CS low16
+	mov	$0x9a93,%ax
+	mov	%ah,%es:(0x3f)	#cs access byte
+	mov	%al,%es:(0x45)	#ss access byte
+
+# stack segment cache entry
+	mov	%ss,%ax
+	mov	$12,%cl
+	shr	%cl,%ax
+	mov	%al,%es:(0x44)	#SS high8
+	mov	%ss,%ax
+	mov	$4,%cl
+	shl	%cl,%ax
+	mov	%ax,%es:(0x42)	#SS low16
+
+	mov	20(%bp),%cx	# byte count
+
+	push	%ds
+	push	%ss		# needed for seg reg cache reset
+	push	%cs		# after the transfer
+
+	//push	%cs
+	//call	modechange2
+	//mov	%sp,%bp
+	//mov	-6(%bp),%ax
+	//add	$2,%ax
+	//mov	%ax,%es:(0x1a)	#new IP
+	movw	$modechange,%es:(0x1a)	# where to start after loadall
+	//mov	%ax,%bx
+	//sub	$7,%bx
+	//mov	$0x050f,%ax		#loadall
+	//mov	%ax,%cs:(%bx)
+	//pushf			# no flags needed, zero is OK (IF == off)
+	//pop	%ax
+	//mov	%ax,%es:(0x18)
+	//smsw	%es:(0x06)	# not useful
+
+	mov	%ds,%es:(0x1e)
+	mov	%ss,%es:(0x20)
+	mov	%cs,%es:(0x22)
+	mov	%es,%es:(0x24)
+	//mov	%di,%es:(0x26)	# conveniently left at zero
+	//mov	%si,%es:(0x28)	# conveniently left at zero
+	//mov	%bp,%es:(0x2a)
+	mov	%sp,%es:(0x2c)
+	mov	%bx,%es:(0x2e)
+	mov	%dx,%es:(0x30)
+	mov	%cx,%es:(0x32)
+	//mov	%ax,%es:(0x34)
+exe:
+	.byte 0x0f	#loadall
+	.byte 0x05
+modechange2:
+	//call	modechange3
+	retf		# use the push %cs above
+modechange3:
+	//ret
+modechange:
+	shr	$1,%cx	# make words
+	rep
+	movsw		# do the block move
+	rcl	$1,%cx
+	rep
+	movsb		# get the final byte if any
+
+	call	modechange2	#reset CS
+	pop	%ax		#reset SS
+	mov	%ax,%ss
+	pop	%ax		#reset DS
+	mov	%ax,%ds
path: tlvc/arch/i86/lib/bios15.S
user: login: ghaerr
body: > Are you saying that an interrupt handler may actually make assumptions about what's in the ES upon entry?

Well, no, it wouldn't unless it was buggily written. But who knows, the segment's base could still be pointing into XMS somewhere and I thought probably a good idea to at least reset the cached descriptor base for all segment registers. We already know that the limit and access byte aren't reset.

This is all eerily similar to unreal mode - except that in unreal mode, one has to use the addr32 prefix in order to get at XMS memory, since the kernel runs continuously with 4GB limits for DS and ES. But in that case the cached descriptor base register is zero. Since the 286 doesn't use have addr32 prefix "protection", I thought it wise to ensure that at least the segment base values are always zero (and the limits always FFFF, your idea) after leaving loadall_block_move. Who knows what other "undocumented inconsistency" we might run into - I would hate to try to track a bug like that down!

> I don't think it's needed, it's on my list to verify that.

Thanks for your testing! I realize that I will be able to use PCem to emulate the IBM PC (not PC-98 though) since IBM ROMs are available. I'll be trying to get PCem running in the next couple days. Have you every used PCem?
created_at: 2025-05-03T13:42:07Z
author_association: NONE
diff_hunk: @@ -84,6 +86,178 @@ block_move:
 	pop	%es
 	lret
 
+
+#ifdef CONFIG_FS_XMS_LOADALL
+#  EXPERIMENTAL -- based on code from @drachen6jp for ELKS
+# int FARPROC loadall_block_move(addr_t src, addr_t dst, size_t bytes)
+#
+# Prepare a loadall register block which changes the DS cache entry to
+# point to the source address of the move, while the ES cache entry 
+# points to the destination address. Either can be anywhere in the 16M
+# address space. Filling the other two segment register caches (CS, SS)
+# is also required since this is what the processor will be running on
+# after the LOADALL instruction has been executed. 
+# The caches in the processor will be individually reset to 'normal' real
+# mode values (high 4 bits clear) whenever the corresponding register is
+# changed in the code. 
+# Since this metnhod copies the current SS and CS values into the corresponding
+# caches, resetting them after is not critical. What's needed is
+# that the DS and ES registers get updated (restored) before we exit.
+#
+# The return value is usually ignored, but may be useful for debugging.
+
+loadall_block_move:
+
+	push	%es
+	push	%si
+	push	%di
+	push	%bp
+	mov	%sp,%bp
+
+	//xor	%si,%si
+	xor	%di,%di
+
+	cld
+	mov	$0x80,%cx
+	mov	%cx,%es		#ES=0080h segment
+	mov	$0x7f,%cx
+	xor	%ax,%ax
+	rep
+	stosw			#800h-880h clear
+
+	xor	%di,%di
+	dec	%ax		# limit is always 0xffff
+	mov	%ax,%es:(0x3a)	# ES limit
+	mov	%ax,%es:(0x4c)	# DS limit
+	mov	%ax,%es:(0x40)	# CS limit
+	mov	%ax,%es:(0x46)	# SS limit
+	mov	%al,%es:(0x5f)	# IDT - important
+
+	# if highword(dst) = 0xffff, do memset 
+#src DS
+	xor	%bx,%bx
+	mov	12(%bp),%ax	#src addr low16
+	mov	%ax,%es:(0x48)	#new DS low16
+	mov	14(%bp),%ax	#src addr high8
+	cmp	$0xffff,%ax
+	jnz	1f
+	mov	$1,%bl		# memset flag
+	mov	%bl,%al		# make the high byte inoccuous
+				# we're not using it anyway
+1:	mov	$0x93,%ah	# & access byte
+	mov	%ax,%es:(0x4a)	#new DS high8 & access byte
+	
+#dst ES
+	mov	16(%bp),%ax	#dst addr low16
+	mov	%ax,%es:(0x36)	#new ES low16
+	mov	18(%bp),%ax	#dst addr high8 
+	mov	$0x93,%ah	# & access byte
+	mov	%ax,%es:(0x38)	#new ES high8 & access byte
+
+# code segment cache entry
+	cli
path: tlvc/arch/i86/lib/bios15.S
user: login: ghaerr
body: CLI needs to be moved to beginning of function since this  can be called by interrupt handler, and function uses global data (0800-0865).

I have now just also realized that the original int15_fmemcpy routine is also buggy - it uses the global `static struct gdt_table gdt_table[8]`. The kernel could be beginning the execution of a normal filesystem buffer INT 15-based xms_fmemcpy when the direct floppy driver interrupt I/O complete handler runs, which then calls xms_fmemcpy which calls int15_fmemcpy again, overwriting the global GDT table! I'll be adding a CLI/STI around the beginning/end of int15_fmemcpy as well shortly.

I am guessing we've not seen problems here before due to lack of real-world testing on 286 processors requiring INT 15 block move. Another good warning about the dangers of using global data in kernel routines! :)
created_at: 2025-05-03T19:58:16Z
author_association: NONE
diff_hunk: @@ -84,6 +86,178 @@ block_move:
 	pop	%es
 	lret
 
+
+#ifdef CONFIG_FS_XMS_LOADALL
+#  EXPERIMENTAL -- based on code from @drachen6jp for ELKS
+# int FARPROC loadall_block_move(addr_t src, addr_t dst, size_t bytes)
+#
+# Prepare a loadall register block which changes the DS cache entry to
+# point to the source address of the move, while the ES cache entry 
+# points to the destination address. Either can be anywhere in the 16M
+# address space. Filling the other two segment register caches (CS, SS)
+# is also required since this is what the processor will be running on
+# after the LOADALL instruction has been executed. 
+# The caches in the processor will be individually reset to 'normal' real
+# mode values (high 4 bits clear) whenever the corresponding register is
+# changed in the code. 
+# Since this metnhod copies the current SS and CS values into the corresponding
+# caches, resetting them after is not critical. What's needed is
+# that the DS and ES registers get updated (restored) before we exit.
+#
+# The return value is usually ignored, but may be useful for debugging.
+
+loadall_block_move:
+
+	push	%es
+	push	%si
+	push	%di
+	push	%bp
+	mov	%sp,%bp
+
+	//xor	%si,%si
+	xor	%di,%di
+
+	cld
+	mov	$0x80,%cx
+	mov	%cx,%es		#ES=0080h segment
+	mov	$0x7f,%cx
path: tlvc/arch/i86/lib/bios15.S
user: login: ghaerr
body: This still clears over twice as many 16-bit words (not bytes) as required. CX is decremented by one each STOSW. Proper and fastest correct result is obtained using `mov $0x33,%cx` which clears 33 words/66 bytes, rather than 127 words/254 bytes cleared using 0x7F as count.
created_at: 2025-05-03T20:23:05Z
author_association: NONE
diff_hunk: @@ -84,6 +86,178 @@ block_move:
 	pop	%es
 	lret
 
+
+#ifdef CONFIG_FS_XMS_LOADALL
+#  EXPERIMENTAL -- based on code from @drachen6jp for ELKS
+# int FARPROC loadall_block_move(addr_t src, addr_t dst, size_t bytes)
+#
+# Prepare a loadall register block which changes the DS cache entry to
+# point to the source address of the move, while the ES cache entry 
+# points to the destination address. Either can be anywhere in the 16M
+# address space. Filling the other two segment register caches (CS, SS)
+# is also required since this is what the processor will be running on
+# after the LOADALL instruction has been executed. 
+# The caches in the processor will be individually reset to 'normal' real
+# mode values (high 4 bits clear) whenever the corresponding register is
+# changed in the code. 
+# Since this metnhod copies the current SS and CS values into the corresponding
+# caches, resetting them after is not critical. What's needed is
+# that the DS and ES registers get updated (restored) before we exit.
+#
+# The return value is usually ignored, but may be useful for debugging.
+
+loadall_block_move:
+
+	push	%es
+	push	%si
+	push	%di
+	push	%bp
+	mov	%sp,%bp
+
+	//xor	%si,%si
+	xor	%di,%di
+
+	cld
+	mov	$0x80,%cx
+	mov	%cx,%es		#ES=0080h segment
+	mov	$0x7f,%cx
path: tlvc/arch/i86/lib/bios15.S
user: login: Mellvik
body: You're right, thanks!
created_at: 2025-05-04T07:29:38Z
author_association: OWNER
diff_hunk: @@ -84,6 +86,178 @@ block_move:
 	pop	%es
 	lret
 
+
+#ifdef CONFIG_FS_XMS_LOADALL
+#  EXPERIMENTAL -- based on code from @drachen6jp for ELKS
+# int FARPROC loadall_block_move(addr_t src, addr_t dst, size_t bytes)
+#
+# Prepare a loadall register block which changes the DS cache entry to
+# point to the source address of the move, while the ES cache entry 
+# points to the destination address. Either can be anywhere in the 16M
+# address space. Filling the other two segment register caches (CS, SS)
+# is also required since this is what the processor will be running on
+# after the LOADALL instruction has been executed. 
+# The caches in the processor will be individually reset to 'normal' real
+# mode values (high 4 bits clear) whenever the corresponding register is
+# changed in the code. 
+# Since this metnhod copies the current SS and CS values into the corresponding
+# caches, resetting them after is not critical. What's needed is
+# that the DS and ES registers get updated (restored) before we exit.
+#
+# The return value is usually ignored, but may be useful for debugging.
+
+loadall_block_move:
+
+	push	%es
+	push	%si
+	push	%di
+	push	%bp
+	mov	%sp,%bp
+
+	//xor	%si,%si
+	xor	%di,%di
+
+	cld
+	mov	$0x80,%cx
+	mov	%cx,%es		#ES=0080h segment
+	mov	$0x7f,%cx
+	xor	%ax,%ax
+	rep
+	stosw			#800h-880h clear
+
+	xor	%di,%di
+	dec	%ax		# limit is always 0xffff
+	mov	%ax,%es:(0x3a)	# ES limit
+	mov	%ax,%es:(0x4c)	# DS limit
+	mov	%ax,%es:(0x40)	# CS limit
+	mov	%ax,%es:(0x46)	# SS limit
+	mov	%al,%es:(0x5f)	# IDT - important
+
+	# if highword(dst) = 0xffff, do memset 
+#src DS
+	xor	%bx,%bx
+	mov	12(%bp),%ax	#src addr low16
+	mov	%ax,%es:(0x48)	#new DS low16
+	mov	14(%bp),%ax	#src addr high8
+	cmp	$0xffff,%ax
+	jnz	1f
+	mov	$1,%bl		# memset flag
+	mov	%bl,%al		# make the high byte inoccuous
+				# we're not using it anyway
+1:	mov	$0x93,%ah	# & access byte
+	mov	%ax,%es:(0x4a)	#new DS high8 & access byte
+	
+#dst ES
+	mov	16(%bp),%ax	#dst addr low16
+	mov	%ax,%es:(0x36)	#new ES low16
+	mov	18(%bp),%ax	#dst addr high8 
+	mov	$0x93,%ah	# & access byte
+	mov	%ax,%es:(0x38)	#new ES high8 & access byte
+
+# code segment cache entry
+	cli
path: tlvc/arch/i86/lib/bios15.S
user: login: Mellvik
body: That's a very good point - and a great catch! I may actually have been hit but this one a few times over the past few weeks. I've been running INT15 a lot, not the least while working on the interrupt blocking, and had an occasional data corruption which I never got to repeat. Not immediate crashes but file system corruption. 

Like you I've been eager to shrink the amount of code protected against interrupts, now we're expanding it instead. all the more important to optimize the protected code.

Thanks.

created_at: 2025-05-04T07:39:50Z
author_association: OWNER
diff_hunk: @@ -84,6 +86,161 @@ block_move:
 	pop	%es
 	lret
 
+
+#ifdef CONFIG_FS_XMS_LOADALL
+#  EXPERIMENTAL -- based on code from @drachen6jp for ELKS
+# int FARPROC loadall_block_move(addr_t src, addr_t dst, size_t bytes)
+#
+# Prepare a loadall register block which changes the DS cache entry to
+# point to the source address of the move, while the ES cache entry 
+# points to the destination address. Either can be anywhere in the 16M
+# address space. Filling the other two segment register caches (CS, SS)
+# is also required since this is what the processor will be running on
+# after the LOADALL instruction has been executed. 
+# The caches in the processor will be individually reset to 'normal' real
+# mode values (high 4 bits clear) whenever the corresponding register is
+# changed in the code. 
+# Since this metnhod copies the current SS and CS values into the corresponding
+# caches, resetting them after is not critical. What's needed is
+# that the DS and ES registers get updated (restored) before we exit.
+#
+# The return value is usually ignored, but may be useful for debugging.
+
+loadall_block_move:
+
+	push	%es
+	push	%si
+	push	%di
+	push	%bp
+	mov	%sp,%bp
+
+	//xor	%si,%si
+	xor	%di,%di
+
+	cld
+	mov	$0x80,%cx
+	mov	%cx,%es		#ES=0080h segment
+	//mov	$0x80,%cx
+	xor	%ax,%ax
+	rep
+	stosw			#800h-880h clear
+
+	//xor	%di,%di
+	dec	%ax		# limit is always 0xffff
+	mov	%ax,%es:(0x3a)	# ES limit
+	mov	%ax,%es:(0x4c)	# DS limit
+	mov	%ax,%es:(0x40)	# CS limit
+	mov	%ax,%es:(0x46)	# SS limit
+	mov	%al,%es:(0x5f)	# IDT - important
+
+#src DS
+	mov	12(%bp),%ax	#src addr low16
+	mov	%ax,%es:(0x48)	#new DS low16
+	mov	14(%bp),%ax	#src addr high8
+	mov	$0x93,%ah	# & access byte
+	mov	%ax,%es:(0x4a)	#new DS high8 & access byte
+	
+#dst ES
+	mov	16(%bp),%ax	#dst addr low16
+	mov	%ax,%es:(0x36)	#new ES low16
+	mov	18(%bp),%ax	#dst addr high8 
+	mov	$0x93,%ah	# & access byte
+	mov	%ax,%es:(0x38)	#new ES high8 & access byte
+
+# code segment cache entry
+	cli
+	mov	%cs,%ax
+	mov	$12,%cl
+	shr	%cl,%ax
+	mov	%al,%es:(0x3e)	#CS high8
+	mov	%cs,%ax
+	mov	$4,%cl
+	shl	%cl,%ax
+	mov	%ax,%es:(0x3c)	#CS low16
+	mov	$0x9a93,%ax
+	mov	%ah,%es:(0x3f)	#cs access byte
+	mov	%al,%es:(0x45)	#ss access byte
+
+# stack segment cache entry
+	mov	%ss,%ax
+	mov	$12,%cl
+	shr	%cl,%ax
+	mov	%al,%es:(0x44)	#SS high8
+	mov	%ss,%ax
+	mov	$4,%cl
+	shl	%cl,%ax
+	mov	%ax,%es:(0x42)	#SS low16
+
+	mov	20(%bp),%cx	# byte count
+
+	push	%ds
+	push	%ss		# needed for seg reg cache reset
+	push	%cs		# after the transfer
+
+	//push	%cs
+	//call	modechange2
+	//mov	%sp,%bp
+	//mov	-6(%bp),%ax
+	//add	$2,%ax
+	//mov	%ax,%es:(0x1a)	#new IP
+	movw	$modechange,%es:(0x1a)	# where to start after loadall
+	//mov	%ax,%bx
+	//sub	$7,%bx
+	//mov	$0x050f,%ax		#loadall
+	//mov	%ax,%cs:(%bx)
+	//pushf			# no flags needed, zero is OK (IF == off)
+	//pop	%ax
+	//mov	%ax,%es:(0x18)
+	//smsw	%es:(0x06)	# not useful
+
+	mov	%ds,%es:(0x1e)
+	mov	%ss,%es:(0x20)
+	mov	%cs,%es:(0x22)
+	mov	%es,%es:(0x24)
+	//mov	%di,%es:(0x26)	# conveniently left at zero
+	//mov	%si,%es:(0x28)	# conveniently left at zero
+	//mov	%bp,%es:(0x2a)
+	mov	%sp,%es:(0x2c)
+	mov	%bx,%es:(0x2e)
+	mov	%dx,%es:(0x30)
+	mov	%cx,%es:(0x32)
+	//mov	%ax,%es:(0x34)
+exe:
+	.byte 0x0f	#loadall
+	.byte 0x05
+modechange2:
+	//call	modechange3
+	retf		# use the push %cs above
+modechange3:
+	//ret
+modechange:
+	shr	$1,%cx	# make words
+	rep
+	movsw		# do the block move
+	rcl	$1,%cx
+	rep
+	movsb		# get the final byte if any
+
+	call	modechange2	#reset CS
+	pop	%ax		#reset SS
+	mov	%ax,%ss
+	pop	%ax		#reset DS
+	mov	%ax,%ds
path: tlvc/arch/i86/lib/bios15.S
user: login: Mellvik
body: No, I've never used PCem, the picture I have in my head is that it requires an intel machine with windows or linux. I fail on all three accounts... I may be wrong though ??
created_at: 2025-05-04T17:41:16Z
author_association: OWNER

PR comments:
user: login: Mellvik
created_at: 2025-05-03T09:26:40Z
body: @ghaerr - I've had on my list to ask: Is there a reason I don't see for the existence of both `linear32_fmemcpyw` and `linear32_fmemcpyb` when the latter implements both - in there same way as `loadall_block_move` does? 
author_association: OWNER
user: login: drachen6jp
created_at: 2025-05-03T09:43:10Z
body: hi.@Mellvik.
Thanks for inplements of 286 loadall code.

line 126　on bios15.s
stosw instruction with cx=80h
800-8ffh clear ok?

Hello.@ghaerr
why IDT limit FF00?
Igot this value from "sidt [es:bx] execution" with my real 286 machine.

thanks.
author_association: NONE
user: login: ghaerr
created_at: 2025-05-03T13:18:21Z
body: @Mellvik:

> Is there a reason I don't see for the existence of both linear32_fmemcpyw and linear32_fmemcpyb when the latter implements both 

You're catching up fast Mellvik! No, I wrote the original routines before I realized there was a quick way of performing word moves from a byte-count function using the SHL/RCL we've recently discussed. I think also the original kernel code I replaced used fmemset/fmemsetw, and I didn't want to go through the entire kernel converting a word count to byte count too. That was probably 4-5 years ago and I haven't really checked recently whether it'd be worth combining the routines or not. 

Agreed though that the new loadall_block_move should be a byte-count function - as you've probably seen, the ugly mess in xms_fmemcpyb for handling odd byte counts was necessary since INT 15/1F is word count only. Once INT 15/1F is (finally?) thrown out, all that code can be removed.

Also, as you've likely seen, xms_fmemset can't be implemented using INT 15/1F, causing the hack in buffer.c to first copy an XMS buffer into L1 before zeroing. In order to avoid duplicating the LOADALL code almost in entirety, I'm thinking of renaming the loadall_block_move routine, pass an 'int op' (operation type: 0=memcpy, 1=zero fill) parameter, thus allowing it to effectively replace xms_fmemset. If I/we go that direction, xms_fmemset probably ought to be renamed xms_bzero - you'll note that the couple cases were xms_fmemset are called it's always to zero memory.
author_association: NONE
user: login: Mellvik
created_at: 2025-05-03T13:20:23Z
body: Again, @drachen6jp, thanks for your contribrution!

> hi.@Mellvik. Thanks for inplements of 286 loadall code.
> 
> line 126　on bios15.s stosw instruction with cx=80h 800-8ffh clear ok?

In fact you already set the CX register to the correct value two lines before, so we're fine.
> 
> Hello.@ghaerr why IDT limit FF00? Igot this value from "sidt [es:bx] execution" with my real 286 machine.
> 
> thanks.

Yes, this value turns out to be important. I've used 0xff00 from @drachen6jp's code, now using 0xffff, both are fine. Collins is using 0x3ff in his sources, which is presumably also fine.

Thank you.

author_association: OWNER
user: login: ghaerr
created_at: 2025-05-03T13:31:59Z
body: > Yes, this value turns out to be important. I've used 0xff00 from @drachen6jp's code, now using 0xffff, both are fine.

What are you saying, I assume you've just tested this and found something out? What happens, just LOADALL just not work, and the system gets corrupted filesystem data, then crash?

That the LOADALL doesn't work if you don't set the IDT limit to something other than zero is nteresting - perhaps the microcode rejects IDT size == 0 even when not in protected mode. However, I read that LOADALL always loads the registers and won't fire an exception during execution. Not quite sure what the real issue might be, thanks for testing!

> now using 0xffff, both are fine. Collins is using 0x3ff in his sources, which is presumably also fine.

Good to know!

> In fact you already set the CX register to the correct value two lines before, so we're fine.
> on bios15.s stosw instruction with cx=80h 800-8ffh clear ok?

I think perhaps what @drachen6jp might be saying is why take the time to clear more memory than needed? 66h is enough, 80h is used here. Was this just to save an extra MOV CX instruction with 66h?

Also, on another note: with regards to reshuffling lower memory and DEF_OPTSEG, etc: I caught that on ELKS in final review and our commit does not rearrange low memory at all, since DEF_OPTSEG is copied into kernel DS and not subsequently referenced. Actually, I'm surprised I never thought of that before - we can probably add some of that memory (not including 80:0000-006F) back into the main memory area, like is already being done with INITPROC. 

author_association: NONE
user: login: drachen6jp
created_at: 2025-05-03T14:07:44Z
body: >movw	$modechange,%es:(0x1a)	# where to start after loadall

I missed about this.
this is better than my trrricky code. good. and thanks.

if I stay IDT limit == 0,after sti instruction,machine goto reset.(interrupt table is really none?)
therefore GDT LDT TSS are not important,but IDT is verrry important,as comment in source.
I tried get all register from STOREALL,but I couldn't run correctly.so I did SIDT [es:bx]. I did get IDT limit and address.
ff00 && 000000h.
that is strange,but good enough.

thank you.


and PCem can run 286 IBM PC system(I run it with samsung 286).very good information!
or NekoProject2(this is only windows version, but can run with wine?)can run 286 PC-98 system without any ROMs.
author_association: NONE
user: login: Mellvik
created_at: 2025-05-03T15:03:16Z
body: 
> 
> if I stay IDT limit == 0,after sti instruction,machine goto reset.(interrupt table is really none?) therefore GDT LDT TSS are not important,but IDT is verrry important,as comment in source. I tried get all register from STOREALL,but I couldn't run correctly.so I did SIDT [es:bx]. I did get IDT limit and address. ff00 && 000000h. that is strange,but good enough.
> 
> 

That was a really neat trick, @drachen6jp ! And I agree with you, it's good enough.

Back to the 'zeroing' of the 0x800 LOADALL memory block. Thanks for bringing it up. While what I said before is correct, we don't really want to zero out all that memory. 0x7f (128) is more than enough, and I plan to use the 0x880 and up for different purposes, so I would have run into trouble with the current zeroing scheme.

Keep up the good work.

author_association: OWNER
user: login: Mellvik
created_at: 2025-05-03T15:22:09Z
body: > Agreed though that the new loadall_block_move should be a byte-count function - as you've probably seen, the ugly mess in xms_fmemcpyb for handling odd byte counts was necessary since INT 15/1F is word count only. Once INT 15/1F is (finally?) thrown out, all that code can be removed.

Indeed! The currently committed TLVC-code says 
`int FARPROC loadall_block_move(addr_t ssrc, addr_t dst, size_t words); /* use loadall */`
I'm keeping the FARPROC `enable_a20_gate` and - since you mention it - we're now in a position to ditch the INT15 version completely unless we deem it important for debugging or whatever. In the TLVC case that can be done right away since I don't use the GDT stuff in LOADALL. In any case it seems like it's time go get the CONFIG_FS_XMS_INT15 back, but for a different purpose, the 'just in case' purpose.

> Also, as you've likely seen, xms_fmemset can't be implemented using INT 15/1F, causing the hack in buffer.c to first copy an XMS buffer into L1 before zeroing. In order to avoid duplicating the LOADALL code almost in entirety, I'm thinking of renaming the loadall_block_move routine, pass an 'int op' (operation type: 0=memcpy, 1=zero fill) parameter, thus allowing it to effectively replace xms_fmemset. If I/we go that direction, xms_fmemset probably ought to be renamed xms_bzero - you'll note that the couple cases were xms_fmemset are called it's always to zero memory.

Yes, as mentioned in the PR notes, I've a fmemset call using LOADALL brewing, and I'm thinking along (somewhat) the same lines. Actually I have to alternatives, both using the current code with minimal changes. The first is to simply add another entry point to the asm code. The second is even simpler: Given the current `loadall_block_move` (yes, it needs a new name), in which both src and dst are linear addresses (`long_t`), I'm thinking 
```
If (high word(src parameter) ==  0xffff) /* which is illegal as an address anyway */
then 
low word = memset value /* like you say, usually zero*/
and run stosb/stosw for count bytes with (memset value) starting at dst address.
```
 Codewise very simple, and easily turned into a macro with the appropriate fmemset-name.


author_association: OWNER
user: login: Mellvik
created_at: 2025-05-03T16:04:49Z
body: > > Yes, this value turns out to be important. I've used 0xff00 from @drachen6jp's code, now using 0xffff, both are fine.
> 
> What are you saying, I assume you've just tested this and found something out? What happens, just LOADALL just not work, and the system gets corrupted filesystem data, then crash?

It hangs in the first xms access.

> 
> > In fact you already set the CX register to the correct value two lines before, so we're fine.
> > on bios15.s stosw instruction with cx=80h 800-8ffh clear ok?
> 
> I think perhaps what @drachen6jp might be saying is why take the time to clear more memory than needed? 66h is enough, 80h is used here. Was this just to save an extra MOV CX instruction with 66h?

yes, that was the somewhat hastily reasoning, see comment above.

> 
> Also, on another note: with regards to reshuffling lower memory and DEF_OPTSEG, etc: I caught that on ELKS in final review and our commit does not rearrange low memory at all, since DEF_OPTSEG is copied into kernel DS and not subsequently referenced. Actually, I'm surprised I never thought of that before - we can probably add some of that memory (not including 80:0000-006F) back into the main memory area, like is already being done with INITPROC.

You may recall our discussion about macros in particular when I redid the low memory organization while working on `meminfo`about 6 months ago. So, in TLVC, low memory is very streamlined, including OPTSEG being re-used as a BOUNCE buffer, which means that the LOADALL disturbance has more consequences. As mentioned in the PR notes, the LOADALL 'block' can overlay OPTSEG, since the latter is long gone by the time LOADALL will be used, but the BOUNCEbuffer needs relocation. Which likely means that the area between 0x50:0 and 0x80:0 will go to waste.  I may move SETUPDATA to this region though - now it's sharing space with the floppy cache. So it doesn't take up any space really - unless there is no floppy cache.

Before LOADALL:
```
 | kernel text     |
 +-----------------+ Kernel CS = REL_SYSSEG = (XD_BOUNCESEG + (XD_BOUNCESEGSZ>>4))
 | Other bounce    |  Optional, 1k
 | (MFM, Lance)    |
 +-----------------+ XD_BOUNCESEG = REL_INITSEG + (HAS_FDCACHE ? (FD_CACHESEGSZ>>4):0x10)
 | Floppy cache    |  Optional, variable, max 6.5 + 0.5k
 | setup data      |  Mandatory, 512B
 +-----------------+ 0x90 REL_INITSEG = FD_CACHESEG = DMASEG
 | Floppy bounce   |  1k
 | bootopts buffer |
 +-----------------+ 0x50 DEF_OPTSEG FD_BOUNCESEG
 | IRQ vectors/BDA |
 +-----------------+ 000
```


author_association: OWNER
user: login: Mellvik
created_at: 2025-05-03T18:48:48Z
body: This commit simply tests the fmemset idea mentioned above, posted for comments. Debug prints etc included. Just briefly tested, works so far.
author_association: OWNER
user: login: ghaerr
created_at: 2025-05-03T19:49:39Z
body: > simply tests the fmemset idea mentioned above

Nice! Glad you got it working!
author_association: NONE