第七章中 中断和8259A

1 8259A

1.1 简介

8259A的作用是负责所有的外设中断.

cpu每次只能执行一个任务,而中断可能同时发生,所以8259A用来收集所有的中断,然后挑选出一个优先级最高的中断,传送给CPU

8259A的功能有:管理和控制可屏蔽中断,表现在屏蔽外设中断,对他们实行优先级判决,向cpu提供中断向量好等功能

每个8259A智能管理8个中断,而intel的cpu由256个中断,因此采用级联的方式,使用多个8259A管理256个中断.每个8259A有一个额外的引脚可以链接其他的8259A,被连接的那个8259A需要占用一个引脚,但是主片需要占用一个引脚去连接CPU.

1.2 8259A与cpu通信

8259A中的端口有:

  1. INT:8259A选出优先级最高的中断请求,发送信号给CPU
  2. INTA:中断响应信号
  3. IMR:中断屏蔽器,用来屏蔽某个外设的中断
  4. IRR:中断请求其,用与接受结果IMR过滤后的中断信号,次寄存器中保存的都是等待处理的中断,相当于8259A中维护的未处理中断信号队列
  5. PR:优先级仲裁器,多个中断同时发生时,将他与当前正在处理的中断进行比较,挑选出优先级更高的中断
  6. ISR:中断服务器,当某个中断正在被处理的时候,该中断就被保存在这个寄存器中

所有的寄存器都是8位的.

当8259A接收到一个中断后:

当某个外设发出一个中断信号是,有序主板上已经将信号通路指向8259A芯片的某个IRQ接口,所以该中断被送入8259A.8259A首先检查IMR寄存器是否已经屏蔽了该IRQ接口的中断,IMR寄存器中的位为1,表示屏蔽,直接丢弃该次中断,0表示放行.当中断放行是,IRQ接口所在的IRR寄存器中对应位设置位1,表示发生中断.IRQ接口的接口号越小,中断优先级越大.PR从IRR中挑选一个优先级最大的中断.然后8259A通过INT接口想CPU发送INTR信号,吸纳好被送入cpu的INTR接口后,cpu就知道有新的中断来了,然后通过自己的INTA接口向8259A的INTA回复一个中断响应号,8259A收到信号后,将挑选出优先级最大的中断在ISR寄存器对应的位设置位1,表示正在处理该终端,同时从IRR中置位0,之后cpu再次发送INTA信号给8259A,表示要获取中断向量好.8259A通过数据总线发送给cpu,cpu拿到以后,就用它在中断向量表或是中断描述符表中索引,找到相应的中断处理程序执行.

如果8259A的EOI通知设为手动模式,那么中断处理程序结束后必须想8259A发送EOI的代码,8259A收到后将ISR寄存器中对应的位设置位0,如果设置位自动模式,那么在接收到cpu要求中断向量号的信号后,8259A自动将ISR中的位设置位0.

1.3 8259A编程

8259A成为可编程中断控制器,说明他的工作方式很多,需要他进行设置.也就是对他进行初始化,设置为基连的方式,指定中断向量号,以及工作模式.

中断向量号是楼机上的东西,物理上他是8259A的IRQ接口号,8259A上的IRQ接口号排列顺序是固定的.但是对应的中断向量号不是固定的,是由硬件到软件的映射,通过对8259A进行设置,可以将IRQ接口映射到不同的中断向量号.

8259A内由两组寄存器,一组是用来初始化的,用来保存初始化命令字ICW1~ICW4,一共4组.另一组寄存器是操作命令寄存器,用来保存操作命令字,OCW1~OCW3一共三组.

对ICW初始化,用来设置是否使用级联,设置其实中断向量号,设置中断结束模式.某些设置之间可能相互关联,因此需要一次写入ICW1~4

对OCW的初始化,来操作8259A,就是中断屏蔽和中断结束.OCW的发送顺序不固定

ICW1用来初始化8259A的连接方式和中断信号的触发方式.连接方式是指单片工作还是多片的级联工作.触发方式是指中断请求信号是水平触发还是边缘触发.ICW1需要写入到主片的0x20和葱片的0xA0端口:

  1. IC4为1表示在后面是否要写入ICW4
  2. SNGL为1表示单片0还是级联1
  3. ADI用来设置8085的调用时间间隔
  4. LTIM设置中断的检测方式,0表示边缘触发,1表示水平触发
  5. 4位为1,固定的
  6. 其余位为0

ICW2用于设置其实中断向量号,,就是前面的硬件IRQ接口到逻辑中断向量号的映射.,ICW2写到主片的0x21端口和葱片的0xA1端口.只需要设置IRQ0中断向量号,其他的是顺序向下排列的.只需要填写高5位的T3~T7.高5位其实表示该8259A芯片的序号,低3位则表示8个向量号

ICW3在级联模式下需要.且主片和从片有自己不同的结构,主片ICW3中设置1的哪一位,对应IRQ接口用与链接从片,为0则表示外部设备.从片ICW3不需要指定那个IRQ与主片相连,因为他有一个专门的线.从片上设置的是主片上与自己相连的那个IRQ号.当中断相应是,主片发送与从片做基连的IRQ号,所有从片都收到该号,然后与自己的低3位对比,如果一直,那么表示是发给字节的(低3位表示主片只有8个引脚).

ICW4有些低位选项基于高位

  1. SFNM,特殊全嵌套模式,0表示开启
  2. BUF,是否工作在缓冲模式下,当多个8259A级联的时候,当工作在缓冲模式下,M/S用来规定是主片还是从片,1表示主片.当非缓冲模式时,该位无效
  3. AEOI表示自动结束中断,0表示非自动模式,1表示自动模式
  4. uPM.兼容捞处理器,0表示8080或是8085,1表示x86

OCW1用来屏蔽在8259A上的外部设备的中断信号,实际上就是把OCW1写入IMR寄存器,OCW1写入主片0x21,或从片0xA1

M0~M7对应IRQ1~7.设置为1标识屏蔽.

OCW2用来设置中断结束方式和优先级模式.写入主片的0x20和从片的0xA0

OCW2配置复杂,各个属性位要配合在一起,组合出8259A的各种工作模式

跳过了

OCW3用来设置特殊屏蔽方式以及查询方式写入写入主片的0x20和从片的0xA0

跳过了

1.4 代码

跳过了,最后直接贴最终的代码

2 中断处理程序

中断发生的时候,都需要进行上下文的保存,这一部分的代码是相同的,因此使用汇编的模板macro,来编写.

使用这种方法来编写,需要记录模板生成的每一个函数的地址.然后将这些地址在内存空间中顺序保存,取第一个函数的地址,作为中断处理函数的地址.

2.1 中断处理函数

中断处理函数使用汇编来编写,是一个模板:

%macro 模板名称 参数个数
    ...
%endmacro 

在使用模板参数的时候,使用%n,编译器,会将其自动替换.

然后模板需要填充的信息为:

模板名称 参数1,参数2,...
模板名称 参数1,参数2,...
模板名称 参数1,参数2,...
...

然后完整的代码为,这里定义了20个函数:

[bits 32]
; %define用于定义文本替换标号,类似于C语言里面常用的宏替换。
; equ用于 对标号赋值,equ可放在程序中间,而%define则只能用于程序开头。
%define ERROR_CODE nop
%define ZERO push 0

; 引用外部函数
extern put_str
extern idt_table

section .data
; 中断处理程序中打印的字符串
intr_str db "interrupt occur!",0xa,0

; 暴露给外部的接口,没有参数
global idt_entry_table
idt_entry_table:
    ; 模板,一共两个参数,第一个参数是中断的编号,第2个参数,根据需要压入一个0
    %macro VECTOR 2
        section .text
        ; %1 表示第一个参数,直接替换
        intr%1entry:
            %2

            push ds
            push es
            push fs
            push gs
            pushad  ; PUSHAD指令压入32位寄存器,其入栈顺序是: EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI

            push %1 ;压栈中断号,作为 idt_handle 的参数

            call [idt_table+%1*4]

            add esp,4   ;跳过参数

            ; 手动模式下,需要主动的向主片和从片发送EOI信号
            mov al,0x20
            out 0xa0,al
            out 0x20,al

            popad
            pop gs
            pop fs
            pop es
            pop ds
            add esp,4 ;跳过 error_code

            iret
        ;这一段,主要是为了获取每个函数的地址
        section .data
            dd intr%1entry
    %endmacro
VECTOR 0x00,ZERO
VECTOR 0x01,ZERO
VECTOR 0x02,ZERO
VECTOR 0x03,ZERO
VECTOR 0x04,ZERO
VECTOR 0x05,ZERO
VECTOR 0x06,ZERO
VECTOR 0x07,ZERO
VECTOR 0x08,ERROR_CODE
VECTOR 0x09,ZERO
VECTOR 0x0a,ERROR_CODE
VECTOR 0x0b,ERROR_CODE
VECTOR 0x0c,ZERO
VECTOR 0x0d,ERROR_CODE
VECTOR 0x0e,ERROR_CODE
VECTOR 0x0f,ZERO
VECTOR 0x10,ZERO
VECTOR 0x11,ERROR_CODE
VECTOR 0x12,ZERO
VECTOR 0x13,ZERO
VECTOR 0x14,ZERO
VECTOR 0x15,ZERO
VECTOR 0x16,ZERO
VECTOR 0x17,ZERO
VECTOR 0x18,ERROR_CODE
VECTOR 0x19,ZERO
VECTOR 0x1a,ERROR_CODE
VECTOR 0x1b,ERROR_CODE
VECTOR 0x1c,ZERO
VECTOR 0x1d,ERROR_CODE
VECTOR 0x1e,ERROR_CODE
VECTOR 0x1f,ZERO
VECTOR 0x20,ZERO

在编译器编译后,text段和data段就分开保存了.因此dd intr%1entry这一段代码,编译结束有以后是在内存地址上紧靠的.并且加上了section .data来保证,这些数据连续.(后面的时候会发生不连续的事情,解决办法只需要将 .data改一个名字就行了)

为了验证,这些数据在内存上连续,首先,给每个地址加上一个标签:

也就是在dd intr%1entry之前加上intr%1addr:

        section .data
            intr%1addr:
            dd intr%1entry

