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));
这样做意味着在 limit
和 base
之间没有额外的字节用于对齐。未能有效处理对齐问题会导致 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
如果您要手动保存和恢复所有易失性寄存器,您必须保存和恢复 EAX、ECX 和EDX 因为它们不需要在 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
的默认值。调试信息使得使用 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 时显示:
我正在尝试为学习目的编写一个非常简单的内核。看了一堆关于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));
这样做意味着在 limit
和 base
之间没有额外的字节用于对齐。未能有效处理对齐问题会导致 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
如果您要手动保存和恢复所有易失性寄存器,您必须保存和恢复 EAX、ECX 和EDX 因为它们不需要在 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
的默认值。调试信息使得使用 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 时显示: