관리 메뉴

드럼치는 프로그래머

[O.S.] 인터럽트 프로그래밍 .. 본문

★─Programing/☆─O. S.

[O.S.] 인터럽트 프로그래밍 ..

드럼치는한동이 2009. 2. 25. 04:53

인터럽트(Interrupt)

(1)

* 명확하고 간략한 의사전달을 위해 경어를 사용하지 않았음을 양해바랍니다.
-------------------------------------------------------------------
본 강좌에서 거론될 예문들은 Borland의 Turbo C/C++, Borland C/C++ 컴파일
러를
기준하였음을 알려드립니다.  예제들을 제외한다면 인터럽트에 대한 기본적인 내
용들은 어느 특정 언어에 국한되지 않습니다.
-------------------------------------------------------------------

 [ 인터럽트의 의미와 발생과정 ]

'인터럽트(Interrupt)'는 우리말로 흔히 '끼어들기' 또는 '가로채기'라고 표현되
고 있다.  인터럽트가 발생하는 순간은 항상 CPU가 어떤 일을 하고 있을 때인데,
예를 들어 우리가 한창 컴퓨터 통신을 하고 있을 때를 생각해 볼 수 있다.
대화실에서 새로 사귄 친구와 한창 재미난 이야기를 주고 받고 있는데(열심히 키
보드를 두드리고 있는데..) 갑자기 그 친구와의 대화 중간에 누군가 다른 사람이
대화실로 들어 섰다. 그러면 우리는 통신 예절을 중시하므로 그냥 모른 채 할 수
없어서 인사를 하게 된다.

민이>그래서 말인데..., 난 왜 이렇게 잘 생겼지?
석이>그러게 넌 너무 잘생겼어

#더 잘생긴 사람님이 입장했습니다.#      <--- 대화 중간에 '끼어'들었다!

민이>어소세요~ 일루 앉으시죠~           <--- 민이와 석이는 통신예절이 투철
석이>어솨요~                                 하므로 이를 무시하지 않는다.
더 잘생긴 사람>안냐세요~ 날씨 춥죠?

. . .
                                        <--- 종전에 하던 대화를 계속한다.


이렇게, 어떤 연속된 과정 중간에 급하게 처리해야할 다른 일이 발생하게 되면
우리는 이러한 상황을 '끼어들었다'라고 하며, 일이 급하니 만큼 하던 일을 잠
시 멈추고 그 일을 먼저 처리하게 된다.  위의 대화실에서의 예에서 처럼 민이
와 석이는 한참 재밌게 대화하고 있는 중이었지만  다른 사람의 입장으로 하고
있던 대화를 중단하고 인삿말을 주고 받는다. 그러나 인사를 주고받고 나면 민
이와 석이는 좀 전에 하던 대화를 계속할 것이다.

  대화실에서 민이와 석이가 재미난 대화를 하고 있던 상황과 흡사하게, CPU는
항상 어떤 일을 하고 있다. 괜히 시간을 보내던,  무언가 특별한 일을 하던 간
에 무언가를 하고 있는 것이다. 이런 중에 CPU가 인터럽트 신호를 감지하게 되
면(이것을 '인터럽트가 걸렸다'라고 한다)  CPU는 하고 있던 일을 잠시 멈추고
인터럽트 신호에 따라 적절한 동작을 취하게 된다.   여기에서 말하는 '적절한
동작'이란, CPU가 현재 하고 있던 동작에 대한 여러가지 주변 상황을 저장하고
이미 준비되어 있는  '인터럽트 처리루틴(또는 핸들러)'을 실행하는 것을 말한
다.

인터럽트 처리 루틴이 수행되고 나면 CPU는 다시 '인터럽트 처리 종료' 신호를
받게 된다. 그러면 CPU는 주변상황을 인터럽트가 걸리기 전의 상태로 돌려놓고
(인터럽트 처리루틴을 수행하기 전에 저장해 두었었다) 종전에 하던 일을 계속
수행하게 된다.(민이와 석이가 새로 대화실에 들어 온 사람에게 인사하고 나서
하던 대화를 계속했던 것처럼...)


 [ 인터럽트의 종류 ]

인터럽트의 종류는 크게 두가지로 분류된다.

        인터럽트 -----+------ 하드웨어 인터럽트
                      |
                      +------ 소프트웨어 인터럽트

하드웨어 인터럽트는  키보드,디스크드라이브,마우스 등과 같은 시스템의 각종
주변장치로 부터 생성되는 기계동작에 따른 인터럽트를 말하며, 소프트웨어 인
터럽트는 프로그램에 의해 발생하는 인터럽트를 말한다(DOS인터럽트, 바이오스
인터럽트 등과 같은...).


 >>> 소프트웨어 인터럽트 <<<

IBM PC에는 모두 256개의 소프트웨어 인터럽트가 마련되어 있다(사실은  256개
모두가 실행가능한 인터럽트는 아니다.  어떤 것은 비어 있고, 어떤 것은 실행
할 수 없는, 단순히 데이타 만을 가지고 있다). 이 256개의 소프트웨어 인터럽
트에 대한 정보는 '벡터테이블(Vector Table)'이라는 곳에서 찾을 수 있는데
벡터테이블은 메모리의 가장 낮은 곳(00000h..003FFh)에 위치한다.

벡터테이블은 소프트웨어 인터럽트가 있는 곳의 주소를 저장하는 '목차'이다.
이 주소는 4바이트로(세그먼트주소 2바이트+옵셋주소 2바이트) 구성되며, 인터
럽트 0번 부터 255번까지의 주소가 차례대로 저장되어 있다.
소프트웨어 인터럽트의 갯수가 256개 이고, 각 인터럽트의 주소를 위해서 4바
이트씩이 필요하므로 전체 벡터테이블의 크기는 4 x 256 = 1024바이트가 된다.
이것이 벡터테이블이 00000h에서 003FFh까지의 400h바이트 범위에 저장된 이유
이다.

예를 들어서 소프트웨어 인터럽트 0에 대한 주소를 확인하기 위해 MS-DOS가 제
공하는 debug를 사용해서 벡터테이블의 내용을 덤프해 보자.

        C:\>debug
        -

명령행 상에서 'debug'를 실행하면 디버거의 프롬프트인 '-'를 볼 수 있다. 여
기에서 다음과 같이 입력한다(debug의 모든 수치는 16진수이다).

        -d 0000:0000 L4

그러면 다음과 같은 화면을 볼 수 있을 것이다.

        0000:0000  8A 10 16 01                            ....

이 값은 기종마다 조금씩 다를 수 있으므로 위와 다르다고 해서 걱정할 것은
없다.

위의 내용은 소프트웨어 인터럽트 0에 대한 주소인데 역워드 저장을 고려해서
4바이트를 2바이트씩 word형으로 구분해서 표현하면,

        0116:8A10          <--- 세그먼트:옵셋

이 된다. 즉, 소프트웨어 인터럽트 0은 주소 0116:8A10의 위치에 있다는 것을
알 수 있는 것이다.
이제  이 주소에 있는 내용이 무엇인지 확인해 보기 위해서 다음과 같이 입력
해 보자.

        -u 0116:8A10

그러면 다음과 같은 내용을 볼 수 있다.

        0116:8A10 FF7403  PUSH    [SI+03]
        0116:8A13 B000    MOV     AL,00
        0116:8A15 CF      IRET
            .              .
            .              .

소프트웨어 인터럽트 0번은 일명 'Zero devide'라고 하며, 프로그램에서 어떤
수를 0으로 나누려고 할 때 이 인터럽트가 발생된다.
이 인터럽트 프로그램의 길이는 시작 주소로 부터 'IRET'이 나오는 곳 까지이
다.

위와 같이 같이 모든 인터럽트의 끝은 반드시  'IRET(Interrupt Return)'으로
표현되는데  이 'IRET'은 일반적인 프로그램 함수의 종료를 알리는 'RET'과는
다소 차이가 있다. 만약 인터럽터의 끝에 'IRET'이 없다면, CPU는 인터럽트의
종료 신호를 받지 못하게 되고 시스템은 멎어버린다.


-------------------------------------------------------------------
인터럽트(Interrupt)

(2)

* 명확하고 간략한 의사전달을 위해 경어를 사용하지 않았음을 양해바랍니다.
-------------------------------------------------------------------
본 강좌에서 거론될 예문들은 Borland의 Turbo C/C++, Borland C/C++ 컴파일
러를
기준하였음을 알려드립니다.  예제들을 제외한다면 인터럽트에 대한 기본적인 내
용들은 어느 특정 언어에 국한되지 않습니다.
-------------------------------------------------------------------

지난 강좌에서는 인터럽트의 의미와 종류, 그리고 소프트웨어 인트럽트에 대한
개괄적인 내용을 알아 보았다. 아래에 제시된 프로그램은 256개의 소프트웨어 인
터럽트 중에서 주로 사용되는 39개의 인터럽트에 대한 주소를 롬바이오스 벡터테
이블에서 확인하고 이를 화면에 출력하는 내용을 담고 있다.  컴파일해서 실행해
보기 바란다.

/* 인터럽트 벡터테이블에서 주소리스트를 확인하는 프로그램 */

#include <dos.h>
#include <stdio.h>

typedef unsigned int    WORD;
typedef unsigned char   BYTE;

BYTE far *intr_vector = (BYTE far *)0L;
char *description[] = {
        "CPU Division by zero",
        "CPU Single Trap",
        "CPU None Maskable Interrupt(NMI)",
        "CPU Break point",
        "CPU Overflow",
        "ROM-BIOS Print Screen Service",
        "-----",
        "-----",
        "Hardware Timer",
        "Hardware Keyboard",
        "-----",
        "-----",
        "-----",
        "Floppy Diskette",
        "Printer Controll",
        "ROM-BIOS Video Service",
        "ROM-BIOS Equipment List",
        "ROM-BIOS RAM Size",
        "ROM-BIOS Disk Service",
        "ROM-BIOS Serial Communication Service",
        "ROM-BIOS System Service",
        "ROM-BIOS Keyboard Service",
        "ROM-BIOS Printer Service",
        "ROM-BASIC",
        "ROM-BIOS Boot Strap Loader",
        "ROM-BIOS Date/Time",
        "ROM-BIOS Ctrl-Break",
        "Clock Tick",
        "Video Controll Parameter Table",
        "Diskette Drive Parameter Table",
        "CGA Video Graphics Characters",
        "DOS Program-end Service",
        "DOS General Service",
        "-----",
        "DOS Key-Break Handler",
        "DOS FATAL Error Handler",
        "DOS Disk Read",
        "DOS Disk Write",
        "DOS TSR"
};

int main(void)
{
        WORD count;

        for(count = 0; count < 39; ++count)
        {
                printf("\n[%02Xh] %-50s %02X%02X:%02X%02X",
                        count,
                        description[count],
                        intr_vector[count*4+3],
                        intr_vector[count*4+2],
                        intr_vector[count*4+1],
                        intr_vector[count*4+0]);
        }

        return 0;
}


 >>> 하드웨어 인터럽트 <<<

하드웨어 인터럽트 역시 소프트웨어 인터럽트와 개념적인 차이점은 없다.  다만
인터럽트의 생성과정과 처리하는 곳이 어디인가의 차이가 있을 뿐이다.
하드웨어 인터럽트는 또다시 "내부 하드웨어 인터럽트", "외부 하드웨어 인터럽
트"로 구분될 수 있는데, 내부 하드웨어 인터럽트는 프로그램 실행중 특정한 상
황(0으로 나누기 등과 같은...)을 만나게 되면 CPU에 의해 발생되며, 외부 하드
웨어 인터럽트는 코프로세서의 동작, 디스크 입출력 등과 같은 상황에 생성되고
이들 하드웨어에 의해 인터럽트가 처리되어진다.


 [ 인터럽트 콘트롤러(Interrupt Controller) ]

인터럽트 콘트롤러는 하드웨어 인터럽트의 처리를 담당하는 특별한 Chip이다.
IBM PC에는 인텔(Intel)의 8259A Chip을 인터럽트 콘트롤러로 장착하고 있으며
XT에는 1개, AT에는 2개의 콘트롤러가 존재한다.

한개의 콘트롤러는 8개의 인터럽트에 대한 제어권을 가지며, 이 콘트롤러는 프
로그램에 의해 제어될 수 있기 때문에 일반적으로 '프로그램 가능한 인터럽트
제어기(Programmable Interrupr Controller, PIC)'라고 불리고 있다.

PIC의 세부적인 동작과 제어는 상당히 복잡한 내용을 담고 있으므로 에 관해서
는 시중의 서점에서 구할 수 있는 관련 서적을 참조하시기 바라며, 여기서는
개괄적인 흐름만을 보도록 하자..

 >>> PIC(8259A Programmable Interrupt Controller)의 동작 <<<

PIC는 시스템에서 발생되는 하드웨어 인터럽트를 제어하기위한 몇개의 내부 레
지스터(Register)를 가지고 있다. 프로그래머에게 중요한 의미를 가질 수 있는
것은

        IRR - Interrupt Request Register(인터럽트 요구)
        ISR - Interrupt Service Register(인터럽트 처리)
        IMR - Interrupt Mask Register(인터럽트 마스크)

의 세가지 레지스터인데 이들이 가지는 값은 인터럽트의 처리과정에서 서로 연
관되어 적용된다. 즉,


                인터럽트 요구(IRR)  -----------------+
                                                     |
                처리해도 좋은 인터럽트인가?(IMR) <---+
                                          |
                                          |
                           No             | Yes
       무시 <-----------------------------+------> 인터럽트 처리(ISR)

그림과 같이 PIC의 IRR에는 특정 인터럽트가 요구될 때 특정값으로 설정되고,
PIC는 이 IRR의 특정값과 IMR에 마스크된 값을 비교해서 요구된 인터럽트가 처
리해도 되는 것인지를 판단하게 된다.   IMR의 역할은 요구된 인터럽트에 대한
처리의 가능/불가능을 판단한다. 인터럽트 처리가 불가능하다면 이 인터럽트에
대한 처리는 유보되고 다음단계(ISR)로 넘어가지 못한다.
인터럽트를 불가능하게 하기 위한 어셈블리 명령은  'CLI(Clear Interrupt)'이
며, 이 명령은 일반적으로 'STI(Start Interrupt)' 명령과 짝을 이루게 된다.

        CLI                     ; Disable interrupt

        . . .

        STI                     ; Enable interrupt


C에서는 이와 호환되는 명령으로 'disable()'과 'enable()'을 제공한다.

        disable();              /* 인터럽트를 불가능하게 한다 */

        . . .

        enable();               /* 인터럽트를 가능하게 한다 */


-------------------------------------------------------------------
인터럽트(Interrupt)

(3) [ 소프트웨어 인터럽트의 호출 ]

Turbo/Borland C/C++ 에서 인터럽트를 처리하기 위해 우선 이해되어야 할 자료

이 있다. 구조체와 공용체가 그것인데, 인터럽트라는 것이 레지스터를 통해 자료
가 전달되는 관계로  C에서는 IBM PC의 레지스터들을 묶어서 구조체 또는 공용

의 형태로 이들을 정의하고 있다.   'dos.h'에 정의된 이들 구조체와 공용체들의
형태는 다음에 열거된 것과 같다.

        struct WORDREGS {
                unsigned int  ax, bx, cx, dx, si, di, cflag, flags;
        };

        struct BYTEREGS {
                unsigned char   al, ah, bl, bh, cl, ch, dl, dh;
        };

        union   REGS    {
                struct  WORDREGS x;
                struct  BYTEREGS h;
        };

        struct  SREGS   {
                unsigned int    es;
                unsigned int    cs;
                unsigned int    ss;
                unsigned int    ds;
        };

        struct  REGPACK {
                unsigned    r_ax, r_bx, r_cx, r_dx;
                unsigned    r_bp, r_si, r_di, r_ds, r_es, r_flags;
        };

이들 중에서 일반적으로 사용되고 있는 레지스터 구조체/공용체는

        union REGS
        struct SREGS
        struct REGPACK

의 세가지이다.

이들 자료형이 준비되고 레지스터들이 적절하게 설정되었다면 인터럽트를 호출

야 하는데 이는 인터럽트 호출을 위해 준비된 int86, int86x, intdos, intdosx,
intr 등의 함수들에 의해 이루어 진다.  이 중에서 intdos와 intdosx는 인터럽트
21h를 위해 마련된 것으로 일반적으로 '도스인터럽트'라고 불리운다.
위의 레지스터 자료형 중의 'struct REGPACK'을 사용할 수 있는 유일한 함수는
intr이며 나머지는  'union REGS'혹은 'struct SREGS'의 포인터를 매개변수로 가
진다. 예를 들어서, 화면에 하나의 문자를 출력하기 위해서 인터럽트 21h의 서브
함수 02h를 사용하는 경우를 int86, intdos, intr의 세가지 인터럽트 호출함수를
사용하여 작성해 보자. INT 21h의 서브함수 02h를 호출하기 위해서는 AH에 02h

넣고 DL에 출력할 문자의 아스키코드를 넣은 후 INT 21h를 호출한다.

#include

/* int86()을 사용한 문자 출력 */
void putch(char ch)
{
        union REGS reg;

        reg.h.ah = 0x0200;
        reg.h.dl = ch;
        int86(0x21, ®, ®);
}

/* intdos()를 사용한 경우 */
void putch(char ch)
{
        union REGS reg;

        reg.h.ah = 0x02;
        reg.h.dl = ch;
        intdos(®, ®);     /* 인터럽트 21h를 위한 함수 이므로 인터럽트 */
                                /* 번호를 지정하지 않는다 */
}

/* intr()를 사용한 경우 */
void putch(char ch)
{
        struct REGPACK regpack;

        regpack.r_ax = 0x0200;  /* 02h가 AH에 들어가야 하므로 */
                                /* regpack.r_ax = 0x02 라고 하면 r_ax에는 */
                                /* 0x0002가 들어간다. 이것은 AL에 02h를 할당 */
                                /* 하게되는 결과가 되므로 0x0200이라고 해야 */
                                /* AH에 정확히 0x02가 할당된다. */
        regpack.r_dx = ch;
        intr(0x21, ®pack);
}

앞서의 인터럽트에 대한 고리타분하고 지루한 설명에 비하면 인터럽트를 호출하

일은 의외로 간단함을 알 수 있다. 그러나 이것이 인터럽트의 전부는 아니다.

위에서는 구조체를 사용했지만 이번에는 인라인 어셈블리를 사용해 보자. 목적과
결과는 동일하다.

#pragma inline          /* 이 코드를 컴파일하려면 어셈블러가 있어야 한다 */

void putch(char ch)
{
        asm mov ah, 02h
        asm mov dl, ch
        asm int 21h
}

인라인 어셈블리를 사용하면 C의 내장함수를 사용했을 때 보다 간결해진다.
소스에서 뿐만이 아니라 목적파일의 크기도 줄어든다.

Turbo/Borland C/C++에서는 위와 같은 전통적인(?) 방법외에 아주 특별하고도
손쉬운 방법을 제공하고 있다. 아래의 코드를 보자.

#include
void putch(char ch)
{
        _AH = 0x02;
        _DL = ch;
        geninterrupt(0x21);
}

어떤가? 인라인 어셈블리를 사용했을 때 만큼이나 간결하고 손쉬워 졌다.
위에서 보인 '_AH'나 '_DL'과 같은 변수는 '레지스터 가상(pseudo)변수'라고 불

는 특별한 변수형태이다.

        _AX, _BX, _CX, _DX,
        _AH, _AL, _BH, _BL, _CH, _CL, _DH, _DL,
        _SI, _DI, _SP, _BP, _FLAGS
        _CS, _DS, _SS, _ES

이렇게 80x86의 기본 레지스터들에 대한 모든 형태가 정의되어 있고, 이들을 사용
하면 레지스터에 대한 직접적인 입출력을 할 수 있다.
하지만 이들은 다른 컴파일러와는 호환성이 없다. 다시 말하면, 다른 컴파일러에
서는 사용할 수 없고 Turbo/Borland C/C++에서만 사용할 수 있다는 말이다.

#include

void main(void)
{
        printf("\nCS = %p", _CS);
        printf("\nDS = %p", _DS);
        printf("\nSS = %p", _SS);
        printf("\nES = %p", _ES);
}

이 코드를 실행하면 현재의 각 세그먼트 레지스터들의 값을 출력한다.

또 다른 예로, 레지스터 가상변수를 활용해서 시스템의 기본메모리가 얼마인지
알아 보는 코드를 작성해 보자. 기본 메모리의 양을 얻기 위해서는 INT 12h를
사용한다.

/***********************************************
 INT 12h - 입력: 없음
           출력: AX = 메모리 크기(단위=KB)
***********************************************/

#include
#include

void main(void)
{
        unsigned base_mem;

        geninterrupt(0x12);             /* 인터럽트 호출 */
        base_mem = _AX;                 /* AX = 기본 메모리 크기(KB) */

        printf("Base Memory: %uKB", base_mem);
}

*** 주의! 레지스터라는 것은 CPU가 항상 사용하는 것이므로 수시로 값이 바뀐
다.
    그러므로 인터럽트를 호출하고 난 후 필요하다면 곧바로 레지스터의 내용을
    저장해 두어야 한다. 그렇지 않으면 그 값이 그대로 유지될 것이라고 기대할
    수 없다. 만약 위의 코드에서

        printf("Base Memory: %uKB", _AX);

    라고 한다면 _AX의 내용을 출력하는 당시의 값이 앞서 얻어진 메모리의 양이
    될 확률은 거의 제로에 가깝다. printf를 호출하면서 AX의 값이 변경되기 때
    문이다. 반드시 먼저 다른 변수에 저장해 두었다가 사용되어야 한다.

-------------------------------------------------------------------
메모리의 양을 얻는 코드를 int86이나 intr등의 함수를 사용해서 작성해 보세요.


-------------------------------------------------------------------
인터럽트(Interrupt)

(4)

* 명확하고 간략한 의사전달을 위해 경어를 사용하지 않았음을 양해바랍니다.
-------------------------------------------------------------------
본 강좌에서 거론될 예문들은 Borland의 Turbo C/C++, Borland C/C++ 컴파일
러를
기준하였음을 알려드립니다.  예제들을 제외한다면 인터럽트에 대한 기본적인 내
용들은 어느 특정 언어에 국한되지 않습니다.
-------------------------------------------------------------------

 [ 훅킹(Hooking)과 체이닝(Chaining) ]

IBM PC는 쉴 새 없이 인터럽트를 수행하고 있다. 도스의 프롬프트가 깜박이는 순
간에도, 프로그램을 사용하고 있는 순간에도....
CPU에 의해서건, 주변장치에 의해서건, 또는 소프트웨어에 의해서건 인터럽트는
끊임없이 일어나고 있으며, 덕분에 컴퓨터는 시계를 유지하고 커서를 깜박일 수
있으며 그 경황중에도 파일을 복사할 수 있는 것이다. 이것은 마치 하나의 컴퓨
터가 동시에 여러가지 일을 하고 있는 것처럼 느껴지게 한다.
하지만 불행하게도(?) 일반적인 PC의 기준은 단 한개의 CPU에 의해 시스템 전체
가 통제되므로 한 순간에 한가지 일밖에는 할 수가 없다.  다만 '인터럽트'라고
하는 기발한 착상에 의해 마치 여러가지 일이 동시에 일어나고 있는 것처럼  보
여질 뿐인 것이다.

인터럽트를 호출하는 입장에서 봐서,  인터럽트는 동작하는 방법에 따라 두가지
의 형태로 분류된다.

        (1) 훅킹(Hooking)
        (2) 체이닝(Chaining)

'훅킹'이란 원래의 인터럽트를 자신의 프로그램으로 대체하는 것을 말하는 것으
로 인터럽트가  걸리게 되면 원래의 인터럽트 내용은 무시되고 지정된 프로그램
내용을 수행하게 된다. 이의 대표적인 예로 'Ctrl-C'처리 루틴이 있다.
전통적인 경우에, 프로그램 수행 중 'Ctrl-C'를 누르게 되면 의도적이었던 실수
였던 그런 시비를 가릴 틈도 주지 않고 인터럽트 23h가 걸리고 프로그램은 비정
상적으로 종료하게 된다.
이 과정에서 보기 좋게 장식해 놓은 화면이 깨지고(^C) 보기 흉하게 되고 만다.

대부분의 상업적인 목적의 프로그램에서는 이러한 문제점을 극복하기 위해,
키보드로 부터 'Ctrl-C'가 감지되었을 때(INT 23h가 걸렸을 때) 경고 메세지를
보내거나 이를 무시하게 된다.

이렇게 인터럽트를 가로채고('가로채기'를 가로챈다) 특별한 내용을 수행하도록
계획된 루틴을 '인터럽트 핸들러(Interrupt Handler)'라고 한다.

'체이닝'은 훅킹과는 조금 다른 양상을 띠고 있다. '훅킹'이 특정한 인터럽트의
원래 내용 전체를 무시하고 자신의 루틴을 수행하는 반면 '체이닝'은 원래의 내
용을 추가 또는 수정한 것이라 할 수 있다. 말하자면, 원래의 루틴은 반드시 수
행된다는 것이다. 다만 그 루틴은 프로그램에 의해 '가공'된 내용이 된다.
  이에 대한 예로 통상 '도스 인터럽트'라고 불리는 인터럽트 21h를 가로챈다고
생각해 보자. 인터럽트 21h는 수많은 서브루틴을 포함하고 있다. 그러나 프로그
램에서 필요한 것은 간단하다. 한 개의 문자를 받아 들이는 부분에서 문자를 검
색해서 선택적으로 받아 들이기 위해 작은 부분의 내용만 수정하기를 원하는 것
이다(만약 인터럽트 21h의 내용 전부를 가로채고자 하는 사람이 있다면... 열심
히 공부해서 Microsoft의 MS-DOS개발팀에 참가하는  빠를 것이다).
이 경우의 수정된 루틴의 형태는 아래와 같다.

        - 한 문자 입력 함수를 호출하고 있는가?
          서브루틴의 번호가 AH에 저장되어 있을 것이므로 AH를 검사해 보면
          알 수 있다(예: AH = 01h, 한 문자 입력(Echo))

        - 프로그램에서 정의된 검사루틴과 이에 따른 처리

        - 원래의 인터럽트로 연결(Chaining)

위의 동작 과정이 잘 이해되지 않는다고 해서 걱정할 것은 없다. 나중에 실제로
해볼 것이다.


 [ 인터럽트 핸들러(Interrupt Handler) ]

여러분은 아마도 '이벤트핸들러(Event Handler)'나 '에러핸들러(Error Handler)'
라는 말을 들어 보았을 것이다.

'핸들러(Handler)'는 어떤 것일까? 사전적인 의미로 본다면 '조종하는 것'이라고
할 수 있다. 그럼 무엇을 조종하는가?
'에러 핸들러'라면 '에러'를 '조종하는 것'이라는 말이 되겠는데... 대체 에러를
어떻게 조종한다는 말일까?

다음의 프로그램은 '훅킹'방법을 사용한 'Ctrl-C'인터럽트(23h)를 가로채는 예제
이다. 먼저 이를 보도록 하자.

#include
#include

#ifdef __cplusplus
#       define CPPARG   ...
#else
#       define CPPARG
#endif

unsigned ctrl_c_flag = 0;
void interrupt (*old_ctrl_c)(CPPARG);   /* 이전의 인터럽트 핸들러 */

void interrupt new_ctrl_c(CPPARG)       /* 새로운 인터럽트 핸들러 */
{
        sound(1000);                    /* '삑' 소리를 출력한다 */
        delay(100);
        nosound();
        ctrl_c_flag = 1;                /* Ctrl-C가 눌렸다면 1, 아니면 0 */
}

void main(void)
{
        int c = 32;                     /* 아스키문자를 찍기위해... */

        old_ctrl_c = getvect(0x23);     /* 원래의 인터럽트를 저장한다 */
        setvect(0x23, new_ctrl_c);      /* 새로운 인터럽트를 설정한다 */

        while(!ctrl_c_flag)             /* Ctrl-C가 눌려질 동안 계속... */
        {
                putc(c++, stdout);      /* 표준 출력 */
                if(c >= 255) c = 32;    /* 다시... */
        }

        setvect(0x23, old_ctrl_c);      /* 원래대로 되돌려 놓자 */
}

위의 프로그램을 실행하면 표준출력(화면)에 끊임없이 아스키문자를 출력한다.
프로그램을 중지하기 위해서는 'Ctrl-C'를 누른다.
'Ctrl-C'를 누르면 '삑'소리(이것은 새로운 인터럽트 핸들러에서 구현됐다)를
내고 프로그램을 종료하게 된다.

위의 프로그램이 정확히 Ctrl-C인터럽트(23h)를 가로채고 있는지를 비교하기
위해서 새로운 인터럽트 핸들러를 빼고 단순히 문자 출력 부분만을 가지고
실행한 후 Ctrl-C를 눌러 보라!

***** 주의!
***********************************************************
'훅킹'할 수 있는 것과 할 수 없는 것의 구분을 할 필요가 있다. 'Ctrl-C'루
틴의 경우에, 이 루틴은 다른 루틴으로 완전히 교체된다고 해도 시스템의 운
영에 지장이 없다.  또다른 예로 '치명적인 에러'에 대한 핸들러인 24h 등은
훅킹할 수 있다. 그러나 이와 다르게 인터럽트 21h를 훅킹하겠다고 한다면..
이는  100% 시스템 다운의  슬픔을 맛봐야 할 것이다. 이런 경우는 '훅킹'이
아니라 '체이닝'을 해야 한다.  체이닝은 기존의 인터럽트 루틴에 크게 영향
을 주지 않으면서 인터럽트를 가로챌 수 있다. 또다른 예로 '시스템 클럭'을
들 수 있다. 시스템 클럭은 컴퓨터의 심장인데, 컴퓨터의 모든 일은 이 클럭
의 박자에 맞춰서 행해진다.  그런데 이것을 훅킹하려고 한다면 이는 자살행
위와 같다고 할 수 있다. 반드시 '체이닝'이 되어야 한다.
*******************************************************************
****
아래의 내용은 본 강좌와는 크게 상관이 없다고 할 수 있지만 들어 두어서
나쁠건 없겠다 싶어서 적어 본다.

>>>> 참고: 윈도우즈의 멀티태스킹(Multi-Tasking) <<<<

윈도우즈를 한번이라도 사용해 본 분들은 윈도우즈의 능력에 무척 놀랄 것이다.
일반 사용자들은 도스는 그렇게 못해도 윈도우즈는 여러가지 일을 동시에 처리
할 수 있는 것으로 알고 있다. 그러나 아쉽게도 윈도우즈 또한 진정한 의미에서
의 '다중작업'을 하지는 못한다. 이는 윈도우즈의 능력(?)탓이 아니라 IBM PC의
하드웨어적인 특성 때문이다. '머리(CPU)'가 한개 뿐인데 어떻게 동시에 두가지
를 생각할 수 있겠는가? 인간에 있어서도 이는 마찬가지이다. 작은 자료들이 상
상할 수 없을 정도의 속도로 모여서 하나의 완성된 정보로 표현되어지는 것이니
결국 순간 순간의 내용은 동시에 일어나는 것이 아니라 차례대로 일어나고,  그
간격이 크던 작던 얼마만큼의 차이는 있게 마련인 것이다.

여기에서 별반 관계없을 듯한 윈도우즈 이야기를 꺼낸 것은 나름대로 이유가 있
어서인데,  이유야 어찌되었던 진정한 '다중작업'을 수행할 수 없는 IBM PC에서
형식적이나마  '다중작업'을 흉내내기 위해서 윈도우즈가 사용한 '기교'를 이해
할 필요가 있기 때문이다.  윈도우즈는 어지럽게 열려 있는 여러개의 '창'과 윈
도우즈 간에 메세지를 주고받기 위해 '반복(Loop)'을 사용한다.
말하자면 'for'나 'while'문을 사용한다는 말이다(이에 비해 도스에서는 여러가
지 복합된 처리를 위해 일반적으로 인터럽트가 소용된다).

윈도우즈는 현재 선택된 창을 기준으로 모든 것을 판단한다.   즉, 현재 눌려진
키나 마우스의 움직임 또는 버튼의 눌림이 현재 선택되어 있는 창(작업)을 기준
으로 해석되고 평가된다는 것이다. 이 말은 또한 현재 선택되어진 창에 모든 권
한을 부여하고 있다는 말과 같다.  엄밀히 따지자면, 윈도우즈는 여러개의 작업
을 동시에 처리하고 있는 것이 아니라  오직 하나의 창을 엄밀히 감시하고 있다
고 보아야 할 것이다.  마우스 버튼이 눌린 위치가 현재 창의 범위를 벗어 난다
면 윈도우즈는 그 위치가 선택 가능한 위치인지를 판단한다. 만약 다른 창의 범
위라면 기존의 창은 '비선택창'으로 바뀌고 새롭게 선택 창을 설정하며 그 창을
전면에 내세운다.  여기에서 윈도우즈가 '창의 위치를 판단하는 기준'이 문제가
되는데...,  이러한 기준은 윈도우즈가 가지고 있는 것이 아니라 별개의 '창'들
이 가지고 있다.  이것이 '창'을 '독립된 객체'로 존재하게 하는 것이다(아마도
윈도우즈의  이러한 내용이 C++의 OOP에 좀 더 부합된다 해서 C보다는 C++가

도우즈 프로그래밍에 더 충실할 것이라고 생각되게 하는 요인이 아닌가 싶다.
그러나, 우스게 소리지만 그것은 '어림 반푼 어치도 없는 생각'이다).
이를 위해서 윈도우즈의 각 '창'들은 윈도우즈와 끊임없이 통신을 '반복'한다.
인터럽트가 아닌 '반복(Looping)'에 의해...

 

-------------------------------------------------------------------
인터럽트(Interrupt)

(5)

* 명확하고 간략한 의사전달을 위해 경어를 사용하지 않았음을 양해바랍니다.
-------------------------------------------------------------------
본 강좌에서 거론될 예문들은 Borland의 Turbo C/C++, Borland C/C++ 컴파일
러를
기준하였음을 알려드립니다.  예제들을 제외한다면 인터럽트에 대한 기본적인 내
용들은 어느 특정 언어에 국한되지 않습니다.
-------------------------------------------------------------------

지난 강좌에서 예제로 보였던 'Ctrl-C' 인터럽트(23h)에 대한 '훅킹'은 대표적인
'예외 상황에 대한 대처'의 한 방법이다.   자기가 만든 프로그램을 직접 사용할
때는 프로그램에 대해 잘 알고 있는 상황이므로 자연히 조심성을 가지게 되고 이
는 예외적인 에러를 만나기 어렵게 한다.  그러나 다른 사람이 그 프로그램을 사
용하는 경우라면 상황은 매우 나빠진다.  프로그램이 가진 최대한의 능력을 시험
해 보고 싶어지는 것이다. 그래서 이것 저것 건드려 봄으로서 프로그램을 만든이
가 생각하지 못했던 여러가지 오동작(?)을 일으키게 된다. 이러한 상황은 프로그
머에게 있어서 '예외상황'이 된다.  그래서 프로그래머는 가능성 있는 모든 경우
에 대비해야만 하는 것이다(이것은 정말 큰 부담이다. 사용자가 메뉴얼에서 지시
된 대로만 프로그램을 동작해 준다면 프로그래머가 이런 부담까지 가지지 않아도
되겠지만, 무정하게도 사용자는 그렇질 못하다. 항상 새로운, 좀 더 색다른 일을
시도해 보고자 하는 것이다).

이렇게,  발생할 수 있는 에러에 대비하기 위해 프로그래머가 취할 수 있는 최선
의 대비책은 발생할 수 있는 에러를 가늠하고 이에 대한 '자신의' 루틴을 개발하
여 적절한 상황에서 그 루틴을 실행하는 것이다. 이러한 역할을 하게되는 루틴을
'에러 핸들러'라고 한다.

극단적인 예로 플로피 드라이브에 접근하는 경우가 있다.  사용자는 종종 플로피
드라이브에 디스켓을 넣지 않은 상태에서, 또는 드라이브의 문을 잠그지 않은 상
태에서 디스켓 간의 복사 명령을 수행할 수 있다.  적당한 '에러 핸들러'가 준비
되지 않았다면 화면은 일그러지고 다음과 같은 전통적인 도스 메세지를 출력한다.

        Not ready reading drive A
        Abort, Retry, Fail?

도스에는 분명히 이런 메세지를 내는 루틴을 가지고 있을 것이다. 우리의 '에러
핸들러'는 그 루틴을 가로채고 보다 우아하게, 그리고 보다 친절하게 메세지 창
을 열어서 사용자에게 무엇이 잘못되었는지를 가르쳐 줄 수 있다.

        +--------------------------------------+
        | 드라이브 문을 닫지 않았군요...       |
        | 문을 닫고 를 누르면 다시 하고 |
        | 를 누르면 취소합니다.           |
        +--------------------------------------+

 사용자가 이런 메세지를 본다면 도스의 불친절한 메세지를 볼 때 보다는 훨씬
믿음직스럽게 생각할 것이다.


[ 에러 핸들러(Error Handler)의 구현 ]

아래에 소개된 도스의 '치명적인 에러'에 대한 핸들러는 Borland C/C++의
'Library Reference'에 소개된 내용에 일일이 해설을 붙였다. 직접 작성해 볼
수도 있겠지만 가장 무난하고, 또 작성이 손쉬울 것 같아서 그 내용을 발췌한
것이다.

#include
#include
#include

/* 사용자의 반응에 따른 되돌림 값을 정의한다. 인터럽트 0x24가 발생된 후
   인터럽트 핸들러는 이 값 중의 하나를 AL에 되돌려 프로그램의 진행을 가
   늠하게 된다.
*/
#define IGNORE          0       /* AL=0 이면 에러를 무시한다 */
#define RETRY           1       /* AL=1 이면 다시 시도한다 */
#define ABORT           2       /* AL=2 이면 프로그램을 종료한다 */

/* 전역변수는 정적 메모리에 위치한다. 여기서의 'buf'는 명시적으로 사용
   되지는 않지만 프로그램에 의해 정의된 에러핸들러가 수행되면서 행여
   스택이 파괴되는 현상이 발생할 수 있기 때문에 이를 예방하기 위해 스택
   과 맞닿은 메모리 위치에 얼마간의 공간을 확보해 둘 필요가 있다.
   프로그램이 실행되었을 때의 메모리의 배치에 관한 내용은 여타의 시스템
   프로그램 관련 서적이나 Turbo/Borland C/C++의 메뉴얼을 참조하기 바란다.
*/
int buf[500];

/* 도스의 '치명적인 에러'에 의해 보고되어지는 에러의 유형을 정의한다.
   여기에서는 모든 에러의 유형이 소개된 것이 아니다(확장된 것들에 관한
   보다 상세한 내용은 MS-DOS 프로그밍 메뉴얼들을 참조)
*/
static char *err_msg[] =
{
        "write protect",
        "unknown unit",
        "drive not ready",
        "unknown command",
        "data error (CRC)",
        "bad request",
        "seek error",
        "unknown media type",
        "sector not found",
        "printer out of paper",
        "write fault",
        "read fault",
        "general failure",
        "reserved",
        "reserved",
        "invalid disk change"
};

/* 화면에 에러의 유형을 표시하고 사용자에게 어떻게 처리하면 좋을까를
   물어 그에 대한 응답을 에러 핸들러에게 돌려준다.
*/
message(char *msg)
{
        int retval;     /* 에러핸들러에게 되돌릴 값을 저장할 변수 선언 */

        cputs(msg);     /* 에러의 유형을 표준 출력(stdout)에 표시한다 */

        while(1)        /* 적절한 입력이 있을 때 까지 계속 반복한다 */
        {
                retval = getch();       /* 키입력을 기다린다 */

                if(retval == 'a' || retval == 'A')      /* Abort인가? */
                {
                        retval = ABORT;
                        break;
                }
                if(retval == 'r' || retval == 'R')      /* Retry인가? */
                {
                        retval = RETRY;
                        break;
                }
                if(retval == 'i' || retval == 'I')      /* Ignore인가? */
                {
                        retval = IGNORE;
                        break;
                }
        }

        return (retval);        /* 사용자의 응답을 되돌린다 */
}

/* 도스의 '치명적인 에러'에 대한 새로운 핸들러.
   에러가 발생하면 기존의 에러 핸들러 대신 이 루틴이 실행된다.
*/
#pragma warn -par       /* 이 부분은 전해진 파라미터가 사용되지 않았다는
                           경고 메세지를 무시하기 위해서 쓰여졌다.
                           아래의 새로운 핸들러인 'handler'에 전달된 파
                           라미터들이 루틴내에서 모두 사용되지 않았기
                           때문이다. 그러나 파라미터들을 생략해서는 안된
                           다. 'handler'에 전달된 파라미터들은 안전하게
                           보존되어야만 하는 레지스터를 위해 필요하다.
                           ax, bp, si이외에도 ss, sp, ds, es, bx, cx, dx
                           등이 보존되어야 하지만 이에 대한 것은 Turbo C에
                           의해 이루어 진다. */

int handler(int errval, int ax, int bp, int si)
{
        static char msg[80];
        unsigned di;
        int drive;
        int errorno;

        di = _DI;       /* DI 레지스터의 하위바이트에는 에러 유형에 대한
                           인덱스 값이 들어 있다. 레지스터는 언제 바뀔지
                           모르므로 필요하다면 저장해 두어야 한다 */

        if(ax < 0)      /* AH의 최상위 비트(부호 비트)가 설정되었다면
                           복구할 수 없는 에러(장치 자체에 문제가 있는)
                           가 발생한 것이다. 이 경우에는 사용자의 입력이
                           무의미 하므로 바로 프로그램을 종료시킨다. */
        {
                message("Device error");
                hardretn(ABORT);
        }

        drive = ax & 0x00FF;    /* AL에 드라이브의 번호(0=A,1=B,...)가
                                   들어 있다 */
        errorno = di & 0x00FF;  /* DL에는 에러의 유형값이 들어 있다 */

        sprintf(msg, "Error: %s on drive %c\r\nA)bort, R)etry, I)gnore: ",
                err_msg[errorno], 'A' + drive);

        hardresume(message(msg));       /* 사용자의 응답을 기다린다 */
        return ABORT;
}

#pragma warn +par       /* 위에서 무시했던 경고 메세지를 원상태로 복구한다.
                           이후 부터는 전해진 파라미터가 사용되지 않으면
                           '파라미터가 사용되지 않았다'는 경고를 출력한다. */

int main(void)
{
        harderr(handler);       /* 새로운 에러 핸들러를 설정한다.
                                   'harderr'를 사용하지 않는다면 직접
                                   인터럽트 24h에 대한 벡터를 가로채야만
                                   하는데(setvect등을 통해) 과정이 다소
                                   복잡해지게 된다. 보다 손쉬운 방법이
                                   있다면 그걸 사용하는 것이 좋지않을까? */

        printf("Open the door of drive A:\n");  /* A드라이브의 문을 열고 */
        printf("press a key...\n");             /* 키를 하나 누르기를    */
        getch();                                /* 기다린다 */

        printf("Trying to access drive A:\n");
        fopen("A:TEST.ERR", "w");               /* 문이 열려 있는 A드라이브
                                                   에서 파일을 열려고 하면
                                                   당연히 에러가 발생할 것
                                                   이다. 이에 의해 위에서
                                                   설정된 새로운 핸들러 루틴
                                                   이 실행된다 */

        return 0;               /* 도스에게 무사히 수행되었음을 알린다 */
}


위의 에러 핸들러는 '훅킹'과 '체이닝' 중에서 어디에 속하는가?
아시겠지만, Ctrl-C에 대한 핸들러와 함께 대표적인 '훅킹'에 의한 인터럽트
가로채기의 하나라고 할 수 있다.

다음 강좌에서는 '체이닝'에 의한 인터럽트 가로채기의 예로 '시계'를 만들어 보
려고 한다. 자신이 만든 프로그램에서 프로그램 수행 중에 끊임없이 '똑딱'거리는
시계가 화면의 한 모퉁이에 보여 진다면 매우 편리할 것이다. 다음 강좌에서는
그런 시계를 '체이닝'을 통해 구현해 보도록 하자.


-------------------------------------------------------------------
인터럽트(Interrupt)

(6)

* 명확하고 간략한 의사전달을 위해 경어를 사용하지 않았음을 양해바랍니다.
-------------------------------------------------------------------
본 강좌에서 거론될 예문들은 Borland의 Turbo C/C++, Borland C/C++ 컴파일
러를
기준하였음을 알려드립니다.  예제들을 제외한다면 인터럽트에 대한 기본적인 내
용들은 어느 특정 언어에 국한되지 않습니다.
-------------------------------------------------------------------
체이닝(Chaining)에 의한 인터럽트 가로채기는 기존의 인터럽트를 손상하지 않

면서 하고자 하는 일을 온전히 처리해야만 하는 까닭에 매우 신중하게  처리되어
야 한다.

지난 강좌에서 예고 한 바와 같이 이번 강좌에서는 체이닝 방법에 의한 '시계'를
구현해 볼 것인 바, 그 전에 체이닝의 간단한 예를 보고 그 형태를 가늠해 보자.

/* ----------------------------------------------------------------
---
   Program ..... KEYBOARD.C
   Author ...... Seon Geun, Park. 1995. 01.
   Notice ...... 이 코드는 키보드 인터럽트인 09h를 가로챈다.
                 프로그램을 실행한 후 키를 누를 때 마다 소리를 내게 되고
                 ESC를 누르면 종료한다.
------------------------------------------------------------------
*/
#include
#include

#ifdef __cplusplus
#       define __CPPARGS        ...
#else
#       define __CPPARGS
#       pragma warn -pro        /* '원형이 없다'는 경고를 억제한다 */
#endif

typedef int             BOOL;
#define TRUE            1
#define FALSE           0

#define K_ESC           27

BOOL pressed = FALSE;           /* 이 변수가 필요한 이유가 뭘까? */

void interrupt (*old_keyboard)(__CPPARGS);

