[ 1] linux0.11引导程序阅读注释
[ 2] linux0.11由实模式进入保护模式程序阅读注释
[ 3] linux0.11护模式初始化程序阅读注释
[ 4] linux0.11主存管理程序阅读注释

篇幅较长,可通过浏览器的搜索功能(Ctrl + f)搜索函数名了解相应函数的实现机制,如 trap_init。

[5] linux0.11中断/异常机制初始设置相关程序阅读注释

在主存管理程序模块中,除最后有几个函数与 任务和文件管理 有些许主动耦合外,其余程序几乎没有去主动耦合与其他程序模块。此文希望能够先阅读与其他模块主动耦合较低的程序模块。

浏览进程管理程序,缓冲区管理程序,文件系统管理程序,外设管理程序…等模块,它们都有较多去主动耦合其他程序模块函数的情况。对于耦合性较高的程序模块,对他们的阅读可能需要同时进行,可以先将它们放一放, 先捏捏相对来说较软的柿子吧。

经初步决定,就先阅读中断设置程序吧。该程序模块与其他程序模块的耦合性似乎不如以上几个模块大。


main.c
void main(void)
{
/* ... */

    /* 在主存管理程序模块中,
     * 除最后有几个函数与 任务和文件管理 有些许主动耦合外,
     * 其余程序几乎没有去主动耦合与其他程序模块。
     * 此文希望能够先阅读与其他模块主动耦合较低的程序模块。
     * 
     * 浏览
     * 进程管理程序,
     * 缓冲区管理程序,
     * 文件系统管理程序,
     * 外设管理程序...
     * 这些模块, 它们都有较多去主动耦合其他程序模块函数的情况。
     * 对于耦合性较高的程序模块, 对他们的阅读可能需要同时进行,
     * 可以先将它们放一放, 先捏捏相对来说较软的柿子吧。
     * 
     * 经初步决定, 就先阅读中断设置程序吧。
     * 该程序模块与其他程序模块的耦合性似乎不如以上几个模块大。*/
     
    /* [2] trap_init(),
     * 中断/异常相关初始化。如
     * 初始化IDT, 设置8259A允许某外部中断。*/
    trap_init();

/* ... */
}

跳转阅读kernel/traps.c/trap_int()。

traps.c
/*
 *  linux/kernel/traps.c
 *
 *  (C) 1991  Linus Torvalds
 */

/*
 * 'Traps.c' handles hardware traps and faults after we have saved some
 * state in 'asm.s'. Currently mostly a debugging-aid, will be extended
 * to mainly kill the offending process (probably by giving it a signal,
 * but possibly by killing it outright if necessary).
 */
/* traps.c 根据 asm.s 处理硬件陷阱和故障。目前主要用于调试,将来可扩展用
 * 来杀死不正常的进程(可能通过发送信号方式,若有必要也可直接杀死)。*/

#include <string.h>

#include <linux/head.h>
#include <linux/sched.h>
#include <linux/kernel.h>
#include <asm/system.h>
#include <asm/segment.h>
#include <asm/io.h>

/* get_seg_byte(seg, addr),
 * 将seg段中addr处的1字节内容读出。
 * 
 * ({}), 包含在{}中的复合语句中的最后一个表达式
 * 将作为({})的最终值, 相当于get_seg_byte的返回值。
 *
 * 内联汇编的输入。
 * "0" (seg), 将seg赋给%0的约束即eax中;
 * "m" (*(addr)), 内存变量*(addr)。
 *
 * 内联汇编中的指令。
 * push %%fs, fs寄存器入栈;
 * mov %%ax, %%fs, fs=ax;
 * movb %%fs:%2, %%al, al = fs:(*(addr));
 * pop %%fs, 恢复fs寄存器的值。
 * 
 * 内联汇编的输出。
 * __res = eax。
 *
 * __res将作为该宏最终的值。*/
#define get_seg_byte(seg,addr) ({ \
register char __res; \
__asm__("push %%fs;mov %%ax,%%fs;movb %%fs:%2,%%al;pop %%fs" \
    :"=a" (__res):"0" (seg),"m" (*(addr))); \
__res;})

/* get_seg_long(seg, addr),
 * 将seg段(选择符所指向段)中addr处4字节内容读出。
 *
 * ({}), 包含在{}中的复合语句中的最后一个表达式
 * 将作为({})的最终值, 相当于get_seg_long的返回值。
 *
 * 内联汇编中的输入。
 * "0" (seg), 将seg赋给%0中的约束即eax;
 * "m" (*(addr)), 内存变量*addr。
 *
 * 内联汇编中的指令。
 * push %%fs, fs寄存器入栈;
 * fs=ax;
 * mov fs:%2, eax, 将fs:*(addr)中的4字节内容赋给eax;
 * pop %%fs, 恢复fs寄存器内容。
 *
 * 内联汇编的输出。
 * 将eax的值赋值给__res变量,
 * __res变量本身也希望由某个空闲寄存器来表示。
 *
 * __res作为整个宏(表达式)的"返回值"。*/
