我正在编写一个非常简单的内核,以供学习。在阅读了大量关于x86架构中PIC和IRQ的文章后,我发现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条答案
按热度按时间yftpprvb1#
你的代码有很多问题,主要的问题将在下面单独讨论。
HLT指令将暂停当前CPU,等待下一个中断。此时您确实启用了中断。在第一次中断(击键)之后,* HLT * 之后的代码将被执行。它将开始执行内存中的任何随机数据。您可以修改
kmain
,使用 * HLT * 指令执行无限循环。类似于以下内容:在此代码中:
一般来说,最好在更新中断表之后而不是之前使用 * STI *。这样做会更好:
您的中断处理程序需要执行
iretd
才能从中断正确返回。您的函数keyboard_handler
将执行ret
以返回。要解决此问题,您可以创建一个程序集 Package ,该 Package 调用 * C *keyboard_handler
函数,然后执行IRETD。在 * NASM * 汇编文件中,可以定义一个名为
keyboard_handler_int
的全局函数,如下所示:设置 * IDT * 条目的代码如下所示:
您的
kb_init
函数最终会启用(通过掩码)键盘中断。不幸的是,您在启用该中断后设置了键盘处理程序。在启用中断后和将输入放入 * IDT * 之前,可能会按下按键。快速解决方法是在调用kb_init
之前设置键盘处理程序,如下所示:可能导致内核出现三重故障(并导致虚拟机重启)的最严重问题是您定义
idt_pointer
结构的方式。问题是默认的对齐规则会在
limit
之后和base
之前放置2个字节的填充,这样unsigned int
就会在结构中以4个字节的偏移量对齐。要改变这种行为并打包没有填充的数据,可以在结构上使用__attribute__((packed))
。定义如下:这样做意味着在
limit
和base
之间没有放置用于对齐目的的额外字节。如果不能有效地处理对齐问题,则会产生一个base
地址,该地址在结构中的位置不正确。* IDT * 指针需要一个表示 * IDT * 大小的16位值,后跟一个32-表示 * IDT * 基地址的位值。关于结构对齐和填充的更多信息可以在Eric Raymond的博客中找到。由于
struct idt_entry
成员的放置方式,因此没有额外的填充字节。如果您创建的结构不需要填充,我建议使用__attribute__((packed));
。当您将 * C * 数据结构与系统定义的结构进行Map时,通常会出现这种情况。记住这一点,我将"d为了清楚起见,还打包了struct idt_entry
。其他注意事项
在中断处理程序中,虽然我建议使用 * IRETD *,但还有另一个问题。随着内核的增长,您添加了更多的中断,您会发现另一个问题。您的内核可能会不稳定地运行,寄存器可能会意外地更改值。问题是充当中断处理程序的 * C * 函数会破坏一些寄存器的内容,但我们不会保存和恢复它们。其次,在调用函数之前,需要清除(CLD)方向标志(根据32-bit ABI)。您不能假设在进入中断例程时清除方向标志。ABI说明:
EFLAGS标志寄存器包含系统标志,如方向标志和进位标志。在进入和退出函数之前,方向标志必须设置为"向前"(即零)方向。其它用户标志在标准调用序列中没有指定的角色,因此不被保留
你可以单独压入所有的volatile寄存器,但是为了简洁起见,你可以使用PUSHAD和POPAD指令。中断处理程序看起来更好:
如果您要手动保存和恢复所有volatile寄存器,则必须保存和恢复 * EAX 、 ECX * 和 * EDX *,因为它们不需要在 * C * 函数调用中保留。在中断处理程序中使用x87 FPU指令通常不是一个好主意(主要是为了提高性能),但如果您这样做,则还必须保存和恢复x87 FPU状态。
样品代码
你没有提供完整的例子,所以我填补了一些空白(包括一个简单的键盘Map),并对键盘处理程序进行了细微的更改。修改后的键盘处理程序只显示按下键事件,并跳过没有Map的字符。在所有情况下,代码都会一直跳到处理程序的末尾,以便向 * PIC * 发送 * EOI *(中断结束)。当前光标位置是一个静态整数,在中断调用中保持其值。这允许光标位置在每次字符按下之间前进。
我的
kprintd.h
文件是空的,我把所有的汇编程序原型都放到了你的port_io.h
文件中,原型应该适当地分成多个头文件,我这样做只是为了减少文件的数量,我的lowlevel.asm
文件定义了所有的低级汇编例程,最终代码如下:kernel.asm
:lowlevel.asm
:port_io.h
:kprintf.h
:keyboard_map.h
:keyb.c
:main.c
:为了链接这个内核,我使用了一个定义如下的文件
link.ld
:我使用 GCC i686 cross compiler编译并链接此代码,命令如下:
结果是一个名为
kernel.elf
的内核,带有调试信息。我更喜欢-O3
的优化级别,而不是默认的-O0
。调试信息使使用 QEMU 和 GDB 进行调试变得更容易。可以使用以下命令调试内核:如果希望在汇编代码级别进行调试,请将
layout src
替换为layout asm
。当使用输入the quick brown fox jumps over the lazy dog 01234567890
QEMU 运行时,显示如下: