February 18th, 2020

A bootable PC program that allows insertion and execution of code

As a follow-up to the program I posted yesterday, which allows users to read and write to memory, here is a program that incorporates the two features I felt were most missing from that other program, namely the ability to set the data segment (DS) so that you can decide where memory gets read and written, and the ability to jump to a specific point in memory and start executing code there. With these features in place, this program now gives you the ability to do theoretically anything with your computer, since you can use it to manually enter bytes into memory space and then run those bytes as a program. Admittedly, this is a pretty clunky way to do it and there are better ways, but I think this isn't too bad for a program that's less than 512 bytes and thus fits on the boot sector of a floppy disk.

The program doesn't provide the option of changing the code segment (CS), which would be an interesting feature as it would allow for true self-modifying code as well as other interesting effects stemming from the program suddenly being run in a completely different memory segment, but while this might be a cool feature, I feel like it wouldn't be very useful for the things that people might actually end up using this program for. The other thing which I feel is missing from the program is the ability to read and write to I/O ports. This functionality would be fairly easy to add, because you can read and write to an I/O port with a single instruction: IN AL, 1234h will read from I/O address 1234h (on the PC, I/O addresses are 16 bits) and store the value read from the port in AL, and OUT 1234h, AL outputs the contents of AL to the same I/O address. However, again, I feel like this functionality is not that likely to be used by people who might actually use this program for anything, and adding additional "I" and "O" options would make the program's ugly main prompt "R/W/S/G? " even uglier. (Yes, the prompts have been cut down to a minimum to save on program space; text strings are often one of the biggest sources of code size in these small assembly- and machine-language programs). If I/O functionality is really desired, then perhaps the addition of it to the program should be left as an exercise to the reader.

If it's not apparent, the four options in the program's main prompt allow you to (respectively) read from a memory byte, write to a memory byte, set the data segment (DS), and "go" to a memory location by starting to execute the instructions stored there. There are still a few bytes left to spare in the program's 512-byte space, so you could theoretically add a minor feature or two if you wanted.

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/W/S/G? " to the screen
MOV CX,11 ;The string contains 11 characters, including the carriage return/line feed at the end
MOV SI,OFFSET rwsgprompt
PUSH DS ;We change DS to use LODSB, so let's save it so we can restore it later.
RWSGSTRINGLOOP:
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 RWSGSTRINGLOOP ;...then repeat the loop until the whole string has been printed
POP DS ;Restore DS so we don't undo a change made to it.

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
CMP AL,73h ; is it s?
JE CHOSESETSEGMENT
CMP AL,53h ; is it S?
JE CHOSESETSEGMENT
CMP AL,67h ; is it g?
JE CHOSEGO
CMP AL,47h ; is it G?
JE CHOSEGO

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

CHOSEREAD:

CALL SHOWADDRSTRING

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.

CALL OUTPUTALASHEXADECIMAL

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:

CALL SHOWADDRSTRING

CALL GET4HEX

;Write "Value? " to the screen
MOV CX,9 ;String contains 9 characters, including the CR/LF at the end
MOV SI,OFFSET valueprompt
PUSH DS ;We change DS to use LODSB, so let's save it so we can restore it later.
VALSTRINGLOOP:
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 VALSTRINGLOOP
POP DS ;Restore DS so we don't undo a change made to it.

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

CHOSESETSEGMENT:

;Write "Segment? " to the screen
MOV CX,11 ;String contains 11 characters, including the CR/LF at the end
MOV SI,OFFSET segmentprompt
PUSH DS ;We change DS to use LODSB, so let's save it so we can restore it later.
SEGSTRINGLOOP:
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 SEGSTRINGLOOP
POP DS ;Restore DS so we don't undo a change made to it.

CALL GET4HEX

CALL CONVERTFOURBYTESTOHEXADECIMAL
;The desired segment is now in CX.
MOV DS,CX

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

CHOSEGO:

CALL SHOWADDRSTRING

CALL GET4HEX

CALL CONVERTFOURBYTESTOHEXADECIMAL
;The desired jump target is now in CX.
JMP CX