#define get_seg_long(seg,addr) ({ \
register unsigned long __res; \
__asm__("push %%fs;mov %%ax,%%fs;movl %%fs:%2,%%eax;pop %%fs" \
    :"=a" (__res):"0" (seg),"m" (*(addr))); \
__res;})

/* 获取fs寄存器的值。
 * "=a" (__res), 将eax赋值给__res,
 * __res; 语句作为宏的最终值。*/
#define _fs() ({ \
register unsigned short __res; \
__asm__("mov %%fs,%%ax":"=a" (__res):); \
__res;})

/* 声明kernel/exit.c中的do_exit函数 */
int do_exit(long code);

/* 以typedef void(funt)(void)函数类型声明page_exception。
 * page_exception的定义在哪里呢? */
void page_exception(void);

/* 以typedef void (funt)(void)函数类型声明divide_error,
 * 该函数定义在kernel/asm.s中。*/
void divide_error(void);

/* 除以下的page_fault外
 * (因为已经读过主存管理程序了, 了解以下页错误的处理吧),
 * 就不再此处一一跟踪每一个中断入口程序啦,
 * 若有需要时再去跟踪阅读。*/
void debug(void);
void nmi(void);
void int3(void);
void overflow(void);
void bounds(void);
void invalid_op(void);
void device_not_available(void);
void double_fault(void);
void coprocessor_segment_overrun(void);
void invalid_TSS(void);
void segment_not_present(void);
void stack_segment(void);
void general_protection(void);
void page_fault(void);
void coprocessor_error(void);
void reserved(void);
void parallel_interrupt(void);
void irq13(void);

/* die,
 * 打印一些提示信息, 然后退出程序。
 *
 * str - 提示信息,
 * esp_ptr - 中断发生时eip的入栈地址,
 * nr - 错误码。
 *
 * 此处与用户程序管理的直接耦合还是挺大,
 * 由于此程序的功能是打印一些提示信息后退出用户程序,
 * 所以涉及LDT表、任务、进程等相关部分, 可暂不细读。*/
static void die(char * str,long esp_ptr,long nr)
{
    long * esp = (long *) esp_ptr;
    int i;
    
    /* 输出提示信息, 错误号,
     * 输出中断处的cs, eip, eflag, ss, esp信息,
     * 输出fs,
     * ...
     * (printk定义在kernel/printk.c中, 暂不细读) */
    printk("%s: %04x\n\r",str,nr&0xffff);
    printk("EIP:\t%04x:%p\nEFLAGS:\t%p\nESP:\t%04x:%p\n",
            esp[1],esp[0],esp[2],esp[4],esp[3]);
    printk("fs: %04x\n",_fs());

    /* 打印当前用户程序地址空间信息,
     * 0x17为用户程序程序段描述符LDT[2]的选择符,
     * 之前在setup.s和head.s中涉及了GDT, LDT会在任务管理程序中涉及。*/
    printk("base: %p, limit: %p\n",get_base(current->ldt[1]),get_limit(0x17));
    /* 如果是用户程序,
     * 则打印用户程序中断发生处esp寄存器所指向内存中的16字节内容。
     * esp[4]为ss在用户程序程序中的值,
     * esp[3]为用户程序在中断发生时esp寄存器的值。*/
    if (esp[4] == 0x17) {
        printk("Stack: ");
        for (i=0;i<4;i++)
            printk("%p ",get_seg_long(0x17,i+(long *)esp[3]));
        printk("\n");
    }
    /* 获取当前任务TSS在GDT中的编号给变量i */
    str(i);
    /* 打印当前任务的进程号, TSS号,
     * 以及中断发生处的10字节内容,
     * esp[1]为用户程序中断发生时eip的值。*/
    printk("Pid: %d, process nr: %d\n\r",current->pid,0xffff & i);
    for(i=0;i<10;i++)
        printk("%02x ",0xff & get_seg_byte(esp[1],(i+(char *)esp[0])));
    printk("\n\r");

    /* 释放用户程序资源, 退出用户程序。*/
    do_exit(11);    /* play segment exception */
}

/* 以do开头的程序是各中断的C中断处理函数,
 * 这些函数在中断入口汇编程序中被调用,
 * 包含了真正处理中断的代码。*/

void do_double_fault(long esp, long error_code)
{
    die("double fault",esp,error_code);
}

void do_general_protection(long esp, long error_code)
{
    die("general protection",esp,error_code);
}

/* 除法溢出中断的C中断处理函数, 由asm.c/_divide_error调用。
 *
 * esp - 中断发生时eip的入栈地址,
 * error_code - 错误码, 此处为0。*/
