IRQ клавиатуры в ядре x86

Я пытаюсь запрограммировать очень простое ядро ​​для целей обучения. Прочитав кучу статей о PIC и IRQ в архитектуре x86, я понял, что IRQ1 это обработчик клавиатуры. Я использую следующий код для печати нажимаемых клавиш:

#include "port_io.h"

#define IDT_SIZE 256
#define PIC_1_CTRL 0x20
#define PIC_2_CTRL 0xA0
#define PIC_1_DATA 0x21
#define PIC_2_DATA 0xA1

void keyboard_handler();
void load_idt(void*);

struct idt_entry
{
    unsigned short int offset_lowerbits;
    unsigned short int selector;
    unsigned char zero;
    unsigned char flags;
    unsigned short int offset_higherbits;
};

struct idt_pointer
{
    unsigned short limit;
    unsigned int base;
};

struct idt_entry idt_table[IDT_SIZE];
struct idt_pointer idt_ptr;

void load_idt_entry(char isr_number, unsigned long base, short int selector, char flags)
{
    idt_table[isr_number].offset_lowerbits = base & 0xFFFF;
    idt_table[isr_number].offset_higherbits = (base >> 16) & 0xFFFF;
    idt_table[isr_number].selector = selector;
    idt_table[isr_number].flags = flags;
    idt_table[isr_number].zero = 0;
}

static void initialize_idt_pointer()
{
    idt_ptr.limit = (sizeof(struct idt_entry) * IDT_SIZE) - 1;
    idt_ptr.base = (unsigned int)&idt_table;
}

static void initialize_pic()
{
    /* ICW1 - begin initialization */
    write_port(PIC_1_CTRL, 0x11);
    write_port(PIC_2_CTRL, 0x11);

    /* ICW2 - remap offset address of idt_table */
    /*
    * In x86 protected mode, we have to remap the PICs beyond 0x20 because
    * Intel have designated the first 32 interrupts as "reserved" for cpu exceptions
    */
    write_port(PIC_1_DATA, 0x20);
    write_port(PIC_2_DATA, 0x28);

    /* ICW3 - setup cascading */
    write_port(PIC_1_DATA, 0x00);
    write_port(PIC_2_DATA, 0x00);

    /* ICW4 - environment info */
    write_port(PIC_1_DATA, 0x01);
    write_port(PIC_2_DATA, 0x01);
    /* Initialization finished */

    /* mask interrupts */
    write_port(0x21 , 0xff);
    write_port(0xA1 , 0xff);
}

void idt_init()
{
    initialize_pic();
    initialize_idt_pointer();
    load_idt(&idt_ptr);
}

load_idt просто использует lidt Инструкция x86. После этого я загружаю обработчик клавиатуры:

void kmain(void)
{
    //Using grub bootloader..
    idt_init();
    kb_init();
    load_idt_entry(0x21, (unsigned long) keyboard_handler, 0x08, 0x8e);
}

Это реализация:

#include "kprintf.h"
#include "port_io.h"
#include "keyboard_map.h"

void kb_init(void)
{
    /* 0xFD is 11111101 - enables only IRQ1 (keyboard)*/
    write_port(0x21 , 0xFD);
}

void keyboard_handler(void)
{
    unsigned char status;
    char keycode;
    char *vidptr = (char*)0xb8000;  //video mem begins here.
    /* Acknownlegment */

    int current_loc = 0;
    status = read_port(0x64);
    /* Lowest bit of status will be set if buffer is not empty */
    if (status & 0x01) {
        keycode = read_port(0x60);
        if(keycode < 0)
            return;
        vidptr[current_loc++] = keyboard_map[keycode];
        vidptr[current_loc++] = 0x07;
    }

    write_port(0x20, 0x20);
}

Это дополнительный код, который я использую:

section .text

global load_idt
global keyboard_handler

extern kprintf
extern keyboard_handler_main

load_idt:
    sti
    mov edx, [esp + 4]
    lidt [edx]
    ret

global read_port
global write_port

