February 17th, 2020

A bootable PC program that allows users to read and write from memory

I'm becoming increasingly convinced that most of the problems which exist with computer software today stem from the operating systems. Usually when a computer crashes, it can be traced somehow back to the operating system. Usually when something is difficult to do in software, it's because the operating system prevents the user (or programmer) from doing that thing, since programming now can only be done by putting together a series of pre-built functions, and if the functionality you want isn't built into whatever API you're using, then too bad, you can't do it. And usually when a computer forces you to do something, it's because of the operating system. Simply put, operating systems are the root of all computing evil today. Every operating system in widespread use today serves only to restrict what users and programmers can do by trying to force people into acting a certain way. Once again: "Every OS Sucks". (Alternate link: here)

In recognition of this problem, I thought about making a simple bootable program--meaning one which runs without an operating system because it runs directly from the boot sector of a storage medium--that allows the user to do the most fundamental things with their computer: Read and write to memory and I/O, and more generally perform any CPU opcode. The paradigm of such software is partly inspired by the Apple II "monitor" program built into the ROM of every Apple II computer, as well as the debug program which came with MS-DOS. Obviously such software is more fully-featured than what I've written here, but the program below is sort of a baby step: It allows the user to read and write any value they want to/from any memory location they want. (Well, any location within the program's own 64-kilobyte memory segment.) As a nice added bonus, the whole program is less than 512 bytes long, meaning it fits into the boot sector of a floppy disk or other boot medium.