void do_divide_error(long esp, long error_code)
{
    die("divide error",esp,error_code);
}

/* do_int3的参数很多, asm.s中记录了
 * 从中断发生到调用C中断处理函数过程中的栈内容,
 * 可根据该知识点理解这些参数。*/
void do_int3(long * esp, long error_code,
    long fs,long es,long ds,
    long ebp,long esi,long edi,
    long edx,long ecx,long ebx,long eax)
{
    int tr;

    /* 取当前任务的任务号与tr中 */
    __asm__("str %%ax":"=a" (tr):"0" (0));

    /* 打印当事程序设置断点中断发生时eax ebx ecx edx寄存器的值 */
    printk("eax\t\tebx\t\tecx\t\tedx\n\r%8x\t%8x\t%8x\t%8x\n\r",
        eax,ebx,ecx,edx);

    /* 打印当事程序设置断点中断发生时esi edi ebp esp寄存器的值 */
    printk("esi\t\tedi\t\tebp\t\tesp\n\r%8x\t%8x\t%8x\t%8x\n\r",
        esi,edi,ebp,(long) esp);
    
    /* 打印当事程序设置断点中断发生时ds es fs寄存器的值, 当事程序所在任务号 */
    printk("\n\rds\tes\tfs\ttr\n\r%4x\t%4x\t%4x\t%4x\n\r",
        ds,es,fs,tr);
    /* 打印当事程序设置断点中断发生时eip cs eflag寄存器的值 */
    printk("EIP: %8x   CS: %4x  EFLAGS: %8x\n\r",esp[0],esp[1],esp[2]);
}

void do_nmi(long esp, long error_code)
{
    die("nmi",esp,error_code);
}

void do_debug(long esp, long error_code)
{
    die("debug",esp,error_code);
}

void do_overflow(long esp, long error_code)
{
    die("overflow",esp,error_code);
}

void do_bounds(long esp, long error_code)
{
    die("bounds",esp,error_code);
}

void do_invalid_op(long esp, long error_code)
{
    die("invalid operand",esp,error_code);
}

void do_device_not_available(long esp, long error_code)
{
    die("device not available",esp,error_code);
}

void do_coprocessor_segment_overrun(long esp, long error_code)
{
    die("coprocessor segment overrun",esp,error_code);
}

void do_invalid_TSS(long esp,long error_code)
{
    die("invalid TSS",esp,error_code);
}

void do_segment_not_present(long esp,long error_code)
{
    die("segment not present",esp,error_code);
}

void do_stack_segment(long esp,long error_code)
{
    die("stack segment",esp,error_code);
}

void do_coprocessor_error(long esp, long error_code)
{
    if (last_task_used_math != current)
        return;
    die("coprocessor error",esp,error_code);
}

void do_reserved(long esp, long error_code)
{
    die("reserved (15,17-47) error",esp,error_code);
}

/* trap_init,
 * 设置IDT(见head.s)和8259A, 初始化中断/异常机制。
 *
 * 
 * 粗略理解CPU引用IDT描述符的过程。
 * 在head.s中有提到, IDT描述符描述了一段子程序的相关信息,
 * CPU引用IDT描述符是为了得到该段子程序的内存地址。
 *
 * 此文将触发CPU引用IDT描述符的方式总结为两种。
 * [1] CPU执行int指令。
 * [2] CPU在完成某条指令的执行后, 接收/检测到某中断或异常(后统称为中断)信号时。
 *
 * CPU执行int指令或检测到中断信号时具体引用哪一个IDT描述符呢。
 * [1] CPU执行"int n"指令时将其操作数n用作引用IDT描述符的索引, 即引用IDT[n]。
 * [2] IDT[0..16]是Intel为17个中断在硬件层面为CPU分配的IDT描述符,
 * 即当CPU检测到这17个中断中的一个中断发生时便自动引用其对应的IDT描述符。
 * IDT[0] ... 除法溢出
 * IDT[1] ... 调试(标志寄存器TF=1时)
 *        ...    
 * (参考《INTEL 80386 PROGRAMMER'S REFERENCE MANUAL》"9.8 Exception Conditions"章节)
 *
 * 其余IDT描述符可由编程指定给外设(I/O设备)中断。
 * 在setup.s中编程为8259A分配了IDT[20h..2fh], 键盘中断的IDT描述符为IDT[21h],
 * 键盘输入发生键盘中断发生时(键盘I/O)会向CPU发出中断信号,
 * 该中断信号包含引用IDT描述符的索引21h(俗称中断号/码), CPU从而引用IDT[21h]。
 *
 * CPU在跳转执行IDT描述符中的子程序前,
 * 会自动在ss:esp维护的栈内存中备份中断发生处的信息(中断现场保护),
 * 供中断处理程序返回(若有必要)以继续执行中断发生处的后续程序。
 *
 * 中断机制常应用于发生频率远低于CPU频率的I/O中或某些事件上,
 * 在某事件发生时以中断机制让CPU执行相应的处理程序就好,
 * 待处理程序执行完毕再返回到中断发生处。
 * 而不用CPU执行扫描低频率事件是否发生的程序。*/
void trap_init(void)
{
    int i;

    /* set_trap_gate 是include/asm/system.h中定义的宏,
     * divide_error 是kernel/asm.s中定义的一段子程序。
     * 在粗略了解IDT和中断机制后, 跟踪下IDT[0]和divide_error
     * 以了解下在IDT描述符中设置子程序的过程吧。
     * 这个过程大概是include/asm/system.h set_trap_get -> _set_gate 和 
     * divide_error -> kernel/asm.c _divide_error -> do_divide_error ->
     * die(粗略)。然后就返回到到这里了。*/
    set_trap_gate(0,&divide_error);
    /* 再粗略回味一下除法溢出中断过程。
     *
     * 当除法溢出中断发生时, CPU完成中断现场保护后引用IDT[0]。
     * IDT[0]位内容。
     * |31                         |15              |7        0
     * --------------------------------------------------------
     * |divide_error offset[31..16]| 1 | 00 | 01111 | 000 |0x0|
     * -------------------------------------------------------- 4
     * |            0x08           |divide_error offset[15..0]|
     * -------------------------------------------------------- 0
     * P=1, 不检查CPL是否小于等于DPL(非int或into指令触发CPU引用IDT[0]),
     * 检查0x08对应段描述符(P=1?DPL<=CPL? etc.)并将其加载到cs中,
     * 然后跳转执行0x08对应可执行段中的divide_error程序。*/

    set_trap_gate(1,&debug);
    set_trap_gate(2,&nmi);

/* 通过int n指令跳转执行IDT[n]中设置的处理程序时需满足
 * CPL <= IDT[n].DPL, CPL >= IDT[n].GDT.DPL。
 * 由于所有的中断处理函数都在内核可执行段中(DPL=0),
 * 所以在CPL > 0的程序中不能通过int指令跳转执行在set_trap_gate中
 * 设置的处理程序。
 * 
 * 在CPL=n的程序中, 
 * 可以通过int指令访问通过set_system_gate在IDT[n]中设置的程序
 * (通过set_system_gate设置的IDT.DPL=3),
 * 若IDT[n].GDT[n].DPL<=n, 则可跳转访问IDT中设置的处理程序。*/
    set_system_gate(3,&int3);   /* int3-5 can be called from all */
    set_system_gate(4,&overflow);
    set_system_gate(5,&bounds);
    set_trap_gate(6,&invalid_op);
    set_trap_gate(7,&device_not_available);
    set_trap_gate(8,&double_fault);
    set_trap_gate(9,&coprocessor_segment_overrun);
    set_trap_gate(10,&invalid_TSS);
    set_trap_gate(11,&segment_not_present);
    set_trap_gate(12,&stack_segment);
    set_trap_gate(13,&general_protection);
    set_trap_gate(14,&page_fault);
    set_trap_gate(15,&reserved);
    set_trap_gate(16,&coprocessor_error);

    /* 先用reserved再次初始化IDT[17..48],
     * 在head.s中, IDT表由 ignore_int子程序初始化。*/
    for (i=17;i<48;i++)
        set_trap_gate(i,&reserved);

    /* 设置协处理器中断描述符IDT[45],
     * 编程8259A开启协处理器中断。*/
    set_trap_gate(45,&irq13);
    /* out_p outb inb_p 定义在include/asm/io.h中 */
    outb_p(inb_p(0x21)&0xfb,0x21);
    outb(inb_p(0xA1)&0xdf,0xA1);
/* 0x21/0xA1是中断控制器8259A的端口地址
 * (见setup.s中的I/O端口地址空间)。
 * 在完成对中断控制器8259A-1/2的初始化后(见setup.s),
 * 8259A进入接收操作命令状态。
 *
 * 当端口地址最低位为0时,
 * 表示下发OCW1操作命令到8259A, 即操作中断屏蔽寄存器IMR(P68)。
 * 保持IMR其他位不变,
 * 设置8259A-1允许IRQ2中断, 即允许来自从片8259A-2的中断(P65),
 * 设置8259A-2允许IRQ13中断, 即协处理器中断
 * (P65中IRQ13标注的年代较早, 可能有误),
 * 根据8259A引脚图和中断号的分配, 协处理器中断码为45(2dh)。*/

    set_trap_gate(39,&parallel_interrupt);
}
/* 关于当时的链接器???
 * 在本文件中将divide_error声明为函数类型void divide_error(void)后,
 * 再引用divide_error时它就应该被解析成函数在OS代码段中的偏移地址。
 * 而在以上set_trap_gate和set_system_gate中,
 * 需在divide_error前加取地址符&, 
 * 又像是在将divide_error当成全局变量在对待。
 * 不过, 这只是一个语法问题, 没有太大关系。 */