;Subroutines GET4HEX, GET2HEX, CONVERTFOURBYTESTOHEXADECIMAL, CONVERTTWOBYTESTOHEXADECIMAL,
;CONVERTALFROMASCIITOHEXADECIMAL, OUTPUTALASHEXADECIMAL, and SHOWADDRSTRING 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.
;OUTPUTALASHEXADECIMAL: Output the value in AL as a 2-digit hexadecimal number.
;SHOWADDRSTRING: Write the string "Addr? " to the screen.
;There are three places where this is done in the program (once when reading from memory, another
;when writing to memory, and again when jumping to a new program location with "Go"), so it makes
;sense to turn this into a subroutine to cut down on program size.

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

OUTPUTALASHEXADECIMAL:
;Output the value in AL as a 2-digit hexadecimal number.
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
RET

SHOWADDRSTRING:
;Write "Address? " to the screen
MOV CX,11 ;String contains 11 characters, including the CR/LF at the end
MOV SI,OFFSET addressprompt
PUSH DS ;We change DS to use LODSB, so let's save it so we can restore it later.
ADDRSTRINGLOOP:
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 ADDRSTRINGLOOP
POP DS ;Restore DS so we don't undo a change made to it.
RET

fourbyteinputbuffer DB 4 DUP (?)
twobyteinputbuffer DB 2 DUP (?)
rwsgprompt DB 'R/W/S/G? ', 0Dh, 0Ah
addressprompt DB 'Address? ', 0Dh, 0Ah
valueprompt DB 'Value? ', 0Dh, 0Ah
segmentprompt DB 'Segment? ', 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 15 DUP (?)
bootablefile DB 55h, 0AAh


And here are the fully assembled contents of the 512-byte boot sector that you can boot a PC with. Go boot it and have some fun writing to your memory and then running stuff from it:

B9 0B 00 BE C5 7D 1E 0E 1F FC AC B4 0E B7 00 CD
10 49 83 F9 00 75 F0 1F B4 00 CD 16 3C 72 74 1E
3C 52 74 1A 3C 77 74 28 3C 57 74 24 3C 73 74 4F
3C 53 74 4B 3C 67 74 6A 3C 47 74 66 EB DA E8 65
01 E8 69 00 E8 E6 00 8B D9 8A 07 E8 28 01 EB B0
E8 53 01 E8 57 00 B9 09 00 BE DB 7D 1E 0E 1F FC
AC B4 0E B7 00 CD 10 49 83 F9 00 75 F0 1F E8 7C
00 E8 B9 00 51 E8 DE 00 5B 88 C8 88 07 EB 81 B9
0B 00 BE E4 7D 1E 0E 1F FC AC B4 0E B7 00 CD 10
49 83 F9 00 75 F0 1F E8 13 00 E8 90 00 8E D9 E9
5E FF E8 01 01 E8 05 00 E8 82 00 FF E1 BB BF 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 C3 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 BF 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 C3 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 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 C3 B9 0B 00 BE D0 7D 1E 0E 1F FC
AC B4 0E B7 00 CD 10 49 83 F9 00 75 F0 1F C3 00
00 00 00 00 00 52 2F 57 2F 53 2F 47 3F 20 0D 0A
41 64 64 72 65 73 73 3F 20 0D 0A 56 61 6C 75 65
3F 20 0D 0A 53 65 67 6D 65 6E 74 3F 20 0D 0A 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 AA

Cannons - Fire For You

I was on fire for you
Where did you go?
I could die for you
How could you not know?

I was alive with you
But you brought in the cold
Was I being lied to?
Wish I never met you
Started to regret you

My heart just drops
Thinking about you
My heart just stops
When I'm without you

I was on fire (fire, fire, fire...)

I was fire for you
You're breaking me down
Don't know what I should do
when you come around

I'm leaving without you, love
I have no choice
I know I'm being lied to
I just need some time to
stop thinking about you

My heart just drops
Thinking about you
My heart just stops
When I'm without you

I was on fire (fire, fire, fire...)