3.3. start_kernel() 함수에서의 IDT 의 초기화

start_kernel() 함수는 몇가지 초기화를 하고, trap_init() 함수와 init_IRQ() 함수를 차례로 호출해서 프로세서의 IDT 에 trap gate 와 interrupt gate 를 초기화시킨다. start_kernel() 함수는 /usr/src/linux/init/main.c 에 있다.

3.3.1. trap_init() 함수

trap_init() 함수는 다음과 같다 :

---------------------------------------
/usr/src/linux/arch/i386/kernel/traps.c
---------------------------------------
void __init trap_init(void)
{
	set_trap_gate(0,&divide_error);
	set_trap_gate(1,&debug);
	set_intr_gate(2,&nmi);
	set_system_gate(3,&int3);	/* int3-5 can be called from all */
	set_system_gate(4,&overflow);
	set_system_gate(5,&bounds);
	set_trap_gate(6,&invalid_op);
	set_trap_gate(7,&device_not_available);
	set_trap_gate(8,&double_fault);
	set_trap_gate(9,&coprocessor_segment_overrun);
	set_trap_gate(10,&invalid_TSS);
	set_trap_gate(11,&segment_not_present);
	set_trap_gate(12,&stack_segment);
	set_trap_gate(13,&general_protection);
	set_intr_gate(14,&page_fault);
	set_trap_gate(15,&spurious_interrupt_bug);
	set_trap_gate(16,&coprocessor_error);
	set_trap_gate(17,&alignment_check);
	set_trap_gate(18,&machine_check);
	set_trap_gate(19,&simd_coprocessor_error);

	set_system_gate(SYSCALL_VECTOR,&system_call);
		:
		:	
}
여기 나오는 set_xxxx_gate() 함수는 모두 다음과 같은 _set_gate() 매크로로 매핑된다 :
---------------------------------------
/usr/src/linux/arch/i386/kernel/traps.c
---------------------------------------
#define _set_gate(gate_addr,type,dpl,addr) \
do { \
  int __d0, __d1; \
  __asm__ __volatile__ ( \
	"movw %%dx,%%ax  \n\t" \
	"movw %4,%%dx    \n\t" \
	"movl %%eax,%0   \n\t" \
	"movl %%edx,%1" \
	:"=m" (*((long *) (gate_addr))), \
	 "=m" (*(1+(long *) (gate_addr))), "=&a" (__d0), "=&d" (__d1) \
	:"i" ((short) (0x8000+(dpl<<13)+(type<<8))), \
	 "3" ((char *) (addr)),"2" (__KERNEL_CS << 16)); \
} while (0)

또한, set_trap_gate(), set_system_gate(), set_intr_gate() 등의 함수는 다음과 같이 정의되어 있다 :

---------------------------------------
/usr/src/linux/arch/i386/kernel/traps.c
---------------------------------------
void set_intr_gate(unsigned int n, void *addr)
{
	_set_gate(idt_table+n,14,0,addr);
}

static void __init set_trap_gate(unsigned int n, void *addr)
{
	_set_gate(idt_table+n,15,0,addr);
}

static void __init set_system_gate(unsigned int n, void *addr)
{
	_set_gate(idt_table+n,15,3,addr);
}

static void __init set_call_gate(void *a, void *addr)
{
	_set_gate(a,12,3,addr);
}
이제 trap_init() 함수에서 정의하는 게이트들 중 set_intr_gate(14,&page_fault); 를 선택해서 어떻게 확장이 되며, 어떠한 결과를 낳게 되는지 생각해 보겠다. 이 라인은 다음과 같이 확장된다 : (편의상 탭(\t) 과 개행문자(\n), 행계속표시(\), 따옴표 일부는 생략하겠다)
_set_gate(idt_table+14, 14, 0, page_fault);
-------------------------------------------
#define _set_gate(gate_addr,type,dpl,addr)
do {
  int __d0, __d1;
  __asm__ __volatile__ (
	movw %%dx,%%ax
	movw %4,%%dx
	movl %%eax,%0
	movl %%edx,%1
	:"=m" (*((long *) (gate_addr))),
	 "=m" (*(1+(long *) (gate_addr))), "=&a" (__d0), "=&d" (__d1)
	:"i" ((short) (0x8000+(dpl<<13)+(type<<8))),
	 "3" ((char *) (addr)),"2" (__KERNEL_CS << 16));
} while (0)

