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.
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:
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:
I believe this is similar to the experience you are seeing based on the available information. The question is why this is happening?
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.
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
.