INT 21h 입출력 함수 문서의 검증 코드다. if __name__ == "__main__": 아래가 자체 테스트다.

# 51.INT 21h 입출력 함수 문서의 확인. 실행: python3 51_int21h-io-functions_verify.py
# MS-DOS가 없어서 INT 21h를 실제로 부르지 않는다. 문서에 적은 규칙(AH에 번호, 입력·출력 레지스터)을
# 파이썬 함수로 흉내 내고, 문서 코드의 레지스터 값과 계산만 단언한다.

class Dos:
    """INT 21h 가운데 문서에 나온 몇 개만 흉내 낸다."""

    def __init__(self, keys=b""):
        self.keys = list(keys)   # 표준 입력에 기다리는 글자
        self.out = bytearray()   # 표준 출력
        self.mem = {}            # 오프셋 -> 바이트 (DS 세그먼트라고 본다)
        self.zf = 0

    def load(self, off, data):
        for i, b in enumerate(data):
            self.mem[off + i] = b

    def int21(self, ah, al=0, bx=0, cx=0, dx=0):
        dl = dx & 0xFF
        if ah == 0x01:                       # 글자 하나 읽기 (에코 있음, 기다림)
            ch = self.keys.pop(0)
            self.out.append(ch)
            return ch
        if ah == 0x02:                       # 글자 하나 쓰기
            self.out.append(dl)
            return al
        if ah == 0x06:                       # DL=FFh면 기다리지 않고 읽기, 아니면 DL을 쓰기
            if dl == 0xFF:
                if self.keys:
                    self.zf = 0
                    return self.keys.pop(0)
                self.zf = 1
                return 0
            self.out.append(dl)
            return al
        if ah == 0x09:                       # '$'로 끝나는 문자열 쓰기
            off = dx
            while self.mem[off] != ord("$"):
                self.out.append(self.mem[off])
                off += 1
            return al
        if ah == 0x0B:                       # 입력 버퍼 상태: 있으면 FFh, 없으면 0
            return 0xFF if self.keys else 0
        if ah == 0x40:                       # CX바이트 쓰기, AX = 쓴 바이트 수
            for i in range(cx):
                self.out.append(self.mem[dx + i])
            return cx
        raise ValueError(hex(ah))


def read_string(dos, cx):
    """문서의 ReadString을 한 줄씩 옮겼다. (AX, 버퍼)를 돌려준다."""
    stack = [cx, "si", cx]      # push cx / push si / push cx
    buf = []
    cx -= 1                     # dec cx
    while True:
        al = dos.int21(0x01)    # L1: mov ah,1 / int 21h
        if al == 0x0D:          # cmp al,0Dh / je L2
            break
        buf.append(al)          # mov [si],al / inc si
        cx = (cx - 1) & 0xFFFF  # loop L1
        if cx == 0:
            break
    buf.append(0)               # L2: mov BYTE PTR [si],0
    ax = stack.pop()            # pop ax
    ax = (ax - cx) & 0xFFFF     # sub ax,cx
    return ax, buf


if __name__ == "__main__":
    # 02h: 'A' 쓰기. 05h 예의 65도 같은 글자다
    d = Dos()
    d.int21(0x02, dx=ord("A"))
    assert d.out == b"A" and ord("A") == 65 == 0x41
    # 제어 문자 값
    assert (0x08, 0x09, 0x0A, 0x0C, 0x0D, 0x1B) == (8, 9, 10, 12, 13, 27)

    # 09h: '$' 앞까지만 나온다
    d = Dos()
    d.load(0x100, b"This is a string$")
    d.int21(0x09, dx=0x100)
    assert d.out == b"This is a string"
    # '$'가 없으면 멈출 곳이 없다: 뒤에 놓인 다른 자료까지 나온다
    d = Dos()
    d.load(0x100, b"Hello" + b"SECRET$")
    d.int21(0x09, dx=0x100)
    assert d.out == b"HelloSECRET"

    # 40h: LENGTHOF message = 31, AX = 31
    msg = b"Writing a string to the console"
    assert len(msg) == 31
    d = Dos()
    d.load(0x200, msg)
    assert d.int21(0x40, bx=1, cx=len(msg), dx=0x200) == 31 and d.out == msg

    # 06h + DL=FFh: 글자가 없으면 ZF=1, 있으면 ZF=0이고 AL에 글자
    d = Dos(b"x")
    assert d.int21(0x06, dx=0xFF) == ord("x") and d.zf == 0
    assert d.int21(0x06, dx=0xFF) == 0 and d.zf == 1
    # 0Bh: 버퍼가 비면 AL=0
    assert Dos(b"").int21(0x0B) == 0 and Dos(b"k").int21(0x0B) == 0xFF

    # 문자열 암호화: XORVAL = 239
    XORVAL = 239
    assert XORVAL == 0xEF

    def encrypt(data):
        d = Dos(data)
        while True:
            al = d.int21(0x06, dx=0xFF)      # L1
            if d.zf:                         # jz L2
                break
            al ^= XORVAL                     # xor al,XORVAL
            d.int21(0x06, dx=al)             # mov dl,al / int 21h
        return bytes(d.out)

    # 0FFh가 되는 글자(10h)는 "쓰기"가 아니라 "읽기"로 해석되므로 빼고 확인한다
    plain = bytes(b for b in range(256) if b ^ XORVAL != 0xFF)
    assert len(plain) == 255 and 0x10 ^ XORVAL == 0xFF
    cipher = encrypt(plain)
    assert cipher == bytes(b ^ XORVAL for b in plain)
    # 같은 값으로 두 번 XOR하면 원래대로 (8비트 전수)
    assert all((b ^ XORVAL) ^ XORVAL == b for b in range(256))
    assert ord("A") ^ XORVAL == 0xAE

    # ReadString: CX = 20, "abc" + Enter
    ax, buf = read_string(Dos(b"abc\r"), 20)
    assert buf == [ord("a"), ord("b"), ord("c"), 0]
    assert ax == 4            # 문서의 코드 그대로면 글자 수 3이 아니라 4(글자 수 + 1)가 나온다
    # 19글자를 꽉 채운 경우: 20이 나온다 (글자 수 19)
    ax, buf = read_string(Dos(b"x" * 25), 20)
    assert len(buf) == 20 and buf[-1] == 0 and ax == 20
    # Enter만 누른 경우: 1 (글자 수 0)
    ax, buf = read_string(Dos(b"\r"), 20)
    assert buf == [0] and ax == 1
    # 어느 길이든 "글자 수 + 1"
    for n in range(0, 20):
        ax, buf = read_string(Dos(b"y" * n + b"\r"), 20)
        assert ax == n + 1 and len(buf) == n + 1

    # 날짜·시간 함수의 성공 검사: 코드는 AL != 0이면 failed로 간다
    def set_date_branch(al):
        return "failed" if al != 0 else "ok"   # cmp al,0 / jne failed
    assert set_date_branch(0) == "ok" and set_date_branch(0xFF) == "failed"

    # 4Ch: AH = 4Ch, AL = 반환 코드
    assert 0x4C == 76
    print("ALL CHECKS PASSED")