위의 어셈블리 명령들을 자세히 보면, 먼저, 출력부에 gate_addr 번지와, gate_addr+1 번지를 지정해 두고 있다. 이것은, 예외상황 14번, 즉, 페이지 폴트 예외상황의 경우, 14번째 게이트이므로, idt_table+14 의 위치이다. 그리고, 이것은 메모리 오퍼랜드이므로 "=m" 을 사용하였다.

그리고, "=&a" (__d0) 와 "=&d" (__d1) 을 이용해서 eax 레지스터와 edx 레지스터를 각각 %2 와 %3 에 early clobber 로 할당했다. early clobber 옵션(&)으로 할당했으므로, 입력부에 나오는 "2" 와 "3" 의 값이 어셈블리 인스트럭션들이 해석되기 전에 할당되어 들어간다. 즉, eax 레지스터에 (__KERNEL_CS << 16) 의 값이, edx 에 ((char *)(addr)) 의 값, 즉, 인터럽트 게이트에 저장되는 핸들러의 코드 세그먼트에서의 오프셋의 값이 저장된다.

그리고서, movw %%dx, %%ax 를 수행한다. 그렇게 하면, 다음 그림과 같이 edx 레지스터의 하위 16비트가 eax 레지스터의 하위 16비트로 복사되게 된다. [1]

그림 3-1. Making Interrupt Gate 1

그리고, movw %4, %%dx 에 의해서 4번째 오퍼랜드, 즉, ((short)(0x8000+(dpl<<13)+(type<<8))) 이 dx 레지스터에 복사되는데, 인터럽트 게이트의 경우, dpl 은 0, type 은 14 이므로, 복사되는 값은, 0x8e00 이 된다. 그러면, eax 와 edx 의 값은 다음 그림과 같다 :

그림 3-2. Making Interrupt Gate 2

그리고 나서, eax 레지스터의 값은 gate_addr 번지에, edx 값은 gate_addr+1 번지에 각각 저장하게 되는데, 이것을 인텔에서 제공하는 매뉴얼의 인터럽트 게이트 디스크립터와 비교해 보도록 하자. 인텔 프로세서들의 interrupt gate descriptor 는 다음과 같은 구조를 가진다 :

그림 3-3. Interrupt gate descriptor

그림에서, P 는 segment present flag 이며, dpl 은 descriptor privilege level, D 는 게이트의 사이즈를 결정하는 것으로써, 1 이면, 32비트, 0 이면 16비트이다. 앞서 설명한 eax 와 edx 를 각각 해당하는 곳에 넣어서 비교해 보면, 정확히 __KERNEL_CS 세그먼트의 (우리가 살펴보는 것이 14번, 페이지 폴트, 핸들러 : page_fault) page_fault 레이블(주소)이 해당 필드에 들어가게 되고, dpl 은 0,즉, 커널 레벨의 권한을 가지며, 게이트의 타입을 결정하는 8-12번 비트는 01110 으로써, D=1, 즉 32비트 게이트에, 인터럽트 게이트의 타입과 일치하게 된다.

나머지 set_system_gate(), set_trap_gate(), set_call_gate() 와 같은 함수들도, 이와 마찬가지로 적절하게 게이트 디스크립터를 지정된 번지 (첫번째 인수) 에 만들어서 넣어 주게 된다.

이제, 다시 trap_init() 함수로 돌아가서 소스코드를 살펴보겠다.

함수에서는 0 - 19 번까지의 idt 엔트리에 각각 특정 함수의 주소를 넣는 식으로 idt 엔트리들을 초기화 시킨다. 그 함수들은 모두 /usr/src/linux/arch/i386/kernel/entry.S 에 정의된 어셈블리 함수들로써, 인텔 프로세서에서 정의하는 0 - 19번까지의 미리 예약된 예외상황 및 인터럽트 에 대한 핸들러를 구현하고 있다.