include/asm/system.h中的set_trap_gate/set_system_gate

system.h
/* ... */

/* _set_gate(gate_addr,type,dpl,addr),
 * 设置IDT描述符。
 *
 * 参数。
 * gate_addr - IDT描述符首地址;
 * type - IDT描述符类型;
 * dpl -  IDT描述符特权级;
 * addr - IDT所描述处理程序在其可执行段的偏移地址。
 *
 * 粗略理解此处内联汇编含义。
 * [1] 占位符与输入。
 * %0 - "i" ((short) (0x8000+(dpl<<13)+(type<<8))),
 * "i"表示将引用立即数((short) (0x8000+(dpl<<13)+(type<<8)))
 * (立即数一般会被编译器存在某空闲通用寄存器中)。
 *
 * %1 - "o" (*((char *) (gate_addr))),
 * %2 - "o" (*(4+(char *) (gate_addr))),
 * "o"表示引用内存变量(char类型),
 * 且变量所在内存地址加一点正偏移所得到的内存地址也是有效可用的。
 * 在C语言中, 变量作为左值时表示往其所在内存赋值, 变量作为右值时表示取其值。
 *
 * "d" ((char *) (addr)),"a" (0x00080000),
 * "d"表示 edx = ((char *) (addr)), 保存处理函数在其可执行段中的偏移地址;
 * "a"表示 eax = (0x00080000)。
 * 
 * [2] 无输出部分。
 *
 * [3] 汇编语句。
 * 将含addr低16位的dx赋值给eax低16位ax;
 * 将IDT描述符第5和6字节内容((short) (0x8000+(dpl<<13)+(type<<8)))赋给edx低16位dx;
 * 将含段描述符选择符0x8和addr低16位的eax赋给%1(IDT描述符低4字节);
 * 将含addr高16位和type等的edx赋给%2(IDT描述符高4字节)。
 *
 * _set_gate执行后, gate_addr处IDT描述符的内容如下。
 * |31          |15                |7        0
 * -------------------------------------------
 * |addr[31..16]| 1 | dpl | 0 type | 000 |0x0|
 * -------------------------------------------4
 * |    0x08    |       addr[15..0]          |
 * -------------------------------------------0
 * P=1, IDT描述符有效。
 * segment descriptor selector = 0x8, 为操作系统代码可执行内存段的选择符。
 * dpl=0, IDT描述符特权级为最高特权级(系统级);
 * dpl=3, IDT描述符特权级为最低特权级(用户级)。
 * type=15时, IDT描述符为陷阱门描述符;
 * type=14时, IDT描述符为中断门描述符。*/
#define _set_gate(gate_addr,type,dpl,addr) \
__asm__ ("movw %%dx,%%ax\n\t" \
    "movw %0,%%dx\n\t" \
    "movl %%eax,%1\n\t" \
    "movl %%edx,%2" \
    : \
    : "i" ((short) (0x8000+(dpl<<13)+(type<<8))), \
    "o" (*((char *) (gate_addr))), \
    "o" (*(4+(char *) (gate_addr))), \
    "d" ((char *) (addr)),"a" (0x00080000))
/* GCC内联输入输出中的约束含义可参考 
 * http://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html。
 * 不过此文至今也没有真正明白 "o" "m" "v"三个约束之间的区别。*/

/* set_trap_gate(n,addr),
 * 将IDT[n]设置为陷阱门描述符(位格式见head.s中的记录)。
 * 
 * 参数。
 * n - IDT描述符索引(中断码);
 * addr - IDT[n]中处理程序在其可执行段中的偏移地址。
 * 15 - IDT[n]中的TYPE字段, 表征IDT[n]为陷阱门描述符类型;
 * 0 - IDT[n]中的dpl, 表示IDT[n]的特权级为最高特权级(系统级)。
 *
 * set_trap_gate所设置IDT[n]的位格式。
 * |31          |15              |7       0|
 * -----------------------------------------
 * |addr[31..16]| 1 | 00 | 01111 | 000 |0x0|
 * -----------------------------------------4
 * |    0x08    |       addr[15..0]        |
 * -----------------------------------------0 */
#define set_trap_gate(n,addr) \
    _set_gate(&idt[n],15,0,addr)

