DEV Community

Raymond Mwaura
Raymond Mwaura

Posted on

Enabling the A20 Line in BeaconOS

Introduction

This past week, I have been working on enabling the A20 line, as part of the early boot sequence for BeaconOS.

The A20 line, or the 21st address line refers to a specific signal on the system bus that controls the CPU's ability to access memory beyond the first megabyte. Enabling the A20 line is essential because it allows the CPU to access memory beyond the 1MB limit.


Historical Context

The Intel 8088 microprocessor had 20 address lines (A0-A19). This allowed it to access a maximum of 2²⁰ bytes, or 1 MB of RAM. The processor used a segmented memory model to form a 20-bit physical address from two 16-bit values (a segment and an offset), calculated as (segment * 16) + offset.

Due to the 20-bit address bus, any attempt to access an address beyond 1 MB resulted in the 21st bit being silently truncated. For example, the address FFFF:0010 would be calculated as 0xFFFF0 + 0x0010 = 0x100000. However, since the 8088 only had 20 address lines, the 1 at bit 20 was lost, causing the address to wrap around to 0x00000. Some programmers, seeking performance optimizations, began to rely on this wrap-around behavior.

When IBM introduced the PC AT (1984) based on the Intel 80286, a significant problem emerged. The 286 had 24 address lines, enabling it to access up to 16 MB of memory. In its real mode (intended for 8086 compatibility), the 286 did not force the A20 line to zero. Consequently, addresses above 1 MB no longer wrapped around to zero. Programs that depended on the old wrap-around behavior would now access unexpected memory regions above 1 MB, causing them to malfunction or crash.

To maintain compatibility with 8086 software, IBM introduced a mechanism to forcibly control the A20 address line. This was the "A20 gate" or "Gate A20". When this gate was disabled (the default state at boot), the A20 line was forced to zero, mimicking the 8088's wrap-around behavior. When enabled, the A20 line could carry its true signal, allowing the CPU to access the full range of physical addresses. The gate was originally implemented by routing the A20 line through an AND gate controlled by a spare pin on the Intel 8042 keyboard controller.


Methods for Controlling the A20 Gate

The primary method involved the Keyboard Controller. In this, commands were sent to I/O ports 0x64 and 0x60 to set the A20 bit. This method was notoriously slow and required careful status polling.

A faster alternative, the System Control Port A (or Fast A20 Gate), utilized I/O port 0x92. Setting bit 1 of this port would enable the A20 line. This method was significantly faster and simpler than the keyboard controller approach.

A third approach was through a BIOS Interrupt. By invoking the BIOS interrupt INT 0x15, with AX=0x2401, software could request the BIOS to enable the A20 line. This method was simple and abstracted the hardware details, but its major drawback was inconsistent support across different BIOS versions.

Operating systems would attempt several methods to ensure successful activation.


Implementation

To enable the A20 line, I implemented two common methods:

  • BIOS Interrupt (INT 15h, Function 24h)
  • Fast A20 Gate (port 0x92)

The program begins by checking whether the A20 line is already active. If it is, execution ends immediately with a success message. If not, the system first attempts to enable it using the BIOS interrupt. Should that fail (likely due to BIOS incompatibility), the program falls back to the fast A20 gate method.

If all attempts fail, the program halts and reports the failure. The logic ensures graceful fallback and clear reporting at each stage.

; Test A20.
call test_a20
cmp al, 1
je  a20_enabled ; If enabled.
jmp bios_enable ; If disabled.

bios_enable:
    lea si, [bios_attempt_msg]
    call print_string

    mov ah, 0x24
    mov al, 0x01
    int 0x15
    jc fast_a20_enable

    call test_a20
    cmp al, 1
    je a20_enabled
    jmp fast_a20_enable

fast_a20_enable:
    lea si, [fast_a20_attempt_msg]
    call print_string

    in al, 0x92
    or al, 0x02
    out 0x92, al

    call test_a20
    cmp al, 1
    je a20_enabled
    jmp a20_disabled

a20_enabled:
    lea si, [a20_enabled_msg]
    call print_string
    jmp done

a20_disabled:
    lea si, [a20_disabled_msg]
    call print_string
    jmp done

done:
    cli
    hlt
Enter fullscreen mode Exit fullscreen mode

Explanation

  1. Initial check:
    The program begins by calling test_a20 to verify whether the A20 line is already enabled. If it is, execution jumps directly to a20_enabled.

  2. BIOS Method:
    If the A20 line is disabled, the BIOS interrupt INT 15h (Function 24h, Subfunction 01h) is invoked to enable it. Some BIOS implementations provide this service, but many modern systems do not.

  3. Fallback to Fast A20 Gate:
    If the BIOS call fails (as indicated by the Carry Flag) or if the test still shows A20 is disabled, the program attempts the fast A20 gate method. This method writes to I/O port 0x92, setting bit 1 to enable the A20 line directly via the system control port.

  4. Final verification:
    After each enabling attempt, test_a20 is called again to confirm success. The program prints a message indicating the result and halts cleanly.

