3

I managed to load a small kernel into memory via a bootloader that performs a far jump to 0x0090:0x0000. The kernel is loaded successfully as I print a character from there to test it and it works properly.

I wanted to remap interrupts 0x08->0x0F and 0x70->0x77 to interrupts 0x20->0x2F, so the exception/reserved interrupts are not overlapped. So far, I am only handling a keyboard press and attempting to print it to the screen.
I went over it a bunch of times and for some reason, I just don't know why but nothing happens when I press a key.
The keymap is just an array of the scancodes to their respected ASCII value.

If this is of any help: I tested running a loop and printing a character then HLTing, and once 2 characters were printed, it hanged. I made sure to enable interrupts.
(I'm loading 4 sectors (from sector 2 to sector 5) for the bootloader which is why I'm padding this to make it 2048 bytes in size).

By the way, I don't have to CLI in my interrupt procedures since it's done for me, right? I remember reading this but I am not too sure.

BITS 16
ORG 0x0000

; Setup Segments ;
cli
cld
mov ax, cs
mov ds, ax              ; this program was far-jumped to (0x0090:0x0000) so ds = cs = 0x0090
mov ax, VIDEO_ORIGIN
mov es, ax

; Remap PIC Interrupt Vector Offsets to 0x20 -> 0x35 ;
remapInterrupts:
    ; Send Initialization Command (expecting ICW4)
    mov al, 0x11
    out 0x20, al
    out 0xA0, al

    ; Remap Vector Offsets (ICW2)
    mov al, 0x20        ; Master IRQ lines mapped to 0x20 -> 0x27
    out 0x21, al
    mov al, 0x28        ; Slave IRQ lines mapped to 0x28 -> 0x2F
    out 0xA1, al

    ; Set Cascade Lines between Master and Slave PICs (ICW3)
    mov al, 0x04        ; 00000100 (line 2)
    out 0x21, al
    mov al, 0x02        ; 00000010 (line 2 in binary, cascade identity)
    out 0xA1, al

    ; Set 80x86 Mode (ICW4)
    mov al, 0x01
    out 0x21, al
    out 0xA1, al

    ; Set Masks
    mov al, 0xFD        ; 11111101 (keyboard)
    out 0x21, al
    mov al, 0xFF        ; 11111111
    out 0xA1, al

setInterrupts:
    push ds
    mov ax, 0x0000
    mov ds, ax
    mov [ds:0x84], word interrupt21     ; 0x84 = 0x21 * 4
    mov [ds:0x86], cs
    pop ds
    jmp start

    interrupt20:                ; Programmable Interval Timer
        ; NOT SUPPORTED, place holder
        push ax
        mov al, 0x20
        out 0x20, al
        pop ax
        iret
    interrupt21:                ; Keyboard
        push ax
        push bx
        in al, 0x60
        test al, 0x80           ; high-bit set = keyup = don't print
        jnz .finish
        movzx bx, al
        mov al, [keymap + bx]
        mov ah, 0x07
        stosw

        .finish:
        mov al, 0x20
        out 0x20, al

        pop bx
        pop ax
        iret
    interrupt22:                ; Slave Cascade
    interrupt23:                ; COM2 / COM4
    interrupt24:                ; COM1 / COM3
    interrupt25:                ; LPT2
    interrupt26:                ; Floppy controller
    interrupt27:                ; LPT1
        ; NOT SUPPORTED, place holder
        push ax
        mov al, 0x20
        out 0x20, al
        pop ax
        iret
    interrupt28:                ; RTC
    interrupt29:                ; Unassigned
    interrupt2A:                ; Unassigned
    interrupt2B:                ; Unassigned
    interrupt2C:                ; Mouse Controller
    interrupt2D:                ; Math Coprocessor
    interrupt2E:                ; Hard Disk Controller 1
    interrupt2F:                ; Hard Disk Controller 2
        ; NOT SUPPORTED, place holder
        push ax
        mov al, 0x20
        out 0xA0, al
        out 0x20, al
        pop ax
        iret

start:
    sti
    xor di, di
    jmp $

; --- CONSTANTS --- ;
VIDEO_ORIGIN EQU 0xB800

; --- DATA --- ;
drive db 0
keymap:
    db 00h, 1Bh, '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=', 08h, 09h
    db 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '[', ']', 00h, 00h
    db 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ';', "'", '`', 00h, '\'
    db 'Z', 'X', 'C', 'V', 'B', 'N', 'M', ',', '.', '/', 00h, 00h, 00h, ' ', 00h,
    db 00h, 00h, 00h, 00h, 00h, 00h, 00h, 00h, 00h, 00h, 00h, 00h, 00h, 00h, 00h,
    db '-', 00h, 00h, 00h, '+', 00h, 00h, 00h, 00h, 00h

times 2048 - ($ - $$) db 0
Michael Petch
  • 46,082
  • 8
  • 107
  • 198
