In some previous chapters we have enabled A20 line which is the most basic one in order to access the complete memory of the system.
In this chapter we will build a sample kernel in assembly file that will only print the welcome string on the screen. It would be of size 64 KB
. We will load it from the stage 2. It's code will be of 16 Bits
as we are still in real mode. Later on we will switch to protected mode then change the kernel code to 32 bit and jump to it. As of now it's okay to be in real mode land.
Even if we enable the A20 line, we can't access the memory beyond the 1 MB + 64 KB
because of the limit of segment : offset. All segments have a limit of 0xFFFF
. The maximum addressable memory using segment : offset is 0xFFFF:0xFFFF
, which translates:
(Segment Shifted 4 Bits left) + (Offset to add)
0xFFFF0 + 0xFFFF = 0x10FFEF
which is 1,114,095 bytes
(or 1 MB + 64 KB - 16 bytes), just short of the 1 MB + 64 KB
mark.
Maximum Addressable Memory Calculation:
Since both the segment and offset are 16-bit
values, they range from 0x0000
to 0xFFFF
.
Segment Range: 0x0000
to 0xFFFF
Offset Range: 0x0000
to 0xFFFF
Let's look at the maximum values for both segment and offset:
- Maximum Segment =
0xFFFF
- Maximum Offset =
0xFFFF
When the segment is set to its maximum value (0xFFFF
) and the offset is set to its maximum value (0xFFFF
), the physical address is calculated as:
Physical Address = (0xFFFF * 16) + 0xFFFF
Physical Address = 0xFFFF0 + 0xFFFF = 0x10FFEF
Thus, the maximum addressable memory in real mode is : 0x10FFEF
- The lowest possible address is
0x00000
(Segment:0x0000
, Offset:0x0000
). - The highest possible address is
0x10FFEF
. - So, the total amount of addressable memory is:
0x10FFEF + 1 = 1,114,096 bytes
~ 1.06 MB.
The maximum address you can access is (0xFFFF0 + 0xFFFF) = 0x10FFEF, which is about 64 KB higher than 1 MB. (This area above 1 MB used to be called "HMA" or "high memory area" back in the DOS days: https://en.m.wikipedia.org/wiki/High_memory_area

Real Mode Memory Access (Below 1MB)
In real mode, you are limited to accessing the first 1MB of memory, known as conventional memory:
- Addressable range:
0x00000
to0xFFFFF
(1MB) - This includes:
- Conventional Memory (640 KB): Usable by programs.
- Upper Memory Area (UMA): From
0xA0000
to0xFFFFF
, often reserved for hardware like video memory (e.g., VGA), BIOS, and other system components.
This 1MB limitation exists because real mode uses 20-bit addresses (derived from 16-bit segment pair). However, higher memory, or extended memory, lies beyond this boundary.
How to load large kernel in real mode especially one that exceeds the 64 KB segment limit:
Loading a kernel larger than 1 MB in real mode requires the use of unreal mode or switching to protected mode because real mode can only address up to 1 MB of memory (0x00000 to 0xFFFFF
). Unreal mode allows access to all of the system's physical memory while operating in real mode by leveraging the 32-bit addressing capabilities.
I have explained the real mode in this chapter: https://thejat.in/learn/unreal-mode-in-x86
Let me give basic understanding here:
Unreal mode extends real mode to allow access to more than 1 MB of memory. This is achieved by loading segment registers with 32-bit addresses while still in real mode. Here’s a step-by-step guide on how to load a kernel larger than 1 MB
using unreal mode.
Steps to Load a Kernel Larger than 1 MB in Unreal Mode
- Enable A20 Line: The A20 line must be enabled to access memory above 1 MB.
- Switch to Unreal Mode: Load the segment registers with 32-bit addresses.
- Load Kernel: Use INT 13h AH=42h to read the kernel from the disk.
- Jump to Kernel: Transfer control to the loaded kernel.
Loading Kernel of Size 1 KB after 1 MB Mark in Real Mode:
As we discussed above that After enabling the A20
gate, we can access the full memory of the system, However that's not the case in the real mode. It has 16 Bit registers and can only access the memory up to 0xFFFF:0xFFFF
which is translated to 0x10FFEF
and it is 64 KB - 16 bytes
mark above the 1 MB mark.
In this section we will try to load the kernel of size 1 KB (means two sector of 512 bytes each). We will write this sector into disk image after the stage 2 boot. We will use the ATA/ATAPIO
driver to do so because the other two ways are not working which are:
- INT 0x13, AH = 0x02
- INT 0x13, AH = 0x42, (Extended Boot)
These both options were not working, maybe they not able to cross the 1 MB mark. Some protection by the BIOS could be possible. But I will provide of their code as well maybe after someday I may have any idea to achieve this with these.
kernel/kernel.asm
:
Below will be our kernel code which will be of size 1024 bytes (1 KB). It will be placed at 0x100000
memory address by the stage 2. I tried printing the string without declaring it, by printing every character hard coding. Because, when I declared a string and tried printing it, it was not printing at all. Normally the ORG
directive is required if you are declaring any string or data. However it was not working in our case. Because our output file is flat binary so there is no sections of text or data like in elf files, that's why the data declaration statements need org directive. So, the below code will work with or without org
directive.
; can work with or without org directive since no data declaration.
ORG 0xFFF0:0x0100 ; 0x100000 ; This origin directive is important if you
; you are declaring any string or data
; And i tried printing the string by
; declaring it and it didnt work,
; maybe because of i can't use
; org 0xFFF0:0x0100, or 0x100000
; but when i tried doing the same by
; loading the kernel at 0x0b00
; it worked then.
BITS 16
kernel_entry:
mov al, 'k'
mov ah, 0x0e
int 0x10
mov al, 'e'
mov ah, 0x0e
int 0x10
mov al, 'r'
mov ah, 0x0e
int 0x10
mov al, 'n'
mov ah, 0x0e
int 0x10
mov al, 'e'
mov ah, 0x0e
int 0x10
mov al, 'l'
mov ah, 0x0e
int 0x10
jmp $
times 1024 - ($ - $$) db 0

Below is the code of it which is not working:
ORG 0xFFF0:0x0100 ;|| 0x100000
; Used both the values for the origin
BITS 16
kernel_entry:
mov ah, 0x0e
mov al, 'K'
int 0x10
mov si, sKernelWelcomeSentence
print_welcome_string:
lodsb
test al, al
jz .print_done
mov ah, 0x0e
int 0x10
jmp print_welcome_string
.print_done:
jmp $
sKernelWelcomeSentence: db 'Welcome to the Kernel', 0
times 1024 - ($ - $$) db 0