This dual-method approach increases compatibility across different hardware and firmware environments, ensuring the A20 line is enabled regardless of BIOS support.


Verification

To verify whether the A20 line is enabled, I used a helper function, test_a20. It performs the check using two methods: a BIOS test and a memory wraparound test.

; ---------------------------------------------------------------
; Test if A20 is enabled or disabled using either
; BIOS subfunction 02h, or
; memory wraparound method.
; Return value: Places the current A20 state in the AL register.
;   1: Enabled
;   2: Disabled
; ---------------------------------------------------------------
test_a20:
    call bios_test_a20
    cmp al, 2
    jb .test_return

    call memory_test_a20
    jmp .test_return

    .test_return:
        ret
Enter fullscreen mode Exit fullscreen mode

The BIOS test (INT 15h, Function 24h, Subfunction 02h) checks the A20 state directly if the BIOS supports it.

; ---------------------------------------------------------------
; Test if A20 is enabled using BIOS subfunction 02h.
; Return value: Places the current A20 state in the AL register.
;   0: Disabled
;   1: Enabled
;   2: Error
; ---------------------------------------------------------------
bios_test_a20:
    mov ah, 0x24
    mov al, 0x02
    int 0x15
    jc .bios_test_fail

    cmp al, 1
    je .bios_a20_enabled
    jmp .bios_a20_disabled

    .bios_a20_enabled:
        mov al, 1
        jmp .bios_test_return

    .bios_a20_disabled:
        mov al, 0
        jmp .bios_test_return

    .bios_test_fail:
        mov al, 2
        jmp .bios_test_return

    .bios_test_return:
        ret
Enter fullscreen mode Exit fullscreen mode

If the BIOS does not support this interrupt, the function falls back to the memory wraparound test, which is more hardware-oriented and universally reliable.

This method compares memory contents at two addresses: 0x000000 and 0x100000.
If the A20 line is disabled, both addresses point to the same physical location (due to 20-bit address wraparound), and the values read back are identical.
If the A20 line is enabled, they reference different memory cells, producing distinct values.

; ------------------------------------------------------------------------
; Test whether A20 is enabled or disabled using memory wraparound method.
; Return value: Places the current A20 state in the AL register.
;   1: Enabled
;   2: Disabled
; ------------------------------------------------------------------------
memory_test_a20:
    push ds
    push es
    push si
    push di

    mov ax, 0x0000
    mov ds, ax
    mov si, ax

    mov ax, 0xFFFF
    mov es, ax
    mov di, 0x0010

    ; Preserve original values.
    mov al, [ds:si]
    push ax
    mov al, [es:di]
    push ax

    ; Write.
    mov byte [es:di], 0x00
    mov byte [ds:si], 0xFF

    ; Read.
    mov al, [es:di]
    cmp al, 0xFF
    je .memory_a20_disabled
    jmp .memory_a20_enabled

    .memory_a20_enabled:
        mov al, 1
        jmp .memory_test_ret

    .memory_a20_disabled:
        mov al, 0
        jmp .memory_test_ret

    .memory_test_ret:
        ; Restore original values.
        pop bx
        mov [es:di], bl
        pop bx
        mov [ds:si], bl

        pop di
        pop si
        pop es
        pop ds

        ret
Enter fullscreen mode Exit fullscreen mode

Explanation

  1. BIOS Test:
    Uses INT 15h, AX=2402h to query the BIOS for the A20 state. If supported, this method is simple and direct. If unsupported, the Carry Flag is set, and the test returns an error code (AL=2).

  2. Memory Wraparound Test:
    Performs a direct hardware-level test by writing and comparing memory values.

  • If values at 0x000000 and 0x100000 are identical, the A20 line is disabled (address wraparound occurred).
  • If they differ, the A20 line is enabled. After testing, the function restores the original memory contents to prevent corruption.
  1. Return Values: The final A20 state is stored in the AL register:
  • 1; Enabled
  • 0; Disabled
  • 2; Error or unsupported BIOS call

This layered verification ensures reliability across systems and helps confirm that the A20 line is properly active.


Conclusion and Next Step

Enabling the A20 line marks a significant milestone in the early development of BeaconOS. It sets the stage for the transition to protected mode; a part of the journey I've been anticipating since the start. I'm excited to finally take that step and see BeaconOS come to life in a more capable environment.

The complete source code and accompanying notes are available in this repository.


Top comments (0)