void interrupt new_keyboard(__CPPARGS)
{
        old_keyboard();                         /* 원래의 인터럽트에 연결 */

        pressed = (pressed) ? FALSE : TRUE;     /* pressed가 TRUE이면
FALSE로,
                                                   아니면 TRUE로 설정한다.
                                                   이런 상태를 일반적으로
                                                   'Toggle'이라 한다. */

        if(pressed == TRUE)                     /* pressed가 TRUE인 경우에만 */
        {
                sound(500);
                delay(20);
                nosound();
        }
}

int main(void)
{
        int c;

        old_keyboard = getvect(0x09);   /* 기존의 키보드 인터럽트를 보관하자
*/
        setvect(0x09, new_keyboard);    /* 새로운 핸들러를 설정하자 */

        while((c = getch()) != K_ESC)   /* 입력이 ESC가 아닌 동안 계속... */
        {
                putch(c);               /* 입력된 내용을 출력하자 */
        }

        setvect(0x09, old_keyboard);    /* 원래 인터럽트 내용을 복구 */
        return 0;                       /* 잘 수행 되었다 */
}

이 프로그램은 키보드 인터럽트(09h)를 가로채고, 체이닝한다.
새로운 핸들러인 new_keyboard는 가장 먼저 원래의 인터럽트 루틴으로 자신을
연결(Ch
aining)시키고 있다. 훅킹에 비해 달라진 점은 바로 이것인데,  만약 이런 연결
이 없다면 이후 어떠한 키보드 입력도 받을 수 없게 된다. 우리의 새로운 핸들러에
는 소리를 내는 부분만 있을 뿐 입력 받은 것을 처리하는 어떠한 부분도  포함하고
있지 않은 것이다. 게임 프로그래밍에 있어서 키보드의 조작은 매우 중요하다.
물론 다른 프로그램에 있어서도 마찬가지겠지만 게임은 보다 능률적인 키보드 조

을 요구한다.  키를 누를 때 마다 소리를 낼 수 있다는 것도 게임에 있어서는 요긴
한 기술이 될 수 있을 것이다.

위의 예로 보아 체이닝에 의한 인터럽트 가로채기 라고 해서 훅킹에 의한 방법과
크게 다를게 없고, 중요한 것이라면 기존의 인터럽트에 적절히 연결시키는 것과
해당 인터럽트가 걸렸을 때 새로운 핸들러에서 어떤 처리를 추가로 할 수 있는가
하는 것이다. 사실 C에서의 체이닝에 의한 인터럽트 가로채기 프로그램들은 위에
소개된 코드의 기본 골격을 바탕으로 이루어 진다. 기본적인 모양은 크게 변화가
없는 것이다.


 [ 시계프로그램의 구현 ]

인터럽트 1Ch는 시분할을 통해 어느 정도(?)의 다중작업을 가능하게 한다.
이 인터럽트는 1초에 약 18회 호출되는데, 프로그램은 이를 가로챔으로써 인터럽

가 걸릴 때 마다 특정한 일을 할 수 있게 된다. 시계프로그램은 1Ch를 가로채고,
1초에 18회 만큼 시간을 검사해서 초가 바뀌게 되는 순간에 그 내용을 화면에 출

할 것이다.

/* ----------------------------------------------------------------
---
   Program ..... CLOCK.C
   Author ...... Seon Geun, Park. 1995. 01.
   Notice ...... 이 코드는 키보드 인터럽트인 1Ch를 가로채고 1초 마다 '틱~'
                 소리를 내며 ESC를 누르면 종료한다.
------------------------------------------------------------------
*/
#include
#include

#ifdef __cplusplus
#       define __CPPARGS        ...
#else
#       define __CPPARGS
#       pragma warn -pro        /* '원형이 없다'는 경고를 억제한다 */
#endif

typedef int             BOOL;
#define TRUE            1
#define FALSE           0

#define K_ESC           27

unsigned int clock_tick = 0;

void interrupt (*old_timertick)(__CPPARGS);

void interrupt new_timertick(__CPPARGS)
{
        old_timertick();        /* 원래의 인터럽트를 수행 */

        clock_tick++;           /* 클럭-틱을 증가한다 */

        if(clock_tick > 18)     /* 18번 호출되었다면 1초가 지난 것이다 */
        {
                sound(400);
                delay(20);
                nosound();
                clock_tick = 0; /* 클럭-틱을 처음부터 다시 센다 */
        }
}

int main(void)
{
        int c;

        old_timertick = getvect(0x1C);  /* 기존의 클럭-틱 인터럽트를 보관하자 */
        setvect(0x1C, new_timertick);   /* 새로운 핸들러를 설정하자 */

        while((c = getch()) != K_ESC)   /* 입력이 ESC가 아닌 동안 계속... */
        {
                putch(c);               /* 입력된 내용을 출력하자 */
        }

        setvect(0x1C, old_timertick);    /* 원래 인터럽트 내용을 복구 */
        return 0;                       /* 잘 수행 되었다 */
}

이 프로그램은 화면에 시간을 표시하지는 않는다. 대신 1초마다 '틱~'소리를 내어
시간이 가고 있음을 알린다. 우선은, 위에서 보인 키보드 인터럽트 처리의 경우와
형태적으로 동일함을 알 수 있다. 바뀌었다면 인터럽트 09h대신 1Ch를 가로챘다

것 뿐...
이 프로그램에서 중요한 것은 프로그램을 수행한 후 어떤 것이던 키보드를 계속
누르고 있어도 프로그램은 계속적으로 시간이 가고 있음을 알린다는 사실이다.
확인해 보라.

이 강좌를 보시는 분께 죄송한 말씀을 드려야 할 것 같다. 이번 강좌에서 시간을
화면에 출력하는 프로그램을 선보이려 했으나 필자의 개인적인 일로 프로그램을
미처 완성하질 못했다. 솔직히, 만들기는 했으나 중요한 결함을 발견했고, 그것
을 바로잡을 시간을 갖질 못했다. 결함의 원인은 이미 알고 있으나 그 대책을 세
우고 바로잡기 위해서는 다소의 시간이 필요할 것 같다.

핸들러나 램상주 등의 프로그램은 인터럽트 제어 프로그램의 핵심이지만 그렇다

MS-DOS가 프로그래머를 위해 제공하는 자료가 풍부한 것은 결코 아니다.  오히
려 빈약
하다. 여러 시스템에서 무리 없이 돌아가는 프로그램을 작성한다는 것은 그만
큼 풍부한 자료를 필요로 하지만 주변 여건은 그렇질 못한 것이다. 이에 죄송한
말씀을 드리고, 강좌가 너무 늦어 지는 것 같아 이렇게 나마 글을 올리고 조만간
결함을 고쳐 다음 강좌에서 완성된 프로그램을 보여드리려고 한다.


-------------------------------------------------------------------
인터럽트(Interrupt)

(7)

* 명확하고 간략한 의사전달을 위해 경어를 사용하지 않았음을 양해바랍니다.
-------------------------------------------------------------------
본 강좌에서 거론될 예문들은 Borland의 Turbo C/C++, Borland C/C++ 컴파일
러를
기준하였음을 알려드립니다.  예제들을 제외한다면 인터럽트에 대한 기본적인 내
용들은 어느 특정 언어에 국한되지 않습니다.
-------------------------------------------------------------------

이번 강좌는 프로그램 설명하는 내용으로 채워야 할것 같다.
아래에 소개된 코드는 지난 강좌에서 약속했던 체이닝에 의한 시간 출력 프로그램
이다. 주의할 것은, 여기에 소개된 시계는 '정확성'에 대해 그다지 신경을 쓰지
않은, 다만 인터럽트 핸들러의 한 예를 보여주기 위한 목적으로 작성된 것임을 염
두에 두기 바란다.
내용을 보면 아시겠지만, 간단히 핸들러가 호출된 횟수를 비교함으로써 시간을 검
사하고 있다. 정확한 시계를 얻기 위해서는 타이머 칩을 프로그램하는 등의 보다
세밀한 조작이 요구된다. 그렇다고 해서 이 프로그램이 실제 시간과 전혀 맞지 않
다는 것은 아니다. 어느 정도의 시차는 있을 수 있다는 것이다.
그리고 이 프로그램은 텍스트 모드에서만 원하는 결과를 볼 수 있음도 주의해야
한다.

/* ----------------------------------------------------------------
---
   Program ..... CLOCK2.C
   Author ...... Seon Geun, Park. 1995. 01.
   Notice ...... 이 코드는 타이머-틱 인터럽트인 1Ch를 가로채고 현재의 시스템
                 시간을 화면에 출력한다. ESC를 누르면 종료한다.
-------------------------------------------------------------------
 */
#include
#include
#include

#ifdef __cplusplus
#       define __CPPARGS        ...
#else
#       define __CPPARGS
#       pragma warn -pro
#endif

typedef int             BOOL;
#define TRUE            1
#define FALSE           0

#define K_ESC           27

char far *screen = (char far *)0xB8000000L;

char time_str[10] = "00:00:00";                 /* 시간 문자열을 위한 버퍼 */
unsigned clock_tick = 0;                        /* 타이머-틱 횟수 계산용 */
unsigned hour = 0, minute = 0, second = 0;      /* 시,분,초 */

void interrupt (*old_timertick)(__CPPARGS);     /* 원래의 핸들러 보관용
                                                   함수 포인터 */

/* 시간 문자열을 화면 한 귀퉁이에 출력한다 */

void printtime(void)
{
        int i;

        /* 아래의 내용은 시간을 문자열로 변환하는 과정을 담고 있다.
           화면에 나타낼 문자열은 '시:분:초(00:00:00)'의 형태이다.
           시,분,초는 각각 2자리의 문자열로 이루어 지므로 시,분,초
           에 대해 수치값이 2자리 이상인지를 파악해야 한다.
           현재의 '시'를 담고 있는 hour를 문자열로 변환하는 과정을
           통해 그 방법을 알아 보자.

           먼저 hour가 10보다 크다면 이 수치는 2자리 이상이므로 문
           자열의 형태는 'xx'가 된다. 11이라면 문자열 '11'이 될 것
           이다.  반대로 10보다 작다면 문자열의 형태는 '0x'가 되므
           로 숫자가 9일 때 문자열 표현은 '09'가 된다.

           이렇게 수치가 2자리인지 1자리인지가 판명되고 나면 수치를
           문자값으로 바꿔야 하는데,  문자 '0'의 아스키 코드는 십진
           수 30이다. 그러므로 어떤 숫자(1자리)에 30을 더하면 그 숫
           자에 해당하는 문자의 아스키 코드를 얻을 수 있다. 예를 들
           어서 숫자가 9라면 9+30 = 39가 되는데 39는 바로 문자 '9'
           에 대한 아스키 코드가 되는 것이다. 이런 것이 얼른 이해되
           지 않는다면 책에 나오는 아스키 코드를 직접 보면서 상황을
           상상해 보라.

           이제 문제가 되는 것은 숫자가 2자리일 때 그 숫자를 한자리
           씩 분리하는 것인데 이 또한 매우 간단하다.
           해당 수치를 10으로 나누게 되면 그 수치에 대한 몫을 얻을
           수 있다. 우리가 다루는 시,분,초는 모두 최대 2자리 숫자값
           이므로 10으로 나누었을 때의 몫은 0에서 9사이의 값이 된다
           하지만 '시'는 00~23의 사이에 있으므로 결국 10으로 나누었
           을 때의 몫은 0~2중의 하나이고, '분'은 00~59사이에 있으므
           로 10으로 나눈 몫은 0~5중의 하나가 된다. '초'는 '분'과
           동일하다. 결과적으로, 2자리 숫자를 10으로 나눈 몫은 그
           숫자의 10자리수(상위자리수)가 되어 두 자리 중에서 한자리
           를 분리할 수 있게 되었다.
           그 다음으로 1자리수(하위자리수)는 10으로 나누었을 때의
           나머지가 된다는 것도 예상할 수 있다. C는 몫을 얻을 때의
           나눗셈과 나머지를 얻을 때의 나눗셈을 위해서 '/'와 '%'의
           두가지 연산자를 제공한다.
           위의 연산 결과로 얻어진 몫과 나머지에 문자 '0'에 대한 아
           스키 코드인 30을 더하게 되면 원하는 숫자에 대한 아스키
           문자(코드)를 얻을 수 있다. 이러한 일을 시,분,초에 대해
           각각 수행하고 그 결과들을 차례대로 준비된 문자열변수에
           저장하면 시간을 문자열로 변형하는 작업은 완료된다.
           */

        if(hour >= 10)  /* 2자리 수일 때 */
        {
                time_str[0] = (hour / 10) + '0';        /* '0' ~ '2' */
                time_str[1] = (hour % 10) + '0';        /* '0' ~ '3' */
        }
        else            /* 1자리 수일 때 */
        {
                time_str[0] = '0';                      /* '0'       */
                time_str[1] = (hour % 10) + '0';        /* '0' ~ '2' */
        }

        if(minute >= 10)
        {
                time_str[3] = (minute / 10) + '0';      /* '0' ~ '5' */
                time_str[4] = (minute % 10) + '0';      /* '0' ~ '5' */
        }
        else
        {
                time_str[3] = '0';                      /* '0'       */
                time_str[4] = (minute % 10) + '0';      /* '0' ~ '5' */
        }

        if(second >= 10)
        {
                time_str[6] = (second / 10) + '0';      /* '0' ~ '5' */
                time_str[7] = (second % 10) + '0';      /* '0' ~ '5' */
        }
        else
        {
                time_str[6] = '0';                      /* '0'       */
                time_str[7] = (second % 10) + '0';      /* '0' ~ '5' */
        }

        /* 시간에 대한 문자열이 완성되었으므로 그것을 화면에 출력한다.
           문자열이 '00:00:00'의 형태를 가지고 있으므로 모두 8문자로
           이루어 졌음을 알 수 있다.
           그런데 아래의 내용 중에서 '*(screen+142+(i*2))'라는 표현이
           있다. 'screen'은 char형 원거리 포인터로 선언되고 비디오 메
           모리의 처음 주소값을 갖도록 초기화되어 있다.
           비디오에서의 한 줄은 메모리에서 160바이트를 차지한다.
           ((문자 1바이트+색상값 1바이트) * 80문자)
           screen이 그 비디오 메모리의 처음 위치값을 갖고 있으므로 여
           기에 142를 더한 곳은 화면의 가장 첫째줄에서 거의 끝부분에
           해당한다(우리는 시간을 화면의 가장 윗줄, 오른쪽에 출력할
           것이다). 그리고 'i*2'가 더해진 것은, i가 0..7이고 비디오
           메모리에서 문자가 찍힐 위치는 screen+142+0, screen+142+2,
           screen+142+4,...이기 때문이며, 그 아래 문장에서 또 1이 더
           해진 것은 문자에 대한 색상값이 screen+142+1, screen+142+3,
           screen+142+5,...의 위치에 들어 가야 하기 때문이다.

           비디오 메모리에 관한 보다 자세한 내용은 다른 분들의 강좌나
           관련 서적을 참조하세요(그것만 해도 새로 강좌를 열어야 할 만큼
           분량이 많기 때문에 여기선 더이상 상세한 설명을 않겠습니다.
        */
        for(i = 0; i < 8; i++)
        {
                *(screen+142+(i*2)) = time_str[i];      /* 시간 문자열 */
                *(screen+142+(i*2)+1) = 0x70;           /* 역상 문자 속성 */
        }
}

