x86 内核中的键盘 IRQ

Keyboard IRQ within an x86 kernel

我正在尝试为学习目的编写一个非常简单的内核。看了一堆关于x86架构中的PIC和IRQs的文章, 我发现 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...

如何解决这个问题?有什么建议吗?

您的代码有很多问题。下面分别讨论主要的。


HLT 指令将停止当前 CPU 等待下一个中断。此时您确实启用了中断。在第一次中断(击键)之后,HLT 之后的代码将被执行。它将开始执行内存中的任何随机数据。您可以修改 kmain 以使用 HLT 指令进行无限循环。这样的事情应该有效:

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

在此代码中:

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

在更新中断 table 之后而不是之前使用 STI 通常是更好的主意。这样会更好:

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

您的中断处理程序需要执行 iretd 才能从中断中正确地 return。您的函数 keyboard_handler 将执行 ret 到 return。要解决此问题,您可以创建一个调用 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;
};

问题是默认对齐规则将在 limit 之后和 base 之前放置 2 个字节的填充,以便 unsigned int 将在结构内的 4 字节偏移处对齐.要改变此行为并在不填充的情况下打包数据,您可以在结构上使用 __attribute__((packed))。定义如下所示:

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

这样做意味着在 limitbase 之间没有额外的字节用于对齐。未能有效处理对齐问题会导致 base 地址被错误地放置在结构中。 IDT 指针需要一个 16 位值来表示 IDT 的大小,紧接着是一个 32 位值来表示您的 IDT.

可以在 Eric Raymond 的 blogs 中找到有关结构对齐和填充的更多信息。由于 struct idt_entry 成员的放置方式,没有额外的填充字节。如果你正在创建你不想填充的结构,我建议使用 __attribute__((packed));。当您使用系统定义的结构映射 C 数据结构时,通常就是这种情况。考虑到这一点,为了清楚起见,我还会打包 struct idt_entry


其他注意事项

在中断处理程序中,虽然我建议使用 IRETD,但还有另一个问题。随着内核的增长和添加更多的中断,您会发现另一个问题。您的内核可能运行不稳定,寄存器可能会意外更改值。问题是 C 函数充当中断处理程序会破坏某些寄存器的内容,但我们不会保存和恢复它们。其次,调用函数之前的方向标志(根据 32-bit ABI) is required to be cleared (CLD)。您不能假设方向标志在进入中断例程时被清除。 ABI 表示:

EFLAGS The flags register contains the system flags, such as the direction flag and the carry flag. The direction flag must be set to the ‘‘forward’’ (that is, zero) direction before entry and upon exit from a function. Other user flags have no specified role in the standard calling sequence and are not preserved

您可以单独压入所有易失性寄存器,但为简洁起见,您可以使用 PUSHAD and 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

如果您要手动保存和恢复所有易失性寄存器,您必须保存和恢复 EAXECXEDX 因为它们不需要在 C 函数调用中保留。在中断处理程序中使用 x87 FPU 指令通常不是一个好主意(主要是为了性能),但如果你这样做了,你还必须保存和恢复 x87 FPU 状态。


示例代码

您没有提供完整的示例,因此我填补了一些空白(包括一个简单的键盘映射)并对您的键盘处理程序进行了细微的更改。修改后的键盘处理程序仅显示按键按下事件并跳过没有映射的字符。在所有情况下,代码都会下降到处理程序的末尾,以便向 PIC 发送 EOI(中断结束)。当前光标位置是一个静态整数,它将在中断调用期间保留其值。这允许位置在每个字符按下之间前进。

我的 kprintd.h 文件是空的,我把所有的汇编程序原型都放了进入你的 port_io.h。原型应适当分成多个headers。我这样做只是为了减少文件数量。我的文件 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:
    lgdt [gdtr]                 ; Load our own GDT, the GDTR of Grub may be invalid

    jmp CODE32_SEL:.setcs       ; Set CS to our 32-bit flat code selector
.setcs:
    mov ax, DATA32_SEL          ; Setup the segment registers with our flat data selector
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    mov ss, ax
    mov esp, stack_space        ; set stack pointer

    call kmain

; If we get here just enter an infinite loop
endloop:
    hlt                         ; halt the CPU
    jmp endloop

; Macro to build a GDT descriptor entry
%define MAKE_GDT_DESC(base, limit, access, flags) \
    (((base & 0x00FFFFFF) << 16) | \
    ((base & 0xFF000000) << 32) | \
    (limit & 0x0000FFFF) | \
    ((limit & 0x000F0000) << 32) | \
    ((access & 0xFF) << 40) | \
    ((flags & 0x0F) << 52))

section .data
align 4
gdt_start:
    dq MAKE_GDT_DESC(0, 0, 0, 0); null descriptor
gdt32_code:
    dq MAKE_GDT_DESC(0, 0x00ffffff, 10011010b, 1100b)
                                ; 32-bit code, 4kb gran, limit 0xffffffff bytes, base=0
gdt32_data:
    dq MAKE_GDT_DESC(0, 0x00ffffff, 10010010b, 1100b)
                                ; 32-bit data, 4kb gran, limit 0xffffffff bytes, base=0
end_of_gdt:

gdtr:
    dw end_of_gdt - gdt_start - 1
                                ; limit (Size of GDT - 1)
    dd gdt_start                ; base of GDT

CODE32_SEL equ gdt32_code - gdt_start
DATA32_SEL equ gdt32_data - gdt_start

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)
{
    /* This is a very basic keyboard initialization. The assumption is we have a
     * PS/2 keyboard and it is already in a proper state. This may not be the case
     * on real hardware. We simply enable the keyboard interupt */

    /* Get current master PIC interrupt mask */
    unsigned char curmask_master = read_port (0x21);

    /* 0xFD is 11111101 - enables only IRQ1 (keyboard) on master pic
       by clearing bit 1. bit is clear for enabled and bit is set for disabled */
    write_port(0x21, curmask_master & 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)
{
    signed char keycode;

    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 on black characters */
            vidptr[current_loc++] = 0x07;
    }

    /* Send End of Interrupt (EOI) to master PIC */
    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 这个内核,我使用了一个定义如下的文件 link.ld

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

我使用 GCC i686 cross compiler 和这些命令编译并 link 这段代码:

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 的默认值。调试信息使得使用 QEMUGDB 进行调试更加容易。可以使用这些命令调试内核:

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 时显示: