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 отобразил это: