[컴][디버그] INT 3 의 동작분석

int3 in linux / how to work INT3 interrupt / 인터럽트 핸들러



여기서는 INT3 interrupt 가 발생했을 때, 실제 어떻게 동작하는지 대략적으로 살펴보자.



INT3 이 발생하면, 정해진 handler (interrupt handler) 가 수행된다.

어떤 경로를 통해 interrupt handler 가 호출되는 지를 확인해 보도록 하자. os 부분은 source code 를 확인할 수 있는 linux 를 택했다.

<그림. interrupt call> 을 먼저 확인하자. interrupt call 의 그림을 중심으로 이야기를 풀어나가 보자.

전체적인 흐름은 밑에 "Linux 에서 INT3 동작 정리" 를 확인하자.


interrupt 동작방식

from operating-system concepts 10th edition
  1. I/O 작업이 실행되기 위해서, device driver 가 적절한 register 들을 device controller 에 load 해야 한다. 
  2. 그러면, device controller 는 이 registers 의 내용을 확인해서 어떤 동작을 할지 정한다.
  3. controller 는 device 에서부터 local buffer 로 data 전송을 시작한다.
  4. data 전송이 끝나면, device controller 가 device driver 에게 작업이 끝났다고 알려준다.
  5. 그리고 나서 device driver 가 os 의 다른 부분으로 control 을 넘겨준다.
  6. 이때 만약 read i/o 작업이라면 data 의 pointer 를 넘기는 식으로 결과 data 를 주기도 한다.
  7. 다른 작업에서는 status 값을 준다.
  8. controller 가 작업이 끝난것을 device driver 에게 알려줄때 interrupt 를 사용하게 된다.

  1. cpu 는 interrupt-request line 이라는 wire 를 갖는다. 그래서 cpu 가 매 instruction 을 수행할 때마다, 그것을 sense 한다.
  2. 그래서 만약 controller 가 signal 을 interrupt-request line 에 보냈다면,
  3. cpu 가 그것을 인지하고, interrupt number 를 읽고, interrupt-handler routine 으로 jump 한다. 이때 이 interrupt number 가 interrupt vector 의 index 가 된다.
  4. interrupt handler 가 작업중에 변경이 돼야 하는 state 를 저장하고,
  5. interrupt 의 원인을 파악, 필요한 작업을 하고, 끝난후 state 를 돌려놓는다.
  6. 그리고 return_from_interrupt instruction 을 수행해서 CPU 를 interrupt 일어나기 이전의 execution state 로 돌려놓는다.



IDT

IDT 가 무엇인지 알아보자.

interrupt vector

interrupt vector 는 intrrupt handler 의 memory 주소이다.[ref. 4]
그리고 이 interrupt vector 를 여러 개 모아놓은 table 이 intrrupt vector table 이다.

IDT

Interrupt Descriptor Table(IDT) 는 interrupt vector table 을 구현해 놓은 것으로 x86 architecture 에서 쓰인다. 이 녀석은 cpu(processor) 가 intterupt 나 exception 발생 시에 쓰게 된다. IDT 는 총 256개의 interrupt vector 로 되어 있다.

exception 의 종류

exception 는 2개의 category 로 나눌 수 있다.
  1. Hardware generated exceptions : Processor 가 만들어 내는 것(Faults, Traps, Aborts)
  2. Software generated exceptions : int, int3 같은 programmed exceptions

IDT 에서 위치에 따른 interrupt 종류

IDT 는 아래 3가지 경우에 쓰인다.
  1. software inturrupt (software exceptions)
  2. hardware inturrupt ( hardware exceptions)
  3. processor exceptions
첫 32개는 processor exception 을 위해 예약되어 있다.
  • 0~31 : hardware generated exceptions[ref. 3]
  • 32~47 : maskable interrupts(such as, IRQs)
  • 48~255 : software interrupts

IDT 는 이름에서 알 수 있듯이 table 이다. 그럼 이 table 의 하나의 entry 는 어떻게 구성되어 있을까. 이 entry 들을 intel 에서는 gate 라고 하는 듯 하다. 이 gate 에서 알아보자.


intel gates

intel CPU 에서 제공하는 mode

  1. real mode
  2. protected mode
OS 들은 protected mode 를 사용해서 user process 가 critical register 에 접근하는 것을 막는다.

4가지 previliege level

  1. ring0 : kernel 은 이 level 에서 실행된다. kernel 은 그래서 cpu 의 모든 register, 모든 hardware 와 memory 에 접근 가능하다.
  2. ring1
  3. ring2
  4. ring3 : 보통 user application 이 실행되는 level
현재 수행되고 있는 program 의 privilege level 은 CPL register(Current Privilege Level Register)에 저장된다.

gates

protected mode 에서 IDT 는 8-byte의 descriptor 의 배열로 되어 있다. 이 8-byte descriptor 가 IDT 의 entry 가 된다.
이 descriptor 들은 셋 중에 하나다.
  1. interrupt gates
  2. trap gates
  3. task gates
gate 는 그냥 intel 에서 정의한 struct 의 하나인데, 호출할 procedure 의 주소,  privilege level 에 대한 정보와 같은 것들을 가지고 있다고 한다.[ref. 5]
1, 2 는 code 가 있는 memory loaction 을 가리킨다.
이 둘의 차이는
  • interrupt gates 는 hardware interrupt 를 위해 만들어진 녀석이라 interrupt 하는 것 이외에 다른 일은 불가능하다.
  • trap gates 는 software interrupt 나 exceptions 을 처리하는 데에 쓰인다.
는 것이다.
  • task gates 는 현재 task-stae 가 active 인 segment 를 switch 가 되게 만든다.
이렇게 task가 switch 를 할 때 hardware task switching mechanism 을 이용한다. 이것을 이용해서 processor 의 사용을 효과적으로 다른 프로그램이나, 쓰레드, 프로세스로 넘길 수 있다. 참고로 linux 에서는 이 task gates 를 사용하지 않는다.[ref. 6]

protected mode IDT 는 물리적인 memory 어느 곳에 상주하지만, 딱히 정해진 위치에 있지 않다. 그래서 이 녀석의 주소를 저장하고 있을 곳이 필요하다. 그 녀석이 IDTR 이다. 그리고, 이 register에 IDT 의 주소를 load 해 줄 때 쓰는 명령어가 LIDT 이다.

image
< IDT Gate descriptor / from: ref.2 Figure 6-2 >

IDTR

CPU에는 IDT 를 위한 register(IDTR)가 하나 있는데, 얘가 table 의 physical base address 주소와 length 를 가지고 있게 된다.

IDTR 은 base address 를 저장하는 부분 4byte 와 length(limit) 를 저장할 수 있는 부분 2byte 로 되어 있다. limit 은 IDT 의 마지막 1byte 의 주소를 알아내기 위한 값이다. 그래서 8N-1 의 계산을 하게 된다. 1개의 interrupt vector 가 있으면 마지막 byte 는 7 이 된다.

그래서 interrupt 가 발생하면 그 숫자에 8을 곱해서 base address 에 더해서 나온 주소(이 주소를 A라 하자.)에 해당 descriptor 가 있게 된다.

이 A 주소가 존재하는 주소인지에 대한 검사는 length를 가지고 하게 된다. 만약 주소 A가 너무 크면 exception 이 발생하고, 정상인 경우에는 주소 A에 있는 descriptor 를 불러오고 불러온 descriptor의 type 과 contents 에 따라 동작이 취해진다.

IDT 는 2KB(8 byte 의 entry 가 256) 의 크기를 갖는다. 하지만, IDT 는 더 작은 수의 descriptor 를 갖고 있을 수 있다. 왜냐하면, 발생할 것 같은 interrupt 나 exception 에 대한 descriptor 만 있으면 되기 때문이다. 단, 비어 있는 slot 은 'P' 가 '0' 으로 set 돼야 한다.[ref. 2]

IDT instructions

이 IDT 를 위한 instruction 이 2개가 있다.
  1. LIDT : load IDT register, IDT register에 IDT의 base address 와 limit 을 불러오는 명령어. CPL 이 '0'일 때만 가능하다. 보통 OS 가 초기화될 때 한 번 호출된다.
  2. SIDT : store IDT register, IDT register 에 있는 내용을 memory 로 copy 해 준다. CPL 에 상관없이 가능하다.

image
< IDT와 IDTR, ref. 5>



LINUX 에서 INT3 동작 정리

  1. IDTR 을 통해서 interrupt vector 를 구하게 된다.(< 그림. IDT와 IDTR >)
  2. 이 interrupt vector 를 이용해서 IDT 에 있는 trap gate 를 보고 interrupt procedure 의 주소를 계산해 낸다.( < 그림. interrupt call >)
  3. 이 interrupt procedure 주소가 linux 에서는 intermediate handler 의 주소가 되고, 이 녀석을 통해서 ENTRY(int3) 이 실행될 것이다.
  4. ENTRY(int3)이 실행되면, error_code 를 통해 do_int3() 이 호출된다.


이제 실제로 source code 로 구현된 부분을 보면서 어떻게 동작하는 지 확인 해 보자.



In Source code


Event

event 는 하드웨어에서 전기적인 신호가 감지되는 것이다. 이 신호를 받아서 cpu 가 수행하고 있던 instruction 을 멈추고 다른 instruction 을 수행하는 것이다. 즉, instruction 의 순서를 바꾸는 것이다.[ref. 6]

image
< interrupt call / from : ref. 2, figure 6-3 >

아래는 IDT 를 만드는 것과 관련된 linux source 이다. IDT 는 BIOS routine 에서 만들어지지만, linux OS 에서는 한 번 더 만든다. 그게 아래 코드이다.[ref. 6]