贴出最终的kernel.bin`中的信息:

readelf -a kernel.bin

2.2 安装中断处理程序

首先定义门描述符的结构,然后定义一个数组,存储所有的中断描述符

// 定义门描述符结构.
struct GateDesc
{
    uint16_t func_addr_l; // 低16位是中断处理程序的 0~15位
    uint16_t selector;    // 接下来16位是中断处理程序所在的段的段选择子,因为是平坦模式,因此都是一个段选择子
    uint8_t not_use;      // 没有使用,直接填充为0
    uint8_t attr;         // 都一样,在global中构建号了
    uint16_t func_addr_h; // 最后16位,是中断处理程序的 16~31位
};

#define IDT_DESC_COUNTS 0x21 // 目前总共支持的中断数
// 定义一个数组,他就是将来的中断描述符表
static struct GateDesc idt[IDT_DESC_COUNTS];

然后一个函数用来填充,需要传入attr的原因在于,中断描述符有DPL,不同的中断描述符可能可以被用户进程调用,或者只允许在内核态使用.因此需要传入这个参数.而func_addr就是实际的中断处理函数,也就是在上面使用汇编模板编写的函数idt_entry_table.

static void make_idt_desc(struct GateDesc *desc, uint8_t attr, void *func_addr)
{
    desc->func_addr_l = (uint32_t)func_addr & 0x0000FFFF;
    desc->selector = SELECTOR_CODE;
    desc->not_use = 0; // 没有使用直接为0
    desc->attr = attr;
    desc->func_addr_h = ((uint32_t)func_addr & 0xFFFF0000) >> 16;
}

static void idt_desc_init()
{
    // 循环中,填充每一个中断描述符表中的表项
    for (int i = 0; i < IDT_DESC_COUNTS; i++)
    {
        // IDT_DESC_ATTR_DPL0 在global.h 中构建好了
        make_idt_desc(&idt[i], IDT_DESC_ATTR_DPL0, idt_entry_table[i]);
    }
    put_str("idt_desc_init done \n");
}

主要的就是这些

3 代码

这一部分先添加了很多的代码,首先看一下目录结构:

└── bochs
├── 02.tar.gz
├── 03.tar.gz
├── 04.tar.gz
├── 05a.tar.gz
├── 05b.tar.gz
├── 06a.tar.gz
├── 07a.tar.gz
├── 07b
│?? ├── boot
│?? │?? ├── include
│?? │?? │?? └── boot.inc
│?? │?? ├── loader.asm
│?? │?? └── mbr.asm
│?? ├── build
│?? ├── kernel
│?? │?? ├── global.h
│?? │?? ├── idt.asm
│?? │?? ├── init.c
│?? │?? ├── init.h
│?? │?? ├── interrupt.c
│?? │?? ├── interrupt.h
│?? │?? └── main.c
│?? ├── lib
│?? │?? ├── kernel
│?? │?? │?? ├── io.h
│?? │?? │?? ├── print.asm
│?? │?? │?? └── print.h
│?? │?? └── libint.h
│?? └── start.sh
└── hd60m.img

旧的文件,只有main.c文件更改了,其他的文件都没有更改.

首先解释一下,每个新添加的文件的内容:

  1. /lib/io.h :该文件是使用内联汇编编写的,主要封装了in,out操作段的一些代码,在第6章里添加了该文件
  2. /kernel/global.h:该文件主要用于保存以后kernel所有用到的一些宏和常量
  3. /kernel/idt.asm:该文件中主要用于构建了33个中断处理程序,还有一个由这些中断处理程序的地址组成的数组.
  4. /kernel/interupt.h,/kernel/interrupt.c:这两个文件,主要是用于构建中断描述符表,设置8259A,加载数据到idtr寄存器中.对外提供一个总的idt_init()函数用于,初始化和中断相关的事情
  5. /kernel/init.h,/kernel/init.c:中主要是一个集合,用于调用所有和初始化相关的函数.

以后,每添加一个功能,就在kernel文件夹中,添加文件,该文件向外暴露一个XXX_init()函数,然后由init.c文件夹中的init_all()函数调用,而main.c函数中则调用init_all()

3.1 global.h(新加)

该文件暂时,定义了4个段选择子,然后还有和中断描述符有关的宏,

#ifndef _ERNEL_GLOBAL_H
#define _ERNEL_GLOBAL_H
#include "libint.h"

// -------------------- 段选择子 --------------------
#define  RPL0  0
#define  RPL1  1
#define  RPL2  2
#define  RPL3  3

#define TI_GDT 0
#define TI_LDT 1

// 这是在保护模式下,c语言时候,使用的选择子
#define SELECTOR_CODE      ((1 << 3) + (TI_GDT << 2) + RPL0)
#define SELECTOR_DATA      ((2 << 3) + (TI_GDT << 2) + RPL0)
#define SELECTOR_STACK   SELECTOR_DATA
#define SELECTOR_GS    ((3 << 3) + (TI_GDT << 2) + RPL0)
// -------------------- 段选择子 --------------------

// -------------------- IDT --------------------
// 只定义了门描述符中,属性部分,就是p位,s位,type位
// 因为其他的位,是使用c语言动态补全的
#define  IDT_DESC_P  1
#define  IDT_DESC_DPL0   0
#define  IDT_DESC_DPL3   3
#define  IDT_DESC_32_TYPE     0xE
#define  IDT_DESC_16_TYPE     0x6
#define  IDT_DESC_ATTR_DPL0  ((IDT_DESC_P << 7) + (IDT_DESC_DPL0 << 5) + IDT_DESC_32_TYPE)
#define  IDT_DESC_ATTR_DPL3  ((IDT_DESC_P << 7) + (IDT_DESC_DPL3 << 5) + IDT_DESC_32_TYPE)
// -------------------- IDT --------------------

#endif

3.2 idt.asm(新加)

首先我们构建中断描述符表,构建前32个中断处理程序,都是打印一个字符串.因此在该文件中需要使用put_str函数,但是又不能引入头文件(因为中断处理程序使用汇编编写),所以使用extern引用外部符号.

然后,因为每个中断号,cpu可能会压入一个错误代码,也可能是不压入,所以需要处理这个错误代码,需要主动的跳过.biao

再然后如果开启的是手动模式,就需要在中断处理程序中显示的发送EOI信号.

再者,因为要手写全部的32个中断处理信号过于麻烦,所以使用模板的方式:

%macro 模板名字 参数个数

%endmacro
模板名字 参数1,参数2
模板名字 参数1,参数2
模板名字 参数1,参数2
模板名字 参数1,参数2
...
模板名字 参数1,参数2

当在模板中使用参数的时候,就使用%n数字,取对应的参数,直接在模板中替换

因此,对于那些cpu不压入参数的中断,统一的在中断处理程序的一开始压入一个0,然后在最后的时候,再统一的esp-4

所以最终的代码为:

[bits 32]
; %define用于定义文本替换标号,类似于C语言里面常用的宏替换。
; equ用于 对标号赋值,equ可放在程序中间,而%define则只能用于程序开头。
%define ERROR_CODE nop
%define ZERO push 0

; 引用外部函数
extern put_str

section .data

; 中断处理程序中打印的字符串
intr_str db "interrupt occur!",0xa,0

; 暴露给外部的接口,没有参数
global intr_entry_table
intr_entry_table:
    ; 模板,一共两个参数,第一个参数是中断的编号,第2个参数,根据需要压入一个0
    %macro VECTOR 2
        section .text
        ; %1 表示第一个参数,直接替换
        intr%1entry:
            push ds
            push es
            push fs
            push gs
            pushad

            %2
            push intr_str
            call put_str
            add esp,4

            ; 手动模式下,需要主动的向主片和从片发送EOI信号
            mov al,0x20
            out 0xa0,al
            out 0x20,al

            popad
            pop gs
            pop fs
            pop es
            pop ds

            add esp,4
            iret
        section .data
            dd intr%1entry
    %endmacro
VECTOR 0x00,ZERO
VECTOR 0x01,ZERO
VECTOR 0x02,ZERO
VECTOR 0x03,ZERO
VECTOR 0x04,ZERO
VECTOR 0x05,ZERO
VECTOR 0x06,ZERO
VECTOR 0x07,ZERO
VECTOR 0x08,ERROR_CODE
VECTOR 0x09,ZERO
VECTOR 0x0a,ERROR_CODE
VECTOR 0x0b,ERROR_CODE
VECTOR 0x0c,ZERO
VECTOR 0x0d,ERROR_CODE
VECTOR 0x0e,ERROR_CODE
VECTOR 0x0f,ZERO
VECTOR 0x10,ZERO
VECTOR 0x11,ERROR_CODE
VECTOR 0x12,ZERO
VECTOR 0x13,ZERO
VECTOR 0x14,ZERO
VECTOR 0x15,ZERO
VECTOR 0x16,ZERO
VECTOR 0x17,ZERO
VECTOR 0x18,ERROR_CODE
VECTOR 0x19,ZERO
VECTOR 0x1a,ERROR_CODE
VECTOR 0x1b,ERROR_CODE
VECTOR 0x1c,ZERO
VECTOR 0x1d,ERROR_CODE
VECTOR 0x1e,ERROR_CODE
VECTOR 0x1f,ZERO
VECTOR 0x20,ZERO

这里用的代码比较巧妙,首先section .datasection .code在编译后,一定是不在同一个segment内的。而且,section .data的数据会紧凑的靠在一起。而section .data中的数据有两部分:intr_entry_tabledd intr%1entry

因此,最终编译后intr_entry_table后面就会跟着好几个dd intr%1entry这样,就成为一个数组。

3.3 interupt.h/interupt.c(新加)

interupt.h文件主要是暴露了interupt.c中的,那个idt_init()函数,所以很简单:

#ifndef __KERNEL_INTERRUPT_H
#define __KERNEL_INTERRUPT_H
#include "libint.h"
void idt_init(void);
#endif

interupt.c文件的主要内容:

  1. 定义门描述符的结构GateDesc,然后用它定义一个数组static struct GateDesc idt[IDT_DESC_COUNTS],其地址作为中断描述符表.
  2. 根据idt.asm构建好的intr_entry_table,去填充完整的idt,也就是最终的中断描述符表.
  3. 初始化8259A
#include "interrupt.h"
#include "libint.h"
#include "global.h"
#include "io.h"
#include "print.h"

// 和8259A 设置相关的端口
#define PIC_M_CTRL 0x20 // 这里用的可编程中断控制器是8259A,主片的控制端口是0x20
#define PIC_M_DATA 0x21 // 主片的数据端口是0x21
#define PIC_S_CTRL 0xa0 // 从片的控制端口是0xa0
#define PIC_S_DATA 0xa1 // 从片的数据端口是0xa1

#define IDT_DESC_COUNTS 0x21 // 目前总共支持的中断数

// 引用 idt.asm 中构建的那个数组.这个数组中保存了所有33个中断处理程序的地址
extern void *intr_entry_table[IDT_DESC_COUNTS];

// 定义门描述符结构.
struct GateDesc
{
    uint16_t func_addr_l; // 低16位是中断处理程序的 0~15位
    uint16_t selector;    // 接下来16位是中断处理程序所在的段的段选择子,因为是平坦模式,因此都是一个段选择子
    uint8_t not_use;      // 没有使用,直接填充为0
    uint8_t attr;         // 都一样,在global中构建号了
    uint16_t func_addr_h; // 最后16位,是中断处理程序的 16~31位
};

// 定义一个数组,他就是将来的中断描述符表
static struct GateDesc idt[IDT_DESC_COUNTS];

// 该函数用来填充一个中断描述符表中的表项.
static void make_idt_desc(struct GateDesc *desc, uint8_t attr, void *func_addr)
{
    desc->func_addr_l = (uint32_t)func_addr & 0x0000FFFF;
    desc->selector = SELECTOR_CODE;
    desc->not_use = 0; // 没有使用直接为0
    desc->attr = attr;
    desc->func_addr_h = ((uint32_t)func_addr & 0xFFFF0000) >> 16;
}

static void idt_desc_init()
{
    // 循环中,填充每一个中断描述符表中的表项
    for (int i = 0; i < IDT_DESC_COUNTS; i++)
    {
        // IDT_DESC_ATTR_DPL0 在global.h 中构建好了
        make_idt_desc(&idt[i], IDT_DESC_ATTR_DPL0, intr_entry_table[i]);
    }
    put_str("idt_desc_init done \n");
}

// 初始化 8259A
static void pic_init()
{
    /* 初始化主片 */
    outb(PIC_M_CTRL, 0x11); // ICW1: 边沿触发,级联8259, 需要ICW4.
    outb(PIC_M_DATA, 0x20); // ICW2: 起始中断向量号为0x20,也就是IR[0-7] 为 0x20 ~ 0x27.
    outb(PIC_M_DATA, 0x04); // ICW3: IR2接从片.
    outb(PIC_M_DATA, 0x01); // ICW4: 8086模式, 正常EOI

    /* 初始化从片 */
    outb(PIC_S_CTRL, 0x11); // ICW1: 边沿触发,级联8259, 需要ICW4.
    outb(PIC_S_DATA, 0x28); // ICW2: 起始中断向量号为0x28,也就是IR[8-15] 为 0x28 ~ 0x2F.
    outb(PIC_S_DATA, 0x02); // ICW3: 设置从片连接到主片的IR2引脚
    outb(PIC_S_DATA, 0x01); // ICW4: 8086模式, 正常EOI

    /* 打开主片上IR0,也就是目前只接受时钟产生的中断 */
    outb(PIC_M_DATA, 0xfe);
    outb(PIC_S_DATA, 0xff);

    put_str("pic_init done\n");
}

// 一个总的函数,调用以上的两个初始化函数.并加载idtr
void idt_init()
{
    put_str("idt_init start \n");
    idt_desc_init();
    pic_init();

    // 低 16位是界限,界限是idt的长度-1,高16位是所在地址.都没问题
    uint64_t idtr = ((sizeof(idt) - 1) | ((uint64_t)(uint32_t)idt << 16));
    // 然后加载 iidtr
    asm volatile("lidt %0"
                 :
                 : "m"(idtr));
}

3.4 /kernel/init.h,/kernel/init.c(新加)

/kernel/init.h 头文件暴露接口而已:

#ifndef __KERNEL_INIT_H
#define __KERNEL_INIT_H
void init_all(void);
#endif

/kernel/init.c文件页是很简单的,就是调用各个文件暴露出来的那个xxx_init()而已:

#include "init.h"
#include "print.h"
#include "interrupt.h"

/*负责初始化所有模块 */
void init_all() {
   put_str("init_all\n");
   idt_init();   //初始化中断
}

3.5 main.c

调用/kernel/init.h文件中的init_all()

并且打开中断:

#include "print.h"
#include "init.h"
int main(int argc, char const *argv[])
{
    set_cursor(880);
    put_char('k');
    put_char('e');
    put_char('r');
    put_char('n');
    put_char('e');
    put_char('l');
    put_char('\n');
    put_char('\r');
    put_char('1');
    put_char('2');
    put_char('\b');
    put_char('3');
    put_str("\n put_char\n");

    // 初始化
    init_all(); 

    put_str("interrupt on\n");
    asm volatile("sti");          // 开中断

//  asm volatile("cli");          //关中断

    while (1)
    {
    }

    return 0;
}

3.6 start.sh

就是新添加了几个文件的编译,还有最终链接的时候要加上链接的文件.

#! /bin/bash

# 编译mbr.asm
echo "----------nasm starts----------"

if !(nasm -o mbr.bin ./boot/mbr.asm -I ./boot/include/);then
    echo "nasm error"
    exit
fi

# 刻录mbr.bin
echo "----------dd starts  ----------"
if !(dd if=./mbr.bin of=./hd60m.img bs=512 count=1 conv=notrunc);then
    echo "dd error"
    exit
fi

# 编译 loader.asm
echo "----------nasm starts----------"

if !(nasm -o loader.bin boot/loader.asm -I ./boot/include/);then
    echo "nasm error"
    exit
fi

# 刻录loader.bin
echo "----------dd starts  ----------"
if !(dd if=./loader.bin of=./hd60m.img bs=512 count=4 seek=2 conv=notrunc);then
    echo "dd error"
    exit
fi

# 编译 print.asm
echo "----------nasm print----------"
if !(nasm -f elf -o print.o ./lib/kernel/print.asm -I ./boot/include/ -I ./lib);then
    echo "nasm error"
    exit
fi

# 编译 interrupt.c
echo "----------nasm interrupt----------"
if !(gcc -o interrupt.o -m32 -fno-stack-protector -c ./kernel/interrupt.c  -I ./lib -I ./lib/kernel);then
    echo "nasm error"
    exit
fi

# 编译 init.c
echo "----------nasm init----------"
if !(gcc -o init.o -m32 -c ./kernel/init.c  -I ./lib -I ./lib/kernel);then
    echo "nasm error"
    exit
fi

# 编译 idt.asm
echo "----------nasm idt----------"
if !(nasm -f elf -o idt.o ./kernel/idt.asm -I ./boot/include/ -I ./lib);then
    echo "nasm error"
    exit
fi

# 编译内核
echo "----------gcc -c kernel.bin  ----------"
if !(gcc -o kernel.o -m32 -c ./kernel/main.c  -I ./lib -I ./lib/kernel);then
    echo "dd error"
    exit
fi
# 链接
echo "----------ld  kernel starts  ----------"
if !(ld -Ttext 0xc0001500 -m elf_i386 -e main -o kernel.bin ./kernel.o ./idt.o ./print.o ./init.o ./interrupt.o);then
    echo "dd error"
    exit
fi

# 刻录 kernel.bin
echo "----------dd starts  ----------"
if !(dd if=./kernel.bin of=./hd60m.img bs=512 count=40 seek=9 conv=notrunc);then
    echo "dd error"
    exit
fi

# 删除临时文件
sleep 1s
rm -rf mbr.bin
rm -rf loader.bin
rm -rf kernel.bin
rm -rf kernel.o
rm -rf print.o
rm -rf idt.o
rm -rf init.o
rm -rf interrupt.o

# 运行bochs
bochs

另外,要注意:

在编译interrupt.c的时候,加上了选项-fno-stack-protector,是因为,该文件中的asm volatile("lidt %0": : "m"(idtr));造成了:

因此要加上这个选项相关资料

3.7 运行

下面是运行的结果.开中断以后不停地打印字符串.

4 改进

现在的中断例程中调用函数都一样,是使用汇编编写的,以后中断例程中调用函数需要用c语言编写.

因此,我们在interrupt.c中新添加一个函数idt_hander(uint8_t vec)用于接受一个中断号,然后内部根据中断号,执行不同的中断程序.

然后,新建一个数组idt_table,这个数组长为IDT_DESC_COUNTS,元素位void*,然后里面值都是统一的:是idt_hander(uint8_t vec)的地址.(以后可能会根据需要,不同元素赋不同的函数地址)

再然后,idt.asm中不再call put_str而是,call [idt_table+%1*4],%1就是中断号,乘4就是对应中断号的处理程序.

4.1 interrupt.h/interrupt.c

在interrupt.h/interrupt.c中加入开关中断的函数,以后开关中断就不用使用内联汇编.同时加上,能够获取当前中断开关状态的函数.

首先需要定一个enum枚举两种开关状态.然后一个获取当前状态的函数,主要是获取eflags寄存器的值,然后判断第10位是否是1.然后返回是否开关中断了.然后开关中断的函数,先获取当前状态,再进行开关.返回之前的状态.至于为什么要这样做,可能后面会用到吧.暂时不清楚.

当然最主要的,还是加上让idt.asm使用的中断例程中调用函数地址的数组.构建该数组.

#ifndef __KERNEL_INTERRUPT_H
#define __KERNEL_INTERRUPT_H
#include "libint.h"

void idt_init( void );

/* 定义中断的两种状态:
 * INTR_OFF值为0,表示关中断,
 * INTR_ON值为1,表示开中断 */
enum IntrStatus
{              // 中断状态
    INTR_OFF,  // 中断关闭
    INTR_ON    // 中断打开
};

enum IntrStatus intr_get_status( void );
enum IntrStatus intr_set_status( enum IntrStatus );
enum IntrStatus intr_enable( void );
enum IntrStatus intr_disable( void );
void            register_handler( uint8_t vector_no, void* function );

#endif

interrupt.h新添加的代码主要是为了,暴露开关中断的函数.

而,interrupt.c中,新代码1,新代码5,是添加开关中断的部分.

其他的新代码则是为了构建一个中断例程中调用函数地址的数组.

#include "interrupt.h"
#include "libint.h"
#include "global.h"
#include "io.h"
#include "print.h"

// 和8259A 设置相关的端口
#define PIC_M_CTRL 0x20 // 这里用的可编程中断控制器是8259A,主片的控制端口是0x20
#define PIC_M_DATA 0x21 // 主片的数据端口是0x21
#define PIC_S_CTRL 0xa0 // 从片的控制端口是0xa0
#define PIC_S_DATA 0xa1 // 从片的数据端口是0xa1

#define IDT_DESC_COUNTS 0x21 // 目前总共支持的中断数

// 用于获取 eflags 寄存器中内容的宏,本身很简单:
// EFLAG_VAR 是个用来存放 eflags 变量,使用寄存器传参.
// 然后先 pushfl ,压栈eflags,然后 popl 到 EFLAG_VAR 所在的寄存器
// 那么EFLAG_VAR 中就是 eflags 的值了
#define GET_EFLAGS(EFLAG_VAR) asm volatile("pushfl; popl %0" \
                                           : "=g"(EFLAG_VAR))
//  这个宏只是避免使用魔数而已,使用 EFLAGS_IF 表示第10位为1,也就是if位为1
#define EFLAGS_IF 0x00000200

// 引用 idt.asm 中构建的那个数组.这个数组中保存了所有33个中断处理程序的地址
extern void *idt_entry_table[IDT_DESC_COUNTS];

// 定义门描述符结构.
struct GateDesc
{
    uint16_t func_addr_l; // 低16位是中断处理程序的 0~15位
    uint16_t selector;    // 接下来16位是中断处理程序所在的段的段选择子,因为是平坦模式,因此都是一个段选择子
    uint8_t not_use;      // 没有使用,直接填充为0
    uint8_t attr;         // 都一样,在global中构建号了
    uint16_t func_addr_h; // 最后16位,是中断处理程序的 16~31位
};

// 定义一个数组,他就是将来的中断描述符表
static struct GateDesc idt[IDT_DESC_COUNTS];

// 用来存储每一个中断例程中调用函数的地址,暂时全都是 idt_handle
void *idt_table[IDT_DESC_COUNTS];
// 用来存储每一个中断例程中调用函数的名字,在idt_handle中打印一下而已
char *idt_name[IDT_DESC_COUNTS];

// 该函数用来填充一个中断描述符表中的表项.
static void make_idt_desc(struct GateDesc *desc, uint8_t attr, void *func_addr)
{
    desc->func_addr_l = (uint32_t)func_addr & 0x0000FFFF;
    desc->selector = SELECTOR_CODE;
    desc->not_use = 0; // 没有使用直接为0
    desc->attr = attr;
    desc->func_addr_h = ((uint32_t)func_addr & 0xFFFF0000) >> 16;
}

static void idt_desc_init()
{
    // 循环中,填充每一个中断描述符表中的表项
    for (int i = 0; i < IDT_DESC_COUNTS; i++)
    {
        // IDT_DESC_ATTR_DPL0 在global.h 中构建好了
        make_idt_desc(&idt[i], IDT_DESC_ATTR_DPL0, idt_entry_table[i]);
    }
    put_str("idt_desc_init done \n");
}

// 初始化 8259A
static void pic_init()
{
    /* 初始化主片 */
    outb(PIC_M_CTRL, 0x11); // ICW1: 边沿触发,级联8259, 需要ICW4.
    outb(PIC_M_DATA, 0x20); // ICW2: 起始中断向量号为0x20,也就是IR[0-7] 为 0x20 ~ 0x27.
    outb(PIC_M_DATA, 0x04); // ICW3: IR2接从片.
    outb(PIC_M_DATA, 0x01); // ICW4: 8086模式, 正常EOI

    /* 初始化从片 */
    outb(PIC_S_CTRL, 0x11); // ICW1: 边沿触发,级联8259, 需要ICW4.
    outb(PIC_S_DATA, 0x28); // ICW2: 起始中断向量号为0x28,也就是IR[8-15] 为 0x28 ~ 0x2F.
    outb(PIC_S_DATA, 0x02); // ICW3: 设置从片连接到主片的IR2引脚
    outb(PIC_S_DATA, 0x01); // ICW4: 8086模式, 正常EOI

    /* 打开主片上IR0,也就是目前只接受时钟产生的中断 */
    outb(PIC_M_DATA, 0xfe);
    outb(PIC_S_DATA, 0xff);

    put_str("pic_init done\n");
}

//
static void idt_handle(uint8_t vec)
{
    // 这里是处理伪中断的,不清楚是什么....
    if (vec == 0x27 || vec == 0x2f)
    {
        return;
    }
    // 目前只是简单的打印一下和该中断号相关的信息
    put_str("int vector: 0x");
    put_char(':');
    put_str(idt_name[vec]);
    put_char('\n');
}

// 初始化 idt_table
static void exception_init()
{
    for (int i = 0; i < IDT_DESC_COUNTS; i++)
    {
        // 都设置位一个值.
        idt_table[i] = idt_handle;
        idt_name[i] = "unknow";
    }
    idt_name[0] = " 0:#DE Divide Error";
    idt_name[1] = " 1:#DB Debug Exception";
    idt_name[2] = " 2:NMI Interrupt";
    idt_name[3] = " 3:BP Breakpoint Exception";
    idt_name[4] = " 4:#OF Overflow Exception";
    idt_name[5] = " 5:#BR BOUND Range Exceeded Exception";
    idt_name[6] = " 6:#UD Invalid Opcode Exception";
    idt_name[7] = " 7:#NM Device Not Available Exception";
    idt_name[8] = " 8:#DF Double Fault Exception";
    idt_name[9] = " 9:Coprocessor Segment Overrun";
    idt_name[10] = "10:#TS Invalid TSS Exception";
    idt_name[11] = "11:#NP Segment Not Present";
    idt_name[12] = "12:#SS Stack Fault Exception";
    idt_name[13] = "13:#GP General Protection Exception";
    idt_name[14] = "14:#PF Page-Fault Exception";
    // idt_name[15] 第15项是intel保留项,未使用
    idt_name[16] = "16:#MF x87 FPU Floating-Point Error";
    idt_name[17] = "17:#AC Alignment Check Exception";
    idt_name[18] = "18:#MC Machine-Check Exception";
    idt_name[19] = "19:#XF SIMD Floating-Point Exception";
    idt_name[32] = "32:timer";
}

// 一个总的函数,调用以上的两个初始化函数.并加载idtr
void idt_init()
{
    put_str("idt_init start \n");
    idt_desc_init();
    exception_init();

    pic_init();

    // 低 16位是界限,界限是idt的长度-1,高16位是所在地址.都没问题
    uint64_t idtr = ((sizeof(idt) - 1) | ((uint64_t)(uint32_t)idt << 16));
    // 然后加载 iidtr
    asm volatile("lidt %0"
                 :
                 : "m"(idtr));
}

enum intr_status intr_enable()
{
    enum intr_status old_status;
    if (INTR_ON == intr_get_status())
    {
        old_status = INTR_ON;
        return old_status;
    }
    else
    {
        old_status = INTR_OFF;
        asm volatile("sti"); // 开中断,sti指令将IF位置1
        return old_status;
    }
}

/* 关中断,并且返回关中断前的状态 */
enum intr_status intr_disable()
{
    enum intr_status old_status;
    if (INTR_ON == intr_get_status())
    {
        old_status = INTR_ON;
        asm volatile("cli"::: "memory");
        return old_status;
    }
    else
    {
        old_status = INTR_OFF;
        return old_status;
    }
}

/* 将中断状态设置为status */
enum intr_status intr_set_status(enum intr_status status)
{
    return status & INTR_ON ? intr_enable() : intr_disable();
}

/* 获取当前中断状态 */
enum intr_status intr_get_status()
{
    uint32_t eflags = 0;
    GET_EFLAGS(eflags);
    return (EFLAGS_IF & eflags) ? INTR_ON : INTR_OFF;
}

// 为指定的中断,设置一个新的中断处理函数
void register_handler( uint8_t vector_no, void* function )
{
    idt_table[ vector_no ] = function;
}

4.2 idt.asm

这里面相对简单,就是,先extern idt_table,然后,在中断例程中调用函数的代码中,压栈中断号push %1,call [idt_table+%1*4]

也就是改变的是,不在去put_str,而是去调用idt_table中的中断例程中调用函数.配合模板macro可以构建出33个不同的函数例程.

这里要区分,中断例程中调用函数和中断例程:中断例程在中断描述符表中的,而中断例程中调用的函数,则是,恩,他的字面意思.

[bits 32]
; %define用于定义文本替换标号,类似于C语言里面常用的宏替换。
; equ用于 对标号赋值,equ可放在程序中间,而%define则只能用于程序开头。
%define ERROR_CODE nop
%define ZERO push 0

; 引用外部函数
extern put_str
extern idt_table

section .data
; 中断处理程序中打印的字符串
intr_str db "interrupt occur!",0xa,0

; 暴露给外部的接口,没有参数
global idt_entry_table
idt_entry_table:
    ; 模板,一共两个参数,第一个参数是中断的编号,第2个参数,根据需要压入一个0
    %macro VECTOR 2
        section .text
        ; %1 表示第一个参数,直接替换
        intr%1entry:
            %2

            push ds
            push es
            push fs
            push gs
            pushad  ; PUSHAD指令压入32位寄存器,其入栈顺序是: EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI

            push %1 ;压栈中断号,作为 idt_handle 的参数

            call [idt_table+%1*4]

            add esp,4   ;跳过参数

            ; 手动模式下,需要主动的向主片和从片发送EOI信号
            mov al,0x20
            out 0xa0,al
            out 0x20,al

            popad
            pop gs
            pop fs
            pop es
            pop ds
            add esp,4 ;跳过 error_code

            iret
        section .data
            dd intr%1entry
    %endmacro
VECTOR 0x00,ZERO
VECTOR 0x01,ZERO
VECTOR 0x02,ZERO
VECTOR 0x03,ZERO
VECTOR 0x04,ZERO
VECTOR 0x05,ZERO
VECTOR 0x06,ZERO
VECTOR 0x07,ZERO
VECTOR 0x08,ERROR_CODE
VECTOR 0x09,ZERO
VECTOR 0x0a,ERROR_CODE
VECTOR 0x0b,ERROR_CODE
VECTOR 0x0c,ZERO
VECTOR 0x0d,ERROR_CODE
VECTOR 0x0e,ERROR_CODE
VECTOR 0x0f,ZERO
VECTOR 0x10,ZERO
VECTOR 0x11,ERROR_CODE
VECTOR 0x12,ZERO
VECTOR 0x13,ZERO
VECTOR 0x14,ZERO
VECTOR 0x15,ZERO
VECTOR 0x16,ZERO
VECTOR 0x17,ZERO
VECTOR 0x18,ERROR_CODE
VECTOR 0x19,ZERO
VECTOR 0x1a,ERROR_CODE
VECTOR 0x1b,ERROR_CODE
VECTOR 0x1c,ZERO
VECTOR 0x1d,ERROR_CODE
VECTOR 0x1e,ERROR_CODE
VECTOR 0x1f,ZERO
VECTOR 0x20,ZERO

4.3 main.c

改变main.c中开中断的方式:

#include "console.h"
#include "debug.h"
#include "init.h"
#include "interrupt.h"
#include "memory.h"
#include "print.h"
#include "process.h"
#include "thread.h"

// 这里一定要先声明,后面定义
// 不然会出错,我也不知道为啥,应该是因为改变了地址?
// 就是在ld中
void k_thread_a( void* );
void k_thread_b( void* );
void u_prog_a( void );
void u_prog_b( void );
int  test_var_a = 0, test_var_b = 0;

int main( int argc, char const* argv[] )
{
    set_cursor( 880 );
    put_char( 'k' );
    put_char( 'e' );
    put_char( 'r' );
    put_char( 'n' );
    put_char( 'e' );
    put_char( 'l' );
    put_char( '\n' );
    put_char( '\r' );
    put_char( '1' );
    put_char( '2' );
    put_char( '\b' );
    put_char( '3' );
    put_str( "\n put_char\n" );

    init_all();

    put_str( "interrupt on\n" );

    void* addr = get_kernel_pages( 3 );
    put_str( "\n get_kernel_page start vaddr is " );
    put_int( ( uint32_t )addr );
    put_str( "\n" );

    // 改变执行流
    thread_start( "k_thread_b", 8, k_thread_b, "argB " );
    thread_start( "k_thread_a", 31, k_thread_a, "argA1 " );

    process_execute( u_prog_a, "user_prog_a" );

    process_execute( u_prog_b, "user_prog_b" );

    put_str( "\n start" );

    BREAK();

    intr_enable();  // 打开中断,使时钟中断起作用

    while ( 1 )
    {
    }
    return 0;
}

void k_thread_a( void* arg )
{
    char* para = arg;

    while ( 1 )
    {
        console_put_str( para );
    }
}

void k_thread_b( void* arg )
{
    char* para = arg;

    while ( 1 )
    {
        console_put_str( para );
    }
}

/* 测试用户进程 */
void u_prog_a( void )
{
    while ( 1 )
    {
        console_put_str( "a " );
    }
}

/* 测试用户进程 */
void u_prog_b( void )
{
    while ( 1 )
    {
        console_put_str( "b " );
    }
}

4.4 运行

也没什么,就是根据不同的中断打印不同的信息而已,在现在的情况下,能不断产生的中断就只有32号中断,是时钟中断

4 处理器处理中断的完整过程

4.1 用到的调试指令

首先了解几条bochs的调试指令:

b :打断点,在执行到该物理内存的指令的时候,停止.注意是物理内存

show int:让bochs在发生中断的时候,打印中断相关的信息,包括:中断发生前执行了多少指令,也就是指令数时间戳,终端类型:

sb 数字:表示在执行指定数字条指令后停止

sba 数字表示从bochs开始运行到执行执行数字条指令后停止

4.2 思路

总的思路就是我们要捕捉在进入内核运行以后,捕捉一次外部中断,然后跟踪查看堆栈以及cpu的运行.

因为内核是加载在0xc0001500这是虚拟地址,其物理地址是0x1500,因此首先在0x1500处打断点,c让程序执行到此处的时候停止,然后show int显示中断,然后c,找到一次外部中断,查看其执行的总的指令数.

然后重新开始,直接调到该值数之前,再单步跟踪,这一次外部中断.

4.3 开始

因为不涉及到特权级的转移,因此中断时候压栈,没有ssesp,返回的时候也同样.

4.3.1 找到第一个时钟中断

1首先b 0x1500在物理地址0x1500处打上断点,然后c开始执行.当执行停止的时候表示刚要进入刚要进入内核执行,但是还没执行

然后show int打开显示中断信息,并c开始执行.

当中断发生的时候ctrl+c停止,并找到exception外部中断,因为中断发生的很快,所以几乎c回车以后,就要立马ctrl+c:

记录第一个时钟中断发生时候执行的指令数:18241108,和第一个时钟中断要退出的时候iretd时候执行的指令数:18242429

4.3.2 执行到第一个时钟中断之前

然后q关闭程序,重新打开bochs,这次直接定位到sba 18241100,因为在18241108之后就要发生中断了啊,所以要在这之前停下.然后c

然后观察,在18241100~18241107条指令的时候执行的都是jmp -2,这也就是main()while (1)编译的结果.

并且,下一条,如果不发生中断,那么下一条指令任然是jmp -2

4.3.3 查看中断之前的寄存器和栈

接下来,查看通用寄存器r,查看段寄存器sreg,查看栈中信息print-stack:

4.3.4 发生中断

紧接着s执行一条指令:

提示发生中断,此时cpu已经完成了硬件部分需要的压栈,以及中断处理程序中的第一条指令.

也就是idt.asm中模板的第一条指令%2.时钟中断没有errorcode,因此,被替换为push 0,紧接着下一条指令就是push ds

4.3.5 中断发生后的寄存器和栈

然后查看通用寄存器r,查看段寄存器sreg,查看栈中信息print-stack:

4.3.6 找到中断返回的指令

然后sba 18242420 ,c运行.到中断返回前夕.

然后单步s执行到,下一条指令是iret为止:

4.6.4 中断返回前的栈和寄存器

从上到下依次是:eip,cs,eflags,栈中的信息,会在iretd执行的时候,被cpu自动的将对应的数据,pop到对应的寄存器中.

然后,查看一下寄存器的值

4.6.5 中断返回后的栈和寄存器

中断返回之前的栈中的信息,被弹出到eip,cs,eflags中.

原文地址:https://www.cnblogs.com/perfy576/p/9139111.html

时间: 2024-11-08 23:46:32

第七章中 中断和8259A的相关文章

第七章 中斷和中斷處理

1. 異常和中斷 異常:必須考慮與處理器時鐘同步,由軟件產生,亦稱爲同步中斷.如除零異常和缺頁異常 中斷:由硬件產生的異步中斷 2. 中斷處理程序 中斷處理程序是被內核調用來響應中斷的,運行與中斷上文.中斷上下文又稱原子上下文,該上下文的執行代碼不可阻塞. 最起碼,中斷處理程序要負責通知硬件設備中斷已被接受:嗨,硬件,我聽到你了,現在回去工作吧! 3. 上半部和下半部 中斷處理程序是上半部(top half)——接收到一個中斷,它就立即開始執行,但只做有嚴格時限的工作,例如對接受的中斷進行應答或

再读大道之简第七章第八章

有一句话叫做,理想很丰满,现实很骨感.原来,单纯的以为,软件工程不就是码农么,就连工作也是一心趴在课编程编程,各种编程上,可是,现实中的软件工程和理想中或者说,想象中的还是有很大的差距的.就连我们心中的大企业,也并不是想象中的那样.比如IBM知道把握力量总之比创造力量来得经济.我还单纯的以为,所有的公司只是为了盈利呢,依靠完成的软件去盈利.此时不禁自嘲一番,还是太嫩了啊.所有的大公司在标准.理论.语言上的争来夺取,未必全然出于“软件实现”的考虑.对统一理论.统一工具.统一过程的企图,其最终目的是

《大道至简》第七章第八章读后感

第七章中提到“王不如远交而近攻,得寸则王之寸,得尺亦王之尺也.”这句话出自<战国策.秦策三>,翻译过来就是“大王您还不如和远的国家结盟,而进攻近距离的国家,这样得到的一寸土地是大王您的,得到一尺土地也是大王您的一尺.”这是一种十分正确的外交和军事的策略,是和远方的国家结盟,和相邻的国家为敌.这样既可以防止邻国肘腋之变,又能使敌国两面受敌,无法与我方抗衡.这在当今的软件市场也是一样的,各个大企业之间不断并购,对标准.理论.语言上的争夺,未必也就是出于对“软件实现的考虑”.其最终的实质不过也就是一

软件工程 六、七章读书笔记

第六章 在第六章中主要是介绍了Scrum的方法论,在此方法的理论中,其原则主要强调了一个团队的互动互助的开发过程,重点强调了在一个项目里,一个团队是如何通过沟通产生进步,且这个沟通绝不是说有通信便可称之为“沟通”了,而是要有面对面的实时交流,虽然现在的通讯方式早已变得十分强大,但面对面的沟通仍是最有效率的交流方式,故而在此基础上又提出了一个新的团队合作活动——“每日立会”,这是在Sprint中我认为是十分有效的一个活动,将问题摆在明面上,大家互相了解各自的进度,一起解决项目中的问题,持续更新团队

《大道至简》第七章和第八章读后感

第七章中讲到各个大公司之间的竞争,正所谓“物竞天择,适者生存”:总有一个比较有优势的公司会占尽先机.那么这些公司都会有一个共同的特点,不管自己身处一个什么位置,就是都会考虑自己的利益.软件项目中也同样如此,一个好的项目也会取代另一个项目,我们也会考虑软件本身的利益.什么是一个好的软件项目,那么它就要有区别于其他项目的优势,这就需要我们编程人员有一个好的技术,有一个好的团队,当然也得需要一个好的工具,正如书中所说,软件工程=过程+方法+工具,当然这样也不一定会一定成功,还需要有维持项目的资本,一旦

《信息简史》第七章人物、事件、名言收集小感

吴军老师的<文明之光>一书自上市以来,广受读者的称赞,并入选2014中国好书排行榜. 斯坦福大学物理系的张首晟教授为本书写了一篇名为<大数据时代读大历史--感受物理.科技.人文的跨界之美>的序言(http://blog.sina.com.cn/s/blog_537e497a0101hhgw.html).在这篇序言中,作者将文明简单定义为:平行于生物基因,可以代代相传的一个信息系统.同时,作者认为:文明的主线是能量与信息,帝王将相.英雄豪杰,不过是为能量与信息的交流铺路,有效提高了信

C++_第七章函数的基本知识_求阶乘的子函数_ 函数参数类型为数组_ 求数组内所有元素和、部分元素和的方法_实现了先从键盘输入到一个数组中,再用for循环取读出数组中的元素 for循环也可以用break来结束循环的

/* 第七章函数的基本知识 */ /*01)c++对于返回值有一定的限制:可以是常量.变量.指针.结构对象或表达式,但不可以是数组02)c++返回数组的方法:将数组作为结构会对象组成部分来返回03)函数遇到return则结束该函数04)如果一个函数的两房额参数类型相同,则必须分别制定每个参数的类型,而不能像声明常规变量那样,将声明组合在一起05)*/ //本代码注意double类型的写法以及double和int类型数据的转换 1 #include <iostream> 2 3 void che

第七章 程序是在何种环境中运行的

在这一章中,我知道了应用是必须在一定的环境下才能运行的,其运行环境是操作系统和硬件构成的. 同样类型的硬件可以选择安装多种操作系统,而不同的硬件类型需要不同的操作系统.说起硬件,CPU是特别重要的参数,由于CPU能解释其自身固有的机器语言,所以不同的CPU能解释的机器语言也是不同的.还有就是机器语言的程序称为本地代码,而源代码就是用C语言等编写的程序.(应用软件包收录的是本地代码) 20世纪80年代的MS-DOS操作系统,其每个机型都需要有专门的MS-DOS应用,这是因为应用软件的功能中存在着直

第七章-寻找软件的注册码

我们来寻找软件真正的注册码! 寻找软件的注册码就像你小时玩的躲猫猫一样,简单又有趣,虽然后来你会不这样觉的 好的,我们开始. 我不知道你有没有明白我前面在原理中讲的那些东西,如果没明白,我就再说一遍 软件通过你输入的用户名或者机器码什么的生成一个正确的注册码来与你输入的注册码进行比较,如果两个相同,也就是说你输入的注册码是正确的话,那软件就会完成注册.如果你输入的不正确,嘿嘿,当然就不会注册成功. 好的,现在你已经知道软件会有一个比较两个注册码的过程,这就是关键所在.一般如果你遇到的是那种明码比