LiPPGen

The Literate-Programming-based Presentation Generator

Version 1.0, (c) 2013 Hans-Georg Eßer

Title:
Author:
Organisation:

TEXTCODE

Interrupts

Wir implementieren nun die Interrupt- (und im nächsten Kapitel die Fault-) Handler und starten dabei mit den Interrupt-Nummern:
< constants > ≡
  #define IRQ_TIMER      0
  #define IRQ_KBD        1
  #define IRQ_SLAVE      2     // Here the slave PIC connects to master
  #define IRQ_COM2       3
  #define IRQ_COM1       4
  #define IRQ_FDC        6
  #define IRQ_IDE       14     // primary IDE controller; secondary has IRQ 15
Weiter geht es mit den in- und out-Befehlen
< function prototypes > ≡
  unsigned char  inportb (unsigned short port);
  unsigned short inportw (unsigned short port);
  void outportb (unsigned short port, unsigned char data);
  void outportw (unsigned short port, unsigned short data);
die wir wir in der Vorlesung implementieren; der Code für inportb und outportb steht bereits in der Datei printf.c, weswegen wir ihn hier weglassen.
< function implementations > ≡
  unsigned short inportw (unsigned short port) {
    unsigned short retval;
    asm volatile ("inw %%dx, %%ax" : "=a" (retval) : "d" (port));
    return retval;
  }

  void outportw (unsigned short port, unsigned short data) {
    asm volatile ("outw %%ax, %%dx" : : "d" (port), "a" (data));
  }
Damit können wir nun die PICs einrichten. Ihre Ports haben die folgenden Adressen:
< constants > +≡
  // I/O Addresses of the two programmable interrupt controllers
  #define IO_PIC_MASTER_CMD   0x20  // Master (IRQs 0-7), command register
  #define IO_PIC_MASTER_DATA  0x21  // Master, control register

  #define IO_PIC_SLAVE_CMD    0xA0  // Slave (IRQs 8-15), command register
  #define IO_PIC_SLAVE_DATA   0xA1  // Slave, control register
Wir müssen bei der Initialisierung die Interrupt-Nummern umbiegen; detaillierte Erläuterungen dazu finden Sie im Buch-Kapitel 12 auf der Webseite.
< remap the interrupts to 32..47 > ≡
  PIC: program/initialize the PICs >
  
PIC: set the initial interrupt mask >
< PIC: program/initialize the PICs > ≡
  outportb (IO_PIC_MASTER_CMD,  0x11);  // ICW1: initialize; begin programming
  outportb (IO_PIC_SLAVE_CMD,   0x11);  // ICW1: dito, for PIC2
  outportb (IO_PIC_MASTER_DATA, 0x20);  // ICW2 for PIC1: offset 0x20 
                                        // (remaps 0x00..0x07 -> 0x20..0x27)
  outportb (IO_PIC_SLAVE_DATA,  0x28);  // ICW2 for PIC2: offset 0x28 
                                        // (remaps 0x08..0x0f -> 0x28..0x2f)
  outportb (IO_PIC_MASTER_DATA, 0x04);  // ICW3 for PIC1: there's a slave on IRQ 2 
                                        // (0b00000100 = 0x04)
  outportb (IO_PIC_SLAVE_DATA,  0x02);  // ICW3 for PIC2: your slave ID is 2
  outportb (IO_PIC_MASTER_DATA, 0x01);  // ICW4 for PIC1 and PIC2: 8086 mode
  outportb (IO_PIC_SLAVE_DATA,  0x01);
Im zweiten Schritt setzen wir die Interrupt-Maske (und schalten alle Interrupts aus).
< PIC: set the initial interrupt mask > ≡
  outportb (IO_PIC_MASTER_DATA, 0x00);  // PIC1: mask 0
  outportb (IO_PIC_SLAVE_DATA,  0x00);  // PIC2: mask 0
Weiter geht es mit der Interrupt Descriptor Table. Ihre Einträge haben folgenden Aufbau:
< type definitions > ≡
  struct idt_entry {
      unsigned int addr_low  : 16;   // lower 16 bits of address
      unsigned int gdtsel    : 16;   // use which GDT entry?
      unsigned int zeroes    :  8;   // must be set to 0
      unsigned int type      :  4;   // type of descriptor
      unsigned int flags     :  4;
      unsigned int addr_high : 16;   // higher 16 bits of address
  } __attribute__((packed));
und es gibt (wie bei der GDT) zusätzlich einen Pointer auf den Anfang der Tabelle:
< type definitions > +≡
  struct idt_ptr {
      unsigned int limit   : 16;
      unsigned int base    : 32;
  } __attribute__((packed));
