요약

키를 누르면 그 글자가 곧장 프로그램으로 가는 것이 아니다. 우체부가 편지를 우편함에 넣어 두면 집주인이 나중에 꺼내 가듯, 누른 키는 먼저 작은 대기 줄에 쌓이고 프로그램이 필요할 때 꺼내 간다. 대기 줄에는 글자뿐 아니라 “자판의 몇 번째 단추였는지”도 함께 적혀 있어서, 글자가 없는 화살표 키나 F1 같은 키도 알아낼 수 있다. 다만 줄의 길이가 정해져 있어서 프로그램이 꺼내 가지 않으면 금방 찬다.

예시로 보기

INT 21h의 글자 읽기는 글자의 ASCII 코드만 준다. 그런데 F1이나 왼쪽 화살표에는 ASCII 코드가 없다. 게임이나 편집기는 이런 키를 알아야 한다.

키보드는 눌린 단추마다 스캔 코드라는 번호를 보낸다. 글자가 아니라 단추의 번호다. 키 하나가 눌린 뒤 일어나는 일은 다음과 같다.

flowchart TD
  K[키보드] -->|스캔 코드| P[입력 포트]
  P -->|스캔 코드| H9[INT 9h 처리 코드]
  H9 -->|스캔 코드와 ASCII 코드| B[미리 입력 버퍼]
  B -->|스캔 코드와 ASCII 코드| H16[INT 16h 처리 코드]
  B -->|ASCII 코드| H21[INT 21h 처리 코드]
  1. 키보드가 스캔 코드를 입력 포트로 보낸다.
  2. 하드웨어 인터럽트가 걸려 INT 9h의 처리 코드가 돈다.
  3. 그 코드가 스캔 코드와 ASCII 코드 한 쌍을 미리 입력 버퍼(typeahead buffer)에 넣는다. 프로그램이 아직 읽지 않은 키가 쌓이는 대기 줄이다.
  4. 프로그램이 INT 16h를 부르면 스캔 코드와 ASCII 코드를 둘 다 받는다. INT 21h를 부르면 ASCII 코드만 받는다.
검증: 키보드 플래그의 비트 번호와 마스크, 0040:0017 → 00417h, 버퍼 32바이트 = 16쌍, or al,1이 늘 ZF를 0으로 만드는지(8비트 전수), 버퍼 비우기 코드의 두 가지 끝, 카드 C3 — 53_bios-keyboard-int16h_verify.py. BIOS를 실제로 부르지 못해서 버퍼를 큐로 흉내 냈다. 함수의 동작 자체는 실행으로 확인하지 못했다.

BIOS

BIOS(Basic Input-Output System)는 장치를 직접 다루는 낮은 수준의 코드 묶음이다. 16비트 프로그램이 부를 수 있다. MS-DOS 함수와 비교하면 색과 그래픽을 쓸 수 있고, 입출력이 빠르고, 마우스·직렬 포트·병렬 포트를 읽을 수 있고, 디스크를 낮은 수준에서 다룰 수 있다: 입출력 접근 수준.

스캔 코드

기능 키와 화살표 키처럼 글자가 없는 키는 스캔 코드로 구별한다. 같은 키라도 Shift, Ctrl, Alt와 함께 누르면 다른 번호가 나온다. 값은 모두 16진수다.

키 그냥 Shift와 Ctrl과 Alt와
F1 3B 54 5E 68
F2 3C 55 5F 69
F3 3D 56 60 6A
F4 3E 57 61 6B
F5 3F 58 62 6C
F6 40 59 63 6D
F7 41 5A 64 6E
F8 42 5B 65 6F
F9 43 5C 66 70
F10 44 5D 67 71
F11 85 87 89 8B
F12 86 88 8A 8C
키 그냥 Ctrl과   키 그냥 Ctrl과
Home 47 77   위 화살표 48 8D
End 4F 75   아래 화살표 50 91
PgUp 49 84   Ins 52 92
PgDn 51 76   Del 53 93
PrtSc 37 72   Back tab 0F 94
왼쪽 화살표 4B 73   회색 + 4E 90
오른쪽 화살표 4D 74   회색 − 4A 8E

INT 21h로 이런 키를 읽으면 첫 번째 읽기가 0을 돌려준다. 한 번 더 읽어야 스캔 코드가 나온다.

키보드 플래그

Shift, Ctrl, Alt, Caps Lock 같은 키는 글자를 만들지 않고 상태만 바꾼다. BIOS는 이 상태를 16비트에 적어 0040:0017h~0018h에 둔다. 비트가 1이면 그 줄의 내용이 참이다.

비트 뜻   비트 뜻
0 오른쪽 Shift가 눌려 있다   8 왼쪽 Ctrl이 눌려 있다
1 왼쪽 Shift가 눌려 있다   9 왼쪽 Alt가 눌려 있다
2 Ctrl(어느 쪽이든)이 눌려 있다   10 오른쪽 Ctrl이 눌려 있다
3 Alt(어느 쪽이든)가 눌려 있다   11 오른쪽 Alt가 눌려 있다
4 Scroll Lock이 켜져 있다   12 Scroll Lock 키가 눌려 있다
5 Num Lock이 켜져 있다   13 Num Lock 키가 눌려 있다
6 Caps Lock이 켜져 있다   14 Caps Lock 키가 눌려 있다
7 Insert가 켜져 있다   15 SysReq 키가 눌려 있다

