`
ogd539kk
  • 浏览: 22087 次
最近访客 更多访客>>
社区版块
存档分类
最新评论

从头开始编写操作系统(8) 第7章:系统结构

 
阅读更多

从头开始编写操作系统(8) 第7章:系统结构
2011年04月17日
  
  译自:http://www.brokenthorn.com/Resources/OSDev7.html 第7 章:系统结构
  by Mike, 2008
  本系列文章旨在向您展示并说明如何从头开发一个操作系统。 欢迎!在之前的一章里,我们总算完成了引导加载器的工作!到目前为止: 我们详细的了解了FAT12 文件系统,并且了解了价值,解析,执行stage 2 的方法。 
  这章里会继续前面的工作。首先我们会仔细的看看x86 体系结构。这很重要,对于理解保护模式如何工作尤其重要。 
  我们会介绍计算机工作的每件事儿,我们要深入到比特一层。为了理解BIOS 在启动过程中是如何胜任工作的,你得记住你也可以启动其他的处理器。BIOS 仅仅处理注处理器,我们也可以做同样的事儿来支持多处理器。 
  包括以下内容:  就某些方面来说,这更像是一个计算机体系结构的教程。然而我们是在以操作系统开发的角度来看计算机体系结构。当然我们会涉及体系结构的方方面面。 理解这些会使我们更了解保护模式,在下一章里,我们会包括切换到保护模式的所有细节。  享受乐趣吧! 我们都听过这个术语。保护模式 (PMode) 是在80286及之后处理器提供的一个操作模式。保护模式主要用于提高系统的稳定性。 从前面的章节里你知道,实模式有大问题。首先,我们能在任何想要的地方写数据,这会覆盖代码或数据,这些代码或数据可能是软件端口或是处理器或是我们自己的。并且要做到这样的事情,我们有超过4,000 种不同的方法--包括直接的和间接的。
  实模式没有内存保护 。所有的数据和代码被放置在单个的,为通用目的而存在的内存块中。
  实模式,限制使用16 位寄存器,1MB 内存。 
  不支持硬件级,内存保护 和多任务 。
  最重要的问题是,不存在向"环"这样的东西。所有的程序都在环0 级执行,所有的程序都有系统的绝对控制权。这意味着,如果你不够小心,在一个单任务环境里的,一条指令( 如cli/hlt) 会使你的整个操作系统崩溃。
  所有的这些,在我们讨论实模式时都提到过,保护模式解决了所有的这些问题。
  保护模式:  有内存保护
  有虚拟存储器 和任务状态切换(TSS)的硬件机制
  硬件级支持中断编程及执行
  4中操作模式: 环 0, 环 1, 环 2, 环 3
  访问32位寄存器
  访问高达4GB内存
  我们在上一章中的汇编语言的环一节里提到,我们工作在环 0,而通常的程序工作在环 3 (一般情况).我们能够使用特殊的指令访问特殊的寄存器,而一般的程序不行。在这章里,我们将会使用LGDT指令,来完成一个"远跳"使用我们自己定义的段,和处理器控制寄存器。这在普通的程序中是不行的。 了解系统结构并制定处理器如何工作,有利于我们的工作。  x86系列计算机遵循冯诺依曼体系结构。典型的冯诺依曼体系结构的计算机有3部分组成: 例如: 有些事情有注意。如你所知, CPU 重内存中取得数据和指令。内存控制器的工作是确定特定的RAM 芯片和其中的存储单元,所以,CPU 与内存控制器交流。
  另外,注意"I/O 设备" 。他们也连接在系统总线上。所有的I/O 端口是内存位置的映射,这使得我们能够使用IN 和OUT 指令。
  硬件设备使用系统总线访问内存。也允许我们当一些事情发生时通知设备。比如,如果向控制硬件设备控制器要读的位置写一字节,处理器可以通知设备"有数据在数据总线上",这是通过控制总线在操作。这是软件与硬件交互的基础,我们会在后面详细的解释它,因为这是在保护模式中唯一的域硬件交互的方法,很重要。
  我们先分开来介绍,然后,把他们结合起来,并且通过一个指令在硬件层之下的过程了解他们在一起是如何工作的。下面我们将讨论I/O 端口以及软件与硬件之间是如何交互的。
  如果你有x86 的汇编经验,一些甚至是大部分对你来说是很熟悉的。但我们要介绍那些在大部分汇编语言教材里没有深入的内容,特别是环 0 的程序。  系统总线也称作前部总线,它在主板上连接CPU和北桥。 系统总线是数据总线、地址总线和控制总线的集合。总线上的一条电线表示1 比特 。使用电压来表示0 和1 ,基于标准晶体管- 晶体管逻辑(TTL )。我们不需要详细了解。TTL 是数字逻辑的部分,是计算机构造的基础。
  如你所知,系统总线由3 种总线构成,我们分别介绍如下。  数据总线是传输数据的一系列电线。数据总线的宽度有16 线/比特, 32 线/比特,或 64 线/比特. 注意电线和比特信号的直接单元关系。 这表明:32 位处理器有32 位的数据总线 。一次数据传输可以处理4 字节,我们可以注意我们程序中的数据大小,以其提高运行速度。 
  怎么做呢?对于1,2,4,8, 或16 比特的数据,处理器会在数据总线上扩展'0 '。对于大段的数据会切分(并扩展),使发送到数据总线上的数据符合总线宽度。发送与总线宽度一致的数据会更快,因为不需要额外的工作。
  比如,我们有一个64 位的数据,而有32 位总线宽度,在第一个时钟周期里,只有前32 位数据被发送到内存控制器,第二个时钟周期才发送后32 位数据。注意:数据类型越大,需要更多的时钟周期!
  通常的 "32 位处理器", "16 位处理器" 等,代表数据线的宽度。所以"32 位处理器" 有32 位数据总线。  当处理器或I/O设备需要访问内存是,它会在地址总线上放一个地址。我们也知道内存地址代表 内存中的一个位置。这很抽象。 " 内存地址" 是一个内存控制器 使用的数。内存控制器从总线中得到这个数,将它解释为一个内存位置。知道了每个RAM 芯片的大小内存控制器可以简单的访问一个特定的芯片及它的内部偏移。 从内存单元0 开始内存控制器将它解释为我们需要的偏移地址。
  地址总线通过控制单元 (CU )连接处理器与I/O 控制器 。控制单元在处理器里面,我们在后面介绍。I/O 控制器 控制着硬件设备的接口,我们在后面介绍。 
  就像数据总线一样,每根电线代表一个比特信号 ,因为1 比特只有两个不同的值,所以CPU 可以访2^n 个不同的地址。 因此,地址总线的条数/ 位数决定CPU 可访问的最大内存。
  8080 到80186 处理器都有20 条/ 比特地址总线。80286 和80386 有24 条/ 比特, 80386+ 有32 条/ 比特。 
  所有的x86 系列都向老处理器兼容。这也是为什么启动时处在实模式。处理器限制通过20 条地址线访问1MB 内存--line 0 到line 19 。
  对我们来说很重要, 因为这样的限制对我们依旧存在!我们需要使20号地址线有效,才能让我们的操作系统访问高达4GB 的内存。 后文有更详细的解说。  我们把数据放到数据总线,使用地址总线确定内存地址,我们怎么知道如何处理数据呢?是读数据还是写数据呢? 控制总线是一系列表示设备要进行的工作的电线(比特)。比如:处理器设置READ 或WRITE 位使内存控制器知道他要在地址总线上指定的内存位置读到数据总线或是写入数据总线的数据。
  控制总线也允许处理器通知设备。使设备引起注意,比如:我们要设备从perhaps 地址总线指定的内存位置读数据该怎么办呢?这要让设备知道我们希望它做的事。这在I/O  软件端口很重要。
  当然,要知道系统总线并不直接与硬件设备相连。相反的它们连接到一个中心控制器--I/O 控制器 ,用于切换,并给设备发信号。 上面是系统总线的全部内容。它是处理器 (通过控制单元 (CU))和I/O设备(通过I/O控制器) 访问内存的通路。访问内存需要通过内存控制器,它的任务是确定要访问的内存芯片及内存芯片上的哪个存储单元。 " 控制器"这个词 你可能听过很多,我在后面详细解释。 内存控制器是系统总线与物理内存直接的主要接口。 我们前面见过控制器,不是吗?控制器到底是什么?  控制器提供基本的已经控制功能。它也提供基本的软硬件接口。 这很重要。记得吗,在保护模式里,我们不能使用任何中断。在引导加载器里我们使用一系列中断来与硬件交流。而在保护模式使用这些中断会导致三重错误,我们怎么办呢? 我们要与硬件直接通信。我们要通过控制器,(在我们介绍了I/O 系统后,我们会详细介绍控制器是怎么工作的)。 内存控制器为软件提供了一种读写内存位置的方法。内存控制器也有刷新RAM芯片的责任,以保证数据不丢失。 内存控制器有一个多路选择器(Multiplexer) 和一个信号分离器(Demultiplexer) 用于选择特定的RAM 芯片,并且定位地址总线确定的地址。 DDR控制器用于刷新DDR SDRAM, 使用系统时钟脉冲来读写内存。 双通道控制器用在DRAM设备上,它由两组小的总线,可以同时读写两个不同的内存位置,这有助于加快RAM的访问速度。 内存控制器从地址总线接收地址。这很好,但我们怎么告诉内存控制器是读还是写内存呢?还有数据从哪里来的呢?当我们读内存时。处理器设置在控制总线上的Read位;同样,在写内存时,处理器设置在控制总线上的Write位。 处理器使用控制总线控制设备。
  内存控制器使用的数据在数据总线上,使用的地址在地址总线上。  当读内存时,处理器将要读的内存的决定地址放到地址总线上。然后处理器设置读控制线。 内存控制器获得控制权。控制器使用多路选择器 将绝对地址转换为物理RAM 位置,并把数据放到数据总线上,然后将READ 位清0 ,设置READY 位。 
  现在处理器知道了数据在数据总线上。它复制这个数据,并执行剩余的指令……比如把数据保存到BX 里。  写内存类似。 首先,处理器将内存地址放到地址总线上。将要写的数据放到数据总线上。然后,设置控制总线上的WRITE 位。
  内存控制器知道了要往地址总线确定的内存地址写在数据总线上的数据。完成后,内存控制器清空WRITE 位,设置控制总线的READY 位。 我们不直接和内存交流,我们间接的做,无论读还是写内存,我们都使用内存控制器。内存控制器是软件与RAM芯片之间的接口。 下面我们看看I/O 系统,等等!1337  多路选择器是什么样?它是 内存控制器中的物理线路。要了解它的工作方式,我们得知道一点数字逻辑电路 的知识。对我们来说复杂了,如果你想知道更多,Google 一下!  I/O系统简单的表示I/O端口。这个系统提供了软件和硬件控制器之间的接口。 仔细看看。  端口简单的提供软件与硬件设备直接的接口,有两种类型的端口:软件端口和硬件端口。 硬件端口提供两个物理设备之间的接口。这样的接口通常使用"槽"来连接设备,包括,不限于:串口,并口,PS/2口, 1394, 火线,USB口等。 这些端口通常在机箱的边上、前面或是后面。
  如果你想看看这样的接口,顺着一根连到你的电脑上的线,你就找到了。
  一般的电器上,端口上的针承载的信号在不同的设备上有不同的含义。这些针就像系统总线一样代表比特!每根针1 比特。
  一般将硬件端口分为两类,"公"的和"母"的。"公"的端口的针是露出来的,"母"的与它相反。硬件端口通过控制器访问。 后文有更详细的解说。  这对我们相当重要。软件端口是个数,它表示一个(或一种)硬件控制权。 你可能知道有些数代表同一个控制器。原因呢?内存映射I/O 。基本的想法是通过一个特定的内存地址来与硬件交流。端口号就代表这些地址。这表明地址可以代表特定设备的一根寄存器,或是控制寄存器。
  以后会仔细介绍。 x86结构里,处理器使用特殊的内存位置来表示特定的东西。 比如:地址 0xA000:0  表示显卡的VRAM 起始地址。 在这个位置写数据,你直接改变了显存的内容,也就改变了屏幕上显示的内容。 
  其他的内存地址代表其他的一些东西--比如软驱控制器 (FDC) 的某个寄存器。 
  了解哪个地址是什么,是很关键的,也很重要。 一般的x86 实模式内存映射:  0x00000000 - 0x000003FF   实模式中断向量表
  0x00000400 - 0x000004FF   BIOS数据区
  0x00000500 - 0x00007BFF   未使用
  0x00007C00 - 0x00007DFF   引导加载器
  0x00007E00 - 0x0009FFFF   未使用
  0x000A0000 - 0x000BFFFF   显存(VRAM)
  0x000B0000 - 0x000B7777   单色显存
  0x000B8000 - 0x000BFFFF   彩色显存
  0x000C0000 - 0x000C7FFF   显存ROM BIOS
  0x000C8000 - 0x000EFFFF - BIOS Shadow Area
  0x000F0000 - 0x000FFFFF   系统BIOS
  注意:也可能会将上面的设备映射到完全不同的内存区域。BIOS POST 程序来完成上面的设备映射工作。
  好,很好,因为这些地址代表不同的东西,读写这些特殊的地址会得到,或改变计算机的不同部分的状态。 
  比如,还记得我们关于INT 0x19 的讨论吗?我们说在0x0040:0x0072 写0x1234  会跳转到0xFFFF:0 ,实现计算机的热重启(Windows ctrl+alt+del) 。段:偏移寻址方式的0x0040:0x0072 转换为绝对地址是0x000000472 ,这是BIOS 数据区的一部分。
  另一个例子是文本输出,往0x000B8000 写几个字节,我们就直接改变了字符模式的显存。因为在现实的时候不断刷新,这就改变了显示在屏幕上的字符,酷?
  让我们回到端口映射,后面我们会经常查看这张表。 "端口地址"是每个控制器监听的一个特殊的数。当启动的时候,ROM BIOS为这些控制器设备分配一个不同的数。要知道ROM BIOS和BIOS相关,但是不同的软件。ROM BIOS是一个在BIOS芯片上的电子部件。它启动主处理器,,加载BIOS程序到0xFFFF:0 (与上节的表比较一下)。 ROM BIOS 把这些数分配给不同的控制器,这样控制器就有了一个区分自己的方法。这允许BIOS 设置中断向量表,可以使用一个特殊的数字与硬件交流。
  当与I/O 控制器工作时,处理器使用相同的系统总线。处理器在地址总线上放一个特别的端口号, 就像读内存一样。同样会在控制总线READ 或WRITE 位,很酷,但有问题:处理器如何区分读写内存还是访问控制器呢? 
  处理器会设置控制总线上的另一位--I/O ACCESS 位。如果这一位为1 ,则I/O 控制器通过I/O  系统监视地址总线。如果地址总线上的数与分配给设备的数相对,设备则从数据总线接收数据,并处理它。如果 这一位为1 内存控制器忽略所有请求。所以如果这个端口号未被分配,绝对不会有事发生,控制器不响应,内存控制器也忽视它。 
  让我们看看这些端口地址. 这很重要!这是在保护模式下唯一的与硬件交流的方法!
  警告:这个表很大! 默认的x86端口地址分配 地址范围 第1个8字节 第2个8字节 第3个8字节 第4个8字节 0x000-0x00F DMA控制器,通道0-3 0x010-0x01F 系统占用 0x020-0x02F 中断控制器 1 系统占用 0x030-0x03F 系统占用 0x040-0x04F 系统时钟 系统占用 0x050-0x05F 系统占用 0x060-0x06F 键盘/PS2鼠标 (端口 0x60)
  扬声器(0x61) 键盘/PS2鼠标(0x64) 系统占用 0x070-0x07F RTC/CMOS/NMI (0x70, 0x71) DMA控制器,通道0-3 0x080-0x08F DMA 页寄存器 0-2 (0x81 - 0x83) DMA页寄存器3 (0x87) DMA 页寄存器4-6 (0x89-0x8B) DMA页寄存器7 (0x8F) 0x090-0x09F 系统占用 0x0A0-0x0AF 中断控制器 2 (0xA0-0xA1) 系统占用 0x0B0-0x0BF 系统占用 0x0C0-0x0CF DMA控制器通道 4-7 (0x0C0-0x0DF), bytes 1-16 0x0D0-0x0DF DMA控制器通道 4-7 (0x0C0-0x0DF), bytes 16-32 0x0E0-0x0EF 系统占用 0x0F0-0x0FF 浮点单元 (FPU/NPU/Mah Cop处理器) 0x100-0x10F 系统占用 0x110-0x11F 系统占用 0x120-0x12F 系统占用 0x130-0x13F SCSI主适配器 (0x130-0x14F), bytes 1-16 0x140-0x14F SCSI 主适配器 (0x130-0x14F), bytes 17-32 SCSI 主适配器 (0x140-0x15F), bytes 1-16 0x150-0x15F SCSI 主适配器 (0x140-0x15F), bytes 17-32 0x160-0x16F 系统占用 第4 IDE控制器, 主从 0x170-0x17F 第2 IDE控制器, 主设备 系统占用 0x180-0x18F 系统占用 0x190-0x19F 系统占用 0x1A0-0x1AF 系统占用 0x1B0-0x1BF 系统占用 0x1C0-0x1CF 系统占用 0x1D0-0x1DF 系统占用 0x1E0-0x1EF 系统占用 第3 IDE控制器, 主从 0x1F0-0x1FF 主 IDE控制器,主从 系统占用 0x200-0x20F 游戏手柄端口 系统占用 0x210-0x21F 系统占用 0x220-0x22F 声卡 Non-NE2000 网卡 系统占用 0x230-0x23F SCSI 主适配器 (0x220-0x23F), bytes 17-32) 0x240-0x24F 声卡 Non-NE2000 网卡 系统占用 NE2000 网卡 (0x240-0x25F) Bytes 1-16 0x250-0x25F NE2000 网卡 (0x240-0x25F) Bytes 17-32 0x260-0x26F 声卡 Non-NE2000 网卡 系统占用 NE2000 网卡 (0x240-0x27F) Bytes 1-16 0x270-0x27F 系统占用 即插即用系统设备 LPT2   2号并口 系统占用 LPT3   3号并口 (黑白系统) NE2000 网卡 (0x260-0x27F) Bytes 17-32 0x280-0x28F 声卡 Non NE2000 网卡 系统占用 NE2000 网卡 (0x280-0x29F) Bytes 1-16 0x290-0x29F NE2000 网卡 (0x280-0x29F) Bytes 17-32 0x2A0-0x2AF Non NE2000 网卡 系统占用 NE2000 网卡 (0x280-0x29F) Bytes 1-16 0x2B0-0x2BF NE2000 网卡 (0x280-0x29F) Bytes 17-32 0x2C0-0x2CF 系统占用 0x2D0-0x2DF 系统占用 0x2E0-0x2EF 系统占用 COM4   4号串口 0x2F0-0x2FF 系统占用 COM2 - 2号串口 0x300-0x30F 声卡 / MIDI 端口 系统占用 Non NE2000 网卡 系统占用 NE2000 网卡 (0x300-0x31F) Bytes 1-16 0x310-0x31F NE2000 网卡 (0x300-0x32F) Bytes 17-32 0x320-0x32F 声卡 / MIDI 端口 (0x330, 0x331) 系统占用 NE2000 网卡 (0x300-0x31F) Bytes 17-32 SCSI 主适配器 (0x330-0x34F) Bytes 1-16 0x330-0x33F 声卡 / MIDI 端口 系统占用 Non NE2000 网卡 系统占用 NE2000 网卡 (0x300-0x31F) Bytes 1-16 0x340-0x34F SCSI 主适配器 (0x330-0x34F) Bytes 17-32 SCSI 主适配器 (0x340-0x35F) Bytes 1-16 Non NE2000 网卡 系统占用 NE2000 网卡 (0x340-0x35F) Bytes 1-16 0x350-0x35F SCSI 主适配器 (0x340-0x35F) Bytes 17-32 NE2000 网卡 (0x300-0x31F) Bytes 1-16 0x360-0x36F 磁带加速卡 (0x360) 系统占用 第4 IDE控制器 (从设备)(0x36E-0x36F) Non NE2000 网卡 系统占用 NE2000 网卡 (0x300-0x31F) Bytes 1-16 0x370-0x37F 磁带加速卡(0x370) 第2 IDE控制器 (从设备) LPT1   1号并口 (彩色系统) 系统占用 LPT2 -2号并口 (黑白系统) NE2000 网卡 (0x360-0x37F) Bytes 1-16 0x380-0x38F 系统占用 声卡 (FM Synthesizer) 系统占用 0x390-0x39F 系统占用 0x3A0-0x3AF 系统占用 0x3B0-0x3BF VGA/黑白显示器 LPT1   1号并口(黑白系统) 0x3C0-0x3CF VGA/CGA显示器 0x3D0-0x3DF VGA/CGA 显示器 0x3E0-0x3EF 磁带加速卡(0x370) 系统占用 COM3   3号串口 系统占用 第3 IDE控制器 (从设备)(0x3EE-0x3EF) 0x3F0-0x3FF 软盘控制器 COM1   1号串口 磁带加速卡(0x3F0) 主 IDE控制器 (从设备)(0x3F6-0x3F7) 系统占用 这张表不完整,并且希望没什么错。我会随着更多设备的开发增加这张表。
  所有这些内存访问被特定的控制器使用--如上表所示。端口地址的确切含义依赖于控制器。它可能代表一个控制寄存器,状态寄存器或是其他的什么东西。台不幸了。
  强烈建议你打印一份上面的表格,当我们与硬件交流时,会频繁的参考上表。
  我会更新它,如果我更新了,你需要再打印一份,确保它是最新的。 
  知道了这一切,我们一起来看。  X86处理器有指令用于端口I/O。它们是IN和OUT.  这些指令告诉处理器我们想要和设备交流,它们保证处理器与I/O 设备之间的控制线被正确设置。 
  看一个完整的例子,并试着从键盘控制器输入缓冲区读数。
  看看我们上面的端口分配表,我们发现键盘控制器的端口地址在 0x60 到0x6F.  上表中显示的前8 个字节和第2 个8 字节( 从端口地址0x60 开始) 分别用于键盘和PS/2 鼠标。后两个8 字节被系统占用,我们不管它。 
  键盘控制器映射到端口0x60 到端口 0x68 。酷,但对我们来说,这代表什么?这是设备标准,知道吗?
  对键盘而言,端口0x60 是控制寄存器,  端口0x64 是状态寄存器。如果状态寄存器的第1 比特为1 ,则输入缓冲区有数据。所以,如果我们将 控制寄存器设为READ ,我们就能把输入缓冲区的数据复制到什么地方。 WaitLoop:     in      al, 64h   ; 取得状态寄存器的值              and     al, 10b   ; 测试状态寄存器的第1 位              jz      WaitLoop ; 如果这位为0 ,缓冲区中没数据              in      al, 60h   ; 如果为1 从缓冲区( 端口0x60)
  是的,就是这儿,这正是硬件编程和设备驱动开发的基础。 IN 指令执行时,处理器将端口地址- 如0x64- 放到地址总线上,然后设置控制总线上的"I/O 设备"位,和READ 位。那个被ROM BIOS 分配的设备号为0x64 的设备--这里是键盘控制器的状态寄存器 ,知道要执行"读"操作(因为READ 位为1 ),所以它会从键盘寄存器的某个位置上将数据复制到数据总线,清掉控制总线上的READ 位I/O 设备位,并设置READY 位,现在处理器就从数据总线上得到要读的数据了。
  OUT 指令相似。处理器将要写的数据放到数据总线(0 扩展到数据总线宽度) 。 然后,设置控制总线的WRITE 和I/O 设备位。将端口地址--如0x60 --复制到地址总线。因为"I/O 设备位"为1 ,这个信号告诉所有的控制器监视地址总线。如果地址总线上的数正好与其分配的数匹配,该设备处理这个数据。我们的例子中是 键盘控制器。键盘控制器知道要执行"写"操作,因为控制执行的WRITE 位被置1 。它将数据总线上的值复制到它的控制寄存器中(那个寄存器被分配的端口地址是0x60 )。键盘控制器清掉WRITE 和I/O 设备位,并设置READY 位,处理器重新获得控制权。
  端口映射和端口I/O 很重要,这是我们在保护模式下唯一的与硬件交流的方法。要知道如果我们没有编写中断处理代码,我们就不能使用中断。编写中断处理代码(如输入、输出)需要编写设备驱动,所有的这些都需要直接访问设备。如果你对这些没有信心,做些练习吧,有什么其他的问题,告诉我。 多数80x86指令可以被所有的程序使用。但是,有些指令只能被内核程序使用。因此有些指令我们的读者可能不熟悉。我们会大量的使用这些指令,理解他们很重要。 特权级(环0) 指令 指令 描述 LGDT 加载GDT的地址到GDTR LLDT 加载LDT的地址到LDTR LTR 加载任务寄存器到TR MOV Control Register 复制并保存控制寄存器中的数据 LMSW 加载新的机器状态字 CLTS 清空CR0控制寄存器任务切换标志 MOV Debug Register 复制并保存调试寄存器中的数据 INVD 使Cache无写回失效 INVLPG 是TLB实体失效 WBINVD 使Cache有写回失效 HLT 处理器停机 RDMSR 读模式描述寄存器(MSR) WRMSR 写模式描述寄存器(MSR) RDPMC 读性能监视计数器 RDTSC 读时间戳计数器 非内核模式的其他程序执行上面的任意一条指令都会产生一个一般性保护错误, 或者三重错误. 
  不要担心你不了解上面的这些指令。我会在需要的时候解释他们。 X86处理器有很多不同的寄存器用于 保存当前状态。多数应用程序可访问的通用寄存器、段寄存器和eflags。其他寄存器只在向内核那样的环0程序有效。 X86 系列有下列寄存器:RAX (EAX(AX/AH/AL)), RBX (EBX(BX/BH/BL)), RCX (ECX(CX/CH/CL)), RDX (EDX(DX/DH/DL)), CS,SS,ES,DS,FS,GS, RSI (ESI (SI)), RDI (EDI (DI)), RBP (EBP (BP)). RSP (ESP (SP)), RIP (EIP (IP)), RFLAGS (EFLAGS (FLAGS)), DR0, DR1, DR2, DR3, DR4, DR5, DR6, DR7, TR1, TR2, TR3, TR4, TR5, TR6, TR7, CR0, CR1, CR2, CR3, CR4, CR8, ST, mm0, mm1, mm2, mm3, mm4, mm5, mm6, mm7, xmm0, xmm1, xmm2, xmm3, xmm4, xmm5, xmm6, xmm7, GDTR, LDTR, IDTR, MSR, TR.  所有这些寄存器都在处理器内部的一个称为寄存器文件  的内存区中。详细信息参考处理器体系结构 一节,其他的不在寄存器文件中的寄存器有:PC, IR,  向量寄存器和硬件寄存器。
  这些寄存器中的大部分只在环0 程序有效。其中的大部分会在处理器的很多状态都有效。对于它们的错误设置很容易导致三重错误。其他情况可能会导致CPU 做出错误的动作 ( 多数情况下是因为TR4,TR5,TR6,TR7 的错误使用) 。 
  其他的一些寄存器是CPU 内部寄存器 ,不可用在通常情况下被访问。当对处理器本身编程的时候会用到它们。常见的如IR ,向量寄存器。 
  我们得仔细看看一些特殊的寄存器。
  注意:把CPU 当作一个你要与之交流的普通设备。控制寄存器的概念 ( 和寄存器本身) 在我们与其它设备交流时很重要。
  同样,请注意有效寄存器没有官方文档,所以可能有些寄存器没有列在上面。如果 你知道,请告诉我 ,我会把它们加上的:) 。  这些32位寄存器的寄存器可以用于任何目的,同样这些寄存器也有着特殊的用途。 EAX   累加寄存器。主要用途:数学计算
  EBX   基地址寄存器。主要用途:作为直接内存访问的基地址.
  ECX   计数寄存器。主要用途:循环计数
  EDX   数据寄存器。主要用途:存数,是的,就是这样:)
  每个32 位寄存器可以分为两部分。高字 和低字 。高字是高16 位,低字是低16 位。 
  64 位处理器上,这些寄存器64 位宽,名字是RAX, RBX, RCX, RDX 。其低32 位是EAX 寄存器。
  没有给高16 位分配特别的名字。但是低16 位有 , 这些名字后面跟一个'H' ( 低字的高8 位) 或是一个 'L' (低8 位)。
  以RAX 为例:                                               +--- AH -------+--- AL ---+                                              |            |            |         +------------------------------------------------- ------------+         |                     |                |                     | |         +------------------------------------------------- ------------+         |                     |                                      |         |                    +--------EAX  低32 位-----| -- 32 位处理器有效         |                                                            |         |------------------ RAX 的64 位-------------------------------| -- 64
  这是什么意思?AH和AL 是AX的一部分,同样AX是EAX的一部分,因此,无论修改了上面的哪个名字代表的寄存器,都修改了同样的寄存器 - EAX. 这样,也就修改了64 位机上的RAX 。 
  上面的内容对BX,CX, 和DX 都一样。 
  通用目的寄存器可以在从环0 到环3 的任意程序中使用。因为这是最基础的汇编语言,我假设你已经知道它们是如何工作的了。  在实模式中,段寄存器用于记录当前的段地址,它们都是16位的。 CS   代码段寄存器
  DS   地址段寄存器
  ES   附加段寄存器
  SS   堆栈段寄存器
  FS   远程段寄存器
  GS   通用寄存器
  记住:实模式下使用段:偏移的寻址方式。段地址保存在 段寄存器记住,像BP, SP,和BX用于保存偏移地址。 常见的用法如:DS:SI, 其中DS 存有段地址, SI 存有偏移地址。 
  段寄存器可以在从环0 到环3 的任意程序中使用。因为这是最基础的汇编语言,我假设你已经知道它们是如何工作的了。  x86有一些寄存器用于辅助内存访问。 每个寄存器都保存一个16位的地址 (也可能使用偏移地址)。 在32 位处理器上,这些寄存器是32 位的,它们的名字是ESI, EDI, EBP, 和 ESP. 
  在64 位处理器上,这些寄存器是64 位的,它们的名字是RSI, RDI, RBP, 和 RSP. 
  16 位寄存器是32 位寄存器的一个子集, 同样,32 位寄存器是64 位寄存器的一个子集,就像RAX 一样。 
  当特定的指令执行时,栈指针会自动的增加和减少特定的字节。这些指令包括push*, pop*  指令, ret/iret, call, syscall 等。 
  C 语言,实际上是大多数语言,经常使用栈,我们要保证将栈设定到一个合适的位置上,使得C 语言能够正常工作。另外,记住栈向下生长! 指令指针 (IP) 寄存器保存着当前正在执行的质量的偏移地址。记住:是偏移地址, *不是*绝对地址! 指令指针(IP) 有时也称为程序计数器 (PC) 。 
  32 位计算机上,IP 是32 位的名字为 EIP. 
  64 位计算机上,IP 是64 位的名字为 RIP. 这是处理器内部的寄存器,不能以常规方法访问。它在处理器控制单元(CU)的指令Cache里。它保存了将要被翻译为计处理器内部使用的微指令的 当前指令。参看处理器体系结构 一节获取更多信息。 EFLAGS寄存器是x86处理器的状态寄存器。它用于确定当前的状态。我们已经使用过很多次了。简单的例子如:jc, jnc, jb, jnb指令 多数指令都影响EFLAGS 寄存器,这样我们就能产生条件了( 比如一个值是不是比另一个大?) 。 
  EFLAGS 是FLAGS 寄存器的扩展,RFLAGS 是EFLAGS 和FLAGS的扩展 。如:    +---------- EFLAGS (32 位)-------+ |                                  | |-- FLAGS (16 位)---+             | |                   |             | ========================================== 调试。它们是:DR0,DR1,DR2,DR3,DR4,DR5,DR6,DR7。与测试寄存器一样,它们可以用MOV指令访问,并且只能用在环0中。任何其它尝试都将导致一般性保护错误 (GPF)和三重错误. 寄存器DR0, DR1, DR2, DR3保存一个断点的绝对地址 。如果分页有效,这个地址会装换为据对地址。这些断点的执行条件定义在DR7中。 DR7是一个32位寄存器,它使用位模式确定当前的调试任务,位模式为: Bit 0...7   使调试寄存器有效(详见后文)
  Bit 8...14 - ?
  Bit 15...23   当断点被触发是,每2位代表一个单独的调试寄存器。其值可以是下面的一个:
  00   执行时中断
  01   写数据时中断
  10   IO读写时中断。当前你没有硬件支持
  11   数据读写时中断
  Bit 24...31   定义监视的内存大小,每2位代表一个单独的调试寄存器。其值可以是下面的一个:
  00   1字节
  01 - 2字节
  10 - 8字节
  11 - 4字节
  有两种方法使调试寄存器有效,全局 级的或是局部 级的。如果你有不同的任务(比如分页),所有局部级的调试设置,只对这个任务有效,在任务切换是处理器自动的清空这些设置。全局级的,则不会这样。 上面的第0 到第7 位,如下表所示:  Bit 0:开启局部 DR0 寄存器
  Bit 1:开启全局 DR0 寄存器
  Bit 2:开启局部 DR1 寄存器
  Bit 3:开启全局 DR1 寄存器
  Bit 4:开启局部 DR2 寄存器
  Bit 5:开启全局 DR2 寄存器
  Bit 6:开启局部 DR3 寄存器
  Bit 7:开启全局 DR3 寄存器
  这个寄存器,用于决定当错误发生时调试器采取的动作。当处理器碰到一个可处理异常时,它会设置这个寄存器的低4位,并执行错误处理程序。 注意:调试状态寄存器DR6 ,不会自动清除,如果你想让程序继续运行,请先清空该寄存器! 这些特殊的控制寄存器有特定的处理器提供不同的功能,在别的处理器上可能不能使用。由于它们是系统级的寄存器,只有环0的程序可以访问。 应为这些寄存器随着处理器的不同, 这些寄存器可能会改变。
  x86 有两个特殊的指令用于访问这个寄存器:  这个寄存器对于不同的处理器有很大差别。因此所以在使用它们之前先使用CPUID 指令。 
  为了访问这些寄存器,  需要传递一个代表你要访问的寄存器的地址。 
  这些年来,Intel 的一些MSR 不再是每个机器都不一样了,下面是x86 体系下共同的。  模式特定的寄存器 (MSRs) 寄存器地址 寄存器名 IA-32处理器系列 0x0 IA32_PS_MC_ADDR Pentium 处理器 0x1 IA32_PS_MC_TYPE Pentium 4处理器 0x6 IA32_PS_MONITOR_FILTER_SIZE Pentium 处理器 0x10 IA32_TIME_STAMP_COUNTER Pentium 处理器 0x17 IA32_PLATFORM_ID P6 处理器 0x1B IA32_APIC_BASE P6 处理器 0x3A IA32_FEATURE_CONTROL Pentium 4 /处理器673 0x79 IA32_BIOS_UPDT_TRIG P6 处理器 0x8B IA32_BIOS_SIGN_ID P6 处理器 0x9B IA32_SMM_MONITOR_CTL Pentium 4 /处理器672 0xC1 IA32_PMC0 Intel Core Duo 0xC2 IA32_PMC1 Intel Core Duo 0xE7 IA32_MPERF Intel Core Duo 0xE8 IA32_APERF Intel Core Duo 0xFE IA32_MTRRCAP P6 处理器 0x174 IA32_SYSENTER_CS P6 处理器 0x175 IA32_SYSENTER_ESP P6 处理器 0x176 IA32_SYSENTER_IP P6 处理器 有更多的MSR 没有列在上表中。参看附录B Intel开发手册 中的完整列表。
  我不确定在我们的开发过程中是否会涉及MSR ,如果有必要,我会扩充这个列表的。 这个指令将CX指定的MSR复制到EDX:EAX中。 这个指令是特权级 指令,只能在环0( 内核层)使用。当 非特权程序试图执行这条指令,或CS 中不是一个有效的MSR 地址时,会产生一个一般性保护错误, 或三重错误 。 
  这个指令不影响任何标志。 
  下面是使用这条指令的例子 ( 你会在这个教程的后面再见到它):  很酷,不是吗? 这个指令将保存在EDX:EAX中的64位数据保存到CX指定的MSR中。 这个指令是特权级 指令,只能在环0( 内核层)使用。当 非特权程序试图执行这条指令,或CS 中不是一个有效的MSR 地址时,会产生一个一般性保护错误, 或三重错误 。  这个指令不影响任何标志。 这是使用它的例子:  这个对我们很重要。 控制寄存器允许我们改变处理器的动作,它们是:CR0, CR1, CR2, CR3, CR4 。  CR0是主要的控制寄存器。32位定义如下: Bit 0 (PE) : 将系统置于保护模式
  Bit 1 (MP) : 监视协处理器标志,它控制WAIT指令的执行。
  Bit 2 (EM) : 仿真标志。当该位被设置,协处理器指令会产生一个异常
  Bit 3 (TS) :任务切换标志,当处理器由一个任务切换到另一个任务时,该位被置1
  Bit 4 (ET) : 扩展类型标志,它告诉我们,安装的是何种类型的协处理器。
  0   安装的是80287
  1   安装的是80387
  Bit 5 (NE): 数值错误
  0   标准错误报告有效
  1 - x87 FPU内部错误报告有效
  its 6-15 : 不使用
  Bit 16 (WP):写保护
  Bit 17:不使用
  Bit 18 (AM):对齐标志
  0   对齐检查无效
  1   对齐检查有效(要求环3,EFLAGS的AC标志置1)
  Bits 19-28:不使用
  Bit 29 (NW): Not Write-Through
  Bit 30 (CD):禁用Cache
  Bit 31 (PG) :内存分页有效
  0   无效
  1   有效,并使用CR3 寄存器
  Intel保留,未使用。 发生页错误的线性地址。如果发生了一个页错误,CR2保存着那个试图访问的地址。 如果CR0的PG位置1,最低的20位包含页目录的基地址寄存器(PDBR)。 在保护模式中用于控制操作,如v8086模式,开启I/O断点,页大小扩展和机器检测异常。 Bit 0 (VME) :开启虚拟8086模式扩展
  Bit 1 (PVI) :开启保护模式虚拟中断
  Bit 2 (TSD) :开启时间戳
  0 - RDTSC 指令可用于任意特权级
  1 - RDTSC 指令只用于环0
  Bit 3 (DE) :开启调试扩展
  Bit 4 (PSE) :页大小扩展
  0   页大小为4KB
  1   页大小为4MB. PAE开启是,页大小为2MB.
  Bit 5 (PAE) : 无论地址扩展
  Bits 6 (MCE) : 机器检测异常
  Bits 7 (PGE) : 分页全局有效
  Bits 8 (PCE) : 性能监视计数器开启
  0 - RDPMC指令可用于任意特权级
  1 - RDPMC指令只用于环0
  Bits 9 (OSFXSR) : 对FXSAVE和FXSTOR指令(SSE)的操作系统支持
  Bits 10 (OSXMMEXCPT) : 对无标记的SIMD FPU异常的操作系统支持
  Bits 11-12 : 不使用
  Bits 13 (VMXE) : VMX开启
  通过对任务优先级寄存器(TPR)的读写访问。 X86系列使用一些寄存器来保存每个段描述表的线性地址。后文有更详细的解说。 我们会在后面再次讨论引导的过程,只是从更底层来看罢了。这样就可以回答诸如:BIOS POST 到底是怎么开始的,又怎么执行POST ,启动主处理器,加载BIOS 的,一类的问题。我们已经介绍了是什么,还没介绍怎么做呢。
  注意:这一节非常的技术化。如果你不理解,被担心,你不需要全部理解。我在这里写这些是出于完整性的考虑,我们会详细了解组成计算机系统和执行代码的主要部分。它们怎么执行我们给出的代码?为什么机器语言如此特殊?这些问题都会在这里回答。
  当我们后面 学习内核及设备驱动开发时,你会发现学习理解计算机的基本硬件组成不仅仅是一个好的学习经历,有时也是理解控制器编程的必然要求。  我计划扩充这有一节。 好的,还记得IP寄存器保存有当前执行的指令的偏移地址,而CS 保存段地址吗? 当指令执行时,处理器到底发生了什么? 
  首先要计算要读取指令的绝对地址。在段:偏移模式下,绝对地址=  段地址*16+ 偏移地址。 或者说:绝对地址 = CS*16 + IP 。
  处理器将这个地址复制到地址总线上。要知道地址就是一系列电信号,每个电信号代表1 比特。这个位模式表示下一条指令的绝对地址的二进制表示。
  此后,处理器设置" 读内存" 位( 把这一位置1) 。这告诉内存控制器 我们要从内存读数据。 
  内存控制器 获得控制权。内存控制器从地址总线获得数据,并计算它在RAM 芯片上的实际位置。内存控制器更新这个位置,并把它放在到数据总线上。这是因为在控制总线上的" 读内存" 位被置1 。
  内存控制器重置控制, 这样处理器就知道了读内存是让我完成了。处理器从数据总线取值,并使用其内的数字逻辑电路" 执行" 它。这个"值"是机器指令的二进制表示,被一系列的电子脉冲编码。
  比如:指令mov ax, 0x4c00 ,0xB8004C 会被当道数据总线上。0xB8004C 是操作码(OPCode) 。每条指令都有一条对应的opcode 。对于i86 体系,上面的指令会变成opcode 0xB8004C 。我们可以把它变换为二进制形式,这样我们就能看看他们在电线上的情况。其中1 代表高,0 代表低:  指令变得越来越复杂,多数新的处理器实际上是根据其内部的指令级。对处理器来说这不新鲜--许多微控制器使用其内部的指令集以减少电路的复杂度。通常的这些是宏指令 和微指令 。
  宏指令是一个处理器用于将指令解码为微指令的抽象指令集。宏指令通常被电子工程师使用特殊的宏语言开发并保存在控制器内部的ROM 芯片上,使用宏汇编器编译。宏汇编器将宏语言汇编成更低级的语言--那些被控制器使用的语言: 微指令。
  微指令是电子工程师开发的非常低级的语言。微指令被控制器或处理器用来几秒指令--如0xB8004C (mov ax, 0x4c00)  指令. 
  使用处理器的算术逻辑单元(ALU) CPU 就得到了数--0x4C00 。并将它复制到AX ( 简单的位复制) 。 
  这个例子展示了它们是如何一起协同工作的。CPU 如何使用系统总线,内存控制器如何估价控制总线解码内存位置的。
  这是一个重要概念,软件端口在模拟工作方式下依赖于内存控制器。 理解系统结构对我们理解一切有很大的帮助,起码可以使你少犯错误。同样这也给你一种对硬件编程的直接指导--这是我们能做的一切。 
  你可能会想:等等,你承诺的C 内核在哪儿?好的,要知道C 在一定程度上是低级语言。通过行内汇编,你可以创建一个硬件接口层。C 就像C++ 一样只产生那些直接被x86 处理器直接执行的代码。记住:这里没有标准库,尽管你是在使用高级语言在编程,你仍然在一个非常底层的环境下工作
  当我们完成这些,我们就开始内核的编写。 我从没写过这样的教程。它包含了大量的信息,为了更好的理解也包含了少量代码,这很难写,你知道吗?
  我希望我做的够好。我希望包含内存映射,端口映射,x86 端口地址,所有的x86 寄存器,x86 内存映射,系统体系结构,IN/OUT  指令和它们的执行步骤--一步一步的。我们也看了硬件编程的基本步骤--我们会在后面做很多。
  下一章,我们会做个改变--欢迎来到32 位的世界! 我们会详细了解GDT --我们需要它来做这个切换。我也会警告你们每一步会发生的常见错误。如我先前所说的,在保护模式里,一点小错误就会是你的系统崩溃。 
  来吧 
  下次见 
分享到:
评论

相关推荐

    用C编写班级成绩管理系统

    设计课题一:班级成绩管理系统 一、 问题描述: 对一个有N个学生的班级,每个学生有M门课程。该系统实现对班级成绩的录入、显示、修改、排序、保存等操作的管理。 二、功能要求: 1、本系统采用一个结构体数组,...

    C语言入门经典(第4版)--源代码及课后练习答案

    读者基本不需要具备任何编程知识,即可通过本书从头开始编写自己的C程序。 作译者 作者  Ivor Horton是世界著名的计算机图书作家,主要从事与编程相关的咨询及撰写工作,曾帮助无数程序员步入编程的殿堂。他曾在...

    Access 2000数据库系统设计(PDF)---002

    搜索 139第7章 链接、导入和导出表 1417.1 从其他应用或者向其他应用移动数据 1417.2 理解Access如何处理其他数据库文件 格式的表 1427.2.1 识别PC数据库文件格式 1437.2.2 链接和导入外部 ISAM表 1437.2.3 用ODBC...

    Access 2000数据库系统设计(PDF)---018

    1326.5.5 使用复合准则 1346.5.6 将筛选保存为查询与筛选的加载 1356.6 定制数据表视图 1366.7 复制、导出和邮寄排序和筛选后的数据 1386.8 疑难解答 1396.9 现实世界—基于计算机的排序和搜索 139第7章 链接、导入...

    Access 2000数据库系统设计(PDF)---003

    搜索 139第7章 链接、导入和导出表 1417.1 从其他应用或者向其他应用移动数据 1417.2 理解Access如何处理其他数据库文件 格式的表 1427.2.1 识别PC数据库文件格式 1437.2.2 链接和导入外部 ISAM表 1437.2.3 用ODBC...

    Access 2000数据库系统设计(PDF)---011

    1326.5.5 使用复合准则 1346.5.6 将筛选保存为查询与筛选的加载 1356.6 定制数据表视图 1366.7 复制、导出和邮寄排序和筛选后的数据 1386.8 疑难解答 1396.9 现实世界—基于计算机的排序和搜索 139第7章 链接、导入...

    Access 2000数据库系统设计(PDF)---020

    1326.5.5 使用复合准则 1346.5.6 将筛选保存为查询与筛选的加载 1356.6 定制数据表视图 1366.7 复制、导出和邮寄排序和筛选后的数据 1386.8 疑难解答 1396.9 现实世界—基于计算机的排序和搜索 139第7章 链接、导入...

    Access 2000数据库系统设计(PDF)---009

    搜索 139第7章 链接、导入和导出表 1417.1 从其他应用或者向其他应用移动数据 1417.2 理解Access如何处理其他数据库文件 格式的表 1427.2.1 识别PC数据库文件格式 1437.2.2 链接和导入外部 ISAM表 1437.2.3 用ODBC...

    Access 2000数据库系统设计(PDF)---001

    搜索 139第7章 链接、导入和导出表 1417.1 从其他应用或者向其他应用移动数据 1417.2 理解Access如何处理其他数据库文件 格式的表 1427.2.1 识别PC数据库文件格式 1437.2.2 链接和导入外部 ISAM表 1437.2.3 用ODBC...

    Access 2000数据库系统设计(PDF)---012

    1326.5.5 使用复合准则 1346.5.6 将筛选保存为查询与筛选的加载 1356.6 定制数据表视图 1366.7 复制、导出和邮寄排序和筛选后的数据 1386.8 疑难解答 1396.9 现实世界—基于计算机的排序和搜索 139第7章 链接、导入...

    Access 2000数据库系统设计(PDF)---015

    1326.5.5 使用复合准则 1346.5.6 将筛选保存为查询与筛选的加载 1356.6 定制数据表视图 1366.7 复制、导出和邮寄排序和筛选后的数据 1386.8 疑难解答 1396.9 现实世界—基于计算机的排序和搜索 139第7章 链接、导入...

    Access 2000数据库系统设计(PDF)---027

    1326.5.5 使用复合准则 1346.5.6 将筛选保存为查询与筛选的加载 1356.6 定制数据表视图 1366.7 复制、导出和邮寄排序和筛选后的数据 1386.8 疑难解答 1396.9 现实世界—基于计算机的排序和搜索 139第7章 链接、导入...

    Access 2000数据库系统设计(PDF)---025

    1326.5.5 使用复合准则 1346.5.6 将筛选保存为查询与筛选的加载 1356.6 定制数据表视图 1366.7 复制、导出和邮寄排序和筛选后的数据 1386.8 疑难解答 1396.9 现实世界—基于计算机的排序和搜索 139第7章 链接、导入...

    Access 2000数据库系统设计(PDF)---026

    1326.5.5 使用复合准则 1346.5.6 将筛选保存为查询与筛选的加载 1356.6 定制数据表视图 1366.7 复制、导出和邮寄排序和筛选后的数据 1386.8 疑难解答 1396.9 现实世界—基于计算机的排序和搜索 139第7章 链接、导入...

    Access 2000数据库系统设计(PDF)---029

    1326.5.5 使用复合准则 1346.5.6 将筛选保存为查询与筛选的加载 1356.6 定制数据表视图 1366.7 复制、导出和邮寄排序和筛选后的数据 1386.8 疑难解答 1396.9 现实世界—基于计算机的排序和搜索 139第7章 链接、导入...

    Ext+JS高级程序设计.rar

    第7章 Store 192 7.1 Store的结构 192 7.2 Ext.data.Field 197 7.3 Ext.data.Record 198 7.4 ArrayReader、JsonReader和XmlReader 199 7.4.1 JsonReader 200 7.4.2 ArrayReader 200 7.4.3 XmlReader 201 7.5 Store的...

    Android高级编程--源代码

    第7章 地图、地理编码和基于位置的服务 207 7.1 使用基于位置的服务 207 7.2 使用Test Provider构建模拟器 208 7.2.1 更新模拟位置提供器中的位置 208 7.2.2 创建一个应用程序来管理Test Location Provider 209 ...

    Visual.Basic.2010.&.NET4.高级编程(第6版)-文字版.pdf

    第7章 测试驱动的开发 287 7.1 测试的内容和方式 288 7.2 visual studio中的tdd工具 290 7.3 单元测试过程 291 7.3.1 创建测试程序 291 7.3.2 运行测试程序 294 7.3.3 测试数据访问代码 295 7.3.4 ...

Global site tag (gtag.js) - Google Analytics