Wir verwenden zwei globale Variablen für die Tabelle und den Pointer:
< global variables > ≡
  struct idt_entry idt[256] = { 0 };
  struct idt_ptr idtp;
Wir können einen Deskriptor mit
< function prototypes > +≡
  void fill_idt_entry (unsigned char num, unsigned long address, 
      unsigned short gdtsel, unsigned char flags, unsigned char type);
füllen:
< function implementations > +≡
  void fill_idt_entry (unsigned char num, unsigned long address, 
      unsigned short gdtsel, unsigned char flags, unsigned char type) {
    if (num >= 0 && num < 256) {
      idt[num].addr_low  = address & 0xFFFF; // address is the handler address
      idt[num].addr_high = (address >> 16) & 0xFFFF;
      idt[num].gdtsel    = gdtsel;           // GDT sel.: user mode or kernel mode?
      idt[num].zeroes    = 0;
      idt[num].flags     = flags;
      idt[num].type      = type;
    }
  }
Wir definieren Interrupt-Handler-Funktionen irq0 bis irq15 in der Assembler-Datei. Im C-Programm müssen wir die Funktionen als extern deklarieren:
< function prototypes > +≡
  extern void irq0(), irq1(), irq2(),  irq3(),  irq4(),  irq5(),  irq6(),  irq7();
  extern void irq8(), irq9(), irq10(), irq11(), irq12(), irq13(), irq14(), irq15();
Dann können wir die Handler auf die einzelnen IDT-Einträge verteilen:
< install the interrupt handlers > ≡
  install the IDT >
  
install the fault handlers >
  
remap the interrupts to 32..47 >
  set_irqmask (0xFFFF);           // initialize IRQ mask
  enable_interrupt (IRQ_SLAVE);   // IRQ slave

  // flags: 1 (present), 11 (DPL 3), 0; type: 1110 (32 bit interrupt gate)
  fill_idt_entry (32, (unsigned int)irq0,  0x08, 0b1110, 0b1110);
  fill_idt_entry (33, (unsigned int)irq1,  0x08, 0b1110, 0b1110);
  fill_idt_entry (34, (unsigned int)irq2,  0x08, 0b1110, 0b1110);
  fill_idt_entry (35, (unsigned int)irq3,  0x08, 0b1110, 0b1110);
  fill_idt_entry (36, (unsigned int)irq4,  0x08, 0b1110, 0b1110);
  fill_idt_entry (37, (unsigned int)irq5,  0x08, 0b1110, 0b1110);
  fill_idt_entry (38, (unsigned int)irq6,  0x08, 0b1110, 0b1110);
  fill_idt_entry (39, (unsigned int)irq7,  0x08, 0b1110, 0b1110);
  fill_idt_entry (40, (unsigned int)irq8,  0x08, 0b1110, 0b1110);
  fill_idt_entry (41, (unsigned int)irq9,  0x08, 0b1110, 0b1110);
  fill_idt_entry (42, (unsigned int)irq10, 0x08, 0b1110, 0b1110);
  fill_idt_entry (43, (unsigned int)irq11, 0x08, 0b1110, 0b1110);
  fill_idt_entry (44, (unsigned int)irq12, 0x08, 0b1110, 0b1110);
  fill_idt_entry (45, (unsigned int)irq13, 0x08, 0b1110, 0b1110);
  fill_idt_entry (46, (unsigned int)irq14, 0x08, 0b1110, 0b1110);
  fill_idt_entry (47, (unsigned int)irq15, 0x08, 0b1110, 0b1110);
wobei wir noch
< function prototypes > +≡
  static void set_irqmask (unsigned short mask);
  static void enable_interrupt (int number);
  unsigned short get_irqmask ();
implementieren:
< function implementations > +≡
  static void set_irqmask (unsigned short mask) {
    outportb (IO_PIC_MASTER_DATA, (char)(mask % 256) );
    outportb (IO_PIC_SLAVE_DATA,  (char)(mask >> 8)  );
  }

  unsigned short get_irqmask () {
    return inportb (IO_PIC_MASTER_DATA) 
        + (inportb (IO_PIC_SLAVE_DATA) << 8);
  }

  static void enable_interrupt (int number) {
    set_irqmask ( 
      get_irqmask ()        // the current value
      & ~(1 << number)      // 16 one-bits, but bit "number" cleared
    );
  }
Das Installieren der Interrupt Handler im Code Chunk < install the interrupt handlers > muss bei der Systeminitialisierung erfolgen, also:
< kernel main: initialize system > ≡
  install the interrupt handlers >
