• Please review our updated Terms and Rules here

"Fun with BIOS" Programming Thread


Weird. It's a temporary state, though - later down (page 5-118 in the earlier BIOS listing, address E220) the usual value of 99h is output to port 63h causing port A to be a input. I don't know why they set it to be an output initially - port A is connected directly to the output lines of the keyboard shift register.

Also, do you mind if I write a Python script to contact your XT server and send data back without having to use the form (I don't know Perl/don't have it installed)? It would make my testing a bit easier.

Go right ahead. If you send me the script I'll put it on the site like I did with Egan's.
 
Right now, I'm still focusing on only XT and PC class machines (since they're similar enough), but I wanted some feedback this list of features and how they would change my implementation of BIOS, using .IFDEF and .ELSE IFDEF (?) statements:

Hi, I really like the idea of using the empty ROM socket on the PC motherboard to allow either a larger BIOS, or adding some extra utilities like debug, etc. I don't know of any other use that has ever been made of that empty socket!

Not that it will be a problem with your implementation, but just thought I would also mention that the original 10/27/82 IBM BIOS has a problem booting with only a 16K memory fill.

Does anyone else have any ideas for using that extra 8K of space?
 
I don't know of any other use that has ever been made of that empty socket!
Neither do I.

Does anyone else have any ideas for using that extra 8K of space?

I can think of a few things...
  • For testing purposes I could put a copy of the BIOS at that offset, and use an entry point to a function to swap IVT entries between that BIOS and the current one.
  • I have a ROMable printf library that someone sent me- I could write a small debugger using part of that.
  • Extra features, such as boot from A:\ or B:\ could be implemented using that space.
  • One thing I'd like to have seen in a BIOS implementation was built-in interrupt-driven I/O for the serial port, if room permits.
  • Perhaps a diagnostics program akin to MODEM7's ROMs could go there as well?
  • Hook certain DOS routines so that hardware support isn't so abysmal (I'm looking at you, CTTY COM1... "Device not ready" my ass!).
  • Using my perfectly-legal and definitely-not pirated copy of the MS-DOS source code, create a small ROMDOS or OS?

Because the ROM is on the system board, it's difficult to write routines for new hardware/ISA cards, but that could possibly be done as well.

Anyone feel free to add to the list.
 
  • I have a ROMable printf library that someone sent me- I could write a small debugger using part of that.
  • Perhaps a diagnostics program akin to MODEM7's ROMs could go there as well?
This. I always marveled at the Zenith ROMs that had a built-in debugger. I think it would be cool to have a BIOS that contains not only a BASIC but also a debugger, or even a simple monitor.
 
It wasn't unusual for 8-bit (x80) systems to have a monitor. As a matter of fact, the 8085 systems that I have allow one to generate an NMI from the keyboard, which enters the ROM monitor. You can look at and modify memory, registers and p-counter and then resume.
 
Started writing the boot code to a small monitor- at least trying to, but running into a small snag.

I want the stack of my monitor to point to the top of available memory, so that way it's less likely to screw over other applications. On 386 and above to my understanding, swapping stacks can be done just by modifying ESP (well, this link says otherwise, but ignoring that for a second) because no modern 32-bit OS bothers to use the segments.

That being said, is there a specific 'convention' that Intel recommends on the 8088 to use when switching stacks? For example, is there a specific order to push registers or a specific procedure to follow. I know I'd at least have to push the previous values of SS, BP, and SP onto the new stack (temporarily storing them in other registers or using ES segment overrides). Having had no formal class in OS design, I'm not sure if what I'm trying to do is equivalent to a partial context switch or not, where I save only the stack.

Also, for my own knowledge, what is the purpose of BP in the context of modifying the stack in assembly language? I know that BP can be used as an indirect register, and C programs use it to create stack frames- perhaps it's also used in context switches? But in the assembly code I've looked at, I don't think I've personally EVER seen BP used to create a stack frame in assembly language routines... usually it's just general purpose or for indexing. Maybe I haven't been looking at the right code.

Any help is appreciated here to resolve my misunderstandings.
 
It's a start, part 2

It's a start, part 2

I found a manual on MASM 6.1, and it states the conventions used by IBM/Microsoft (basically, use the stack for parameters in order of right-to-left, then local variables and store the previous value of BP last).