/* 将IDT[n]设置为陷阱门描述符。
 *
 * 参数。
 * n - IDT描述符索引;
 * addr - IDT[n]中处理程序在其可执行段中的偏移地址。
 * 15 - IDT[n]的TYPE, 表示描述符类型为陷阱门描述符;
 * 3 - IDT[n]的dpl, 表示特权级为最低特权级(用户级)。
 *
 * set_system_gate所设置IDT[n]的位格式。
 * |31          |15              |7       0|
 * ------------------------------------------
 * |addr[31..16]| 1 | 11 | 01111 | 000 |0x0|
 * ------------------------------------------4
 * |    0x08    |       addr[15..0]        |
 * -----------------------------------------0 */
#define set_system_gate(n,addr) \
    _set_gate(&idt[n],15,3,addr)

/* ... */

以divide_error为例,跟踪中断入口程序,分CPU产生错误码和不产生错误码两种中断入口程序,两者在栈中分配的内容有一小点不同。

asm.s
/*
 *  linux/kernel/asm.s
 *
 *  (C) 1991  Linus Torvalds
 */

/*
 * asm.s contains the low-level code for most hardware faults.
 * page_exception is handled by the mm, so that isn't here. This
 * file also handles (hopefully) fpu-exceptions due to TS-bit, as
 * the fpu must be properly saved/resored. This hasn't been tested.
 */
/* asm.s包含了大部分硬件错误的底层代码。page_exception 由mm模块完成,
 * 所以该异常处理函数不在本文件中。本文件还应处理由TS位所表征的fpu异
 * 常,因为fpu状态必须被正确的保存或重新存储,该功能还未经过测试。*/
 * 
.globl _divide_error,_debug,_nmi,_int3,_overflow,_bounds,_invalid_op
.globl _double_fault,_coprocessor_segment_overrun
.globl _invalid_TSS,_segment_not_present,_stack_segment
.globl _general_protection,_coprocessor_error,_irq13,_reserved

# IDT[0]中的子程序divide_error。
#
# 粗略理解除法溢出中断发生过程。
# 当在内核(CPL=0)中执行除法指令发生溢出时,
# 由于divide_error也在内核可执行段中,
# 跳转执行divide_error时不涉及特权级转换, 此时的中断行为相当于
# pushf, TF=IF=0,
# push cs
# pushl eip
#
# 当CPU执行move_to_user_mode转到用户模式(CPL=3)中执行除法指令发生溢出时,
# 涉及特权级转换, 此时的中断行为相当于
# push  ss
# pushl esp
# pushf, TF=IF=0,
# push cs
# pushl eip
#
# 在备份各寄存器后,
# CPU根据除法溢出的中断码0引用IDT[0],
# 由于非int 0h指令触发中断, 此时CPU会免去CPL是否小于等于IDT[0].DPL的检查
# 而将IDT[0]中段描述符选择符字段(0x08)指向的GDT段描述符加载给cs。
# 即GDT[8h]被加载到cs的隐藏部分(在加载GDT[8h]到cs时, CPU会做CPL >= GDT[8h].DPL的检查),
# 并将IDT[0].divide_error在其可执行段0x08中的偏移地址赋值给eip,
# 最终使得CPU跳转执行divide_error。
# 
# 内核初始化完成后会通过move_to_user_mode转到CPU用户模式下运行程序,
# 此处设置在IDT[0]中的divide_error是为用户模式编写的。
# 看看从用户程序中发生除法溢出时到
# 刚跳转到处理函数divide_error这个过程中的栈内容吧。
# | ... |
# -------
# | ss  |
# -------
# | esp |
# ------|
# |eflag|
# -------
# | cs  |
# -------
# | eip |
# -------
# 16位段寄存器ss和cs以占32位(以32位对齐存储)。

_divide_error:
# 将用C语言编写的do_divide_error函数的地址保存在栈中。
#
# _do_divide_error是在traps.c中定义的C函数do_divide_error,
# 该C函数才是除法溢出中断的处理函数。
# _divide_error只是充当了C处理函数的入口地址,
# 其中所包含的内容属于 用汇编指令更易或才能实现的程序。
# 在调用该程序时, 可以跳转到traps.c中看其定义。
    pushl $_do_divide_error

# CPU不会产生错误码中断处理汇编代码段。
# no_error_code:
#
# 在调用C中断处理函数前的栈内容。
# |  ...   |
# ----------
# |  ss    |
# ----------
# |  esp   |
# ---------|
# | eflag  |
# ----------
# |  cs    |
# ----------
# |  eip   | 基于eip所在栈内存地址的偏移
# ========== 0 <--|
# |  eax   |      |
# ---------- -4   |
# |  ebx   |      |
# ---------- -8   |
# |  ecx   |      |
# ---------- -12  |
# |  edx   |      |
# ---------- -16  |
# |  edi   |      |
# ---------- -20  |
# |  esi   |      |
# ---------- -24  |
# |  ebp   |      |
# ---------- -28  |
# |   ds   |      |
# ---------- -32  |
# |   es   |      |
# ---------- -36  |
# |   fs   |      |
# ---------- -40  |
# |   0    |      |
# ---------- -44  |
# |eip_addr| -----| 该4字节栈内存中保存了eip寄存器的栈地址
# ---------- -48
no_error_code:
# 在此语句处即刚执行完将C中断处理函数压栈指令时,
# esp指向C函数的栈地址, xchagl将eax和esp栈中的内容进行交换,
# 即eax值被保存到当前栈内存中, eax的值为C处理函数地址。
    xchgl %eax,(%esp)