Unsere Interrupt-Handler erhalten die Registerinhalte, dafür legen wir einen eigenen Datentyp an:
< type definitions > +≡
  struct regs {
    unsigned int gs, fs, es, ds;
    unsigned int edi, esi, ebp, esp, ebx, edx, ecx, eax;
    unsigned int int_no, err_code;
    unsigned int eip, cs, eflags, useresp, ss;
  };
In der Assembler-Datei liegen die Anfangsstücke der Interrupt-Handler:
< start.asm > ≡
  global irq0, irq1, irq2,  irq3,  irq4,  irq5,  irq6,  irq7
  global irq8, irq9, irq10, irq11, irq12, irq13, irq14, irq15

  %macro irq_macro 1 
         cli                  ; disable interrupts
         push byte 0          ; error code (none)
         push byte %1         ; interrupt number
         jmp irq_common_stub  ; rest is identical for all handlers
  %endmacro

  irq0:  irq_macro 32
  irq1:  irq_macro 33
  irq2:  irq_macro 34
  irq3:  irq_macro 35
  irq4:  irq_macro 36
  irq5:  irq_macro 37
  irq6:  irq_macro 38
  irq7:  irq_macro 39
  irq8:  irq_macro 40
  irq9:  irq_macro 41
  irq10: irq_macro 42
  irq11: irq_macro 43
  irq12: irq_macro 44
  irq13: irq_macro 45
  irq14: irq_macro 46
  irq15: irq_macro 47

  extern irq_handler          ; defined in the C source file

  irq_common_stub:            ; this is the identical part
         pusha
         push ds
         push es
         push fs
         push gs
         push esp  ; pointer to the struct regs
         call irq_handler     ; call C function
         pop esp
         pop gs
         pop fs
         pop es
         pop ds
         popa
         add esp, 8
         iret
-- wir haben in der Vorlesung besprochen, warum die Register in dieser Reihenfolge auf den Stack gelegt (und später wieder ausgelesen) werden. (\emph{Achtung: Sie können diesen Code Chunk nicht in dieser NoWeb-Datei verändern; er wird nicht exportiert.}) Es fehlt nun noch die C-Funktion irq_handler():
< function implementations > +≡
  void irq_handler (struct regs *r) {
    int number = r->int_no - 32;                      // interrupt number
    void (*handler)(struct regs *r);                  // type of handler functions

    if (number >= 8)  
      outportb (IO_PIC_SLAVE_CMD, END_OF_INTERRUPT);  // notify slave  PIC
    outportb (IO_PIC_MASTER_CMD, END_OF_INTERRUPT);   // notify master PIC

    handler = interrupt_handlers[number];
    if (handler != NULL)  handler (r);
  }
Sie verwendet die Konstante
< constants > +≡
  #define END_OF_INTERRUPT  0x20
und das Array
< global variables > +≡
  void *interrupt_handlers[16] = { 0 };
Um einen Interrupt-Handler zu installieren (also seine Adresse in das Array einzutragen), verwenden wir
< function prototypes > +≡
  void install_interrupt_handler (int irq, void (*handler)(struct regs *r));
mit folgender Implementierung:
< function implementations > +≡
  void install_interrupt_handler (int irq, void (*handler)(struct regs *r)) {
    if (irq >= 0 && irq < 16)
      interrupt_handlers[irq] = handler;
  }
Bei der Initialisierung des Kernels müssen wir noch das IDTR-Register laden:
< install the IDT > ≡
  idtp.limit = (sizeof (struct idt_entry) * 256) - 1;   // must do -1
  idtp.base  = (int) &idt;
  idt_load ();
wobei der letzte Befehl wieder als Assembler-Code vorliegt:
< function prototypes > +≡
  extern void idt_load ();
< start.asm > +≡
  extern idtp  ; defined in the C file
  global idt_load
  idt_load:    lidt [idtp]
               ret

Faults

Das Fault-Handling funktioniert ganz ähnlich wie die Interrupt-Behandlung, darum gehen wir hier auf die Details nur kurz ein. Die Funktionen isr0(), \dots, isr31() implementieren wir wieder als Assembler-Funktionen; im C-Programm müssen wir ihre Namen bekannt machen:
< function prototypes > +≡
  extern void isr0(),  isr1(),  isr2(),  isr3(),  isr4(),  isr5(),  
     isr6(),  isr7(),  isr8(),  isr9(),  isr10(), isr11(), isr12(), 
     isr13(), isr14(), isr15(), isr16(), isr17(), isr18(), isr19(), 
     isr20(), isr21(), isr22(), isr23(), isr24(), isr25(), isr26(), 
     isr27(), isr28(), isr29(), isr30(), isr31();