“켜져 있다”와 “눌려 있다”는 다르다. Caps Lock을 한 번 눌렀다 떼면 비트 6은 1로 남고 비트 14는 0으로 돌아간다1.

INT 16h 함수

함수 번호는 늘 AH에 넣는다. 중요한 함수는 자동 반복 속도 정하기, 버퍼에 키 밀어 넣기, 키 기다리기, 버퍼 들여다보기, 키보드 플래그 읽기다. 아래 표는 그 가운데 세 개다. 실행해서 확인하지는 않았다.

AH 하는 일 돌아오는 값
10h 키 하나를 꺼낸다. 버퍼에 키가 없으면 누를 때까지 프로그램이 멈춰 기다린다 AH = 스캔 코드, AL = ASCII 코드
11h 버퍼를 들여다보기만 한다. 꺼내지 않는다 키가 있으면 ZF = 0, 없으면 ZF = 1
12h 키보드 플래그를 읽는다 AX = 플래그 16비트
mov ah,10h
int 16h
mov scanCode,ah
mov ASCIICode,al

예제

버퍼 비우기. 쌓여 있는 키를 모두 꺼내 버린다. 꺼내다가 정해 둔 키(scanCode)가 나오면 그 자리에서 끝낸다.

L1: mov ah,11h          ; 버퍼에 키가 있나?
    int 16h
    jz  noKey           ; 없으면 끝
    mov ah,10h          ; 있으면 꺼낸다
    int 16h
    cmp ah,scanCode     ; 끝내는 키였나?
    je  quit            ; 그렇다 (ZF = 1)
    jmp L1              ; 아니면 다시

noKey:
    or  al,1            ; ZF를 0으로
quit:

끝났을 때 ZF로 두 경우가 갈린다.

끝난 이유 지나간 길 ZF
끝내는 키를 꺼냈다 cmp가 같음 → je quit 1
버퍼가 비었다 jz noKey → or al,1 0

noKey에 올 때는 ZF가 1이다(그래서 jz가 뛰었다). 이대로 두면 두 경우를 구별할 수 없다. or al,1은 결과의 맨 아랫비트가 늘 1이라 결과가 0이 될 수 없고, 그래서 ZF가 0이 된다: 비트 논리 명령.

활용

  • INT 16h는 화살표, 기능 키, Shift 상태가 필요할 때 쓴다. 글자만 필요하면 INT 21h로 충분하다.
  • 11h로 먼저 들여다보고 10h로 꺼내면, 키가 없을 때 프로그램이 멈추지 않고 다른 일을 할 수 있다.
  • 미리 입력 버퍼는 BIOS 데이터 영역의 001E~003D, 32바이트다. 키 하나가 2바이트(스캔 코드와 ASCII 코드)라 16쌍이 들어간다1.
  • 16비트 프로그램에서만 쓸 수 있다.

연결

확인 문제

C1 키를 누른 뒤 프로그램이 그 키를 받기까지의 길을 순서대로 쓰라. INT 16h와 INT 21h는 각각 무엇을 받는가?

답: 키보드 → 입력 포트(스캔 코드) → INT 9h 처리 코드 → 미리 입력 버퍼(스캔 코드와 ASCII 코드) → 프로그램. INT 16h는 스캔 코드와 ASCII 코드를 둘 다, INT 21h는 ASCII 코드만 받는다.

C2 INT 16h의 함수 10h와 11h는 무엇이 다른가? “키가 눌렸으면 처리하고, 안 눌렸으면 화면을 계속 그리는” 게임은 어느 것을 먼저 불러야 하는가?

답: 10h는 키를 꺼내고, 없으면 누를 때까지 멈춰 기다린다. 11h는 꺼내지 않고 있는지만 ZF로 알려 준다. 게임은 11h를 먼저 불러야 한다.
이유: 10h를 바로 부르면 키를 누르기 전까지 프로그램이 멈춰 화면을 그리지 못한다. 11h로 ZF = 0(키 있음)을 확인한 뒤에만 10h로 꺼낸다.

C3 함수 12h가 AX = 0045h를 돌려주었다. 어떤 키 상태인가?

답: 오른쪽 Shift가 눌려 있고, Ctrl이 눌려 있고, Caps Lock이 켜져 있다.
이유: 0045h = 0000 0000 0100 0101b라 비트 0, 2, 6이 1이다.

C4 버퍼 비우기 코드의 noKey: 아래에 or al,1이 없으면 무엇이 문제인가?

답: 버퍼가 비어서 끝났을 때도 ZF가 1로 남는다. 끝내는 키를 꺼내서 끝났을 때와 구별할 수 없다.
이유: jz noKey는 ZF = 1일 때 뛴다. or al,1의 결과는 0이 될 수 없어서 ZF가 0으로 바뀐다.

  1. 계산과 코드 흐름은 검증 코드로 확인했다. ↩ ↩2