# 依次将以下寄存器入栈保存
    pushl %ebx
    pushl %ecx
    pushl %edx
    pushl %edi
    pushl %esi
    pushl %ebp
    push %ds
    push %es
    push %fs
# 将0和发生中断时eip的入栈地址依次保存在栈中,
# 在将0压栈后, esp+44为发生中断时eip的入栈地址。
    pushl $0		# "error code"
    lea 44(%esp),%edx
    pushl %edx

# 将内核数据段描述符GDT[2]加载给各数据段寄存器,
# 如此才能正确访问内核数据段中的数据。
    movl $0x10,%edx
    mov %dx,%ds
    mov %dx,%es
    mov %dx,%fs
# 调用C中断处理函数(如do_divide_error)
    call *%eax
# 回收C中断处理函数实参栈内存, 回收实参栈内存后,
# esp指向fs栈内存处。
    addl $8,%esp
# 实参以从右向左顺序入栈, 实参栈内存由调用者回收,
# 即函数调用约定为__stdcall哦。

# 中断处理程序执行完毕, 依次恢复栈中所备份的寄存器。
    pop %fs
    pop %es
    pop %ds
    popl %ebp
    popl %esi
    popl %edi
    popl %edx
    popl %ecx
    popl %ebx
    popl %eax
# 恢复因中断行为入栈的寄存器,
# 从内核返回到用户模式时, 此语句相当于
# popl eip, pop cs, popf, popl esp, pop ss
    iret

_debug:
    pushl $_do_int3		# _do_debug
    jmp no_error_code

_nmi:
    pushl $_do_nmi
    jmp no_error_code

_int3:
    pushl $_do_int3
    jmp no_error_code

_overflow:
    pushl $_do_overflow
    jmp no_error_code

_bounds:
    pushl $_do_bounds
    jmp no_error_code

_invalid_op:
    pushl $_do_invalid_op
    jmp no_error_code

_coprocessor_segment_overrun:
    pushl $_do_coprocessor_segment_overrun
    jmp no_error_code

_reserved:
    pushl $_do_reserved
    jmp no_error_code

_irq13:
    pushl %eax
    # F0h为协处理器端口地址, 该设置确保CPU可以响应协处理器中断
    xorb %al,%al
    outb %al,$0xF0
    # 向8259A发中断结束EOI信号, setup.s中设置8259A时, 其不会自动结束中断
    movb $0x20,%al
    outb %al,$0x20
    jmp 1f
    1:  jmp 1f
    1:  outb %al,$0xA0
    popl %eax
    jmp _coprocessor_error

_double_fault:
    pushl $_do_double_fault
# CPU会产生错误码的中断入口处理程序。
# 对于CPU会产生错误码的中断,
# 当发生到此处时栈内存中的内容为
# |  ...   |
# ----------
# |  ss    |
# ----------
# |  esp   |
# ---------|
# | eflag  |
# ----------
# |  cs    |
# ----------
# |  eip   |
# ==========
# |err_code|
# ---------- 
# |  do_*  |
# ---------- <-- esp
# 在进入error_code子程序后的入栈及其他操作同no_error_code。
error_code:
    xchgl %eax,4(%esp)  # error code <-> %eax
    xchgl %ebx,(%esp)   # &function <-> %ebx
    pushl %ecx
    pushl %edx
    pushl %edi
    pushl %esi
    pushl %ebp
    push %ds
    push %es
    push %fs
    pushl %eax          # error code
    lea 44(%esp),%eax   # offset
    pushl %eax
    movl $0x10,%eax
    mov %ax,%ds
    mov %ax,%es
    mov %ax,%fs
    call *%ebx
    addl $8,%esp
    pop %fs
    pop %es
    pop %ds
    popl %ebp
    popl %esi
    popl %edi
    popl %edx
    popl %ecx
    popl %ebx
    popl %eax
    iret

_invalid_TSS:
    pushl $_do_invalid_TSS
    jmp error_code

_segment_not_present:
    pushl $_do_segment_not_present
    jmp error_code

_stack_segment:
    pushl $_do_stack_segment
    jmp error_code

_general_protection:
    pushl $_do_general_protection
    jmp error_code

因为已经阅读过主存管理程序,所以阅读一下mm/page.s中的_page_default子程序吧。

page.s
/*
 *  linux/mm/page.s
 *
 *  (C) 1991  Linus Torvalds
 */