Right here, I have a minimum component ROM-able program that I can jump to from debug and execute... all it does is write 256 bytes of garbage to the screen.

Using Bochs, I loaded my ROM into segment 0xD000. I am assuming it will work just as well when I stick it into the PC EPROM socket at 0xF400. Bochs didn't attempt to execute any code in the room during boot- still haven't quite figured that out yet. However, I managed to jump into the code from DEBUG using g=0xD000:0x0003, and the program executes and exits. Instead of getting a 'program exited normally' message however, my code makes DEBUG quit as well! By a fluke, I managed to get the 'program exited normally' message to occur by setting a breakpoint which was never triggered (it was beyond the 'exit code', and as I recall at that point, I had a retf in place of the DOS exit service. God only knows why that didn't crash the system.). How do I coerce DEBUG to let the program exit gracefully without making DEBUG exit as well?


Below is my trivial sample code. The ENDOFROM class segment is absolutely positioned using WLINK-specific features to make the ROM size a multiple of 512 bytes- it's irrelevant for this example.

Code:
code_seg segment public 'CODE'
	dw 0xAA55
	db 0x04
	;jmp main
main proc
	mov ax, video_seg
	mov ds, ax
	mov si, 0x00
	mov cx, 0x100
	mov ax, 0xAA55
loop_vid:
	mov word ptr ds:[si], ax
	inc si
	inc si
	loop loop_vid
	mov ax, 0x4C00 
	int 0x21
main endp
code_seg ends


;stack_seg segment byte stack
;Why does the above trigger a warning?
stack_seg segment byte 'STACK' at 0xA0000 

stack_seg ends

end_seg segment byte public 'ENDOFROM'
	checksum db 0x00
end_seg ends

video_seg segment at 0xB800
	buffer db ?
video_seg ends

end main
 
An INT 3 will exit to the DEBUG prompt.

It appears as if you forgot to include a checksum byte in your code. Without that, the system can't tell between garbage starting with 55 AA and a real BIOS extension.
 
An INT 3 will exit to the DEBUG prompt.

It appears as if you forgot to include a checksum byte in your code. Without that, the system can't tell between garbage starting with 55 AA and a real BIOS extension.

Ahh yes, I forgot to mention... the checksum is added after linking by an external program. It is patched over the value of 'checksum' in 'end_seg'...
I believe the header for an option ROM is: 0x55 0xAA number of 512-byte sections used, and the first instruction follows?
 
Yes, and note that the checksum is actually the negative sum of all bytes preceding it. That is, summing the all bytes of the extension should come up 00 (ignore carryouts).

I don't understand your use of segments after .CODE. I'm assuming that you're assembling a one-segment (<64KB) "tiny" extension, so there are no other segments, unless they're "AT" segments. You will get a warning that the thing doesn't start at 0100, but you can ignore that.

So, your code (using simplified segments of MASM 6.1 would be:

Code:
	.model tiny,c			    ; I'm assuming that you're using the C calling convention

ROM_PAGES equ 4
ROM_BYTES equ ROM_PAGES*512

video_seg    segment at 0a000h
video_seg     ends

	.code
code_start:

	dw 0AA55h
	db ROM_PAGES
 
main	proc
	mov ax, seg video_seg
	mov ds, ax
	mov si, 00h
	mov cx, 100h
	mov ax, 0AA55h
loop_vid:
	mov word ptr ds:[si], ax
	inc si
	inc si
	loop loop_vid

	int 3
main endp

	db     (ROM_BYTES - ($-code_start) -1) dup (0)
Checksum db 0
	end

Which gives you a 2KB image that produces the following binary:

Code:
00000000  55 aa 04 b8 00 a0 8e d8  be 00 00 b9 00 01 b8 55  |U..............U|
00000010  aa 89 04 46 46 e2 fa cc  00 00 00 00 00 00 00 00  |...FF...........|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00000800
 
Okay, Page 1-32 of the 1984 IBM 5150 Manual shows a diagram of the PPI ports... I understand that you need to program a specific control-word to 0x63, but to be perfectly honest, I have NO clue how to interpret the rest of the page... I'm not sure how to switch between the keyboard and SW1, unless PB7 controls that (and refers to SW1 as sense switches).
Yes. You can see it 'in action' in the top half of http://www.minuszerodegrees.net/5150/misc/5150_test_sw1_sw2_via_basic.htm

Additionally, I do not know why there is a '+' after PA0 indicating that "PA0 must be high for a valid scan code to appear on the port",
I think that is just the author indicating positive polarity, that is, when reading a byte, it will be the keyboard scan code, not an inverted form of it.

as well as a '+' after P04 and P05 for "Display Type".
Again, I'm sure that is polarity, polarity compared to the "**" table in the bottom half of the page. Seems unwarranted to me.

Nor do I understand the purpose of "Read Spare Key", "Enable Read/Write Memory",
PB2 is "+ (Read Read/Write Memory Size) or (Read Spare Key)".
PB2 is associated with input bits PC0-PC3, and both of those are involved in reading the SW2 block switches.

After writing 1 to bit PB2: The lower 4 bits of a read from PC indicate status of switches SW2-1 to SW2-4 ("Read Read/Write Memory Size")
After writing 0 to bit PB2: The lowest bit of a read from PC indicates the status of switch SW2-5 ("Read Spare Key")

The use of "spare key" may be connected to the fact that the first two revisions of 5150 BIOS did not read switch SW2-5 at all. So, early in the 5150's life, the then unused SW-5 would have been considered a 'spare' to be used later on.

See the botttom half of http://www.minuszerodegrees.net/5150/misc/5150_test_sw1_sw2_via_basic.htm

or "I/O channel check" on Port B.

Also, I recall reading there was a provision to disable Parity NMI by itself using the PPI (according to the manual, NMIs can be globally disabled by writing 0x00 to port 0xA0)? Am I confusing that with something else?
Refer to the NMI diagram at http://www.minuszerodegrees.net/5150/misc/5150_nmi_generation.jpg

Bit 7 of port 0xA0 controls the flipflop shown in the bottom right corner of the diagram - either enabling all or disabling all NMIs from reaching the CPU.

PB5, "Enable I/O Channel Check", controls output pin 23 of the 8255 PPI.
 
Weird. It's a temporary state, though - later down (page 5-118 in the earlier BIOS listing, address E220) the usual value of 99h is output to port 63h causing port A to be a input. I don't know why they set it to be an output initially - port A is connected directly to the output lines of the keyboard shift register.
I worked out what is going on.
Turns out to be because initially, the POST is outputting diagnostic codes to PPI port A (I/O port 0x60).
You can see them commented in the source, e.g. "<><><>CHECKPOINT 1<><><>".
'Checkpoint' 4 is the final one, and then the code you found is executed, setting PPI port A from output mode to input mode.
 
After writing 1 to bit PB2: The lower 4 bits of a read from PC indicate status of switches SW2-1 to SW2-4 ("Read Read/Write Memory Size")
After writing 0 to bit PB2: The lowest bit of a read from PC indicates the status of switch SW2-5 ("Read Spare Key")

The use of "spare key" may be connected to the fact that the first two revisions of 5150 BIOS did not read switch SW2-5 at all. So, early in the 5150's life, the then unused SW-5 would have been considered a 'spare' to be used later on.
.

I find the IBM Technical Reference Manuals very useful, compared to some other technical manuals such as Sega2.DOC (A Sega Genesis technical manual leaked possibly before the end of the console's life that is known to have a number of errors, specifically regarding the sound chip)... but calling that a "Spare key" is bordering on Sega2.DOC ambiguity right there.

I felt the analogy was appropriate, but here is some context: As an aside, I occassionally do SNES programming when I'm in the mood... I learned more about how the Genesis worked reading the SNES manuals than I did reading the Genesis manuals! You can get by with reading ONLY the SNES manuals and be able to program a game or at least a small demo- most 'tricks' are documented as features. Genesis however, well- there is a 46 page-forum thread on here dedicated just to reverse engineering the sound chip (which was done successfully). Reading the TI VDP datasheet also helped immensely with both, explaining to me how video generators and layers/priorities work.
 
Which gives you a 2KB image that produces the following binary:

Code:
00000000  55 aa 04 b8 00 a0 8e d8  be 00 00 b9 00 01 b8 55  |U..............U|
00000010  aa 89 04 46 46 e2 fa cc  00 00 00 00 00 00 00 00  |...FF...........|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00000800

This binary code is amazingly simple. I created a file from it using a HEX editor, added the correct checksum and burned it into a ROM for socket U28 on an original IBM PC. When the PC boots it displays a message "F400 ROM" which I take to mean it has detected a valid expansion ROM at the location. However when I look at location A000:0000 using DEBUG I do not see anything except a complete fill of "E8" bytes. Is that the results you would expect? I tried this with both an MDA card and VGA card in the PC, but nothing was displayed on the monitor, except for usual stuff and the "F400 ROM" message.
 
Which gives you a 2KB image that produces the following binary:

Code:
00000000  55 aa 04 b8 00 a0 8e d8  be 00 00 b9 00 01 b8 55  |U..............U|
00000010  aa 89 04 46 46 e2 fa cc  00 00 00 00 00 00 00 00  |...FF...........|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00000800

This binary code is amazingly simple. I created a file from it using a HEX editor, added the correct checksum and burned it into a ROM for socket U28 on an original IBM PC. When the PC boots it displays a message "F400 ROM" which I take to mean it has detected a valid expansion ROM at the location. However when I look at location A000:0000 using DEBUG I do not see anything except a complete fill of "E8" bytes. Is that the results you would expect? I tried this with both an MDA card and VGA card in the PC, but nothing was displayed on the monitor, except for usual stuff and the "F400 ROM" message.

"0xF400 ROM" is the BIOS complaining about a bad checksum at the expansion ROM slot. It's worth noting that I only allocated the bare minimum (2 kB, or 4 512-byte pages) for the ROM, but the BIOS is supposed to check up to and not including segment 0xF600. Maybe the ROM at 0xF400 has to take up the 8 kB? I'll check the BIOS listings later.

As for nothing printing to the screen, that's expected but not intentional. That's because I'm an idiot and forgot that the VGA text buffer is at segment 0xB800 to 0xC000. I don't remember what 0xA000 to 0xB000 is used for. And that leaves 0xB000 for monochrome emulation :D. 0xC000 and beyond is the VGA BIOS extensions.

Change the segment to 0xB800 and try again, if you have an EPROM to spare and/or EPROM eraser. I'll work on it a bit tonight myself.
 
Last edited:
Here's a dirty little trick that I've grown accustomed to when making ROM images to burn:

Code:
	.model tiny,c			

ROM_PAGES equ 4
ROM_BYTES equ ROM_PAGES*512

video_seg    segment at 0b800h
video_seg     ends

	.code
code_start:

	dw 0AA55h
	db ROM_PAGES
 
main	proc
	mov ax, seg video_seg
	mov ds, ax
	mov si, 00h
	mov cx, 1000h
	mov ax, 0AA55h
loop_vid:
	mov word ptr ds:[si], ax
	inc si
	inc si
	loop loop_vid

	int 3
main endp

	db     (ROM_BYTES - ($-code_start) -32) dup (0) ; filler

; The following is entered with a G= in DEBUG.	It computes and stores
; the checksum. It's always located 20h bytes before the end of the ROM.
 
Checkit		proc
	mov	si,100h			; where files get loaded in DEBUG
	mov	di,ROM_BYTES-1
	xor	ax,ax
Check2:
	add	al,cs:[si]
	inc	si
	dec	di
	jnz	Check2
	neg	al
	mov	cs:[si],al	    ; stores result in "Checksum"
	int	3
Checkit	       endp


	db     (ROM_BYTES - ($-code_start) -1) dup (0)
Checksum db 0

	end

(Note that I've changed the segment to B800 and the length to 4K to give you a nice green screen. At any rate, run DEBUG on the .COM file and G=8E0 into the checksum routine. It'll compute and store the checksum and exit to DEBUG. Then issue a -W command to DEBUG and then a -Q. Presto--you have your checksummed image.
 
Here's a dirty little trick that I've grown accustomed to when making ROM images to burn:

Code:
	.model tiny,c			

ROM_PAGES equ 4
ROM_BYTES equ ROM_PAGES*512

video_seg    segment at 0b800h
video_seg     ends

	.code
code_start:

	dw 0AA55h
	db ROM_PAGES
 
main	proc
	mov ax, seg video_seg
	mov ds, ax
	mov si, 00h
	mov cx, 1000h
	mov ax, 0AA55h
loop_vid:
	mov word ptr ds:[si], ax
	inc si
	inc si
	loop loop_vid

	int 3
main endp

	db     (ROM_BYTES - ($-code_start) -32) dup (0) ; filler

; The following is entered with a G= in DEBUG.	It computes and stores
; the checksum. It's always located 20h bytes before the end of the ROM.
 
Checkit		proc
	mov	si,100h			; where files get loaded in DEBUG
	mov	di,ROM_BYTES-1
	xor	ax,ax
Check2:
	add	al,cs:[si]
	inc	si
	dec	di
	jnz	Check2
	neg	al
	mov	cs:[si],al	    ; stores result in "Checksum"
	int	3
Checkit	       endp


	db     (ROM_BYTES - ($-code_start) -1) dup (0)
Checksum db 0

	end

(Note that I've changed the segment to B800 and the length to 4K to give you a nice green screen. At any rate, run DEBUG on the .COM file and G=8E0 into the checksum routine. It'll compute and store the checksum and exit to DEBUG. Then issue a -W command to DEBUG and then a -Q. Presto--you have your checksummed image.

Why is the checksum loaded 32 (decimal) bytes before the end of the ROM?

Also, you asked why I used multiple segments in a COM file and not declare a memory model?

Well, 4 reasons to be honest:

1. I keep a small MASM-compatible assembler on my IBM PC for on-the-fly debugging, and it doesn't appear to support the memory models (Arrowsoft Assembler). Perhaps it even predates them? I know the BIOS is considered COMPACT, but I can't find the memory model declaration in IBM's listings.

2. I once heard that COM files, such as COMMAND.COM need not actually be limited to 64kB and I wanted to figure out how that might be done- turns out, the assembler/linker doesn't care if the memory model is tiny/multiple segments are used and compiling as COM or EXE only changes the properties of the header (or lack thereof).

3. Though I don't care too much if the assembler changes some instructions, I'm big on describing my intent in x86 instructions, and memory models let me hide my intent, i.e. make assumptions, with regard to where I'm trying to access code and data. Quite appropriately, it is the same reason I barely use ASSUME at all.

4. I have never found a satisfactory explanation on what assumptions are made by the programmer/assembler using simplified segment directives, and how the assembler changes how it generates code in response to seeing the simplified segments. Ditto with how the linker links 'full' segments vs 'simplified segments.

x86 is complicated enough... the less assumptions I forced a reader or myself to make, the less painful it is.
 
Okay, I'm just trying to make things easy for you. Rather than compute a checksum the hard way, you simply do it this way (let's call the assembled program xamp.com. So you need to create a ROM checksum. The whole purpose of the code 20H bytes from the end of the code is to put the routine that computes the checksum for you at a known location out of the way. So, to create your checksummed xamp.com, you do this:

Code:
E:\temp>debug xamp.com
-g=8e0

AX=0054  BX=0000  CX=0800  DX=0000  SP=FFFE  BP=0000  SI=08FF  DI=0000
DS=0B33  ES=0B33  SS=0B33  CS=0B33  IP=08F3   NV UP EI PL NZ AC PO CY
0B33:08F3 CC            INT     3
-w
Writing 00800 bytes
-q

E:\temp>

Just that slick--you now have a checksum in your XAMP.COM image.

Use what you want--write your code in JOVIAL. I'm just trying to help and give you the benefit of 30-odd years of writing x86 assembler.
 
Use what you want--write your code in JOVIAL. I'm just trying to help and give you the benefit of 30-odd years of writing x86 assembler.

I see now- that's clever code!

Forgive me if I sounded pretentious, that wasn't my intent. But you're right, I'll keep an open mind. My experience learning x86 has been rather painful and me getting my ass kicked ever since I first attempted to learn it in late-2009... 1 year after starting programming (probably not my brightest idea). Probably the toughest thing for me is the fact that there are literally at least 20 freely-available x86 assemblers out there and none of them are compatible with each other. And this was before I got into vintage computing, so I wasn't aware that I was learning real mode DOS programming (nevermind not having any clue what virtual 8088 mode was).

I suppose it's worth mentioning that I didn't start getting 'good' at RTFMing and gleaming information until mid-2011, and then especially early-2012 while trying out an 8051 microcontroller... two+ years after I tried learning x86.

Was 8088 tough for you when you first learned it? Or did you survive without too many wounds to the 386 before it became a mess?
 
Last edited:
Back
Top