void interrupt new_timertick(__CPPARGS)
{
        old_timertick();        /* 원래의 타이머-틱에 연결(Chaining)한다 */

        /* 타이머-틱 인터럽트(1Ch)는 1초에 약 18회 만큼 반복된다.
           그러므로 핸들러의 호출 횟수가 18번 이상인지 검사해야
           1초가 지났는지를 알 수 있다.
        */
        clock_tick++;

        if(clock_tick > 18)
        {
                second++;                       /* 초를 증가시킨다 */
                if(second > 59)                 /* 60초면 분이 증가한다 */
                {
                        second = 0;             /* 초를 처음 부터 다시 센다 */
                        minute++;               /* 분을 증가한다 */
                        if(minute > 59)         /* 60분이면 시간이 증가한다 */
                        {
                                minute = 0;             /* 분을 다시 센다 */
                                hour++;                 /* 시를 증가한다 */
                                if(hour > 23) hour = 0; /* 하루가 지났다*/
                        }
                }

                printtime();            /* 화면의 귀퉁이에 시간을 출력하자 */
                clock_tick = 0;         /* 처음 부터 다시 센다 */
        }
}

int main(void)
{
        struct time tm;
        char far *vmode = (char far *)0x00000449L;
        int c;

        /* 비디오 메모리에 대한 포인터를 설정한다
           바이오스의 데이타 영역에서 0000:0449에는 현재의 비디오 모드가 있고
           그 값이 7이면 흑백, 2또는 3이면 칼라이다.
           칼라일 경우 비디오 메모리는 B800:0000에서,
           흑백일 경우 B000:0000에서 시작 된다.
        */
        if(*vmode == 7) screen = (char far *)0xB0000000L;
        else screen = (char far *)0xB8000000L;

        gettime(&tm);           /* 프로그램이 시작될 때의 시간을 확인한다 */

        hour = (unsigned)tm.ti_hour;
        minute = (unsigned)tm.ti_min;
        second = (unsigned)tm.ti_sec;

        printtime();            /* 현재 시간을 출력한다 */

        old_timertick = getvect(0x1C);  /* 원래의 핸들러 보관 */
        setvect(0x1C, new_timertick);   /* 새로운 핸들러 설정 */

        while((c = getch()) != K_ESC)   /* ESC가 눌린 동안 계속 */
        {
                putch(c);               /* 눌려진 키를 출력하자 */
        }

        setvect(0x1C, old_timertick);   /* 원래의 핸들러 복구 */
        return 0;
}

 

-------------------------------------------------------------------
인터럽트(Interrupt)

(8)

* 명확하고 간략한 의사전달을 위해 경어를 사용하지 않았음을 양해바랍니다.
-------------------------------------------------------------------
본 강좌에서 거론될 예문들은 Borland의 Turbo C/C++, Borland C/C++ 컴파일
러를
기준하였음을 알려드립니다.  예제들을 제외한다면 인터럽트에 대한 기본적인 내
용들은 어느 특정 언어에 국한되지 않습니다.
-------------------------------------------------------------------

이제 이 강좌도 거의 마무리 단계에 접어 드는 것 같다.
마지막으로 다루게 될 내용은 램상주 프로그램이다. 램상주 프로그램이란 프로그
램을 종료해도 메모리에 계속 남아서 어떤 일을 하는 것을 말한다.
과거에 꽤나 인기가 있었던 볼랜드의 Side-Kick과 같은 프로그램은 램상주 프로
그램의 대표적인 경우이다. 안철수님의 바이러스 백신 프로그램인 V3RES 역시
램상주 기법을 사용한 것이다. 잘 만들어진 램상주 프로그램은 하드웨어적인 제
약이 많은 IBM PC에서 마치 멀티-태스킹(Multi-Tasking)이 구현되는 것과 같은,
매우 유용한 기능을 제공하고 있다. 하지만 그것을 구현하는 일은 꽤나 어렵고
알아야할 내용들이 무척 많다. 램상주 프로그램을 공부하는 것은 실제적인 프로
그램의 구현 보다는 오히려 시스템에 대해 보다 폭넓게 알 수 있는 기회를 제공
한다는 면에서 한번쯤 다뤄 볼 만하다고 생각된다. 이제 부터 시작되는 내용들이
다소 지루하더라도 컴퓨터에서 프로그램을 만들고자 한다면 충분히 숙지하고 있
어야 할 내용들이므로 알뜰히 봐 두도록 하자.

 [ 램상주(TSR, Terminate and Stay Resident) 프로그램 ]

램상주 프로그램의 제작은 인터럽트 핸들러의 제작과 함께 인터럽트 제어의 꽃이
라고 할 수 있는 매우 중요한 과정이다.
과거 램상주 프로그램을 작성하는 것이 무슨 큰 비밀이나 되는 듯이 여겨지던 시
절에는 해커(Hacker)들의 고유한 영역(?)으로  취급되던 램상주 프로그램의 제작
기법들이 오늘날에는 거의 알려져 있고,  왠만한 시스템 프로그램 관련 서적이라
면 한번쯤은 짚고 넘어가는 내용이 되었다.

컴퓨터 프로그래밍에는 왕도가 없다.   프로그램을 하는 사람의 개성이 표현되는
것이다. 비록 각 언어마다의 고유한 절차가 마련되어 있고, 프로그래머는 충실히
그 기본적인 절차를 따라야할 의무가 있지만 그것은 어디까지나 '기본적인 절차'
일 뿐이다.

마이크로소프트가 처음 부터 램상주 프로그램에 대한 정보를 속시원히 털어 놓지
는 않았다. 오히려 의도적으로 숨겨왔으며 누군가에 의해 그 내용이 밝혀지는 것
을 꺼려왔다.
해커(Hacker)들의 지순한 노력으로 어느 정도의 비밀(?)이 벗겨지고 나서야 비로
소 마이크로소프트는 더이상 못버티겠다는 듯 비밀을 털어놓기 시작했던 것이다.

이제, 램상주 프로그램의 제작 기법에 대한 비밀은 없다. 많은 유용한 램상주 프
로그램들이 제작되었고, 현재 사용되고 있다.

이 강좌에서는 램상주에 대한 모든 것을 취급하지는 않을 것이다.
그리고 그건 무척 힘든 일이다.

C는 램상주 프로그램을 작성하는 간단한 방법을 알고 있다.   그러나 C가 최적의
방법을 제공하는 것은 아니다.  램상주를 위한 최적의 코드를 작성하기 위해서는
어셈블리 언어가 가장 좋지만 불행하게도(?) 어셈블리는 그렇게 만만하게 친해지
질 않는 것이다(다른 고급언어에 비해 그렇다는 말이다).

결국 이 강좌에서는 C를 사용한 보다 쉽고, 보다 덜 최적화된 램상주 프로그램을
작성하게 된다는 말이다.
이제 여러분은 램상주 프로그램을 작성하기 위해 알아 두어야할 몇가지의 기본적
인 내용을 이해해야만 한다. 실제 램상주 프로그램의 코드 보다는 부가적으로 알
아 두어야 할 내용이 더 많다.

그리고 이하 부터 '램상주 프로그램'을 'TSR'이라고 부른다.

 [ TSR작성을 위해 알아 두어야 할 것들 ]

 >>> 스택(Stack) <<<

'스택'은 임시적인 데이타 보관 장소이다.  이는 FILO(First In Last Out) 또는
LIFO(Last In First Out)라고 불리는 데이타 삽입/추출 형태를 가지고 있다.
원래의 '스택'의 의미는 식당에서 접시를 쌓아 두는 곳을 말한다. 먼저 씻은 접
시부터 차례대로 쌓아두면 가장 먼저 꺼내는 접시는 맨 나중에 씻었던 접시가
되고 가장 먼저 씻었던 접시는 가장 나중에 꺼내지게 된다. 이런 형태를 가리켜
'후입선출(LIFO)'또는 '선입후출(FILO)'라고 하는 것이다.
컴퓨터에서의 '스택'의 동작도 이와 동일하다. 그림을 보자.

        │        │                │        │
        ├────┤                ├────┤
        │        │                │        │
        ├────┤                ├────┤
        │        │                │        │
        ├────┤                ├────┤
        │        │                │   A    │
        └────┘                └────┘
    [아직 저장한게 없다]           [A를 저장했다]

        │        │                │        │
        ├────┤                ├────┤
        │   C    │                │        │
        ├────┤                ├────┤
        │   B    │                │   B    │
        ├────┤                ├────┤
        │   A    │                │   A    │
        └────┘                └────┘
   [B와 C를 더 저장했다]            [C를 꺼냈다]

그림과 같이 A, B, C를 차례대로 저장하면 A가 가장 밑에,  그리고 C가 가장 위
에 있게 되며, 꺼낼 때는 위에서 부터 꺼내므로 넣을 때와 반대로 C부터 나오게
되어 A가 가장 나중에 나온다.

'스택'은 메모리에서 하나의 세그먼트 단위로 존재한다.  그러므로 '스택'이 있
다면 최소 16바이트(이 단위를 패라그래프(Paragraph)라고 한다), 최대 65536바
이트(한 세그먼트의 최대 크기는 64KB이다)의 크기를 가진다.
이 세그먼트를 '스택 세그먼트(Stack Segment)'라고 하며  CPU 레지스터 명칭은
'SS'이다(단순히 '스택'이라고 하면 '스택 세그먼트'를 말한다).

그런데 우리가 눈으로 볼 수 있는 접시를 넣고 빼는 것과는 달리 스택은 컴퓨터
의 메모리에 존재하는 스택은 눈으로 볼 수가 없다. 그래서 지금 스택의 어디까
지 저장되었는지를 알 수가 없다.   이것을 알기 위해 CPU에는 현재 스택에서의
위치를 알리기 위한 레지스터를 하나 더 준비하고 있는데 그것을 '스택 지정자'
또는 '스택 포인터(Stack Pointer)'라고 한다. CPU 레지스터 명칭은 'SP'이다.

                  │        │
                  ├────┤
                  │        │
                  ├────┤
                  │        │
                  ├────┤
                  │        │
                  └────┘<- SP

         [ 비어 있는 SS(Stack Segment) ]

이 그림은 현재 아무것도 저장되지 않은 스택의 모양이며 SP는 스택의 시작위치
를 가리키고 있다.

그림을 그리고 보니 잘못 그렸다는 생각이 드는데 아마도  위에 그려진 모든 그
림을 거꾸로 보는 것이 스택을 이해하는데 더욱 쉬울 것 같다.   왜냐하면 위와
같이 비어 있는 스택에 데이타를 저장하게 되면 당연히 SP가 증가해야 될텐데
그와는 반대로 '감소'한다. 이유는 아래에 나타낸 메모리의 모양을 보면 알 수
있다. 이 그림은 프로그램이 실행됐을 때의 메모리 형태를 나타낸 것이다.

메모리의 높은 곳 ├──────────────┤
                 │                            │
                 │   ↓ 스택(Stack) 영역      │ 스택은 높은 메모리 위치에서
                 │                            │ 낮은 메모리 위치로 커간다
                 ├──────────────┤
                 │                            │
                 │      데이타(Data) 영역     │
                 │                            │
                 ├──────────────┤
                 │                            │
                 │     실행코드(Code) 영역    │
                 │                            │
                 ├──────────────┤
                 │PSP(Program Segment Prefix) │
메모리의 낮은 곳 ├──────────────┤

그림에서와 같이 스택은 메모리의 높은 곳(메모리의 가장 낮은 곳은 0000:0000,
또 가장 높은 곳은 FFFF:FFFF)에 있고, 스택에 뭔가를 저장하게 되면 위에서 부
터 아래로 차곡차곡 쌓이게 된다.
그러므로 스택에 가장 최근에 저장된 데이타의 위치를 가리키는 SP는 '증가'하는
것이 아니라 '감소'하는 것이다. 이러한 상황이 이해되는가?
완전히 이해하기 위해 수고스럽지만 스택의 모양을 다시 그려보자.

[저장된 것이 없는 SS]     [A와 B가 저장된 SS]      [B를 꺼낸 SS]

  <메모리의 높은 곳>

    ┌────┐<- SP        ┌────┐           ┌────┐
    │        │             │   A    │           │   A    │
    ├────┤             ├────┤           ├────┤<- SP
    │        │             │   B    │           │        │
    ├────┤             ├────┤<- SP      ├────┤
    │        │             │        │           │        │
    ├────┤             ├────┤           ├────┤
    │        │             │        │           │        │

 <메모리의 낮은 곳>

스택에 저장되는 데이타의 최소 단위는 2바이트의 크기이다.  이는 C에서의 정수
(int)형과 크기가 같다(어셈블리에서는 WORD라 한다).
그러므로 만약 SP가 100번지의 스택 메모리를 가리키고 있을 때 스택에 데이타를
저장하게 되면 그 위치는 SP-2가 되고 실제 메모리 번지는 98이 된다. 또 데이타
를 저장한다면 그 위치는 SP-4가 되며 실제 메모리 번지는 96이 되는 것이다.
이 상황을 그림으로 나타내 보면,

        번지:  100    ┌────┐<- SP-0
                      │   A    │
                98    ├────┤<- SP-2
                      │   B    │
                96    ├────┤<- SP-4
                      │   C    │
                94    ├────┤<- SP (SP = SP-6, 현재 여기를 가리킨다)
                      │        │

이제 스택에 관해서는 어느 정도 설명이 되었다. 하지만 모두 설명된 것은 아니
다. 지금까지는 CPU 수준에서 본 스택의 구조였고, 다음 부터는 C와 연관된  스
택의 사용 과정을 알아볼 것이다.
오늘 강좌는 여기까지인데, 혹여 할려면 한꺼번에 하지 감질나게 이게 뭐냐고
하실 분이 계실지도 므르겠다 하는 노파심(?)에서 말씀드리는데 며칠 걸리더라
도 모두 작성해서 한꺼번에 올리면 좋은 일이겠지만 필자의 사정이 한번에 그렇
게 많은 시간을 낼 수 있는 형편이 못된다. 이 점 너그러이 양해해 주시기를 바
란다.


-------------------------------------------------------------------
인터럽트(Interrupt)

(9)

* 명확하고 간략한 의사전달을 위해 경어를 사용하지 않았음을 양해바랍니다.
-------------------------------------------------------------------
본 강좌에서 거론될 예문들은 Borland의 Turbo C/C++, Borland C/C++ 컴파일
러를
기준하였음을 알려드립니다.  예제들을 제외한다면 인터럽트에 대한 기본적인 내
용들은 어느 특정 언어에 국한되지 않습니다.
-------------------------------------------------------------------

지난 강좌에 이어 C와 연관된 스택의 구조와 사용을 알아 보자.

C는 임의적으로 지정하지 않는다면 기본적인 스택의 크기를 4KB(4096바이트)로
설정한다. 그 크기를 임의적으로 바꾸기 위해서는

        extern unsigned int _stklen;

을 조정해야 한다(이 변수는 컴파일러에 의해 미리 선언된 비부호 정수형이다).
스택의 크기를 줄여서 1024바이트의 크기로 하려면 프로그램의 머릿 부분에서

        extern unsigned int _stklen = 1024;

와 같이 새로운 값을 지정하면 된다.

스택은 프로그램에서 매우 중요한 역할을 하는데, 특히 C와 다른 언어를 혼합한
프로그램을 작성할 경우 그렇다. 왜냐하면, 각 함수간에 주고 받는 파라미터(Pa
rameters)는 거의 대부분 스택을 통해 전달되기 때문이다. 즉, 인자를 넘겨주는
쪽이 인자들을 먼저 스택에 저장해 두고 함수를 호출하면 인자를 받는 쪽은  스
택에서 그것들을 꺼내 사용하게 되는 것이다.

    <호출하는 쪽>      →       <스택>       →      <호출되는 쪽>

                            ┌─────┐    ┌────────────┐
                            │    20    │    │ int add(int a, int b)  │