그들 중 주목해야 할 부분은, 14번 page fault 예외상황에 대한 핸들러로써, 이 핸들러는, 리눅스 페이징 시스템에서 매우 중요한 역할을 한다.

또 한가지, SYSCALL_VECTOR 라고 정의된 상수, 즉 0x80 번째 idt 엔트리는 특수한 엔트리로써, 리눅스에서 system call 을 구현할 때 사용하는 예외상황 핸들러인 system_call 을 가리키고 있는 것을 볼 수 있다. 즉, 리눅스의 시스템 콜은 int 80 인스트럭션을 이용하여 구현된다는 것을 알 수 있다.

3.3.2. init_IRQ() 함수

앞 절에서 0-0x19 번 사이의 idt 테이블을 초기화 시키는 커널의 루틴들을 살펴 보았다. 그러면, 나머지 idt 테이블은 어떻게 초기화시키는가? 그부분을 살펴보도록 하자.

이 함수는, 인텔이 예약해둔 0 - 0x19 번 사이의 idt 이후의 부분들에 대한 인터럽트 서비스 루틴을 정의해 주는 부분이다. 0x20 번 부터 매핑을 하는데(소스는 보이지 않겠다. 이 함수는 /usr/src/linux/arch/i386/i8259.c 에 있다.) 0x20 번 idt 엔트리부터 irq0 번으로 할당해서 (idt entry 0x20 : irq0, idt entry 0x21 : irq1 이런식으로) 각각 IRQ0xNN_interrupt() 으로 매핑시켜 준다.

3.3.2.1. 8259A and I/O APIC

인텔의 프로세서들에는 외부 인터럽트 시그널을 받아들이기 위해 준비된 핀들이 있다(다른 프로세서도 마찬가지다 -ㅅ-) 특히 P6 패밀리에는 LINT0 LINT1 이라는 핀과 INTR, NMI 핀의 두가지가 존재하는데, LINT[1:0] 핀은 프로세서 내부의 local APIC(Advanced Programmable Interrupt Controller) 과 연결되어 있다. 이 핀들은, 프로세서에서 local APIC 이 disable 되었을 경우, 각각 INTR 과 NMI 핀으로써 동작한다. 일반적으로, APIC 가 사용되는 경우는 SMP (Symmetic Multi Processor, 대칭형 다중 프로세서) 시스템에서 외부의 I/O APIC 컨트롤러와 함께 사용되어져서 프로세서 간의 통신 등에 사용된다. 만약 single processor 시스템에서 사용된다면, 외부 인터럽트 컨트롤 뿐만 아니라 프로세서 자체의 타이머, performance counter 등과 같은 인터럽트, 또한, Non Maskable Interrupt Watchdog 같은 인터럽트도 사용할 수 있도록 해 준다.

이 문서에서는 single processor 시스템에서 외부 인터럽트 컨트롤러로 8259A 를 사용하고 있는 상황을 예로 들겠다. APIC 시스템에 대해서는 multiprocessor 시스템과 밀접한 관련이 있기 때문에 다루지 않겠다.

우선, 8259A 컨트롤러에 대해서 알아보도록 하겠다.

8259A/82C59A-2 컨트롤러들은 IR0 - IR7 까지의 8 개의 인터럽트 핀을 가지고 있다. I/O 장치들이 cpu 의 처리가 필요할 때에는 이 핀들 중 하나에 시그널을 보낸다. 그러면, 8259 컨트롤러는 자신의 INT 핀을 activate 시키는데, 이 핀은 CPU 의 INTR 라인에 연결되어 있다. INTR 라인이 active 되면, CPU 는 실행하던 instruction 의 수행을 완료하고, 8259 의 INTA 라인에 INTR 신호를 받았다는 뜻으로 (acknowledge) 신호를 해 준다. 자신의 INTA 라인에 CPU 에서 준 신호를 받은 8259 컨트롤러는 자신과 연결되어 있는 데이터 버스에 인터럽트 서비스 루틴의 주소 (8-bit) 를 실어 준다. CPU 의 종류에 따라서 이처럼 데이터 버스(D0 - D7) 에 데이터를 실어주는 phase 가 두번 혹은 한번 일어날 수 있다. 그러면, CPU 는 데이터버스에 실린 내용을 읽어서 interrupt type number 를 결정하고, 해당되는 인터럽트 서비스 루틴(ISR) 을 실행시킨다.