Matthew
  • 268
  • 1
  • 11
  • 1
    On some emulators (not sure which ones), if you do `jmp $` in a tight loop the virtualized video display may not get updated. Rather than `jmp $` do this with interrupts _enabled_ : `jmploop: hlt` `jmp jmploop` . The _hlt_ instruction will halt until the next interrupt occurs, at which time execution will continue after the _HLT_ which is the jmp to go back and do a _HLT_ again. This usually will allow virtual video memory to be updated. This is not an issue on real hardware, but can be a problem in some virtualized(emulated) environments – Michael Petch Dec 28 '15 at 17:35
  • I would highly recommend using Bochs for testing in your case. You seem to be dealing with 16-bit real mode code, and the Bochs built in debugger does understand 16-bit instructions and has an understanding of segment:offset pairs. You could set a breakpoint on the interrupt routine and then step through the code to see what happens. – Michael Petch Dec 28 '15 at 17:41
  • Oh wow I'll definitely use Bochs, actually being able to debug would help a lot. Also yeah, I'll fix that movzx problem and make sure to not rely on registers like di across calls. I'll come back when I figure out the solution. – Matthew Dec 28 '15 at 19:41

1 Answers1

2

Real mode interrupt routines have to be developed as if nothing but the CS register is known (and the interrupt flag is cleared). CS:IP is set via the interrupt vector when we get a hardware interrupt. CS will be the segment we wrote to the interrupt vector table. In your case it was 0x0090 since you did this (with DS=0x0000) to update the interrupt vector table:

mov [ds:0x86], cs

Since we can't rely on DS being what we want when our interrupt handler is called we can either push DS onto the stack, copy CS to DS and access our memory variables via DS, and then restore DS. Alternatively we can modify our memory operands so they explicitly use the CS register. We could modify the interrupt handler to look like this:

interrupt21:                ; Keyboard
    push ax
    push bx
    push di                 ; Save DI
    push es                 ; Save ES

    mov  ax, VIDEO_ORIGIN
    mov  es, ax             ; Set ES to video memory segment
    mov  di, [cs:videopos]  ; Get last videopos into DI
    in al, 0x60
    test al, 0x80           ; high-bit set = keyup = don't print
    jnz .finish
    xor bh, bh              ; set high byte of BX to zero
    mov bl, al              ; low byte of BX is scancode
    mov al, [cs:keymap + bx]; Reference keymap via CS segment (not DS)
    mov ah, 0x07
    cld                     ; Set the direction flag forward
    stosw
    mov [cs:videopos], di   ; Save current video position

.finish:
    mov al, 0x20
    out 0x20, al

    pop es                 ; Restore ES
    pop di                 ; Restore DI
    pop bx
    pop ax
    iret

I've documented the lines I added. But important things are:

  • We can't guarantee that ES will be what we want, so we'll need to set it explicitly to the video memory segment.
  • Registers have to be restored to the same state they were before the interrupt. We will be modifying the ES register and the DI register so we should save them on the stack (and restore them at the end).
  • We can't rely on DI actually being the value we expect, so we have to save its value between interrupt calls so that we can properly advance to the next cell on the screen for writing.
  • The memory operands have been rewritten to use the CS register rather than the DS register. Doing a segment override avoids having to copy CS to DS and save/restore DS in our interrupt handler.
  • We can't rely on the direction flag being what we want. Since you use STOSW in the interrupt handler we want to make sure it is cleared with CLD so that it advances DI forward.
  • Rather than use movzx we can simply clear the upper part of the BX register to zero. movzx is only available on 386 processors. You can keep that instruction if you are on 386+, but if you intend to target 8086/8088/80188/80286 then you can't use it.

Modify your start routine to be:

start:
    mov word [videopos], 0x0000 ; Initialize starting video position
    sti
.progloop:
    hlt
    jmp .progloop

Some emulators don't always do screen refreshes if you do a tight loop with jmp $. A better way to do it is with the HLT instruction. When interrupts are on the CPU will halt the processor until the next interrupt occurs. When one does occur it will be serviced by the interrupt handlers and eventually will fall to the next instruction. In this case we jump back and do the HLT again waiting for the next interrupt.

Since we added a new variable to keep track of the screen cell we are writing to, we will need to add videopos to you .data segment:

; --- DATA --- ;
drive db 0
videopos dw 0

These changes do seem to work on Bochs, QEMU, and VirtualBox. If this doesn't work for you then possibly you aren't loading the second stage (4 sectors worth) properly into 0x0090:0x0000? Since I can't see your first stage code I can't really say for certain.