┌──────────┐저장├─────┤추출│ {                      │
│ sum = add(10, 20); ├──┤    10    ├──┤     return (a+b);      │
└────┬─────┘    ├─────┤    │ }                      │
          │                │          │    └──────┬─────┘
          │                                                │
          └────────────────────────┘
                            ← 값을 되돌림

이 그림에는 호출하는 쪽과 호출되는 쪽이 정확히 어떻게 스택에 인자를 저장하
고 어떻게 값을 꺼내는지에 대해서는 표현되지 않았다.   이 그림을 통해 알 수
있는 것은 그들 사이에 스택이 있어서 자료 전달의 매개체로 활약하고 있다는
것이다. 물론, 둘 사이의 인자 전달이 반드시 스택을 통해서 이루어 져야 하는
것은 아니다. 메모리나 화일을 통해서 전달될 수도 있다. 스택을 통해 인자를
전달하는 방법은 프로그램에 있어서 표준적인 방법인 것이다.

C의 스택을 통한 인자 전달을 설명할 때 주로 파스칼의 그것과 비교되는데 그
이유는 이들이 서로 반대되는 '순서'로 인자를 전달하기 때문이다.

C의 호출하는 쪽이 파라미터를 전달하는 순서 ........ 가장 오른쪽의 인자 부터
파스칼의 호출하는 쪽이 파라미터를 전달하는 순서 ... 왼쪽부터 차례대로

세개의 인자를 받아서 그 합을 돌려주는 함수가 있다고 하자. 파스칼의 형식을
잘 알지 못한다고 해도 그 둘을 비교해서 이해하는데는 별 무리가 없을 것이다.

C 함수 ........ int add3(int a, int b, int c);
파스칼 함수 ... function add3(a, b, c:integer) : integer;

C와 파스칼의 함수들이 다음과 같이 호출된다면,

< C >                           < 파스칼 >
sum3 = add3(10, 20, 30);        sum3 := add3(10, 20, 30);

이 때 세개의 인자 10, 20, 30이 스택에 저장된 형태는 다음 그림과 같다.

< C >                           < 파스칼 >
        ┌────┐                ┌────┐
        │   30   │                │   10   │
        ├────┤                ├────┤
        │   20   │                │   20   │
        ├────┤                ├────┤
        │   10   │                │   30   │
        ├────┤<- SP           ├────┤<- SP
        │        │                │        │

그림을 통해 알 수 있듯이 C는 함수를 호출할 때 가장 오른쪽에 지정된 인자를
가장 먼저 저장하고 있으며, 파스칼은 나타나는 순서대로 왼쪽부터 저장하고
있다.

스택의 효율적인 사용은 프로그래머에게 매우 큰 의미가 있다. 스택은 남는 것
도, 모자라는 것도 전혀 이로울게 없다. 그만큼 스택은 프로그램머에게 있어서
부담되는 존재인 것이다. 그러나 아쉽게도 스택을 정확히 조절하는 일은 매우
힘들다. 스택이 모자란다면 여러분은 'Stack overflow'를 경험하게 될 것이고,
남는다면 하릴없이 메모리를 차지하고 있는 스택으로 인해 뭔가.... 일보고 뒤
를 닦지 않은 것 같은 묘한 찜찜함이 남기 때문이다.

고급언어라고 불리워 지는 모든 언어들(물론 C언어도 포함해서)은 거의 대부분
그런 찜찜함을 제거하기가 힘들다.  스택에 관한 모든 처리를 컴파일러가 독단
적으로 처리하기 때문이다. 요즘 C언어가 이토록 인기 있는 것은, 그래도 다른
언어에 비해 어느 정도 조절할 수 있는 가능성을 주기 때문이 아닌가 싶다.
그만큼 군살없는 프로그램을 만들 수 있는 것이다.


 >>> 힙(Heap) <<<

일전에 '질문/답'란에서 어떤 분이 힙을 '엉덩이'라고 표현한 것을 봤는데 우
스개 소리지만 사전적인 의미는 그것과 다르지 않다.
힙은 특별한 것은 아니다.  그저 '데이타를 쌓아두는 곳' 정도로 이해하면 될
것 같다.

지금껏 스택의 구조와 동작에 대해 알아 보았는데, 스택은 엄격히 LIFO(후입
선출)의 형태임을 알고 있다. 그러나 힙에는 그런 조건이 없다.
프로그램은 언제든지, 어떤 순서로든 힙을 사용할 수 있고 그것을 해제할 수
있다. 즉, 랜덤(Random)한 접근이 가능한 메모리인 것이다.

이러한 힙의 특성은 특히 '동적할당'에서 돋보인다.
C에서의 동적할당을 위해 다음과 같은 표준 함수를 제공한다.

        void *malloc(size_t size);
        void *calloc(size_t nitems, size_t size);

        void far *farmalloc(unsigned long nbytes);
        void far *farcalloc(unsigned long nunits, unsigned long unitsz);

C는 기본적인 힙의 크기를 32KB 또는 64KB로 설정하고 있다.
이것은 스택에서와 마찬가지로 새롭게 설정될 수 있는데 이는

        extern unsigned int _heaplen;

을 조절함으로써 가능하다.
힙의 크기를 4096바이트로 설정하려면,

        extern unsigned int _heaplen = 4096;

과 같이 한다.


 >>> PSP(Program Segment Prefix) <<<

MS-DOS의 모든 프로그램은 실행되면서 PSP를 생성한다.
PSP는 메모리에서 프로그램의 가장 머릿부분에 생성되는 매우 중요한 영역으로
일괄적으로 256바이트의 크기를 갖게 된다.

C는 프로그램이 시작될 때 PSP가 생성된 메모리의 세그먼트를 보관하는데 이것
은 외부 전역변수인

        extern unsigned int _psp;

에 있다.

PSP의 중요한 의미는 이곳이 '프로그램의 시작 위치'라는 점이다.
이에 대한 자세한 내용은 여타의 시스템 프로그래밍 관련 서적을 참고하시기
바란다.


-------------------------------------------------------------------
인터럽트(Interrupt)

(10)

* 명확하고 간략한 의사전달을 위해 경어를 사용하지 않았음을 양해바랍니다.
-------------------------------------------------------------------
본 강좌에서 거론될 예문들은 Borland의 Turbo C/C++, Borland C/C++ 컴파일
러를
기준하였음을 알려드립니다.  예제들을 제외한다면 인터럽트에 대한 기본적인 내
용들은 어느 특정 언어에 국한되지 않습니다.
-------------------------------------------------------------------

 >>> COM과 EXE <<<

MS-DOS는 COM,EXE의 두가지 형태의 직접적인 실행 파일을 제공한다(SYS,
BAT등도
실행 가능하지만 직접적이지 않다).

어느 형태이던 프로그램의 실행 시점에 지난 강좌에서 언급했던 PSP를 프로그램
이 위치하는 메모리의 가장 낮은 곳에 만들고 그 위에 원래의 프로그램 내용을
위치시킨다.

 메모리의 높은 곳 →├─────────────┤ ─┐
                    │           스택           │   │
                    ├─────────────┤   │
                    │          데이타          │   ├─> 프로그램의 내용
                    ├─────────────┤   │
                    │           코드           │   │
                    ├─────────────┤ ─┘
                    │           PSP            │ ← 프로그램 실행시 생성
 메모리의 낮은 곳 →├─────────────┤

여기에서 프로그램이 실행될 때의 메모리 위치는 프로그램이 실행되는 순간까지
알 수 없다. 이는 MS-DOS에 의해 정해진다(옵셋의 위치는 짐작하여 알 수 있다.
옵셋이란 한 세그먼트 내에서의 상대적인 위치이기 때문이다. 그러나 여기서 알
수 없는 것은 세그먼트이다)

*** .COM 프로그램 ***

COM 형태의 프로그램은 파일로 저장되어 있는 상태와 실행되기 위해서 메모리로
읽혀진 상태가 똑같다.  이는 COM이 하나의 세그먼트 크기에 맞도록 설계되었기
때문인데, 이런 COM의 상태를 흔히 '메모리와 닮았다'고 한다.

익히 아시는 바와 같이 IBM PC는 큰 메모리를 여러개의 덩어리로 나누어 관리하
고 있다. 이 나뉘어진 메모리의 덩어리를 세그먼트라 하고, 각 세그먼트 안에서
그 세그먼트가 시작되는 곳을 기준으로 얼마만큼 떨어졌는가를 나타내기 위해
옵셋이란 개념을 쓰고 있다. MS-DOS에서의 프로그램은 이런 하나의 세그먼트

에서 옵셋만을 갖고 주소를 지정할 수 있는 거리를 근거리(near)라 하고 옵셋만
을 갖고는 주소를 지정할 수 없는, 즉 현재 사용하고 있는 세그먼트와는 다른
세그먼트내의 주소를 지정할 수 있는 거리를 원거리(far)라고 한다.
같은 세그먼트 안에서는 근거리 주소지정만을 할 때의 주소는 0~65535번지의 범
위를 갖게 되어 2바이트(16비트) 만으로 어디든 접근이 가능하다.  그러나 다른
세그먼트에 있는 내용을 참조하자면 어느 세그먼트의 어떤 옵셋인지를  함께 지
정해야 하므로 세그먼트 2바이트와 옵셋 2바이트의  4바이트 주소지정을 사용해
야 한다(세그먼트:옵셋).
전자와 후자 중 어느쪽이 접근하는 속도가 빠르겠는가? 당연히 같은 세그먼트내
에서 옵셋만을 갖고 접근하는 쪽일 것이다.

COM프로그램은 메모리의 형태와 닮았기 때문에 그 최대 크기가 64KB로 제한되

있다.그래서 프로그램의 모든 세그먼트 레지스터(CS,DS,SS,ES)가 동일한 세그

트를 가리키게 된다(COM의 모든 내용은 64KB 안에 있다).  이런 이유로 이 형태
의 프로그램은 다소 빠르게 동작한다(같은 이름의 COM과 EXE가 있을 때 그 프로
그램을 실행시키면 EXE는 제쳐 두고 COM이 실행되는 것은 이와 연관이 있다).

사실 가장 큰 COM프로그램이라고 해도 온전히 64KB(65536바이트)의 크기를 가

수는 없는데 그것은 프로그램이 실행되면서 그 선두에 생성되는 PSP가 차지할
256바이트의 메모리와 특별한 목적을 위한 2바이트의 메모리도  고려해야  하기
때문이다.  특별한 목적의 2바이트 메모리라는 것은 '복귀 코드를 위한 주소'인
데 COM은 실행되면서 프로그램이 위치한 메모리의 가장 끝 부분에 최소한 2바이
트의 스택을 마련하고 거기에 가장 먼저 0을 저장한다.  여기에서 '복귀코드'에
대한 자세한 내용을 설명하자면 이러다 날쌔는 경우가 있으므로 이에 대한 것은
어셈블리 프로그래밍 가이드를 참조하시기 바라고  여기서는 debug를 통해 간단
히 그런 내용만을 확인해 보도록 하자.

C:\>debug command.com
-r
AX=0000  BX=0000  CX=D55B  DX=0000  SP=FFFE  BP=0000  SI=0000  DI=0000
DS=286B  ES=286B  SS=286B  CS=286B  IP=0100   NV UP EI PL NZ NA PO
NC
286B:0100 E96D15        JMP     1670                              
-q

여기에서 SP가 FFFE인 점을 주목하라.
SP는 SS내에서의 옵셋의 위치이다. 즉, SP가 높은 메모리 위치에서 낮은 메모리
위치로 내려오므로 COM프로그램의 실행시는 FFFF(64KB의 범위 중 가장 높은
곳)
를 가리키고  있어야 마땅할 것이지만 이미 2바이트의 복귀주소값이 저장되어서
FFFE가 되었다(FFFE~FFFF의 2바이트를 가리킨다).
그리고 CS, DS, SS, ES의 값이 모두 286B로 동일함을 알 수 있다.


<메모리로 읽혀지기 전 파일 상태의 크기>

        COM의 최소 크기 = 1바이트
                          -
                          프로그램을 종료시키는 명령은 있어야 된다
                          (어셈블리의 'RET'은 1바이트 명령)

        COM의 최대 크기 = 65536 - (256 + 2) = 65278바이트
                                  ---------
                                  실행시에 더해질 메모리의 양(PSP+복귀코드)


<메모리로 읽혀진 후 실행 상태의 크기>

        COM의 최소 크기 = 256 + 2 + 1 = 259바이트
        COM의 최대 크기 = 65278 + 256 + 2 = 65536바이트
                                  -------
                                  실행시에 더해진 메모리의 양

* 자신의 디스크 상에 있는 모든 COM파일을 찾아 보라. 어떤 COM파일도 65278

이트 보다 큰 것은 없을 것이다.

*** .EXE 프로그램 ***

EXE는 현재의 MS-DOS에서 가장 보편화된 실행 파일 형태이다.
이는 COM과 같은 64KB의 크기 제한을 갖지 않고, 메모리가 허용하는 한 얼마던
지 커질 수 있다.
EXE는 COM형태와는 다르게 프로그램이 실행되기 전에 '재배치(Relocation)' 라
는 과정을 거쳐 메모리상의 위치를 확보하게 된다. '재배치'란 EXE가 COM과 같
이 메모리로 올려 졌을 때  곧바로 실행될 수 있도록 메모리와 닮아 있지 못하
기 때문에 현재의 메모리 상태에 따라 몇가지의 사전 처리(예를 들자면,  옛날
에는 사글세 방을 얻을 때 별 까다로운 조건 없이 방세만 잘 주면 되었지만 요
즘은 주인 마음에 들어야 한다(?), 계약서를 써야 된다 하는 것과 같이 형식이
까다로와 진 것과 유사한다)를 해서 프로그램이 위치할 메모리 상의 위치를 확
보하는 과정을 말한다.

EXE에서 재배치에 대한 정보는 파일의 머릿 부분에 포함하고 있으며 그 크기는
최소한 512바이트 이상이 된다. 이 정보는 프로그래머에 의해 임의적으로 생성
되는 것이 아니라 컴파일러에 의해 면밀히 계산된 후에 쓰여진 것이다.
(EXE 재배치 정보의 첫머리는 항상 'MZ'로 시작된다.   EXE파일을 덤프해 보면
그러한 사실을 확인할 수 있다)

이제 C를 이용해서 램상주 프로그램을 작성하는데 필요한 기본적인 내용들은
어느 정도 설명이 된 듯하다.
물론 이것이 전부는 아니지만 이 강좌의 목적이 시스템 프로그래밍의 모든 것
을 알고자 하는 것이 아니므로 더 이상의 내용은 생략하도록 한다. 보다 깊이
있고 상세한 것을 알고자 하는 분들은 아래에 열거된 서적들을 참고하시면 도
움이 되리라 믿는다.

        -Inside the IBM PC, Peter Norton
        -The IBM PC & PS/2, Peter Norton/Richard Wilton
        -Advanced MS-DOS Programming 2th, Ray Duncan
        -기타 어셈블리 프로그래밍 참조서
        -기타 MS-DOS 시스템 프로그래밍 참조서

다음 강좌가 마지막이 될 것 같은데, 이제껏 배워 온 내용들을 토대로 실제의
TSR을 제작해 보도록 할 것이다.


-------------------------------------------------------------------
인터럽트(Interrupt)

(11:마지막회)

* 명확하고 간략한 의사전달을 위해 경어를 사용하지 않았음을 양해바랍니다.
-------------------------------------------------------------------
본 강좌에서 거론될 예문들은 Borland의 Turbo C/C++, Borland C/C++ 컴파일
러를
기준하였음을 알려드립니다.  예제들을 제외한다면 인터럽트에 대한 기본적인 내
용들은 어느 특정 언어에 국한되지 않습니다.
-------------------------------------------------------------------