일반적으로 우리가 사용하는 ibm pc 에서는 8259 칩 두개를 cascade 시켜서 총 15 개의 IRQ 라인을 사용하게 된다. IRQ 라인 중 하나는 두개의 컨트롤러를 서로 cascade 할 때에 사용한다. 두개의 8259 컨트롤러는 다음 그림과 같이 cascade 해서 사용할 수 있다 : (그림에선 세개를 cascade 시킨 경우를 보였는데, 여기서 하나만 삭제한 상태로 이해하기 바란다)

그림 3-4. cascaded 8259A [2]

이처럼 cascade 되었을 경우, slave 의 INT 라인이 master 의 IRQ 라인 중 하나로 들어가게 된다.

이처럼 8259 의 IRQ 라인에 신호가 들어오고서, CPU 에서 INT 신호를 8259가 받은 후, 데이터 버스에 실어 주는 내용과, 그것을 CPU 가 어떻게 해석해서 동작하는가에 대한 내용은, 자료를 자세히 읽어보지 않아서 잘 모르겠습니다. 혹시 아시는 분께서는, 혹은, 관심이 있어서 저 대신 조사하신 분들께서는 제게 가르침을 주시면 감사하겠습니다. :) 아마도, 어느날엔가 제가 여유가 생기면 조사해 보겠지만, 언제가 될지는 장담하기 어렵군요 :(

3.3.2.2. init_IRQ() 함수

이제, IRQ n 에 해당하는 벡터들과 서비스 루틴들을 초기화 시키는 함수인 init_IRQ() 함수로 들어가 보도록 하겠다.

첫 단계로, init_IRQ() 함수를 리스트해 보도록 하겠다 :

----------------------------------------------------------
/usr/src/linux/arch/i386/kernel/i8259.c
----------------------------------------------------------
void __init init_IRQ(void)
{
	int i;

#ifndef CONFIG_X86_VISWS_APIC
	init_ISA_irqs();
#else
	init_VISWS_APIC_irqs();
#endif
	/*
	 * Cover the whole vector space, no vector can escape
	 * us. (some of these will be overridden and become
	 * 'special' SMP interrupts)
	 */
	for (i = 0; i < NR_IRQS; i++) {
		int vector = FIRST_EXTERNAL_VECTOR + i;
		if (vector != SYSCALL_VECTOR) 
			set_intr_gate(vector, interrupt[i]);
	}
		:
		:
여기에 나오는 set_intr_gate() 함수는 앞서 설명했던, set_trap_gate() 함수와 인자값만 달리하여서 _set_gate() 함수를 콜하는 매크로로 정의되어 있다. 그러면, 어떤값이 interrupt descriptor table 에 들어가게 되는지 앞에서 설명하였다.

우선, 인텔 아키텍처에서의 인터럽트는 0x00 - 0x1f 까지 32개의 인터럽트는 프로세서가 용도를 미리 지정해두고서 사용하게 되어 있다는 사실을 주지해야 한다. 그러면, 비어있는 최초의 벡터인 0x20 번 벡터로부터는 외부 인터럽트, 즉, 프로세서의 INTR 핀으로부터 signalling 되는 인터럽트들, 즉, 외부 인터럽트 리퀘스트 라인으로부터의 인터럽트를 처리하는 벡터가 들어가게 된다. 즉, IRQ(Interrupt ReQuest Line) 0 번은, 프로세서의 인터럽트 게이트 20번째 엔트리에서 정의하는 벡터에서 처리하게 되는 것이다.

그럼, 다시 함수로 돌아가서 내용을 하나씩 살펴보도록 하자. NR_IRQS 값만큼 루프를 돌면서 FIRST_EXTERNAL_VECTOR 번째 게이트로부터 하나씩 interrupt[] 배열에 정의된 함수 포인터를 벡터로 할당하고 있다. 전역변수 interrupt 는 같은 파일에 다음과 같이 정의되어 있다 :

----------------------------------------------------------
/usr/src/linux/arch/i386/kernel/i8259.c
----------------------------------------------------------
#define IRQ(x,y) \
	IRQ##x##y##_interrupt

#define IRQLIST_16(x) \
	IRQ(x,0), IRQ(x,1), IRQ(x,2), IRQ(x,3), \
	IRQ(x,4), IRQ(x,5), IRQ(x,6), IRQ(x,7), \
	IRQ(x,8), IRQ(x,9), IRQ(x,a), IRQ(x,b), \
	IRQ(x,c), IRQ(x,d), IRQ(x,e), IRQ(x,f)

void (*interrupt[NR_IRQS])(void) = {
	IRQLIST_16(0x0),

#ifdef CONFIG_X86_IO_APIC
			 IRQLIST_16(0x1), IRQLIST_16(0x2), IRQLIST_16(0x3),
	IRQLIST_16(0x4), IRQLIST_16(0x5), IRQLIST_16(0x6), IRQLIST_16(0x7),
	IRQLIST_16(0x8), IRQLIST_16(0x9), IRQLIST_16(0xa), IRQLIST_16(0xb),
	IRQLIST_16(0xc), IRQLIST_16(0xd)
#endif
};
여기서 각각의 #define 매크로를 expand 시키는 것까지는 이야기하지 않겠다. 결과만 이야기하자면, interrupt[] 배열의 각 엔트리는 각각 IRQ0xNN_interrupt() 라는 어셈블리 함수(레이블)를 가리키는 포인터로 구성되게 된다.

다시한번 소스를 읽어보 겠다. (CONFIG_X86_VISWS_APIC 옵션은, SGI Visual Workstation 을 선택했을 때만 생기는 옵션이다. 지금 x86 을 다루고 있으므로 무시하고 넘어가자) 우선, init_ISA_irqs() 함수에서는 다음과 같이 나중에 실제 인터럽트 핸들링시에 사용할 irq_desc[] 배열을 초기화 한다 :

----------------------------------------------------------
/usr/src/linux/arch/i386/kernel/i8259.c
----------------------------------------------------------
void __init init_ISA_irqs (void)
{
	int i;

	init_8259A(0);

	for (i = 0; i < NR_IRQS; i++) {
		irq_desc[i].status = IRQ_DISABLED;
		irq_desc[i].action = 0;
		irq_desc[i].depth = 1;

		if (i < 16) {
			/*
			 * 16 old-style INTA-cycle interrupts:
			 */
			irq_desc[i].handler = &i8259A_irq_type;
		} else {
			/*
			 * 'high' PCI IRQs filled in on demand
			 */
			irq_desc[i].handler = &no_irq_type;
		}
	}
}
이 함수에서는 irq_desc[] 배열의 초기화를 하는데, irq_desc[] 배열에 관한 자세한 내용은 나중에 이야기하도록 하고, 일단, 간단한 사항만 이야기하고 넘어가겠다. irq_desc[] 배열은 다음과 같이 선언되어 있다 :
----------------------------------------------------------
/usr/src/linux/include/linux/interrupt.h
----------------------------------------------------------
struct irqaction {
	void (*handler)(int, void *, struct pt_regs *);
	unsigned long flags;
	unsigned long mask;
	const char *name;
	void *dev_id;
	struct irqaction *next;
};

----------------------------------------------------------
/usr/src/linux/include/linux/irq.h
----------------------------------------------------------
/*
 * Interrupt controller descriptor. This is all we need
 * to describe about the low-level hardware. 
 */
struct hw_interrupt_type {
	const char * typename;
	unsigned int (*startup)(unsigned int irq);
	void (*shutdown)(unsigned int irq);
	void (*enable)(unsigned int irq);
	void (*disable)(unsigned int irq);
	void (*ack)(unsigned int irq);
	void (*end)(unsigned int irq);
	void (*set_affinity)(unsigned int irq, unsigned long mask);
};

typedef struct hw_interrupt_type  hw_irq_controller;

/*
 * This is the "IRQ descriptor", which contains various information
 * about the irq, including what kind of hardware handling it has,
 * whether it is disabled etc etc.
 *
 * Pad this out to 32 bytes for cache and indexing reasons.
 */
typedef struct {
	unsigned int status;		/* IRQ status */
	hw_irq_controller *handler;
	struct irqaction *action;	/* IRQ action list */
	unsigned int depth;		/* nested irq disables */
	spinlock_t lock;
} ____cacheline_aligned irq_desc_t;

extern irq_desc_t irq_desc [NR_IRQS];
이 구조체 배열은 각각의 irq 라인의 상태에 관한 정보와 그에 따른 핸들러들의 포인터를 가지고 있게 된다. 최초에는, init_ISA_irqs() 함수에서 보이듯이, status 에 IRQ_DISABLED 라는 값을 넣고, 0 - 15 번의 원소의 handler 값은 모두 i8259A_irq_type 을 가리키는 포인터로 초기화된다. i8259A_irq_type 은 다음과 같이 정의되어 있다 :
----------------------------------------------------------
/usr/src/linux/arch/i386/kernel/i8259.c
----------------------------------------------------------

static struct hw_interrupt_type i8259A_irq_type = {
	"XT-PIC",
	startup_8259A_irq,
	shutdown_8259A_irq,
	enable_8259A_irq,
	disable_8259A_irq,
	mask_and_ack_8259A,
	end_8259A_irq,
	NULL
};
자세한 것은 뒤에서 다루기로 하고, 다시 init_IRQ() 함수로 돌아가자.

앞서 이야기한 것처럼 IDT 의 각각의 원소의 벡터를 IRQ0xNN_interrupt() 레이블을 가리키도록 설정하고, 리턴한다. IRQ0xNN_interrupt() 레이블은 어떻게 정의되어 있는지 따라가 보겠다. i8259.c 파일의 첫부분에 보면 다음과 같은 매크로가 정의되어 있다 :

----------------------------------------------------------
/usr/src/linux/arch/i386/kernel/i8259.c
----------------------------------------------------------
/*
 * Common place to define all x86 IRQ vectors
 *
 * This builds up the IRQ handler stubs using some ugly macros in irq.h
 *
 * These macros create the low-level assembly IRQ routines that save
 * register context and call do_IRQ(). do_IRQ() then does all the
 * operations that are needed to keep the AT (or SMP IOAPIC)
 * interrupt-controller happy.
 */

BUILD_COMMON_IRQ()

#define BI(x,y) \
	BUILD_IRQ(x##y)

#define BUILD_16_IRQS(x) \
	BI(x,0) BI(x,1) BI(x,2) BI(x,3) \
	BI(x,4) BI(x,5) BI(x,6) BI(x,7) \
	BI(x,8) BI(x,9) BI(x,a) BI(x,b) \
	BI(x,c) BI(x,d) BI(x,e) BI(x,f)

/*
 * ISA PIC or low IO-APIC triggered (INTA-cycle or APIC) interrupts:
 * (these are usually mapped to vectors 0x20-0x2f)
 */
BUILD_16_IRQS(0x0)
처음 나오는 BUILD_COMMON_IRQ() 매크로는 다음처럼 정의되어 있다 :
----------------------------------------------------------
/usr/src/linux/include/asm-i386/hw_irq.h
----------------------------------------------------------

#define BUILD_COMMON_IRQ() \
asmlinkage void call_do_IRQ(void); \
__asm__( \
	"\n" __ALIGN_STR"\n" \
	"common_interrupt:\n\t" \
	SAVE_ALL \
	"pushl $ret_from_intr\n\t" \
	SYMBOL_NAME_STR(call_do_IRQ)":\n\t" \
	"jmp "SYMBOL_NAME_STR(do_IRQ));

주석

[1]

gcc 의 인라인 어셈블리에 대한 자세한 내용은, 이호씨의 Assembly Example, 허태준씨의 GCC Inline Assembly, 그리고, 본인이 번역한 Assembly Howto 를 참조하기 바란다. 모두 kldp에서 구할 수 있다. 그리고, 방금 언급한 문서들에서 설명하지 않는 gcc inline assembly 의 modifier 에 관한 자세한 내용은 gcc 매뉴얼에서 볼 수 있다 : http://gcc.gnu.org/onlinedocs/gcc-2.95.3/gcc_16.html#SEC175

[2]

인텔의 문서에서 허락없이 전제했음