/linux/include/asm/system.h
#define _set_gate(gate_addr,type,dpl,addr) \
__asm__ __volatile__ ("movw %%dx,%%ax\n\t" \
 "movw %2,%%dx\n\t" \
 "movl %%eax,%0\n\t" \
 "movl %%edx,%1" \
 :"=m" (*((long *) (gate_addr))), \
  "=m" (*(1+(long *) (gate_addr))) \
 :"i" ((short) (0x8000+(dpl<<13)+(type<<8))), \
  "d" ((char *) (addr)),"a" (KERNEL_CS << 16) \
 :"ax","dx")

#define set_intr_gate(n,addr) \
 _set_gate(&idt[n],14,0,addr)

#define set_trap_gate(n,addr) \
 _set_gate(&idt[n],15,0,addr)

#define set_system_gate(n,addr) \
 _set_gate(&idt[n],15,3,addr)

#define set_call_gate(a,addr) \
 _set_gate(a,12,3,addr)

software interrupt 는 DPL field 가 3으로 되어 있다. 그러므로 INT3 의 경우도 DPL 이 3 이다.



linux/kernel/linux/arch/i386/kernel/traps.c
void trap_init(void)
{
 ...
 set_call_gate(&default_ldt,lcall7);
 set_trap_gate(0,&divide_error);
 set_trap_gate(1,&debug);
 set_trap_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_trap_gate(14,&page_fault);
 set_trap_gate(15,&spurious_interrupt_bug);
 set_trap_gate(16,&coprocessor_error);
 set_trap_gate(17,&alignment_check);
 ...
}
DO_VM86_ERROR( 3, SIGTRAP, "int3", int3, current)

#define DO_VM86_ERROR(trapnr, signr, str, name, tsk) \
asmlinkage void do_##name(struct pt_regs * regs, long error_code) \
{ \
 if (regs->eflags & VM_MASK) { \
  if (!handle_vm86_trap((struct vm86_regs *) regs, error_code, trapnr)) \
   return; \
  /* else fall through */ \
 } \
 tsk->tss.error_code = error_code; \
 tsk->tss.trap_no = trapnr; \
 force_sig(signr, tsk); \
 die_if_kernel(str,regs,error_code); \
}



Interrupt 발생부터 call interrupt procedure 까지


interrupt 가 발생해서 interrupt procedure 의 주소를 찾아서 수행하게 된다.

exception -----> intermediate Handler -----> Real Handler

linux 에서 대부분의 intermidate Handler 는 entry.S 에 정의되어 있다.

entry.S 에 정의된 int3 의 intermedate Handler 는 아래와 같다.

#define GET_CURRENT(reg) \
 movl $-8192, reg; \
 andl %esp, reg

ENTRY(int3)
 pushl $0
 pushl $ SYMBOL_NAME(do_int3)
 jmp error_code


error_code:
 pushl %ds
 pushl %eax
 xorl %eax,%eax
 pushl %ebp
 pushl %edi
 pushl %esi
 pushl %edx
 decl %eax                       # eax = -1
 pushl %ecx
 pushl %ebx
 cld
 movl %es,%ecx
 movl ORIG_EAX(%esp), %esi       # get the error code
 movl ES(%esp), %edi             # get the function address
 movl %eax, ORIG_EAX(%esp)
 movl %ecx, ES(%esp)
 movl %esp,%edx
 pushl %esi                      # push the error code
 pushl %edx                      # push the pt_regs pointer
 movl $(__KERNEL_DS),%edx
 movl %edx,%ds
 movl %edx,%es
 GET_CURRENT(%ebx)
 call *%edi                      # call do_int3
 addl $8,%esp
 jmp ret_from_exception

error_code에서 call *%edi (call do_int3)이전에 하는 일


  1. do_int3 에 넘겨줄 register 값들을 push 를 통해 stack 에 넣는다.
    image
  2. stack 아래쪽에 %es, -1 을 넣는다.
    image
  3. error code 와 마지막 esp 를 추가로 stack 에 넣는다.
    image
  4. %ds, %es 에는 kernel data segment selector 를 넣고,
  5. %ebx 에는 current process descriptor's address 를 넣는다.

여기서 부족한 부분은
  • LIDT 가 실행되는 시점
  • INT3 instruction 을 만난 후 interrupt vector 를 구하는 부분

이다. 이 부분은 차후에 보충하기로 하자.



References

  1. http://forums.codeguru.com/showthread.php?370029-What-is-INT-3
  2. Intel® 64 and IA-32 Architectures Developer's Manual: Vol. 3A
  3. http://en.wikipedia.org/wiki/Interrupt_descriptor_table
  4. http://en.wikipedia.org/wiki/Interrupt_vector
  5. 1장. 펜티엄 프로세서의 interrupt mechanism, Interrupt Mechanism and Application of Intel IA32 Architecture
  6. Handling the Interrupt Descriptor Table
  7. http://en.wikipedia.org/wiki/Direction_flag

댓글 없음:

댓글 쓰기