Search code examples
assemblyx86-16interrupt-handlingosdevreal-mode

Real mode interrupt works if I use call, doesn't work(won't execute) if I use INT


I was trying to add a system call to my REAL MODE operating system, and it will work if I write this:

call [21h*4]

however it just doesn't work if I try to call it with

int 0x21

Here's the code I used to setup the system call:

 mov word [21h*4],inthandler
 mov word [21h*4+2],CODE_SEG ;which is 0(incorrect)

My interrupt handler is defined as:

inthandler:
    mov ax,0e64h
    int 0x10
    iret

The interrupt should print the letter d on the display when it works. When it fails it doesn't print anything.


Solution

  • Your original question, the comments, and your answer give hints as to what is likely the cause of your problems. You should get in the habit of producing a minimal complete verifiable example. Code snippets without greater context are often very hard to diagnose and often rely on the details you don't tell us.

    In your answer you mention this

    mov word [es:21h*4+2],CODE_SEG ;which is NOT 0, should be 50h
    

    I can infer the 50h means you loaded your kernel starting at 0x0050:0x0000 in memory just above the BIOS Data Area (BDA). From your answer I can also infer that DS is not zero as you had to override with ES which you say is equal to 0 in the code comment. Your DS register is probably set to 0x0050 (as well as CS).

    An minimal complete example would look like this:

    boot.asm:

    org 0x7c00
    
        xor ax, ax
        mov ds, ax                     ; DS=ES=0
        mov es, ax
        mov ss, ax                     ; SS:SP starts from top of first 64KiB in memory
        mov sp, ax                     ;     and grows down
    
        mov ax, 0x0201                 ; AH=2 BIOS disk read, AL=# sectors to read
        mov cx, 0x0002                 ; CH=cylinder 0, CL=sector number 2 
        mov dh, 0                      ; DH=head 0
        mov bx, 0x500                  ; ES:BX(0x0000:0x0500) = memory to read to
        int 0x13                       ; Read 1 sector after bootloader to 0x0000:0x0500
        ; Insert error checking code here. Left out retries etc for brevity 
    
        jmp 0x0050:0x0000              ; Start executing kernel at 0x0050:0x0000 
                                    ;     Sets CS=0x0050, IP=0x0000
    
    ; Disk signature
    TIMES 510-($-$$) db 0x00
    dw 0xaa55
    

    kernel.asm:

    CODE_SEG EQU 0x0050
    
    org 0x0000                     ; Kernel will be run from 0x0050:0x0000
    
    kernel:
        ; CS=0x0050 at this point because of FAR JMP that got us here
        mov ax, CODE_SEG
        mov ds, ax                     ; DS=ES=0x0050
        mov es, ax
        mov ss, ax                     ; SS:SP=0x0050:0x0000 wraps to top of 64KiB on 1st push
        xor sp, sp                     ;     and grows down
    
        mov ax, 0x0e << 8 | 'K'        ; AH=0x0e BIOS TTY print char service,
                                       ;     AL=char to print `K`
        mov bh, 0                      ; Ensure we are using text page 0
        int 0x10                       ; Print 'K' on the display
    
        mov word [21h*4], inthandler   ; Set CS:IP of int 21 handler to CODE_SEG:inthandler
        mov word [21h*4+2],CODE_SEG
     ;   call [21h*4]                  ; This works by printing 'd' to the display
        int 21h                        ; This fails. Doesn't print anything to display
        
    .hltloop                           ; Infinite loop to stop kernel
        hlt
        jmp .hltloop
    
    ; Int 21h interrupt handler
    inthandler:
        mov ax, 0x0e << 8 | 'd'        ; AH=0x0e BIOS TTY print char service, AL=char to print `K`
        int 0x10                       ; Print 'K' to display
        iret                           ; Return from interrupt
    

    Build a disk image with the bootloader and kernel with:

    #!/bin/sh
    
    nasm -f bin boot.asm -o boot.bin
    nasm -f bin kernel.asm -o kernel.bin
    
    # Make 1.44MiB floppy disk image with bootloader followed by kernel    
    dd if=/dev/zero of=floppy.img bs=1024 count=1440
    dd if=boot.bin of=floppy.img conv=notrunc
    dd if=kernel.bin of=floppy.img conv=notrunc seek=1
    

    This can be tested with QEMU using the command:

    qemu-system-i386 -fda floppy.img
    

    If you run the version with call [21h*4] it will show something like:

    enter image description here

    The kernel prints K so I know the kernel is running. My interrupt handler prints d. If I attempt to use my interrupt handler (system call) with int 21h I get this:

    enter image description here

    I believe this is similar to the experience you are seeing based on the available information. The question is why this is happening?


    Solution to the Problem

    There are a couple of issues but the really involve how you write your interrupt handler to the real mode Interrupt Vector Table (IVT) that starts at 0x0000:0x0000 and ends at 0x0000:0x400. You have this code:

        mov word [21h*4], inthandler   ; Set CS:IP of int 21 handler to CODE_SEG:inthandler
        mov word [21h*4+2],CODE_SEG
    

    The code is equivalent to:

        mov word [ds:21h*4], inthandler   ; Set CS:IP of int 21 handler to CODE_SEG:inthandler
        mov word [ds:21h*4+2],CODE_SEG
    

    Every memory access in real mode has a default segment register associated with it. If the memory address contains a reference to register BP the segment is assumed to be SS (Stack Segment) otherwise it is DS (Data Segment). In this code CODE_SEG is 0x0050.

    The idea is to write CS:IP (CODE_SEG:inthandler) of your interrupt handler into the IVT for interrupt 21h. The offset of Interrupt 21h is at 0x0000:(0x0021 * 4) and the segment is at 0x0000:(0x0021 * 4+2).

    Since DS is 0x0050 your code actually wrote your interrupt vector address to to 0x0050:(0x0021 * 4) and 0x0050:(0x0021 * 4+2). This is actually in the middle of your kernel or kernel data somewhere! So when you do int 21h you called the default int 21h routine which is likely just an IRET that does nothing and returns.

    You need to write the interrupt vector to segment 0x0000.. This can be done in a variety of ways. One way would be to set the ES (Extra Segment) to 0x0000 and override the memory operand to use ES instead of the default DS. The revised code would look like:

    ;        push es                        ; Save previous value of ES
            xor ax, ax
            mov es, ax                     ; ES=0
            cli                            ; Make sure no interrupt occurs while we update IVT
            mov word [es:21h*4], inthandler; Set CS:IP of int 21 handler to CODE_SEG:inthandler
            mov word [es:21h*4+2],CODE_SEG
            sti                            ; Re-enable interrupts
    ;        pop es                         ; Restore original value of ES
    

    If you use ES as a scratch segment register and don't care about the contents you can remove the push es and pop es. I have also put a CLI and STI instruction around the update of the IVT. This is a safety precaution in the event that some interrupt occurs that happens to use Interrupt vector 21h before we have completely updated it. This situation is almost non existent in a bootloader but could present itself as an issue had you written the code for DOS.

    Alternatively you could have fixed the problem by changing DS to 0x0000 and avoided the segment override:

    push ds                        ; Save previous value of DS
    xor ax, ax
    mov ds, ax                     ; DS=0
    cli                            ; Make sure no interrupt occurs while we update IVT
    mov word [21h*4], inthandler   ; Set CS:IP of int 21 handler to CODE_SEG:inthandler
    mov word [21h*4+2],CODE_SEG
    sti                            ; Re-enable interrupts
    pop ds                         ; Restore original value of DS
    

    Since you likely want to keep DS set to its original value (0x0050) saving and restoring its value would be required.


    Special Notes

    You can't reliably do this to call Interrupt 21h:

    call [21h*4]
    

    In your code this does a NEAR call in the current segment (CS=0x0050) by getting the offset to jump to from memory offset [ds:21h*4]. The fact it called your interrupt handler was a lucky happenstance. Although it did print d to the display your interrupt handler would likely have never returned. If you printed something else after int 21h it likely would never have appeared because the IRET returned to the wrong place in memory.

    In order to properly simulate an interrupt call with CALL you would have to do something like:

    xor ax, ax
    mov es, ax                     ; ES=0
    pushf                          ; An interrupt pushes current FLAGS on the stack so we need
                                   ;     to do something similar
    call far [es:21h*4]            ; We need to do a FAR CALL (not a NEAR call)
    

    We need to do a FAR CALL instead of the default NEAR CALL so we need to use the FAR attribute on the memory operand. When an IRET returns it pops the old value of IP and CS off the stack and then pops the old FLAGS register contents off the stack. Failure to put a FLAGS value on the stack would not leave the stack in the same state after the call as it was before because the interrupt returns with IRET and not RET.