The program could certainly use a few more features. At the very least, I'd like to be able to set the CPU's data segment (DS) so that the user can control where in memory data is read and written, as well as the code segment (CS) so that the user can control where code is run. (Such functionality is not relevant to something like the Apple II monitor program since the Apple II generally uses a flat memory space where memory bytes are simply numbered linearly, but on a PC which uses segment:offset-style x86 memory segmentation, lacking the ability to set the active segment means, again, that you're restricted to a 64-kilobyte portion of memory.) It would also be nice to have a "go" command to begin executing code at a specific memory location, which would allow this program to function like a true set of old-fashioned "front panel switches" that allow the user to enter code directly into the memory and then run it, but to avoid feature creep and keep the program simple for people who want to see how it works, I've decided to start small and create a program that just reads and writes, nothing more.

Here, then, is the code, written for the A86 PC assembler.

ORG 7C00h
;The above line is to make this program reference addresses relative to
;memory address 7C00h, which is where boot-loader programs get loaded.
;Comment it out if you're testing this program in DOS, because DOS
;loads .COM files beginning from memory address 100h.
;(You can also use a line like "ORG 100h", but A86 does this automatically
;if you don't specify an ORG value.)

MAINLOOP:

;Output "(R)read/(W)rite? " to the screen
MOV CX,19 ;The string contains 19 characters, including the carriage return/line feed at the end
MOV SI,OFFSET rwprompt
RWSTRINGLOOP:
PUSH CS
POP DS ;Sets DS to be the current program segment
CLD ;Clear direction flag so that SI gets incremented by LODSB
LODSB ;Load the byte at DS:SI into AL and increment SI
MOV AH,0Eh ;INT 10,E to output text to the screen
MOV BH,0 ;Page number (for text video modes)
INT 10h
DEC CX
CMP CX,0 ;If this wasn't the last character...
JNE RWSTRINGLOOP ;...then repeat the loop until the whole string has been printed

MAINLOOPINPUT:
MOV AH,0 ;Read keyboard input
INT 16h
;ASCII value of keyboard input is now in AL
CMP AL,72h ; is it r?
JE CHOSEREAD
CMP AL,52h ; is it R?
JE CHOSEREAD
CMP AL,77h ; is it w?
JE CHOSEWRITE
CMP AL,57h ; is it w?
JE CHOSEWRITE

JMP MAINLOOPINPUT ;If the user didn't input r, R, w, or W, then wait for another keyboard input

CHOSEREAD:
;Write "Read from which address? " to the screen
MOV CX,27 ;String contains 27 characters, including the CR/LF at the end
MOV SI,OFFSET readprompt
READSTRINGLOOP:
PUSH CS
POP DS ;Sets DS to be the current program segment
CLD ;Clear direction flag so that SI gets incremented by LODSB
LODSB ;Load the byte at DS:SI into AL and increment SI
MOV AH,0Eh ;INT 10,E to output text to the screen
MOV BH,0 ;Page number (for text video modes)
INT 10h
DEC CX
CMP CX,0
JNE READSTRINGLOOP

CALL GET4HEX ;Read the input of 4 hexadecimal characters from the keyboard

CALL CONVERTFOURBYTESTOHEXADECIMAL ;Convert the 4-byte input into a hexadecimal number in CX.

MOV BX,CX ;Put the resulting hexadecimal number into BX.
;ADD BX,100h ;Add 100 to BX for testing in DOS.
MOV AL,[BX] ;Loads the contents of the desired memory location into AL.

;The memory value we want is now in AL.
;All we have to do is output the contents of AL to the screen in a (relatively) user-friendly way.

;Convert the value in AL into a 2-digit hexadecimal number.
;This would also be a good candidate for a subroutine,
;but in this program, reading from memory is the only time
;we need to convert a hexadecimal number in memory into
;something that gets displayed on the screen, so this routine
;is left here rather than turning it into a subroutine.
PUSH AX ;The routine below destroys the contents of AL for the first character, so save AX for later
SHR AL,4
ADD AL,30h ;The ASCII character 0 has a value of 30h, ASCII 1 is 31h, and so on
CMP AL,39h ;Is the value we read less than or equal to 9?
JLE PRINTFIRSTHEXCHAR ;If so, it's ready to be printed.
ADD AL,7 ;Otherwise, the character is somewhere in the range of A through F, so add 7 to it
;There are 7 characters in the ASCII table between "9" and "A", so you need to add 7 to
;the value to turn it into the correct ASCII letter.
PRINTFIRSTHEXCHAR:
MOV AH,0Eh ;Good old INT 10,E again
MOV BH,0 ;Page number (for text video modes)
INT 10h
POP AX ;Restore AX so we can use the original memory value again
AND AL,0Fh ;Zero out the high nibble, leaving only the low one for the second hexadecimal character
ADD AL,30h ;Conversion from hexadecimal to ASCII, as above
CMP AL,39h
JLE PRINTSECONDHEXCHAR
ADD AL,7 ;Correct for A-F
PRINTSECONDHEXCHAR:
MOV AH,0Eh
MOV BH,0 ;Page number (for text video modes)
INT 10h

;We're done, but let's output a CR/LF to keep things looking better.
MOV AH,0Eh
MOV AL,0Dh ;Carriage return
MOV BH,0 ;Page number (for text video modes)
INT 10h
MOV AL,0Ah ;Line feed
INT 10h

JMP MAINLOOP
;This program does not terminate, but just keep going back to the main loop forever.
;If you want to test this program in DOS in such a way that it ends after the user does
;one operation, you can comment out the above line, and uncomment the two lines below.
;MOV AX,004Ch ;Terminate program
;INT 21h

CHOSEWRITE:
;Write "Write to which address? " to the screen
MOV CX,26 ;String contains 26 characters, including the CR/LF at the end
MOV SI,OFFSET writeprompt
WRITESTRINGLOOP:
PUSH CS
POP DS ;Sets DS to be the current program segment
CLD ;Clear direction flag so that SI gets incremented by LODSB
LODSB ;Load the byte at DS:SI into AL and increment SI
MOV AH,0Eh
MOV BH,0
INT 10h
DEC CX
CMP CX,0
JNE WRITESTRINGLOOP

CALL GET4HEX

;Write "What value to write? " to the screen
MOV CX,23 ;String contains 23 characters, including the CR/LF at the end
MOV SI,OFFSET writesecondprompt
WRITESTRINGSECONDLOOP:
PUSH CS
POP DS ;Sets DS to be the current program segment
CLD ;Clear direction flag so that SI gets incremented by LODSB
LODSB ;Load the byte at DS:SI into AL and increment SI
MOV AH,0Eh
MOV BH,0
INT 10h
DEC CX
CMP CX,0
JNE WRITESTRINGSECONDLOOP

CALL GET2HEX ;Get 2 hexadecimal characters from the keyboard (for the value to store in memory)

CALL CONVERTFOURBYTESTOHEXADECIMAL
;CX now contains the memory address to write to.
PUSH CX ;For a later POP BX
CALL CONVERTTWOBYTESTOHEXADECIMAL
POP BX
;ADD BX,100h ;Add 100 to BX for testing in DOS.
MOV AL,CL ;Move the value to write into AL...
MOV [BX],AL ;...and MOV it into the desired memory location.

JMP MAINLOOP
;This program does not terminate, but just keep going back to the main loop forever.
;If you want to test this program in DOS in such a way that it ends after the user does
;one operation, you can comment out the above line, and uncomment the two lines below.
;MOV AX,004Ch ;Terminate program
;INT 21h

;Subroutines GET4HEX, GET2HEX, CONVERTFOURBYTESTOHEXADECIMAL, CONVERTTWOBYTESTOHEXADECIMAL,
;and CONVERTALFROMASCIITOHEXADECIMAL are below.
;GET4HEX: Receives 4 hexadecimal characters from the user and stores them in the 4-byte input buffer
;(For memory addresses)
;GET2HEX: Receives 2 hexadecimal characters from the user and stores them in the 2-byte input buffer
;(For values to write into memory)
;CONVERTFOURBYTESTOHEXADECIMAL: Converts the contents of the 4-byte input buffer into an actual
;hexadecimal number, and stores the result in CX.
;CONVERTTWOBYTESTOHEXADECIMAL: Converts the contents of the two-byte input buffer into an actual
;hexadecimal number, and stores the result in CL.
;CONVERTALFROMASCIITOHEXADECIMAL: Converts AL from an ASCII character ("0" through "9"
;or "A" through "F") to the corresponding hexadecimal number.

GET4HEX: ;Receive 4 hexadecimal characters from the user and store them in the 4-byte input buffer
MOV BX,OFFSET fourbyteinputbuffer
MOV CX,4 ;Do this whole thing 4 times
GET4HEXINPUTLOOP:
MOV AH,0 ;Read keyboard input
INT 16h
;ASCII value of keyboard input is now in AL
;Begin testing to see whether the user entered a valid hexadecimal character (0-9 or A-F)
CMP AL,30h ;AL needs to be at least 30h for ASCII "0"
JL GET4HEXINPUTLOOP
CMP AL,39h ;9 in ASCII
JLE INPUTISHEX
CMP AL,41h ;A
JL GET4HEXINPUTLOOP
CMP AL,46h ;F
JLE INPUTISHEX
CMP AL,61h ;a
JL GET4HEXINPUTLOOP
CMP AL,66h ;f
JG GET4HEXINPUTLOOP
;The user input a lowercase a through f, so let's go ahead and convert it to uppercase
;so we don't have to worry about dealing with uppercase vs. lowercase later.
SUB AL, 20h ;Converts a through f to A through F.
INPUTISHEX:
;End testing to see whether the user entered a valid hexadecimal character (0-9 or A-F)
PUSH BX ;Save BX, since INT 10,E uses it.
MOV AH,0Eh ;Output the character the user typed
MOV BH,0
INT 10h ;Output the character the user typed
POP BX ;Restore BX
MOV [BX],AL
INC BL
DEC CX
JNZ GET4HEXINPUTLOOP
;We're done, but let's output a CR/LF to keep things looking better.
MOV AH,0Eh
MOV AL,0Dh
MOV BH,0
INT 10h
MOV AL,0Ah
INT 10h
RET

GET2HEX: ;Receive 2 hexadecimal characters from the user and store them in the 2-byte input buffer
MOV BX,OFFSET twobyteinputbuffer
MOV CX,2 ;Do this whole thing 2 times
GET2HEXINPUTLOOP:
MOV AH,0 ;Read keyboard input
INT 16h
;ASCII value of keyboard input is now in AL
;Begin testing to see whether the user entered a valid hexadecimal character (0-9 or A-F)
CMP AL,30h ;AL needs to be at least 30h for ASCII "0"
JL GET2HEXINPUTLOOP
CMP AL,39h ;9 in ASCII
JLE INPUTISHEX2
CMP AL,41h ;A
JL GET2HEXINPUTLOOP
CMP AL,46h ;F
JLE INPUTISHEX2
CMP AL,61h ;a
JL GET2HEXINPUTLOOP
CMP AL,66h ;f
JG GET2HEXINPUTLOOP
;The user input a lowercase a through f, so let's go ahead and convert it to uppercase
;so we don't have to worry about dealing with uppercase vs. lowercase later.
SUB AL, 20h ;Converts a through f to A through F.
INPUTISHEX2:
;End testing to see whether the user entered a valid hexadecimal character (0-9 or A-F)
PUSH BX ;Save BX, since INT 10,E uses it.
MOV AH,0Eh ;Output the character the user typed
MOV BH,0
INT 10h ;Output the character the user typed
POP BX ;Restore BX
MOV [BX],AL
INC BL
DEC CX
JNZ GET2HEXINPUTLOOP
;We're done, but let's output a CR/LF to keep things looking better.
MOV AH,0Eh
MOV AL,0Dh
MOV BH,0
INT 10h
MOV AL,0Ah
INT 10h
RET

CONVERTFOURBYTESTOHEXADECIMAL:
;Convert the four-byte input buffer into an actual hexadecimal number, and store the result in CX.
MOV BX,OFFSET fourbyteinputbuffer
MOV AL,[BX]
CALL CONVERTALFROMASCIITOHEXADECIMAL
MOV CH,AL
SHL CH,4
INC BX
MOV AL,[BX]
CALL CONVERTALFROMASCIITOHEXADECIMAL
ADD CH,AL
INC BX
MOV AL,[BX]
CALL CONVERTALFROMASCIITOHEXADECIMAL
MOV CL,AL
SHL CL,4
INC BX
MOV AL,[BX]
CALL CONVERTALFROMASCIITOHEXADECIMAL
ADD CL,AL
;CX now contains the 4-digit hexadecimal number the user entered,
;converted into an actual 16-bit hexadecimal number.
RET

CONVERTTWOBYTESTOHEXADECIMAL:
;Convert the two-byte input buffer into an actual hexadecimal number, and store the result in CL.
MOV BX,OFFSET twobyteinputbuffer
MOV AL,[BX]
CALL CONVERTALFROMASCIITOHEXADECIMAL
MOV CL,AL
SHL CL,4
INC BX
MOV AL,[BX]
CALL CONVERTALFROMASCIITOHEXADECIMAL
ADD CL,AL
;CL now contains the 2-digit hexadecimal number the user entered,
;converted into an actual 8-bit hexadecimal number.
RET

CONVERTALFROMASCIITOHEXADECIMAL:
CMP AL,39h
JLE ITS0TO9
SUB AL,37h ;Convert ASCII A to F to their numerical values.
RET
ITS0TO9:
SUB AL,30h ;Convert ASCII 0 to 9 to their numerical values.
RET

fourbyteinputbuffer DB 4 DUP (?)
twobyteinputbuffer DB 2 DUP (?)
rwprompt DB '(R)read/(W)rite? ', 0Dh, 0Ah
readprompt DB 'Read from which address? ', 0Dh, 0Ah
writeprompt DB 'Write to which address? ', 0Dh, 0Ah
writesecondprompt DB 'What value to write? ', 0Dh, 0Ah

;This would be the end of the program if we were running it in DOS,
;but to make this program bootable on the boot sector of a storage
;medium, we need to pad out the end and add the all-important bytes
;of 55h and AAh in memory locations 1FEh and 1FFh (510 and 511 in
;decimal) to make this a proper bootable file.
;After assembling this file, if all goes well, the resulting .COM
;file should be exactly 512 bytes in size.
;Then you can just write the assembled .COM file to the boot sector
;of a disk and boot from it.

endoffilepadding DB 18 DUP (?)
bootablefile DB 55h, 0AAh


Assembled into machine language, this is the actual byte content of the 512-byte boot sector:

B9 13 00 BE 8D 7D 0E 1F FC AC B4 0E B7 00 CD 10
49 83 F9 00 75 F0 B4 00 CD 16 3C 72 74 0E 3C 52
74 0A 3C 77 74 57 3C 57 74 53 EB EA B9 1B 00 BE
A0 7D 0E 1F FC AC B4 0E B7 00 CD 10 49 83 F9 00
75 F0 E8 79 00 E8 F6 00 8B D9 8A 07 50 C0 E8 04
04 30 3C 39 7E 02 04 07 B4 0E B7 00 CD 10 58 24
0F 04 30 3C 39 7E 02 04 07 B4 0E B7 00 CD 10 B4
0E B0 0D B7 00 CD 10 B0 0A CD 10 EB 83 B9 1A 00
BE BB 7D 0E 1F FC AC B4 0E B7 00 CD 10 49 83 F9
00 75 F0 E8 28 00 B9 17 00 BE D5 7D 0E 1F FC AC
B4 0E B7 00 CD 10 49 83 F9 00 75 F0 E8 4F 00 E8
8C 00 51 E8 B1 00 5B 88 C8 88 07 E9 42 FF BB 87
7D B9 04 00 B4 00 CD 16 3C 30 7C F8 3C 39 7E 12
3C 41 7C F0 3C 46 7E 0A 3C 61 7C E8 3C 66 7F E4
2C 20 53 B4 0E B7 00 CD 10 5B 88 07 FE C3 49 75
D3 B4 0E B0 0D B7 00 CD 10 B0 0A CD 10 C3 BB 8B
7D B9 02 00 B4 00 CD 16 3C 30 7C F8 3C 39 7E 12
3C 41 7C F0 3C 46 7E 0A 3C 61 7C E8 3C 66 7F E4
2C 20 53 B4 0E B7 00 CD 10 5B 88 07 FE C3 49 75
D3 B4 0E B0 0D B7 00 CD 10 B0 0A CD 10 C3 BB 87
7D 8A 07 E8 37 00 88 C5 C0 E5 04 43 8A 07 E8 2C
00 02 E8 43 8A 07 E8 24 00 88 C1 C0 E1 04 43 8A
07 E8 19 00 02 C8 C3 BB 8B 7D 8A 07 E8 0E 00 88
C1 C0 E1 04 43 8A 07 E8 03 00 02 C8 C3 3C 39 7E
03 2C 37 C3 2C 30 C3 00 00 00 00 00 00 28 52 29
72 65 61 64 2F 28 57 29 72 69 74 65 3F 20 0D 0A
52 65 61 64 20 66 72 6F 6D 20 77 68 69 63 68 20
61 64 64 72 65 73 73 3F 20 0D 0A 57 72 69 74 65
20 74 6F 20 77 68 69 63 68 20 61 64 64 72 65 73
73 3F 20 0D 0A 57 68 61 74 20 76 61 6C 75 65 20
74 6F 20 77 72 69 74 65 3F 20 0D 0A 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 AA