이제 이 강좌는 이번 호로 마무리 하게 된다.
이 강좌는 애초에 이렇게 11회나 되도록 할 것이라는 거창한(?) 계획은 없었다.
그러나 워낙에 없는 글재간이고,또 모자라는 식견에도 어차피 할거면 좀 더 상세
히 해보자 하는 욕심이 일어서 예까지 오게 됐는데 막상 강좌를 끝내려는 시점에
서 보니 상세한 설명은 커녕 혼동만을 더한게 아닌가 하는 부끄러움도 인다.

그동안 부족한 글재간으로 어렵게 써 온 이 강좌에 관심을 보여주신 여러분께 깊
은 감사를 드리고, 부족했던 점들은 추후 스스럼 없이 질문을 주시면 능력닫는한
기꺼이 답해 드릴 것을 약속드린다.

-------------------------------------------------------------------

지난 강좌까지 TSR의 제작시에 미리 알고 있어야 할 내용들을 '주마간산' 격으로
살펴 보았었다.  MS-DOS에서 TSR을 제작할 수 있다는 것은 어쩌면 행운인지 모

다. 그러나 이것은 또다른 면으로 분명히 불행이다.
잘 만들어진 TSR을 사용자는 행복(?)을 느낄 수 있다.  그것을 만들어야 하는 프
로그래머에게는 비록 최대로 머리 아프고 피곤한 작업이 될지라도...

필자는 이번 강좌의 마지막에 소개할 TSR을 구상하는 것에 매우 신중을 기했다.
그다지 부담되지 않는 크기이면서 누구에게나 유용할 수 있는 내용의 프로그램이
되려면...
그래서 지난날 학교다닐 때 교수님들이 내주던 그 머리 아픈 보고서를 할 때 구
상한 바 있던 것을 떠올렸다. 바로 '일부분 화면만 선택적으로 프린트하는'프로
그램이 그것인데 프로그램을 하다 보면 현재의 도스 화면을 프린트해야 하는 경
우가 다반사로 생기게 된다. 그 중에서도 쓸모없는 부분은 제쳐 두고 내가 필요
로 하는 부분 만을 선택적으로 출력할 수 있다면 얼마나 좋겠는가?

이제 작성하게 될 TSR의 기능은 위에서 이야기된 그대로이다. 처음엔 시계를 만
들어 볼까도 생각했지만  TSR 시계는 TSR을 다루는 어떤  책에서도 빠지지 않고
등장하는 만능(?)의 메뉴가 되어 있다.  모든 책이 짠듯이 시계를 예로 들고 있
어 이 강좌에서 역시 시계를 다룬다면 너무나 구태의연 하다는 비난을  받을 것
같아 포기하고, 대신 어떤 곳에서도 다루지 않았다고 자신하는(아마도, 모든 책
을 보지는 못했지만 아직은 못봤기에...) 내용을 다뤄 보고자 한 것이다.

 [ Screen Capture Source ]

/* 이 프로그램의 이름은 'Screen Capture'라는 의미에서 'CAPSCR'으로 정했
다.
   했다. 이름이 그다지 중요한 것은 아니지만, 앞으로 누군가가 이 프로그램을
   좀 더 멋진 Screen Capture로 발전시켜 줬으면 하는 바램에서 이름을 명백히
   해 두고자 한 것이다. 이 프로그램의 발전 가능성은 얼마든지 있다. 이에 대
   한 토론은 뒷부분에서 하도록 하자.
   -----------------------------------------------------------------
--
   프로그램 ...... CAPSCR (Version 1.0)
   기능 .......... 텍스트 화면의 선택적 프린팅
   형태 .......... TSR(Terminate Stay and Resident)
   -----------------------------------------------------------------
--
*/

#include
#include
#include
#include
#include
#include
#include
#include "predefs.h"

#ifdef __cplusplus
#       define __DEFARG ...
#else
#       define __DEFARG
#endif

/* 사용될 키정의 - 키보드 스캔코드표 참조 */
#define K_ESC           0x011B         
#define K_DOWN          0x5000
#define K_UP            0x4800
#define K_PLUS          0x4E2B
#define K_MINUS         0x4A2D
#define K_ENTER         0x1C0D

/* 힙과 스택의 크기를 재조정한다(강좌 8,9의 내용을 참조)  */
extern WORD _heaplen = 2048;    /* 힙의 크기는 최소한 이 크기가 적당하다
*/
extern WORD _stklen = 512;      /* 스택은 많이 사용되지 않았다 */

/* 프린트 에러 메세지들. BIOS 인터럽트 17h가 설명된 서적을 참조 */
static PBYTE    print_msg[] = {
        "Success!",
        "Device time out!",
        "I/O Error!",
        "Out of paper!" };

static PBYTE    presskey_msg = "Press a key to continue...";

static WORD     shift_flag;             /* 시프트 키의 상태를 저장할 변수 */
static BOOL     call_from_user = FALSE; /* 예약된 TSR 호출키가 눌렸으면
TRUE */
static BOOL     running = FALSE;        /* TSR이 표면상에서 실행 중이면
TRUE */
static BYTE     screen[4000];           /* 텍스트 화면을 저장할 버퍼 */
static BYTE     msg_buffer[160];        /* 에러메세지를 위한 버퍼 */

/* 원래의 키보드 인터럽트인 INTR 09h의 루틴이 시작되는 주소를 저장할 함수포

   터.  이 인터럽트는 모든 키입력에 관계하므로 Hooking하게 되면 시스템이 마

   된다. 그러므로 반드시 Chaining하던지, 그 루틴 전체를 다시 만들어야 한다.
*/
static void interrupt (*old_KEYBOARD)(__DEFARG);

/* INTR 28h는 '할일 없을 때' 발생하는 인터럽트이다. 조금 이상하게 들릴 것이

   만 이 인터럽트의 역할은 분명히 그렇다.
   이 말은 바꾸어 말하면,  MS-DOS가 뭔가를 분주히 하고 있을 때면  이 인터럽

   는 발생하지 않는다는 것이다.
   그렇다면,  이 인터럽트를 가로채고 있으면 MS-DOS가 언제 한가한지를 알 수

   다는 말이 된다. 이 인터럽트에 대한 새로운 핸들러를 작성하는 목적은 바로 그
   'MS-DOS가 한가한 시간'을 알기 위한 것이다.

   그럼 무엇 때문에 MS-DOS가 한가한지를 알아야 하는가?
   MS-DOS는 이른 바 '재진입 불가'라는 머리 아픈 코드를 갖고 있다.
   여기서 '재진입(Recursion)'이라는 것은,  인터럽트를 수행하고 있는 중에 다른
   도스 인터럽트를 호출할 수 없다는 것이다. TSR의 경우 인터럽트를 통해서 구

   되므로 이 역시 하나의 인터럽트이다. 그렇다면 MS-DOS의 '재진입 불가'에 따

   TSR내부에서는 어떤 도스 인터럽트도 호출할 수 없게 되어 있다.
   마이크로소프트가 이에 대한 해결책이라고 내놓은 것이 'MS-DOS Busy
Flag'라는
   것인데, 이를 'InDOS Flag'라고 부른다.  이 플래그는 현재 MS-DOS가  처리하

   있는 일의 갯수를 하나의 바이트에 담고 있는 것이다.  그러니까,  이 플래그를
   검사해서 만약 0이면 도스는 아무 일도 하고 있지 않으므로 이 때  만큼은 예외
   적으로 재진입해도 괜찮다는 것이다.  그러나 왠걸...,  InDOS Flag가 0일 때를
   기다리다가는  혼기 다 놓치고 홀아비 한테 시집가기 알맞다 (백마탄 기사는 항
   상 옆에 있는데 여자들은 그걸 알아 보질 못하는게 아닌지.... 쩝).  왜냐하면,
   이 강좌의 가장 처음에 이런 말을 했을 것이다.   '인터럽트는 언제나 걸린다.'
   '커서가 깜박일 때도, 시간을 표시할 때도...'  바로 이때문이다.  도스는 거의
   대부분의 시간을 뭘 하던지 하고 있다.  쉬는 시간은 얼마되지 않는다 (사실 이
   시간에도 뭔가를 한다).
   그래서,  도스가 쉬고 있는  시간을 다르게 알 수 있는 방법을 찾다 보니  바로
   INTR 28h가 있었던 것이다.  INTR 28h는 최대한의 막간의 시간을 이용해서 발
생된
다. 커서가 깜박이는 짧은 순간에도, 시간이 표시되는 그 짧은 순간에도....
   그렇다고 해서 InDOS Flag가 전혀 불필요한 것은 아니다. 필요하다면, InDOS
Fl ag
와 INTR 28h를 병용하는 방법도  있다.여자들이 처녀 때에 한번쯤 백마탄 기사에
대한

  을 꾸어 보는 것이 정서함양에 도움이 되듯이...

   *** 도스의  재진입에 대한 내용은 BIOS 인터럽트에 대해서는 적용되지 않는
다.
   단, 디스크나 디스켓에 대해 어떤 작업을 하는 중이라면(INTR 13h) 이때는 어떤
   형식의 인터럽트도 있어서는 안된다.
*/
static void interrupt (*old_NOTBUSY)(__DEFARG);

/* 바이오스의 키입력 서비스를 통한 키-스캔코드 읽기 */
int getkey(void)
{
        if(bioskey(1)) return bioskey(0);
        else return (0);
}

/* 프린트에 관계된 에러 메세지를 출력하기 위한 함수.
   텍스트 비디오 램을 통한 직접 입출력을 사용했다. */
void message(char *msg)
{
        LPBYTE scr;
        LPBYTE temp;
        int msg_len, x, y = 24, i;

        /* 롬-바이오스 데이타 영역의 0000:0449는 현재의 비디오 모드값을
           갖고 있다. 이 값이 7이면 Mono, 2또는 3이면 Color이다
           Mono의 비디오 램은 b000:0000에서 시작되고 Color는 b800:0000에서
           시작된다(텍스트 비디오에 대한 자세한 내용은 관련 서적이나 강좌를
           참조)
        */
        if(peekb(0, 449) == 7) scr = (LPBYTE)0xb0000000L;
        else scr = (LPBYTE)0xb8000000L;

        /* 화면의 가장 아랫 줄에 메세지를 출력하기 전에 그 줄의 원래 내용을
           msg_buffer에 저장해 둔다.
           비디오 램에 직접 입출력할 때의 x와 y의 값은 0~79, 0~24이지만
           내장함수인 gettext, puttext는 1~80, 1~25의 범위를 사용한다.
        */
        gettext(1, 25, 80, 25, (PBYTE)msg_buffer);

        msg_len = strlen(msg);          /* 출력할 메세지 길이 */

        temp = scr+(y*160);             /* 화면의 가장 아랫 줄에 대한
                                           비디오 램상의 주소를 계산 */

        for(x = 0; x < 80; x++)
        {
                *(temp++) = ' ';        /* 공백을 출력하여 우선 깨끗이... */
                *(temp++) = 0x4E;       /* 색상을 빨간 바탕에 노란 글씨로 */
        }

        temp = scr+(y*160);

        for(i = 0; i < msg_len; i++)    /* 메세지 길이 만큼 */
        {
                *(temp++) = msg[i];
                temp++;
        }

        temp += 2;      /* 메세지를 모두 출력하고 난 2바이트 뒤 */

        for(i = 0; i < strlen(presskey_msg); i++)
        {
                *(temp++) = presskey_msg[i];    /* Press a key... */
                temp++;
        }

        while(!getkey())        /* 키가 하나 눌려질 동안 계속 */
        ;

        puttext(1, 25, 80, 25, (PBYTE)msg_buffer);      /* 원래 내용을 복구 */
}

/* 화면 상의 한 줄을 반전시켜 막대를 생성한다.
   이 반전 막대는 Cursor Up/Down키로 조작된다 */
void get_line(int y, BYTE attr)
{
        LPBYTE scr;
        int x;

        if(peekb(0, 417) == 7) scr = (LPBYTE)0xb0000000L;
        else scr = (LPBYTE)0xb8000000L;

        for(x = 0; x < 160; x += 2)
        {
                *(scr+(y*160)+x+1) = attr;      /* 비디오 램의 구조에 대해서는
                                                   관련 자료를 참조 */
        }
}

/* 반전되었던 막대를 지우고 원래 속성(기본=겅정 바탕에 밝은 회색 글씨)으로
   되돌린다 */
void put_line(int y)
{
        LPBYTE scr;
        int x;

        if(peekb(0, 417) == 7) scr = (LPBYTE)0xb0000000L;
        else scr = (LPBYTE)0xb8000000L;

        for(x = 0; x < 160; x += 2)
        {
                *(scr+(y*160)+x+1) = 0x07;
        }
}

/* 전달된 y줄의 내용을 준비된 버퍼에 저장한다 */
void save_screen_line(int y)
{
        LPBYTE scr;
        int x;

        if(peekb(0, 417) == 7) scr = (LPBYTE)0xb0000000L;
        else scr = (LPBYTE)0xb8000000L;

        for(x = 0; x < 160; x += 2)
        {
                screen[(y*160)+x] = *(scr+(y*160)+x);
                screen[(y*160)+x+1] = *(scr+(y*160)+x+1);
        }
}

/* 저장했던 내용을 버퍼에서 지운다 */
void delete_screen_line(int y)
{
        int x;

        for(x = 0; x < 160; x += 2)
        {
                screen[(y*160)+x] = ' ';
                screen[(y*160)+x+1] = 0x07;
        }
}

/* 버퍼에 저장된 화면의 내용을 프린터로 출력한다.
   프린터로 출력할 때 주의할 것은, 버퍼에 저장할 때 화면의 내용을 그대로
   저장했기 때문에 그 저장 형태가 '문자값+색상값'일 것이므로 색상값은 출
   력하지 않도록 배려해야 한다는 것이다.
*/
int to_printer(int start_y, int end_y)
{
        int x, y, status, i;

        /* 설정되어 있는 영역의 첫줄에서 마지막줄까지 */
        for(y = start_y; y <= end_y; y++)
        {
                /* 각 줄 마다 80문자씩이다 */
                for(x = 0; x < 160; x += 2)
                {
                        status = biosprint(0, screen[(y*160)+x], 0);
                        if(status & 0x01)       /* 에러: time out */
                        {
                                /* 세번의 기회를 줘본다 */
                                for(i = 0; i < 3; i++)
                                {
                                        status = biosprint(0, screen[(y*160)+x],
0);
                                        if(status & 0x01 == 0) break;
                                        /* 다른 에러이면 중지하고 복귀 */
                                        else if(status & 0x08) return 2;
                                        else if(status & 0x20) return 3;
                                        if(i == 2) return 1;
                                }
                        }
                        else if(status & 0x08) return 2;  /* i/o error    */
                        else if(status & 0x20) return 3;  /* out of paper */
                }

                /* 한 줄에 대한 인쇄가 끝났다. 줄을 바꾸기 위해 cr/lf를 송출 */
                biosprint(0, 0x0D, 0);
                biosprint(0, 0x0A, 0);
        }

        return 0;       /* 프린트 작업이 성공적으로 완료되었다 */
}

/* 이 프로그램에서 실제로 화면의 내용을 잡는 일을 하는 부분 */
void capture_screen(void)
{
        BOOL cont_flag = TRUE, add_flag = FALSE;
        int i, x, y = 0, start_y = 0;

        /* 버퍼를 깨끗이 한다 */
        for(i = 0; i < 25; i++) delete_screen_line(i);

        /* 우선 화면의 첫 줄에 반전 막대를 만들자 */
        get_line(y, 0x70);

        while(cont_flag)        /* cont_flag가 TRUE인 동안 계속 */
        {
                switch(getkey())        /* 키입력이 있었다면 */
                {
                        /* 반전된 영역을 원래대로 복구하고 복귀 */
                        case K_ESC:
                                if(add_flag)
                                {
                                        for(i = start_y; i <= y; i++)
                                        {
                                                delete_screen_line(i);
                                                put_line(i);
                                        }
                                }
                                else put_line(y);

                                cont_flag = FALSE;
                                break;

                        /* 현재 반전 막대가 있는 위치 부터 프린터할 영역으로
                           설정할 것을 알린다. */
                        case K_PLUS:
                                if(!add_flag)
                                {
                                        start_y = y;
                                        save_screen_line(start_y);
                                        add_flag = TRUE;
                                }
                                break;

                        /* 설정한 영역을 해제하지만 작업은 계속 수행 */
                        case K_MINUS:
                                if(add_flag)
                                {
                                        for(i = start_y; i <= y; i++)
                                        {
                                                delete_screen_line(i);
                                                if(i >= 0) put_line(i);
                                        }
                                        add_flag = FALSE;
                                        get_line(y, 0x70);
                                }
                                break;

                        /* K_PLUS가 눌려졌었다면 영역을 확대하고, 그렇지 않다면
반전막대를 아래로 이동한다 */
                        case K_DOWN:
                                if(add_flag)
                                {
                                        y++;
                                        if(y > 24) y = 24;
                                        save_screen_line(y);
                                        get_line(y, 0x70);
                                }
                                else
                                {
                                        put_line(y);
                                        y++;
                                        if(y > 24) y = 24;
                                        get_line(y, 0x70);
                                }
                                break;

                        /* K_PLUS가 눌려졌었다면 영역을 축소하고, 그렇지 않다면
반전막대를 위로 이동한다 */
                        case K_UP:
                                if(add_flag)
                                {
                                        delete_screen_line(y);
                                        if(y > 0 && y > start_y)
                                        {
                                                put_line(y);
                                                y--;
                                        }
                                }
                                else
                                {
                                        put_line(y);
                                        y--;
                                        if(y < 0) y = 0;
                                        get_line(y, 0x70);
                                }
                                break;

                        /* 설정된 영역을 프린트 한다 */
                        case K_ENTER:
                                if(!add_flag) break;
                                message(print_msg[to_printer(start_y, y)]);
                                break;
                }
        }
}

#pragma warn -pro

/* 키보드 인터럽트에 대한 새로운 핸들러 */
void interrupt new_KEYBOARD(__DEFARG)
{
        /* 모든 키보드 입력 인터럽트는 INT 09h가 수행된 이후에 호출될 수
           있다. 현재 이 인터럽트를 가로채고 있는 상황이므로 어떠한 다른
           키보드 입력 인터럽트도 호출할 수 없다.   그러므로 직접 키보드
           관련 포트를 통해 입력된 키값을 확인해야 한다.
           입력된 키의 스캔코드 값은 키보드 콘트롤러에 의해 포트번호 60h
           로 전달된다. 우선 그 값을 읽는다. 이 프로그램에서 사용한 TSR
           호출 키는 CTRL+ALT+'['이다. */

        /* '['가 눌려졌다면 */
        if(inportb(0x60) == 0x1A) /* '['의 스캔코드는 0x1A이다 */
        {
                /* 시프트 키에 대한 상태값은 롬바이오스 데이타 영역인
                   0000:0417h에 저장된다. 이곳의 1바이트 값은 Ctrl, Shift
                   Alt등의 키가 눌렸을 경우 특정한 비트가 1로 셋트되는데
                   Ctrl은 2번 비트, Alt는 3번 비트를 통해 알 수 있다. */

                shift_flag = peekb(0, 0x417);

                /* Ctrl과 Alt가 모두 눌려졌다면 */
                if((shift_flag & 8) && (shift_flag & 4))
                {
                        /* 이 부분은 원래의 키보드 인터럽트의 일부분이다.
                           TSR호출키의 처리를 위해 직접 삽입하였다. */

                        shift_flag = inportb(0x61);
                        outportb(0x61, shift_flag | 0x80);
                        outportb(0x61, shift_flag);

                        /* 인터럽트 종료 코드 */
                        outportb(0x20, 0x20);

                        /* 화면 캡쳐 루틴이 반복해서 호출되는 것을 방지하기
                           위해 running이 FALSE일 때만 call_from_user를
                           설정한다 */
                        if(!running) call_from_user = TRUE;

                        /* 인터럽트 리턴(IRET), TSR호출 키가 눌렸을 경우
                           바로 위에서 직접 처리했으므로 chain하지 않고
                           리턴한다 */
                        return;
                }
        }

        old_KEYBOARD(); /* 원래의 핸들러에 chain한다 */
}

/* 도스가 한가할 때 호출되는 인터럽트인 INTR 28h에 대한 새로운 핸들러 */
void interrupt new_NOTBUSY(__DEFARG)
{
        old_NOTBUSY();       /* 원래의 핸들러에 chain한다 */

        /* 화면 캡쳐 루틴이 반복해서 호출되는 것을 방지하기 위해 TSR이
           호출 되었더라도(call_from_user가 TRUE) running이 TRUE이면
           이 부분은 건너뛴다 */
        if(!running && call_from_user)
        {
                running = TRUE;         /* running을 TRUE로 해둬야 중복
                                          실행되지 않는다 */

                capture_screen();       /* 화면 캡춰 루틴을 호출 */

                running = FALSE;        /* 다음 TSR호출을 위해 running과 */
                call_from_user = FALSE; /* call_from_user를 FALSE로... */
        }
}
#pragma warn +pro

void main(void)
{
        old_KEYBOARD = getvect(0x09);   /* 원래의 벡터 저장 */
        old_NOTBUSY = getvect(0x28);

        setvect(0x09, new_KEYBOARD);    /* 새로운 벡터 설정 */
        setvect(0x28, new_NOTBUSY);

        /* 'keep'은 프로그램을 메모리에 상주시킨후 프로그램을 종료한다.
            keep에 전달되는 매개변수는 두가지인데 앞의 것은 항상 0이다.
            그리고 뒤의 것은 메모리에 상주하게될 프로그램의 크기를 패러
            그래프 단위로 환산한 값이다. '패러그래프 단위'라는 것은 16
            으로 나눈 값이다(앞에서 이미 거론한 바 있다).
            메모리에 상주할 프로그램의 크기는 앞서의 강좌 8,9,10에서 설
            명된 내용을 바탕으로 계산된다.

            이미 우리는 PSP가 어떤 프로그램의 시작위치라는 것을 알고 있다.
            그리고 SS는 그 프로그램의 가장 높은 곳, 말하자면 가장 뒤에 있
            다는 것도 알고 있다. 그렇다면 프로그램의 크기는 SS의 가장 마지
            막 바이트 위치에서 PSP의 첫바이트 위치를 뺀 것이 된다는 것을
            미루어 알 수 있게 된다.

                      │        │
                끝위치├────┤ ┬
                      │        │ │ 넓이 = 끝위치-시작위치
                      │        │ │
              시작위치├────┤ ┴
                      │        │

            만약 PSP와 SS에 대해 전혀 들어본 기억 없다는 분들은 강좌 8,9,
            10의 내용을 참조하시기 바란다.

            PSP의 첫머리는 _psp라는 외부 전역변수를 통해 알 수 있다고 했지
            만, SS의 마지막 위치는 어떻게 알 수 있는가?
            앞에서 배우기를 SS에 데이타가 저장됨에 따라 SP가 감소한다고 했
            다. 그렇다면 SP는 프로그램이 시작하는 초기에 이미 SS의 가장 높
            은 곳(마지막)을 가리키고 있었다는 말이 아닌가(스택은 밑으로 커
            지므로)? 결국, SS의 마지막 위치는 SS:SP로 알 수 있다.

            그럼 위와 같은 내용들을 토대로 식을 작성해 보자.

            프로그램 크기(패러그래프 단위) = (스택끝/16) - PSP
                                           = (((SS * 16) + SP) / 16) - PSP
                                           = (SS + (SP / 16)) - PSP

            '스택끝'의 계산에서 '((SS * 16) + SP)'가 된 이유는 이러하다.
            SS:SP의 형태는 메모리의 주소를 지정하는 완전한 방법이 아니다.
            만약 이러한 형태로 주소를 표기한다면 최대 FFFFh번지 밖에 지정하
            지 못한다. 옵셋은 그 세그먼트 내에서의 이동거리이므로 FFFF:FFFF
            의 표현에서 이 둘의 값을 더해야 그 세그먼트에서의 정확한 데이타
            (바이트) 위치가 되는데 만약 그대로 더하게 되면,

                FFFF+FFFF = 0000h

            가 되어 메모리의 가장 낮은 곳을 가리키게 되어 버린다.
            이러한 결과는 분명히 바라는 바가 아니다.
            그래서 IBM PC에서는 1MB 메모리 영역을 모두 주소지정하기 위해서
            세그먼트 주소를 좌측 4비트 시프트(2^4이므로 16을 곱한 효과가 있
            다)한 값에 옵셋값을 더하여 나타내고 있다.

                FFFF << 4 = FFFF0
                          +  FFFF = FFFFFh

            다섯자리의 16진수는 0~1MB의 값을 가질 수 있다.

            여기까지가 모두 준비되었다면, 이제 프로그램을 상주시키면 된다.
        */

        keep(0, (_SS+(_SP/16)-_psp)); /* 메모리 상주후 종료 */
}

이 프로그램을 컴파일하는 방법은 쉽다.

        TCC CAPSCR.C 또는
        BCC CAPSCR.C

라고 하면 된다. 다른 복잡한 옵션은 생각할 필요가 없다.

프로그램을 실행 후에는 다음의 키들을 사용할 수 있다.

        CTRL+ALT+'[': CTRL과 ALT를 누른 상태에서 문자 '['를 누른다.
                      이 키 조합은 TSR을 활성화시켜 화면에 반전막대를
                      표시하고 화면을 프린트할 준비를 한다.

        '+'와 '-'   : 현재의 반전 막대 위치로 부터 프린트할 영역을
                      설정한다. '-'는 그 설정을 해제한다.

        Up/Down     : 반전 막대를 아래/위로 이동한다.

        ESC         : 프린트 작업을 취소하고 TSR을 숨겨 도스가 원래 하던
                      일을 계속하도록 한다. TSR을 다시 활성화시키기 위해
                      서는 CTRL+ALT+'['를 눌러야 한다.

불행하게도, 이 프로그램에는 몇가지의 단점이 있다. 그것은,

        - 램상주를 해제할 수 있는 코드를 갖고 있지 않다.

        - 재상주를 방비하지 않는다.

        - 반전 막대가 지나간 위치는 원래의 색상을 무시하고
          무조건 검정 바탕에 흰색 글씨가 찍힌다.

이런 내용들인데, 첫번째의 경우에는 사실 해결할 수 있는 방법이 없는 것이 아니

램상주를 해제하는 루틴은 필자가 이 프로그램을 모두 작성하고 나서 마지막으로
삽입
했는데,  TSR을 활성화 시킨 뒤에  특정키를 누름으로써 램상주를 해제하는 방법
이었
다. 그
런데 문제가 발생했다. 램상주를 해제하고 나서 메모리의 상태를 검사해 보니 384
바이
트의 메모리가 제대로 해제되지 않은 채  메모리의 도막난 상태로 남는 것이었다.

문제의
발단은 쉽게 파악됐으나 그것을 처리하기 위한 사전 준비를 하기에는 시간이 너
무 부
족했다. 결국, 일단의 욕심은 접어 두고 다음 기회로 돌리게 되었다.  정히 지금 당

램상주
를 해제할 수 있는 코드를 프로그램에 넣고 싶다는 분들을 위해 편법을 한가지 알

드린다. 이 방법은 간단하다.  main함수를 다음과 같이 수정하면 된다.

        void main(void)
        {
                old_KEYBOARD = getvect(0x09);   /* 원래의 벡터 저장 */
                old_NOTBUSY = getvect(0x28);

                setvect(0x09, new_KEYBOARD);    /* 새로운 벡터 설정 */
                setvect(0x28, new_NOTBUSY);

                system("");

                setvect(0x09, old_KEYBOARD);    /* 원래의 벡터 복구 */
                setvect(0x28, old_NOTBUSY);
        }

'keep'이 없어지는 대신 'system'과 그 아래 부분이 들어간다.
system에 넘겨지는 파라미터는 ""(NULL)이다. 램상주를 해제하려면 도스의 프롬

트상에서 'EXIT'를 치면 된다.

이 방법은 정상적인 램상주 방법은 아니다. 도스의 EXEC 기능을 응용한 것인데,
동작하는 것은 램상주와 똑같다. 오히려 더욱 안정적으로 동작한다.

두번째의 단점에 대해서는 확실히  그 대책을 알려 드릴 수도 있으나 이는 권장할
만한 방법이 못될 것 같다. 램상주를 취급한 서적들에서 일반적으로 설명된 방법
인데, 특정의 잘 사용하지 않는 인터럽트의 벡터값을 어떤 값으로 바꿔 놓고, 그
값을 비교하여 램상주 되었는지를 확인하는 방법인데 이는 그다지 신빙성이 없을
것 같다. 또다른 한가지 방법은 마이크로소프트에 의뢰해서 자신이 만든 TSR 프

그램에 대한 ID를 획득하는 것인데 이 또한 세계를 무대로 뛰어다니는 프로그래머
나, 아니면 독특한 TSR 프로그램으로 특별한 일을 한다고 생각되지 않는다면 그

게까지 할 여력은 없을 것이라 판단된다.  좀 더 색다른 방법이 방법이 없다면 우
선은 프로그램을 실행하기 전에 이미 상주되었는지를 확인하는 신중함으로 대책

삼을 일이다.

세번째의 단점은, 이 강좌를 보신 누군가에 의해 개선되었으면 하는 바램에서 아
예 삽입하지 않았다. 그다지 어렵지 않을 것이라 생각된다.

이외에도 개선할 점은 많이 있다. 예를 들자면, 지금은 현재의 화면만을 캡쳐하지
만 지나간 화면도 잡을 수 있다면 그 또한 금상첨화가 아니겠는가?
또, 화면을 잡을 때 줄 단위가 아닌 바이트 단위로 잡을 수 있다면 보다 유용할
수 있으리라 여겨진다.

필자가 이 프로그램을 개발하기 위해 컴퓨터를 재부팅한 횟수는 아마도 수십번
(단 하나의 프로그램을 개발하기 위해)이 될 것이다. 버전을 1.0이라고 했지만
최초에 작성했던 코드와 지금의 코드를 비교하면 버전이 100쯤은 됨직하다.
그러나 이 프로그램을 작성하게 된 동기가 프로그램동을 위한 강좌의 명목이었
고, 사실상 아직도 많은 부족한 점을 지니고 있기에 앞으로의 가능성을 위해
자신의 작은 수고로움은 덮어 두기로 했다(여러분이 좀 더 강화된 버전을 만들
어 준다면 그보다 기쁜 일은 없겠다).


'★─Programing > ☆─O. S.' 카테고리의 다른 글

[O.S.] 프로세스의 병행성 구분  (0) 2009.01.31
[O.S.] 쓰레드와 스케줄링  (0) 2009.01.31
[O.S.] 프로세스 란?  (0) 2009.01.31
Comments