Michael Petch
  • 46,082
  • 8
  • 107
  • 198
  • Alright everything is working. Although after pressing a lot of keys, Bochs gives me an `internal keyboard buffer full` warning so interrupt21 is not invoked. Shouldn't `in al, 0x60` clear the last character so I don't have this problem? – Matthew Dec 28 '15 at 23:19
  • @Matthew Bochs is emulating an 8042 ps/2 keyboard controller. The controller itself has an internal 16 byte buffer. Like a real keyboard controller, if you type too fast the keyboard controller will basically start to throw away characters before the PC even sees them. Generally if the internal buffer is full it takes the newest keystroke and places it on top of the last key in the internal buffer. Bochs is effectively telling you when you type too fast that the internal 8042 controller buffer is full and there are potential lost characters.Bochs seems to be operating normally in that regard. – Michael Petch Dec 28 '15 at 23:50
  • That makes sense, but for some reason nothing more will be placed on the screen (as in, the characters I type after that point are not being printed). – Matthew Dec 28 '15 at 23:55
  • @Matthew If you stop typing after you see that error, let the characters in the buffer get processed, and then start typing again does it still do that? – Michael Petch Dec 28 '15 at 23:57
  • I waited about 30 seconds and tried typing again, it was still ignored. – Matthew Dec 28 '15 at 23:59
  • 1
    @Matthew : I don't see that behavior on my Bochs. Are you using the code I gave as is, or with modifications? – Michael Petch Dec 29 '15 at 00:00
  • I just used your exact code right now and it works. Thank you but why did your changes fix this specific error? I can see it fixing errors if the di and other registers changed but I still don't understand how this specific problem was solved. – Matthew Dec 29 '15 at 00:08
  • @Matthew I can't tell since I don't know what code you were testing with that was failing with that behavior. If I could see the code you were using that produced the problem, I'd be able to explain why my code worked and yours didn't. – Michael Petch Dec 29 '15 at 00:11
  • @Matthew don't replace the code you have already in the question. Add it to the bottom of the existing question. Once I look at it we can remove it if need be. It sounds like your new question about the code you wrote that fails after a lot of characters should probably be asked as a new question. – Michael Petch Dec 29 '15 at 00:14
  • Now that I look at it, it's very similar to the original code. Nothing has changed in terms of the interrupt itself. – Matthew Dec 29 '15 at 00:16
  • @Matthew : Yes, that code pretty much looks like the original with extra stuff, but still has the bugs. Since you could be destroying register data, it is undefined behaviour and eventually it fails. I'm going to revert back to the original code you had in the question. – Michael Petch Dec 29 '15 at 00:20
  • Yeah, the most updated code I have right now works perfectly (it's the old code, but with the changes you provided) – Matthew Dec 29 '15 at 00:22
  • @Matthew If you look at my answer, I list many things that are wrong as bullet points. Like not saving and restoring all the registers; using the wrong segment for accessing your character map (or other variables) etc. Those problems can potentially lead you to reading the wrong data or using values in registers that don't apply and that would lead to bizarre things happening. – Michael Petch Dec 29 '15 at 00:22
  • Yeah, but I assumed I can only get this error if I don't poll port 60h. Your changes fix it, but I don't really know why it fixes this exact problem since the `in` and `out` instructions would be unaffected either way. – Matthew Dec 29 '15 at 00:25
  • 1
    @Matthew In Bochs if you continuously hold down a character for 5-10 seconds (even with my code) it will likely tell you the buffer is filled, then stopping briefly will allow it to catch up and you can continue to type without new warnings. Basically with all the errors in your interrupt handler they probably pooch the system to the point the interrupt handler isn't working anymore. That is the problem with doing things incorrectly - they can manifest in unusual ways. – Michael Petch Dec 29 '15 at 00:27
  • 1
    @Matthew : I had a chance to look at that code that didn't work for you. I believe I know why it fails the way it did. One thing you should notice is that you do this `mov ax, di` `mov bl, 2` `div bl` `mov bx, ax` as part of you cursor update. You should ntoice it stops printing characters at the same poiny everytime. Happens to be when you press the 256th character. DI=512 (decimal) in it. You copy it to _AX_. `div bl` takes _AX_ and divides it by _BL_, **BUT** the division by _BL_ will fail if the result of the division can't fit within an 8 bit value. – Michael Petch Dec 29 '15 at 01:15
  • 1
    @Matthew 512/2=256 . 256 (the quotient) can't be placed in the register _AL_ because it is bigger than 255 (an 8 bit register only holds between 0-255). What happens next is that a Division Exception (actually divide overflow) is thrown and then it seems you have no handler for it, so Bochs goes off into lala land. – Michael Petch Dec 29 '15 at 01:19
  • @Matthew The easiest way to solve the problem is to use the instruction `SHR` (instead of using _DIV_) to shift the bits in _AX_ to the right one place. That is the same as dividing by 2. It will also work on a full 16-bit register. Appears the 4 instruction mentioned could be reduced to `mov bx, di` `shr bx, 1` which basically copies _DI_ to _BX_ and then divides BX by 2 (by shifting it right by 1 bit) and the result is in _BX_ – Michael Petch Dec 29 '15 at 01:29
  • 1
    Yeah I actually changed it to that a couple of minutes ago. Also thank you for explaining why it goes crazy. – Matthew Dec 29 '15 at 01:34