;; An 16-bit x86 Assembly Example for INT8/IRQ0 ;; Back story: ;; I was having trouble understanding how could one possibly ;; implement preemptive multitasking because it involes (somehow) ;; *stopping* any program that the CPU was working on and forces ;; it to work on another one. The answer is to use an interrupt - ;; normally timer interrupt, some utility programs for DOS uses ;; keyboard interrupt as well. I searched around for a bit and ;; found most of the examples are either (1) for 32-bit protected ;; mode or (2) uses C. I want something that's a bit easier for ;; students to work with because protected mode is a big hassle ;; to set up and C compilers that compiles to "pure" 16-bit x86 ;; code are basically non-existent (except for maybe Amsterdam ;; Compiler Kit, but even that is a bit annoying to set up as ;; well), so I did this. ;; This program: ;; 1. setup Programmable Interval Timer (PIT) ;; 2. setup INT8 which handles interrupts generated by PIT. ;; PIT have multiple (3) channels and channel 0 is connected ;; to PIC (which is a chip that arranges interrupts) as ;; IRQ0, which translates to INT8. ;; If you want sound you can use channel 2 because it's ;; connected to the PC Speaker - not the actual speaker you ;; are using; it's a beeper that's on (probably) all (at ;; least desktop) IBM compatibles. But this example isn't ;; about that. ;; We can setup PIT to generates interrupts at specified interval ;; which will force the CPU to *abandon* the current task and work ;; on something else; this can be used to do all sorts of stuff, ;; including preemptive multitasking stuff by hooking a scheduler at ;; INT8. In modern webdev terms it's like having a callback that ;; got called by another party asynchronously and your single ;; thread JavaScript runtime has to handle that (because single ;; thread). Or you can see the INT8 handler as (by itself) another ;; task as well. ;; NOTE that if you type this example in DEBUG (if for any ;; reasons you cannot setup a workflow convenient enough for ;; developing 16-bit programs in assembly), DEBUG uses hex numbers ;; by default so a INT 21h in MASM/NASM would be INT 21 in DEBUG. ORG 100h JMP _MAIN _MAIN: ;; Setting up the INT8 handler. ;; In IBM PC (real mode), there's an Interrupt Vector Table at ;; 0000h:0000h that contains the location for interrupt INT 0~255 ;; in 2 16-bit numbers of "OFFSET-SEGMENT". We can modify the values ;; in this table to control where the CPU will jump to when there's ;; an interrupt. By the design of IBM PC the PIT is connected as IRQ0 ;; through the 8259 PIC, which is eventually mapped to INT 8. ;; Remember that all of this is only for IBM PC; this is only an ;; IBM PC thing. But non-IBM PC x86 machines are kinda rare nowadays; ;; almost all x86 PC nowadays are IBM PC compatible. ;; Make ES:[BX] points to the memory location storing the actual ;; location for INT 8, which is 0000h:0020h. (How to calculate: 2 ;; 16-bit numbers which is 4 bytes per table entry, this multiplied ;; by 8 equals 32, which is 20h in hexadecimal.) MOV AX, 0 MOV ES, AX MOV AX, 0020h MOV BX, AX ;; Fill in the "OFFSET" part: LEA SI, _INT8 MOV ES:[BX], SI ;; Now here's one of the confusing part. We have notified the ;; assembler to compile the code into a .COM file by using ORG 100h ;; in the beginning, when being executed under DOS it'll be loaded ;; at SOMEWHERE:0100h and the machine state will be CS=DS=SS (if ;; you want to learn more about this search "Intel Memory Model") ;; All .COM file is basically compiled using the Tiny model, so we ;; know the interrupt handler _INT8 will be "in the same CS" as the ;; main procedure. "Isn't that pretty vulnerable? I mean you can ;; just modify the content of the memory and all of this will break" ;; you might say; yeah, Real Mode does not have memory protection ;; mechanism, that's just the way it is. ADD BX, 2 MOV AX, CS MOV ES:[BX], AX ;; Setting up the PIT. ;; You setup PIT by sending bytes to specified I/O ports. ;; 0x40: channel 0 data port (r/w) ;; 0x41: channel 1 data port (r/w) ;; 0x42: channel 2 data port (r/w) ;; 0x43: command port (write only) ;; PIT setup works as follows: you send the command that ;; configures the channel, you send the *initial value* of the ;; register for the channel, then PIT will constantly decrease ;; the value of the register, and when the value reaches zero ;; something happens. Depending on the configuration something ;; else might/can happen too. Data ports are for setting or ;; reading register counts. The registers are 16-bit. So it's ;; "SOME_CONSTANT_HERE / value_you_put_in_register" Hz instead ;; of just "value_you_put_in_register" Hz. ;; The constant is 1193182 (Hz). Some sources say it's 1193180, ;; But it really does not matter because the number is big. This ;; allows us to have a frequency of anywhere between a little ;; bit more than 18 Hz and 596591 Hz (the counter value of 1 ;; is prohibited for various reasons. The available frequencies are ;; discrete values of course, e.g. you can't have exactly 8000 Hz). ;; Because we're writing video memory we want to actually *see* ;; the thing working, we'll take the slowest option available, ;; with which we'll have to treat the register as 16-bit binary ;; counter. (PIT has a BCD mode which I can't think of its uses) ;; ;; (2-bit) Channel: Channel 0 ;; (2-bit) Data port access: Access low and high byte ;; (3-bit) Operating mode: Mode 3 square wave generator mode ;; (1-bit) Counter mode: 16-bit binary ;; ;; The interrupt will only be generated when channel 0 output ;; changes from low to high (rising edge). In a lot of sources ;; there's this "Interrupt On Terminal Count" mode and you might ;; think this is the mode we should use. No. In this mode the ;; output will remain high until the next time we send a value ;; to the data port, which certainly isn't desirable in our use ;; case. Normally you'll use mode 2 or mode 3 that will reload ;; the previously configured value repeatedly. ;; All of this will gives us the configuring command for 0x43: MOV AL, 00110110b ; or MOV AL, 36h OUT 43h, AL ;; And now the frequency: MOV AX, 0FFFFh OUT 40h, AL ;; FIX(2021.9.7): You can't just OUT AH (x86 forbids that). MOV AL, AH OUT 40h, AL ;; Wait for any keypress. MOV AH, 0 INT 16H ;; Exit the main process. ;; If all goes well it won't affect the interrupt handling. RET ;; The INT8 handler _INT8: PUSHA ;; We won't force the display mode here. ;; Setting up ES:DI to point to the 80x25 16-color text mode ;; video memory, which is at 0B800h:0000h. MOV AX, 0B800h MOV ES, AX MOV DI, 0 ;; Now we set up DS:SI to point to the data we want to retrieve. ;; This will Make sure DS:SI will point to the correct location ;; (it may contain different value than the current CS when the ;; CPU reaches here). Don't worry, all of the values is saved ;; and restored by PUSHA and POPA. PUSH CS POP DS LEA SI, MSG ;; Setting up the character to be displayed. ;; This piece of code will cycle through A to Z and wraps to A ;; again. MOV AL, DS:[SI] INC AL CMP AL, 'Z' JNA _SKIPPING MOV AL, 'A' _SKIPPING: MOV DS:[SI], AL ;; Write the character into video memory (which is the same as ;; displaying the character) ;; This code assumes that we're in 80x25 16-color text mode, and ;; `MOV BH, 0FH` means to set the written character's color as ;; "bright white foreground and black background". _DISP: MOV BL, DS:[SI] MOV BH, 0FH MOV ES:[DI], BX ;; Sending End Of Interrupt signal to 8259 PIC ;; This is to notify 8259 the interrupt handling is over and it's ;; allowed to send more interrupt signal now. MOV AL, 20H OUT 20H, AL ;; Now we restore the registers. POPA ;; Setting Interrupt Enable flag ;; This flag will be cleared when executing the interrupt handler ;; so that the interrupt itself won't be interrupted, but we have ;; to set it manually so that CPU will acknowledge other interrupts ;; after the handling, or else nothing will display on the screen ;; ever again (unless some code that directly writes the video ;; memory got executed) and keyboard won't work because it requires ;; interrupt to be enabled. STI ;; You need to use IRET inside interrupt handler - it does things ;; a little bit different than normal RET. Also a good indicator ;; that this part of the program is probably not called by normal ;; means. IRET ;; The character that got displayed MSG DB 'A' ;; This piece of code should work with most assemblers (with tiny ;; tweaks on the syntax and assembler directives). Tested under ;; DOSBOX. ;; 2021.8.30 ;; Back