Um die IDT-Einträge für die 32 Fault-Handler anzulegen, benutzen wir ein kleines Makro:
< macros > ≡
  #define FILL_IDT(i) \
    fill_idt_entry (i, (unsigned int)isr##i, 0x08, 0b1110, 0b1110)
und können dann mit wenigen Zeilen die 32 Eintragungen vornehmen:
< install the fault handlers > ≡
  FILL_IDT( 0); FILL_IDT( 1); FILL_IDT( 2); FILL_IDT( 3); FILL_IDT( 4); 
  FILL_IDT( 5); FILL_IDT( 6); FILL_IDT( 7); FILL_IDT( 8); FILL_IDT( 9);
  FILL_IDT(10); FILL_IDT(11); FILL_IDT(12); FILL_IDT(13); FILL_IDT(14); 
  FILL_IDT(15); FILL_IDT(16); FILL_IDT(17); FILL_IDT(18); FILL_IDT(19);
  FILL_IDT(20); FILL_IDT(21); FILL_IDT(22); FILL_IDT(23); FILL_IDT(24);
  FILL_IDT(25); FILL_IDT(26); FILL_IDT(27); FILL_IDT(28); FILL_IDT(29);
  FILL_IDT(30); FILL_IDT(31);
In der Assembler-Datei geben wir an, dass die Symbole exportiert werden sollen:
< start.asm > +≡
  global isr0,  isr1,  isr2,  isr3,  isr4,  isr5,  isr6,  isr7,  isr8
  global isr9,  isr10, isr11, isr12, isr13, isr14, isr15, isr16, isr17
  global isr18, isr19, isr20, isr21, isr22, isr23, isr24, isr25, isr26
  global isr27, isr28, isr29, isr30, isr31
Dann brauchen wir ein Array mit Fehlermeldungen; hinter Fault-Nummer 18 kommt nichts mehr: Die restlichen Werte sind reserviert.
< global variables > +≡
  char *exception_messages[] = {
    "Division By Zero",       "Debug",                        //  0,  1
    "Non Maskable Interrupt", "Breakpoint",                   //  2,  3
    "Into Detected Overflow", "Out of Bounds",                //  4,  5
    "Invalid Opcode",         "No Coprocessor",               //  6,  7
    "Double Fault",           "Coprocessor Segment Overrun",  //  8,  9
    "Bad TSS",                "Segment Not Present",          // 10, 11
    "Stack Fault",            "General Protection Fault",     // 12, 13
    "Page Fault",             "Unknown Interrupt",            // 14, 15
    "Coprocessor Fault",      "Alignment Check",              // 16, 17
    "Machine Check",                                          // 18
    "Reserved", "Reserved", "Reserved", "Reserved", "Reserved",
    "Reserved", "Reserved", "Reserved", "Reserved", "Reserved",
    "Reserved", "Reserved", "Reserved"                        // 19..31
  };
Den eigentlichen Fault-Handler
< function prototypes > +≡
  void fault_handler (struct regs *r);
präsentieren wir hier in der vereinfachten Variante (die noch keine Prozesse berücksichtigt). Der Handler gibt einige Informationen aus und hält dann das System an.
< function implementations > +≡
  void fault_handler (struct regs *r) {
    if (r->int_no >= 0 && r->int_no < 32) {
      
fault handler: display status information >
      
      if ( (unsigned int)(r->eip) < 0xc0000000 ) {     // user mode 
        
fault handler: terminate process >
      }

      printf ("System Stops\n");   
      asm ("cli; \n hlt;");
    }
  }
In der Ausgabe erscheinen Name und Nummer des Faults sowie die Inhalte einiger Register.
< fault handler: display status information > ≡
  printf ("'%s' (%d) Exception at 0x%08x.\n", 
    exception_messages[r->int_no], r->int_no, r->eip);        
  printf ("eflags: 0x%08x  errcode: 0x%08x\n", r->eflags, r->err_code);
  printf ("eax: %08x  ebx: %08x  ecx: %08x  edx: %08x \n",
    r->eax, r->ebx, r->ecx, r->edx);
  printf ("eip: %08x  esp: %08x  int: %8d  err: %8d \n", 
    r->eip, r->esp, r->int_no, r->err_code);
  printf ("ebp: %08x  cs: %d  ds: %d  es: %d  fs: %d  ss: %x \n",
    r->ebp, r->cs, r->ds, r->es, r->fs, r->ss);