/*
 * page.s contains the low-level page-exception code.
 * the real work is done in mm.c
 */
/* page.s 包含了页异常入口程序,主要功能由memory.c完成。*/

.globl _page_fault

# 页异常(IDT[14])处理入口程序。
# 从页异常发生到此处, 栈中的内容为。
# |  ...   |
# ----------
# |  ss    |
# ----------
# |  esp   |
# ---------|
# | eflag  |
# ----------
# |  cs    |
# ----------
# |  eip   |
# ---------- 
# |err_code|
# ---------- <-- esp
_page_fault:
    # 将eax寄存器值入栈于页错误码所在栈内存中, eax保存页错误码
    xchgl %eax,(%esp)
    pushl %ecx
    pushl %edx
    push %ds
    push %es
    push %fs
    # 将内核数据段描述符加载给段寄存器
    movl $0x10,%edx
    mov %dx,%ds
    mov %dx,%es
    mov %dx,%fs
    # 取页面异常地址到edx中,
    # 在页面异常发生时, 引起页面异常的内存地址会存在cr2中
    movl %cr2,%edx
    # 入栈页面异常地址和页错误码
    pushl %edx
    pushl %eax
    # 如果不是缺页异常则向前跳转执行mm/memory.c中的do_wp_page
    # 实现内存页的写时拷贝。
    testl $1,%eax
    jne 1f
    # 如果是缺页异常则跳转执行mm/memory.c中的do_no_page
    # 以实现从共用可执行程序文件共享页或
    # 从磁盘上读取一页数据到内存的操作。
    call _do_no_page
    jmp 2f
1:  call _do_wp_page
2:  addl $8,%esp # 回收传递给C处理函数实参所占用的栈内存
    # 依次恢复相关寄存器
    pop %fs
    pop %es
    pop %ds
    popl %edx
    popl %ecx
    popl %eax
# 中断返回, 从内核程序返回到用户程序相当于
# popl eip, pop cs, popf, popl esp, pop ss
    iret

最后是traps_init()中所使用的outb_p等宏的阅读。

io.h
/* 目前还未在程序中使用过该宏, 顺便也阅读下outb宏吧。
 * 
 * outb(value, port),
 * 将1字节数据value输出到端口地址port处。
 * 
 * 内联汇编输入。
 * "a"(value), 将value赋给eax;
 * "d"(port),  将port赋给edx;
 * 
 * outb %%al, %%dx, 将al赋给dx值表示的端口处。
 * */
#define outb(value,port) \
__asm__ ("outb %%al,%%dx"::"a" (value),"d" (port))


/* 功能: 读端口。
 *
 * 参数:
 * port - 端口地址,
 * 
 * 内联汇编指令描述。
 * 输入: edx = port,
 * 输出: _v = eax。
 *
 * 将port输入edx, 读端口dx数据到al, 
 * 将eax输出到_v, 并将_v作为"返回值"。*/
#define inb(port) ({ \
unsigned char _v; \
__asm__ volatile ("inb %%dx,%%al":"=a" (_v):"d" (port)); \
_v; \
})

/* outb_p(value, port),
 * 写1字节数据value到指定端口地址port处。
 *
 *
 * 内联汇编。
 * edx = port, eax = value,
 * 将al写往端口dx, 向前跳转执行标号1。
 *
 * 跳转语句用作延时供写端口操作完成/稳定,
 * outb_p用于之后会紧跟与port相关的i/o指令。*/
#define outb_p(value,port) \
__asm__ ("outb %%al,%%dx\n" \
    "\tjmp 1f\n" \
    "1:\tjmp 1f\n" \
    "1:"::"a" (value),"d" (port))

/* inb_p(port),
 * 从指定端口地址port处读取1字节内容。
 *
 * 内联汇编。
 * volatile其告知编译器不要优化内联汇编中的代码
 * (如不共享寄存器, 不改变指令顺序等, 不删除未使用的函数)。
 * 
 * 内联汇编指令。
 * edx = port,
 * 读端口地址dx处内容到al,
 * 向前跳转执行标号1(用作延时待读操作完成/稳定)
 * _v = eax.
 *
 * _v为表达式最终值。*/
#define inb_p(port) ({ \
unsigned char _v; \
__asm__ volatile ("inb %%dx,%%al\n" \
    "\tjmp 1f\n" \
    "1:\tjmp 1f\n" \
    "1:":"=a" (_v):"d" (port)); \
_v; \
})

/* 2019.06.08 */
GitHub 加速计划 / li / linux-dash
10.39 K
1.2 K
下载
A beautiful web dashboard for Linux
最近提交(Master分支:22 天前 )
186a802e added ecosystem file for PM2 4 年前
5def40a3 Add host customization support for the NodeJS version 4 年前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