; arg: int, port number.
read_port:
    mov edx, [esp + 4]
    in al, dx   
    ret

; arg: int, (dx)port number
;      int, (al)value to write
write_port:
    mov   edx, [esp + 4]    
    mov   al, [esp + 4 + 4]  
    out   dx, al  
    ret

Это моя точка входа:

bits 32
section .text
;grub bootloader header
        align 4
        dd 0x1BADB002            ;magic
        dd 0x00                  ;flags
        dd - (0x1BADB002 + 0x00) ;checksum. m+f+c should be zero

global start
extern kmain            

start:
;  cli          ;block interrupts
  mov esp, stack_space  ;set stack pointer
  call kmain
  hlt           ;halt the CPU

section .bss
resb 8192       ;8KB for stack
stack_space:

Я использую QEMU для запуска ядра:

qemu-system-i386 -kernel kernel

Проблема в том, что я не получаю никаких символов на экране. Вместо этого я все еще получаю тот же вывод:

SeaBIOS (version Ubuntu-1.8.2-1-ubuntu1)
Booting from ROM...

Как мне решить эту проблему? Какие-либо предложения?

1 ответ

Решение

У вас есть ряд проблем с вашим кодом. Основные из них обсуждаются индивидуально ниже.


Инструкция HLT остановит текущий процессор, ожидающий следующего прерывания. У вас есть прерывания, включенные этой точкой. После первого прерывания (нажатия клавиши) будет выполнен код после HLT. Он начнет выполнять любые случайные данные в памяти. Вы можете изменить свой kmain сделать бесконечный цикл с инструкцией HLT. Примерно так должно работать:

while(1) __asm__("hlt\n\t");

В этом коде:

load_idt:
    sti
    mov edx, [esp + 4]
    lidt [edx]
    ret

Обычно лучше использовать STI после обновления таблицы прерываний, а не до нее. Это было бы лучше:

load_idt:
    mov edx, [esp + 4]
    lidt [edx]
    sti
    ret

Ваш обработчик прерываний должен выполнить iretd правильно вернуться из прерывания. Ваша функция keyboard_handler сделаю ret возвращать. Чтобы решить эту проблему, вы можете создать обертку сборки, которая вызывает C keyboard_handler функция, а затем делает IRETD.

В файле сборки NASM вы можете определить глобальную функцию под названием keyboard_handler_int как это:

extern keyboard_handler
global keyboard_handler_int

keyboard_handler_int:
    call keyboard_handler
    iretd

Код для настройки записи IDT будет выглядеть так:

load_idt_entry(0x21, (unsigned long) keyboard_handler_int, 0x08, 0x8e);

Ваш kb_init функция в конечном итоге включает (через маску) прерывание клавиатуры. К сожалению, вы настраиваете обработчик клавиатуры после включения этого прерывания. Возможно нажатие клавиши после включения прерывания и до помещения записи в IDT. Быстрое решение - настроить обработчик клавиатуры перед вызовом kb_init с чем-то вроде:

void kmain(void)
{
    //Using grub bootloader..
    idt_init();
    load_idt_entry(0x21, (unsigned long) keyboard_handler_int, 0x08, 0x8e);
    kb_init();
    while(1) __asm__("hlt\n\t");
}

Наиболее серьезная проблема, которая может привести к тому, что ваше ядро ​​произойдет тройной сбой (и эффективно перезагрузит виртуальную машину), - это способ, которым вы определили idt_pointer состав. Ты использовал:

struct idt_pointer
{
    unsigned short limit;
    unsigned int base;
};

Проблема в том, что правила выравнивания по умолчанию разместят 2 байта после заполнения limit и раньше base таким образом unsigned int будет выровнен со смещением в 4 байта в структуре. Чтобы изменить это поведение и упаковать данные без заполнения, вы можете использовать __attribute__((packed)) на структуре. Определение будет выглядеть так:

struct idt_pointer
{
    unsigned short limit;
    unsigned int base;
} __attribute__((packed));

Это означает, что между байтами нет дополнительных limit а также base для выравнивания. Неспособность справиться с проблемой выравнивания приводит к base адрес, который неправильно размещен в структуре. Указателю IDT необходимо 16-битное значение, представляющее размер IDT, за которым сразу следует 32-битное значение, представляющее базовый адрес вашей IDT.

Дополнительную информацию о выравнивании и заполнении структуры можно найти в одном из блогов Эрика Рэймонда. Из-за того, как члены struct idt_entry размещены там нет дополнительных байтов заполнения. Если вы создаете структуры, которые вы никогда не хотите дополнить, я рекомендую использовать __attribute__((packed));, Это обычно тот случай, когда вы отображаете структуру данных C с определенной системой структурой. Имея это в виду, я бы также упаковать struct idt_entry для ясности.


Другие соображения

В обработчике прерываний, хотя я предложил IRETD, есть еще одна проблема. По мере роста вашего ядра и добавления новых прерываний вы обнаружите еще одну проблему. Ваше ядро ​​может работать неправильно, и регистры могут неожиданно изменить значения. Проблема в том, что функции C, действующие как обработчики прерываний, будут уничтожать содержимое некоторых регистров, но мы не сохраняем и не восстанавливаем их. Во-вторых, флаг направления (в соответствии с 32-битным ABI) необходимо очистить ( CLD) перед вызовом функции. Вы не можете предполагать, что флаг направления сбрасывается при входе в процедуру прерывания. ABI говорит:

EFLAGS Регистр флагов содержит системные флаги, такие как флаг направления и флаг переноса. Флаг направления должен быть установлен в направлении "вперед" (т. Е. В ноль) перед входом и выходом из функции. Другие пользовательские флаги не имеют определенной роли в стандартной вызывающей последовательности и не сохраняются

Вы можете выдвинуть все энергозависимые регистры по отдельности, но для краткости вы можете использовать инструкции PUSHAD и POPAD. Обработчик прерываний был бы лучше, если бы он выглядел так:

keyboard_handler_int:
    pushad                 ; Push all general purpose registers
    cld                    ; Clear direction flag (forward movement)
    call keyboard_handler
    popad                  ; Restore all general purpose registers
    iretd                  ; IRET will restore required parts of EFLAGS
                           ;   including the direction flag

Если бы вы должны были сохранить и восстановить все энергозависимые регистры вручную, вам пришлось бы сохранять и восстанавливать EAX, ECX и EDX, поскольку их не нужно сохранять при вызовах функций Си. Как правило, не рекомендуется использовать инструкции x87 FPU в обработчике прерываний (в основном для повышения производительности), но если бы вы это сделали, вам также пришлось бы сохранять и восстанавливать состояние x87 FPU.


Образец кода

Вы не предоставили полный пример, поэтому я заполнил некоторые пробелы (включая простую раскладку клавиатуры) и немного изменил ваш обработчик клавиатуры. Переработанный обработчик клавиатуры отображает только события нажатия клавиш и пропускает символы, которые не были сопоставлены. Во всех случаях код переходит к концу обработчика, так что PIC отправляется EOI (End Of Interrupt). Текущее местоположение курсора - это статическое целое число, которое сохранит свое значение при вызовах прерываний. Это позволяет перемещаться между каждым нажатием символов.

мой kprintd.h файл пуст, и я помещаю ВСЕ прототипы ассемблера в ваш port_io.h, Прототипы должны быть правильно разделены на несколько заголовков. Я сделал это только так, чтобы уменьшить количество файлов. Мой файл lowlevel.asm определяет все процедуры сборки низкого уровня. Окончательный код выглядит следующим образом:

kernel.asm:

bits 32
section .text
;grub bootloader header
        align 4
        dd 0x1BADB002            ;magic
        dd 0x00                  ;flags
        dd - (0x1BADB002 + 0x00) ;checksum. m+f+c should be zero

global start
extern kmain

start:
  mov esp, stack_space  ;set stack pointer
  call kmain

; We shouldn't get to here, but just in case do an infinite loop
endloop:
  hlt           ;halt the CPU
  jmp endloop

section .bss
resb 8192       ;8KB for stack
stack_space:

lowlevel.asm:

section .text

extern keyboard_handler
global read_port
global write_port
global load_idt
global keyboard_handler_int

keyboard_handler_int:
    pushad
    cld
    call keyboard_handler
    popad
    iretd

load_idt:
    mov edx, [esp + 4]
    lidt [edx]
    sti
    ret

; arg: int, port number.
read_port:
    mov edx, [esp + 4]
    in al, dx
    ret

; arg: int, (dx)port number
;      int, (al)value to write
write_port:
    mov   edx, [esp + 4]
    mov   al, [esp + 4 + 4]
    out   dx, al
    ret

port_io.h:

extern unsigned char read_port (int port);
extern void write_port (int port, unsigned char val);
extern void kb_init(void);

kprintf.h:

/* Empty file */

keyboard_map.h:

unsigned char keyboard_map[128] =
{
    0,  27, '1', '2', '3', '4', '5', '6', '7', '8',     /* 9 */
  '9', '0', '-', '=', '\b',     /* Backspace */
  '\t',                 /* Tab */
  'q', 'w', 'e', 'r',   /* 19 */
  't', 'y', 'u', 'i', 'o', 'p', '[', ']', '\n', /* Enter key */
    0,                  /* 29   - Control */
  'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';',     /* 39 */
 '\'', '`',   0,                /* Left shift */
 '\\', 'z', 'x', 'c', 'v', 'b', 'n',                    /* 49 */
  'm', ',', '.', '/',   0,                              /* Right shift */
  '*',
    0,  /* Alt */
  ' ',  /* Space bar */
    0,  /* Caps lock */
    0,  /* 59 - F1 key ... > */
    0,   0,   0,   0,   0,   0,   0,   0,
    0,  /* < ... F10 */
    0,  /* 69 - Num lock*/
    0,  /* Scroll Lock */
    0,  /* Home key */
    0,  /* Up Arrow */
    0,  /* Page Up */
  '-',
    0,  /* Left Arrow */
    0,
    0,  /* Right Arrow */
  '+',
    0,  /* 79 - End key*/
    0,  /* Down Arrow */
    0,  /* Page Down */
    0,  /* Insert Key */
    0,  /* Delete Key */
    0,   0,   0,
    0,  /* F11 Key */
    0,  /* F12 Key */
    0,  /* All other keys are undefined */
};

keyb.c:

#include "kprintf.h"
#include "port_io.h"
#include "keyboard_map.h"

void kb_init(void)
{
    /* 0xFD is 11111101 - enables only IRQ1 (keyboard)*/
    write_port(0x21 , 0xFD);
}

/* Maintain a global location for the current video memory to write to */
static int current_loc = 0;
/* Video memory starts at 0xb8000. Make it a constant pointer to
   characters as this can improve compiler optimization since it
   is a hint that the value of the pointer won't change */
static char *const vidptr = (char*)0xb8000;

void keyboard_handler(void)
{
    unsigned char status;
    signed char keycode;

    /* Acknowledgment */
    status = read_port(0x64);
    /* Lowest bit of status will be set if buffer is not empty */
    if (status & 0x01) {
        keycode = read_port(0x60);
        /* Only print characters on keydown event that have
         * a non-zero mapping */
        if(keycode >= 0 && keyboard_map[keycode]) {
            vidptr[current_loc++] = keyboard_map[keycode];
            /* Attribute 0x07 is white character on black background */
            vidptr[current_loc++] = 0x07;
        }
    }

    /* enable interrupts again */
    write_port(0x20, 0x20);
}

main.c:

#include "port_io.h"

#define IDT_SIZE 256
#define PIC_1_CTRL 0x20
#define PIC_2_CTRL 0xA0
#define PIC_1_DATA 0x21
#define PIC_2_DATA 0xA1

void keyboard_handler_int();
void load_idt(void*);

struct idt_entry
{
    unsigned short int offset_lowerbits;
    unsigned short int selector;
    unsigned char zero;
    unsigned char flags;
    unsigned short int offset_higherbits;
} __attribute__((packed));

struct idt_pointer
{
    unsigned short limit;
    unsigned int base;
} __attribute__((packed));

struct idt_entry idt_table[IDT_SIZE];
struct idt_pointer idt_ptr;

void load_idt_entry(int isr_number, unsigned long base, short int selector, unsigned char flags)
{
    idt_table[isr_number].offset_lowerbits = base & 0xFFFF;
    idt_table[isr_number].offset_higherbits = (base >> 16) & 0xFFFF;
    idt_table[isr_number].selector = selector;
    idt_table[isr_number].flags = flags;
    idt_table[isr_number].zero = 0;
}

static void initialize_idt_pointer()
{
    idt_ptr.limit = (sizeof(struct idt_entry) * IDT_SIZE) - 1;
    idt_ptr.base = (unsigned int)&idt_table;
}

static void initialize_pic()
{
    /* ICW1 - begin initialization */
    write_port(PIC_1_CTRL, 0x11);
    write_port(PIC_2_CTRL, 0x11);

    /* ICW2 - remap offset address of idt_table */
    /*
    * In x86 protected mode, we have to remap the PICs beyond 0x20 because
    * Intel have designated the first 32 interrupts as "reserved" for cpu exceptions
    */
    write_port(PIC_1_DATA, 0x20);
    write_port(PIC_2_DATA, 0x28);

    /* ICW3 - setup cascading */
    write_port(PIC_1_DATA, 0x00);
    write_port(PIC_2_DATA, 0x00);

    /* ICW4 - environment info */
    write_port(PIC_1_DATA, 0x01);
    write_port(PIC_2_DATA, 0x01);
    /* Initialization finished */

    /* mask interrupts */
    write_port(0x21 , 0xff);
    write_port(0xA1 , 0xff);
}

void idt_init()
{
    initialize_pic();
    initialize_idt_pointer();
    load_idt(&idt_ptr);
}

void kmain(void)
{
    //Using grub bootloader..
    idt_init();
    load_idt_entry(0x21, (unsigned long) keyboard_handler_int, 0x08, 0x8e);
    kb_init();
    while(1) __asm__("hlt\n\t");
}

Чтобы связать это ядро, я использую файл link.ld с этим определением:

/*
*  link.ld
*/
OUTPUT_FORMAT(elf32-i386)
ENTRY(start)
SECTIONS
 {
   . = 0x100000;
   .text : { *(.text) }
   .rodata : { *(.rodata) }
   .data : { *(.data) }
   .bss  : { *(.bss)  }
 }

Я компилирую и связываю этот код, используя кросс-компилятор GCC i686, с помощью следующих команд:

nasm -f elf32 -g -F dwarf kernel.asm -o kernel.o
nasm -f elf32 -g -F dwarf lowlevel.asm -o lowlevel.o
i686-elf-gcc -g -m32  -c main.c -o main.o -ffreestanding -O3 -Wall -Wextra -pedantic
i686-elf-gcc -g -m32  -c keyb.c -o keyb.o -ffreestanding -O3 -Wall -Wextra -pedantic
i686-elf-gcc -g -m32  -Wl,--build-id=none -T link.ld -o kernel.elf -ffreestanding -nostdlib lowlevel.o main.o keyb.o kernel.o -lgcc

В результате ядро ​​называется kernel.elf с отладочной информацией. Я предпочитаю уровень оптимизации -O3 а не по умолчанию -O0, Отладочная информация облегчает отладку с помощью QEMU и GDB. Ядро может быть отлажено с помощью этих команд:

qemu-system-i386 -kernel kernel.elf -S -s &

gdb kernel.elf \
        -ex 'target remote localhost:1234' \
        -ex 'layout src' \
        -ex 'layout regs' \
        -ex 'break kmain' \
        -ex 'continue'

Если вы хотите отлаживать на уровне кода сборки, замените layout src с layout asm, При запуске с вводом the quick brown fox jumps over the lazy dog 01234567890 QEMU отобразил это:

Рис ядра, работающего в QEMU

Другие вопросы по тегам