A20 게이트
16비트 컴퓨터는 세그먼트 기법을 이용하여 메모리 주소를 나타내었다.
0x0000:0000 ~ 0xFFFF:FFFF 까지 지정이 가능하였다.
0xFFFF:FFFF는 0x10FFEF(0xFFFF0 + 0xFFFF) 이며, 2진수로 나타내면 1 0000 1111 1111 1110 1111이다.
8086의 어드레스 라인은 20개이고 20개까지 표현하면 0 0000 1111 1111 1110 1111이 된다.
최상위 1비트를 표현하지 못하게 되어 0이 되고, 0x100000번지(1MB)를 초과해버리면 0x0번지가 된다.
80286이나 펜티엄이런 CPU를 사용하게 되면서 어드레스 라인 개수도 늘어났지만 옛날 소프트웨어와의 호환을 위해 20번 어드레스를 키보드 컨트롤러 칩과 AND 연산자로 묶게 되었다. 그리고 이는 A20 게이트라고 칭하고 A20 게이트가 활성화 되어야 최상위 비트 1을 사용할 수 있게 된다.
여태까지한 예제에서는 A20 게이트의 활성화를 해주지 않았다. A20게이트가 활성화 되지않으면 21번째 라인만 0으로 고정된다.
(A0~A19, A21~A31은 활성화가 되어있는 상태이다.) 활성화 되어있지 않으면 홀수 MB 영역 (1MB, 3MB...)는 사용하지 못한다.
이제 A20 게이트를 활성화하는 법을 알아보려한다.
bootA20.asm
[org 0]
jmp 07C0h:start
%include "a20.inc"
start:
mov ax, cs
mov ds, ax
mov es, ax
mov ax, 0
mov ss, ax
mov esp, boot_stack
mov ax, 0xB800
mov es, ax
mov di, 0
mov ax, word [msgBack]
mov cx, 0x7FF
paint:
mov word [es:di], ax
add di, 2
dec cx
jnz paint
call a20_try_loop
push ds
mov ax, 0
mov ds, ax
mov si, 1
mov word [ds:si], 0
mov ax, 0xFFFF
mov ds, ax
mov si, 0x11
mov word [ds:si], 0x1234
mov ax, 0
mov ds, ax
mov si, 1
mov bx, word [ds:si]
pop ds
cmp bx, 0x1234
je noA20
yesA20:
mov ax, 0xB800
mov es, ax
mov di, 0
lea si, [msgA20on]
yes_loop:
mov al, byte [si]
cmp al, 0
je stop
mov byte [es:di], al
inc si
inc di
mov byte [es:di], 0x06
inc di
jmp yes_loop
noA20:
mov ax, 0xB800
mov es, ax
mov di, 0
lea si, [msgA20off]
no_loop:
mov al, byte [si]
cmp al, 0
je stop
mov byte [es:di], al
inc si
inc di
mov byte [es:di], 0x06
inc di
jmp no_loop
stop:
jmp $
msgBack db '.', 0x67
msgA20on db "A20 on", 0
msgA20off db "A20 off", 0
times 64 db 0
boot_stack:
times 510-($-$$) db 0
dw 0AA55h
a20.inc
; IF CPU is AMD ELAN
;
; mov al, 0x02
; out 0x92, al
; a20_elan_wait:
; call a20_test
; jz a20_elan_wait
; jmp a20_done
;
A20_TEST_LOOPS equ 32
A20_ENABLE_LOOPS equ 255
A20_TEST_ADDR equ 4*0x80
a20_try_loop:
; Check the A20 gate exists on the computer
a20_none:
call a20_test
jnz a20_done
a20_bios:
mov ax, 0x2401
pushfd
int 0x15
popfd
call a20_test
jnz a20_done
a20_kbc:
call empty_8042
call a20_test
jnz a20_done
mov al, 0xD1
out 0x64, al
call empty_8042
mov al, 0xDF
out 0x60, al
call empty_8042
a20_kbc_wait:
xor cx, cx
a20_kbc_wait_loop:
call a20_test
jnz a20_done
loop a20_kbc_wait_loop
a20_fast:
in al, 0x92
or al, 0x02
and al, 0xFE
out 0x92, al
a20_fast_wait:
xor cx, cx
a20_fast_wait_loop:
call a20_test
jnz a20_done
loop a20_fast_wait_loop
dec byte [a20_tries]
jnz a20_try_loop
a20_die:
hlt
jmp a20_die
a20_tries:
db A20_ENABLE_LOOPS
a20_done:
ret
a20_test:
push cx
push ax
xor cx, cx
mov fs, cx
dec cx
mov gs, cx
mov cx, A20_TEST_LOOPS
mov ax, word [fs:A20_TEST_ADDR]
push ax
a20_test_wait:
inc ax
mov word [fs:A20_TEST_ADDR], ax
call delay
cmp ax, word [gs:A20_TEST_ADDR+0x10]
loop a20_test_wait
pop word [fs:A20_TEST_ADDR]
pop ax
pop cx
ret
empty_8042:
push ecx
mov ecx, 100000
empty_8042_loop:
dec ecx
jz empty_8042_end_loop
call delay
in al, 0x64
test al, 1
jz no_output
call delay
in al, 0x60
jmp empty_8042_loop
no_output:
test al, 2
jnz empty_8042_loop
empty_8042_end_loop:
pop ecx
ret
delay:
out 0x80, al
ret
위 어셈블리를 작성한 뒤 컴파일하여 부팅하면 다음과 같은 화면이 출력된다.

소스코드 분석
이번 소스코드는 흐름을 따라가면서 분석하려 한다.
큰 패턴은 이렇다.
- bootA20.asm으로부터 a20_try_loop 루틴이 호출됨.
- a20_try_loop루틴은 A20 게이트 활성화 상태를 세 가지 방법으로 확인함
- BIOS 인터럽트
- 키보드 컨트롤러(A20 게이트와 키보드 컨트롤러의 1번 핀이 AND 연산자로 연결됨)
- 제어 포트 A
- 해당 루틴이 호출되면 바로 밑으로 코드가 실행되며 a20_none 루틴이 실행된다.
- a20_try_loop루틴은 A20 게이트 활성화 상태를 세 가지 방법으로 확인함
- a20_none은 a20_test 루틴을 호출한다.
- a20_test는 다음과 같이 동작한다.
- 21번째 비트가 0인 A 주소와 21번째 비트가 1인 B 주소가 있다. (하위 비트는 같다, A: 0x00....100, B: 0x10....100)
- A20 게이트가 활성화 되어있다면, A주소의 값과 B주소의 값은 같지 않다.
- A20 게이트가 비활성화 상태라면, A주소의 값과 B주소의 값은 같다.
- 두 값을 cmp 명령어를 통해 비교 시 같으면 ZF를 1로 설정
- jnz명령어를 통해 ZF가 1이 아니면(CMP 명령의 결과가 0이 아니면) a20_done으로 점프한다.
(ZF가 1이 되면 a20_test 루틴에서 A20 게이트 비활성화 상태를 의미하게 된다.)
- a20_test는 다음과 같이 동작한다.
- BIOS를 통한 A20 게이트 활성화
- BIOS 인터럽트를 통해 A20 게이트의 활성화를 시도하는 루틴이다.
- 그리고 다시 a20_test를 통해 A20 게이트 활성화 여부를 확인한다.
- 키보드 컨트롤러를 통한 A20 게이트 활성화
- 키보드 컨트롤러와 A20 게이트는 AND 연산자로 연결되어있다.
- 소스코드에서 0x64 포트와 0x60 포트로 데이터를 출력한다.
- 0x64 포트는 키보드 컨트롤러 사용 가능 여부를 나타낸다.
- 0x60 포트는 1번 비트는 AND 연산자와 연결되어 있다.
- empty_8042
- 0x64포트에 데이터를 출력하고 바로 0x60포트에 데이터 출력을 시도하면 0x60포트는 무시당하게 되므로 데이터를 확실하게 처리하기 위해 버퍼를 확실히 비울 때까지 기다리는 역할을 한다.
- a20_test를 통해 A20 게이트가 최종적으로 활성화 되었는지 확인한다.
- 제어포트 A를 통한 A20 게이트 활성화
- A20 게이트를 활성화하는 하드웨어 직통 스위치이며 0x92포트를 사용한다.
- 8비트이며 0번 비트와 1번 비트와 연관이 있다.
- 0번 비트: 활성화 시 컴퓨터 강제 재부팅
- 1번 비트: A20 게이트 즉시 활성화
- a20_test를 통해 A20 게이트가 최종적으로 활성화 되었는지 확인한다.
bootA20.asm에서는 call a20_try_loop를 통해 cmp bx, 0x1234의 결과에 따라 두 값이 서로 같지 않다면 yesA20 루틴으로 이동하여 "A20 on" 메시지를, 두 값이 서로 같다면 noA20 루틴으로 이동하여 "A20 off" 메시지를 출력한다.
a20_try_loop 루틴이 호출되고 a20_test가 호출된다. 다음은 a20_test의 소스코드이다.
a20_test:
push cx
push ax
xor cx, cx
mov fs, cx
dec cx
mov gs, cx
mov cx, A20_TEST_LOOPS
mov ax, word [fs:A20_TEST_ADDR]
push ax
a20_test_wait:
inc ax
mov word [fs:A20_TEST_ADDR], ax
call delay
cmp ax, word [gs:A20_TEST_ADDR+0x10]
loop a20_test_wait
pop word [fs:A20_TEST_ADDR]
pop ax
pop cx
ret
push cx
push ax
a20_test 루틴이 호출되기 전 레지스터 값을 임시 저장하는 코드이다.
xor cx, cx
mov fs, cx
dec cx
mov gs, cx
mov cx, A20_TEST_LOOPS
mov ax, word [fs:A20_TEST_ADDR]
push ax
xor 연산자를 통해 cx 값을 0으로 만든다.
그 이후 fs에는 0을 저장, gs에는 0xFFFF(0-1)을 삽입한다.
이후 cx에는 32가 저장되고 ax에는 [fs:A20_TEST_ADDR], 즉, 0x0 + 4 * 0x80 = 0x200 주소 내 값이 저장된다.
그리고 0x200번지의 값을 스택에 임시저장한다.
inc ax
mov word [fs:A20_TEST_ADDR], ax
call delay
cmp ax, word [gs:A20_TEST_ADDR+0x10]
loop a20_test_wait
pop word [fs:A20_TEST_ADDR]
pop ax
pop cx
ret
ax 값을 1 올린 뒤 0x200번지 값에 저장한다.
그 후 살짝 딜레이를 주고 0x200번지 값과 0x100200번지 값을 비교한다.
이 과정을 32번 반복한 뒤 임시 저장한 레지스터 값을 CPU에 로드한 후 호출된 곳으로 돌아간다.
inc ax를 하지 않고 비교를 해도 되지만 우연의 일치를 막기 위해 1을 올려 비교를 한다.
만약 0x200번지의 값과 0x100200번지의 값이 같다면, A20 게이트가 비활성화 상태라는 것을 의미한다.
0x100200번지는 2진수로 표현하면 1 0000 0000 0010 0000 0000이고, 빨간색으로 표현한 비트가 21번째, A20 게이트에 해당한다. 이 비트는 A20 게이트 비활성화 시 0이 되므로 0x000200과 같은 주소가 된다.
그렇기에 A20 게이트 비활성화 시 0x100200번지의 값과 0x200번지의 값 비교 시 값이 같을 수밖에 없게 된다.
cmp 명령을 통해 같은 두 값을 빼면 0이 되고, ZF가 1이 된다.
a20_try_loop:
; Check the A20 gate exists on the computer
a20_none:
call a20_test
jnz a20_done
Jump if not zero 명령어이므로 ZF가 0일 때 (cmp 결과가 0이 아닐 때) a20_done 루틴으로 점프하게 된다.
A20 게이트가 활성화가 되지 않았을 때 다음 루틴으로 넘어가게 되는데, BIOS 인터럽트를 통한 A20 게이트 활성화 루틴이다.
BIOS 인터럽트를 통한 A20 게이트 활성화
a20_bios:
mov ax, 0x2401
pushfd
int 0x15
popfd
call a20_test
jnz a20_done
ax에는 0x2401 값이 저장되는데, 이렇게 되면 ah에는 0x24가, al에는 0x01이 저장된다.
ah의 0x24는 A20 게이트를 의미하고, al의 0x01은 활성화를 의미한다.
pushfd를 통해 EFLAGS를 스택에 임시 저장해둔다.
그리고 int 0x15를 통해 A20 게이트 활성화 인터럽트를 발생시킨다.
그리고 나서 a20_test 루틴을 통해 A20 게이트가 활성화 되었는지 확인한다.
여전히 A20 게이트가 활성화 되지 않았다면 다음 루틴으로 넘어간다.
책을 보면 pushfd에 "좀 지나치게 FLAG를 의심한다." 라고 주석이 달려있다.
해당 주석의 의미는 인터럽트를 완료한 뒤에 CF 값이 설정되는데, 이러한 결과들을 popfd를 통해 다 무시하고 a20_test로만 판단하겠다는 의미이다.
키보드 컨트롤러를 통한 A20 게이트 활성화
a20_kbc:
call empty_8042
call a20_test
jnz a20_done
mov al, 0xD1
out 0x64, al
call empty_8042
mov al, 0xDF
out 0x60, al
call empty_8042
a20_kbc_wait:
xor cx, cx
a20_kbc_wait_loop:
call a20_test
jnz a20_done
loop a20_kbc_wait_loop
0x64포트에 데이터를 출력하기 전에 버퍼를 비우기 위해 empty_8042 루틴을 호출하게 된다. empty_8042 루틴은 입력 버퍼의 값이 0이 될 때까지 시간을 지연시키는 루틴이다.
mov al, 0xD1
out 0x64, al
call empty_8042
mov al, 0xDF
out 0x60, al
call empty_8042
버퍼를 비운 후 0x64포트에 0xD1 값을 쓴다. P2 출력 포트를 제어하겠다는 의미이다.
그리고 0x60포트에서 0xDF 값을 통해 A20 게이트를 활성화한다. 0xDF는 1101 1111 이고, 두 번째 비트가 A20 게이트를 의미하는데 두 번째 비트가 1이므로 키보드 컨트롤러의 A20 게이트 비트가 활성화된다.
키보드 컨트롤러는 A20과 함께 AND로 묶여있으므로 A20 게이트를 활성화하려면 키보드 컨트롤러의 A20 게이트 비트도 활성화 해야한다.
제어 포트 A를 통한 A20 게이트 활성화
제어포트 A는 A20 게이트를 활성화하는 하드웨어 직통 스위치이며 0x92포트를 사용한다.
0번 비트는 컴퓨터 강제 부팅 여부를 나타내며, 1번 비트를 통해 A20 게이트를 활성화 시킬 수 있다.
a20_fast:
in al, 0x92
or al, 0x02
and al, 0xFE
out 0x92, al
a20_fast_wait:
xor cx, cx
a20_fast_wait_loop:
call a20_test
jnz a20_done
loop a20_fast_wait_loop
0x92번 비트에 0x02(0000 0010) 값과 0xFE(1111 1110)을 AND 연산 후 0x92포트에 다시 값을 쓴다.
AND 연산을 하는 이유는 0번 비트를 활성화 시켜 재부팅 되지 않기 위함이다.
이렇게 해서 위 세 가지 방법을 통해 a20_done 루틴에 도달하면 A20 게이트가 활성화 할 수 있다.
'OS > 만들면서 배우는 OS 커널의 구조와 원리' 카테고리의 다른 글
| 8-3. 페이징 실험 (0) | 2026.04.25 |
|---|---|
| 8-2. 페이징 - 페이징 구현 (0) | 2026.04.23 |
| 7. 유저모드 Task Switching (0) | 2026.04.19 |
| 6. 보호 (0) | 2026.04.13 |
| 5. 태스크 스위칭, 문맥 교환 (0) | 2026.04.13 |