空间开通了

今天网站正式开通,放了个discuz的论坛,还不错,速度比较块,不过 刚才死活访问不到,估计又是域名解析速度慢,没办法,当时贪图小便宜找了一家小的网站注册,没想到速度 chaooooooooooooooooooooooooooooooooooooooooooooooo级慢,有时候还会报说找不到域名, kao~~~~~~~~~~~~~~~

想看的一些书

没有时间看,不过还是列出来,现在最希望的就是有那么一大段时间,用来看看书。按优先级列出来,不过是并行处理:

LINUX内核完全注释》这个赵郡是在太牛了,给我们这些浮躁的小青年们树立的一个极好的榜样,想学知识,首先心态要端正,http://www.oldlinux.org/ 作者的网站,很不错,有什么问题可以亲自询问赵老师,保证教到你完全明白为止。这本书也是我毕业设计要啃的书。目前这本书正在翻译成英文的,估计今年中旬出版。看这本书最好伴着《Unix操作系统设计》或者《操作系统概念》(英文的第七版已经出来了,国内还没得卖)

深入理解Linux内核(第二版)》不多说了(因为还没看-_-#),圣经级的。翻译的还可以(据说)

Linux内核源代码情景分析 上下》又是中国人的书,并且是难得的好书!看了内存管理一章,爽!不过还不够底层,所以转而看完全分析了,呵呵

UNIX环境高级编程》如果你用过linux,并且在linux上写过C程序,这本书你就一定听说过,大牛!尤其是作者W.Richard Stevens[已故],他的每一本书我都打算好好读一下。

UNIX网络编程卷2:进程间通信(第2版)(英文影印版)》Stevens系列

UNIX 网络编程(第2版)第1卷:套接口API和X/Open 传输接口API》Stevens系列

TCP/IP详解三卷》Stevens系列

深入理解计算机系统(修订版)》 底层的一些东西,“所有想写出更快、更可靠程序的开发人员必读之书!”

Linkers & Loaders》好像没有纸版本的,我把地址贴到了LinuxRen上,看看吧,对理解程序的执行很有帮助的。

中断没有被屏蔽,J2EE类的书籍可能要"陷入"进来

使用 /proc 文件系统来控制系统

详细介绍有关 /proc 中每个文件的用法和确切信息超出了本文所涉及的范围。要获得任何关于本文没有涉及到的 /proc 文件的其它信息,一个最佳来源就是 Linux 内核源代码本身,它包含了一些非常优秀的文档。对于系统管理员,/proc 中的以下文件较有用。这不意味着它是一份详尽的说明,而只是日常使用中便于查阅的参考。

/proc/scsi
/proc/scsi/scsi
作为系统管理员,需要了解的最有用内容是,在有热交换驱动器情况下,如何不重启系统就可以添加更多磁盘空间。假使不使用 /proc,您可以插入驱动器,但为了使系统识别新磁盘,必须随即重新引导系统。这里,可以用以下命令来使系统识别新的驱动器:

echo "scsi add-single-device w x y z" > /proc/scsi/scsi

为使该命令正常运行,必须指定正确的参数值 w、x、y 和 z,如下所示:
w 是主机适配器标识,第一个适配器为零(0)
x 是主机适配器上的 SCSI 通道,第一个通道为零(0)
y 是设备的 SCSI 标识
z 是 LUN 号,第一个 LUN 为零(0)

一旦将磁盘添加到系统中之后,可以挂装任何先前已格式化的文件系统,也可以开始对它进行格式化等。例如,如果不确定磁盘是什么设备,或者想检查任何先前已有的分区,则可以用如 fdisk -l 这样的命令来向您报告这方面的信息。

相反的,在不重新引导系统的情况下将设备从系统中除去的命令是:

echo "scsi remove-single-device w x y z" > /proc/scsi/scsi

在输入这条命令并将热交换 SCSI 磁盘从系统中除去之前,请确保首先卸下已从该磁盘安装的任何文件系统。

/proc/sys/fs/
/proc/sys/fs/file-max
该文件指定了可以分配的文件句柄的最大数目。如果用户得到的错误消息声明由于打开文件数已经达到了最大值,从而他们不能打开更多文件,则可能需要增加该值。可将这个值设置成有任意多个文件,并且能通过将一个新数字值写入该文件来更改该值。

缺省设置:4096

/proc/sys/fs/file-nr
该文件与 file-max 相关,它有三个值:
已分配文件句柄的数目
已使用文件句柄的数目
文件句柄的最大数目
该文件是只读的,仅用于显示信息。

/proc/sys/fs/inode-*
任何以名称“inode”开头的文件所执行的操作与上面那些以名称“file”开头的文件所执行的操作一样,但所执行的操作与索引节点有关,而与文件句柄无关。

/proc/sys/fs/overflowuid 和 /proc/sys/fs/overflowgid
这两个文件分别保存那些支持 16 位用户标识和组标识的任何文件系统的用户标识(UID)和组标识(GID)。可以更改这些值,但如果您确实觉得需要这样做,那么您可能会发现更改组和密码文件项更容易些。

缺省设置:65534

/proc/sys/fs/super-max
该文件指定超级块处理程序的最大数目。挂装的任何文件系统需要使用超级块,所以如果挂装了大量文件系统,则可能会用尽超级块处理程序。

缺省设置:256

/proc/sys/fs/super-nr
该文件显示当前已分配超级块的数目。该文件是只读的,仅用于显示信息。

/proc/sys/kernel
/proc/sys/kernel/acct
该文件有三个可配置值,根据包含日志的文件系统上可用空间的数量(以百分比表示),这些值控制何时开始进行进程记帐:
如果可用空间低于这个百分比值,则停止进程记帐
如果可用空间高于这个百分比值,则开始进程记帐
检查上面两个值的频率(以秒为单位)
要更改这个文件的某个值,应该回送用空格分隔开的一串数字。

缺省设置:2 4 30

如果包含日志的文件系统上只有少于 2% 的可用空间,则这些值会使记帐停止,如果有 4% 或更多可用空间,则再次启动记帐。每 30 秒做一次检查。

/proc/sys/kernel/ctrl-alt-del
该文件有一个二进制值,该值控制系统在接收到 ctrl+alt+delete 按键组合时如何反应。这两个值表示:
零(0)值表示捕获 ctrl+alt+delete,并将其送至 init 程序。这将允许系统可以完美地关闭和重启,就好象您输入 shutdown 命令一样。
壹(1)值表示不捕获 ctrl+alt+delete,将执行非干净的关闭,就好象直接关闭电源一样。

缺省设置:0

/proc/sys/kernel/domainname
该文件允许您配置网络域名。它没有缺省值,也许已经设置了域名,也许没有设置。

/proc/sys/kernel/hostname
该文件允许您配置网络主机名。它没有缺省值,也许已经设置了主机名,也许没有设置。

/proc/sys/kernel/msgmax
该文件指定了从一个进程发送到另一个进程的消息的最大长度。进程间的消息传递是在内核的内存中进行,不会交换到磁盘上,所以如果增加该值,则将增加操作系统所使用的内存数量。

缺省设置:8192

/proc/sys/kernel/msgmnb
该文件指定在一个消息队列中最大的字节数。

缺省设置:16384

/proc/sys/kernel/msgmni
该文件指定消息队列标识的最大数目。

缺省设置:16

/proc/sys/kernel/panic
该文件表示如果发生“内核严重错误(kernel panic)”,则内核在重新引导之前等待的时间(以秒为单位)。零(0)秒设置在发生内核严重错误时将禁止重新引导。

缺省设置:0

/proc/sys/kernel/printk
该文件有四个数字值,它们根据日志记录消息的重要性,定义将其发送到何处。关于不同日志级别的更多信息,请阅读 syslog(2) 联机帮助页。该文件的四个值为:
控制台日志级别:优先级高于该值的消息将被打印至控制台
缺省的消息日志级别:将用该优先级来打印没有优先级的消息
最低的控制台日志级别:控制台日志级别可被设置的最小值(最高优先级)
缺省的控制台日志级别:控制台日志级别的缺省值

缺省设置:6 4 1 7

/proc/sys/kernel/shmall
该文件是在任何给定时刻系统上可以使用的共享内存的总量(以字节为单位)。

缺省设置:2097152

/proc/sys/kernel/shmax
该文件指定内核所允许的最大共享内存段的大小(以字节为单位)。

缺省设置:33554432

/proc/sys/kernel/shmmni
该文件表示用于整个系统共享内存段的最大数目。

缺省设置:4096

/proc/sys/kernel/sysrq
如果该文件指定的值为非零,则激活 System Request Key。

缺省设置:0

/proc/sys/kernel/threads-max
该文件指定内核所能使用的线程的最大数目。

缺省设置:2048

/proc/sys/net
/proc/sys/net/core/message_burst
写新的警告消息所需的时间(以 1/10 秒为单位);在这个时间内所接收到的其它警告消息会被丢弃。这用于防止某些企图用消息“淹没”您系统的人所使用的拒绝服务(Denial of Service)攻击。

缺省设置:50(5 秒)

/proc/sys/net/core/message_cost
该文件存有与每个警告消息相关的成本值。该值越大,越有可能忽略警告消息。

缺省设置:5

/proc/sys/net/core/netdev_max_backlog
该文件指定了,在接口接收数据包的速率比内核处理这些包的速率快时,允许送到队列的数据包的最大数目。

缺省设置:300

/proc/sys/net/core/optmem_max
该文件指定了每个套接字所允许的最大缓冲区的大小。

/proc/sys/net/core/rmem_default
该文件指定了接收套接字缓冲区大小的缺省值(以字节为单位)。

/proc/sys/net/core/rmem_max
该文件指定了接收套接字缓冲区大小的最大值(以字节为单位)。

/proc/sys/net/core/wmem_default
该文件指定了发送套接字缓冲区大小的缺省值(以字节为单位)。

/proc/sys/net/core/wmem_max
该文件指定了发送套接字缓冲区大小的最大值(以字节为单位)。

/proc/sys/net/ipv4
所有 IPv4 和 IPv6 的参数都被记录在内核源代码文档中。请参阅文件 /usr/src/linux/Documentation/networking/ip-sysctl.txt。

/proc/sys/net/ipv6
同 IPv4。

/proc/sys/vm
/proc/sys/vm/buffermem
该文件控制用于缓冲区内存的整个系统内存的数量(以百分比表示)。它有三个值,通过把用空格相隔的一串数字写入该文件来设置这三个值。
用于缓冲区的内存的最低百分比
如果发生所剩系统内存不多,而且系统内存正在减少这种情况,系统将试图维护缓冲区内存的数量。
用于缓冲区的内存的最高百分比

缺省设置:2 10 60

/proc/sys/vm/freepages
该文件控制系统如何应对各种级别的可用内存。它有三个值,通过把用空格相隔的一串数字写入该文件来设置这三个值。
如果系统中可用页面的数目达到了最低限制,则只允许内核分配一些内存。
如果系统中可用页面的数目低于这一限制,则内核将以较积极的方式启动交换,以释放内存,从而维持系统性能。
内核将试图保持这个数量的系统内存可用。低于这个值将启动内核交换。

缺省设置:512 768 1024

/proc/sys/vm/kswapd
该文件控制允许内核如何交换内存。它有三个值,通过把用空格相隔的一串数字写入该文件来设置这三个值:
内核试图一次释放的最大页面数目。如果想增加内存交换过程中的带宽,则需要增加该值。
内核在每次交换中试图释放页面的最少次数。
内核在一次交换中所写页面的数目。这对系统性能影响最大。这个值越大,交换的数据越多,花在磁盘寻道上的时间越少。然而,这个值太大会因“淹没”请求队列而反过来影响系统性能。

缺省设置:512 32 8

/proc/sys/vm/pagecache
该文件与 /proc/sys/vm/buffermem 的工作内容一样,但它是针对文件的内存映射和一般高速缓存。

使内核设置具有持久性
这 里提供了一个方便的实用程序,用于更改 /proc/sys 目录下的任何内核参数。它使您可以更改运行中的内核(类似于上面用到的 echo 和重定向方法),但它还有一个在系统引导时执行的配置文件。这使您可以更改运行中的内核,并将这些更改添加到配置文件,以便于在系统重新引导之后,这些更 改仍然生效。

该实用程序称为 sysctl,在 sysctl(8) 的联机帮助页中,对这个实用程序进行了完整的文档说明。sysctl 的配置文件是 /etc/sysctl.conf,可以编辑该文件,并在 sysctl.conf(8) 下记录了该文件。sysctl 将 /proc/sys 下的文件视为可以更改的单个变量。所以,以 /proc/sys 下的文件 /proc/sys/fs/file-max 为例,它表示系统中所允许的文件句柄的最大数目,这个文件被表示成 fs.file-max。

这个示例揭示了 sysctl 表示法中的一些奇妙事情。由于 sysctl 只能更改 /proc/sys 目录下的变量,并且人们始终认为变量是在这个目录下,因此省略了变量名的那一部分(/proc/sys)。另一个要说明的更改是,将目录分隔符(正斜杠 /)换成了英文中的句号(点 .)。

将 /proc/sys 中的文件转换成 sysctl 中的变量有两个简单的规则:
去掉前面部分 /proc/sys。
将文件名中的正斜杠变为点。

这两条规则使您能将 /proc/sys 中的任一文件名转换成 sysctl 中的任一变量名。一般文件到变量的转换为:

/proc/sys/dir/file --> dir.file
dir1.dir2.file --> /proc/sys/dir1/dir2/file

可以使用命令 sysctl -a 查看所有可以更改的变量和其当前设置。

用 sysctl 还可以更改变量,它所做的工作与上面所用的 echo 方法完全一样。其表示法为:

sysctl -w dir.file="value"

还是用 file-max 作为示例,使用下面两种方法中的一种将该值更改为 16384:

sysctl -w fs.file-max="16384"

或者:

echo "16384" > /proc/sys/fs/file-max

不要忘记 sysctl 不会将所做的更改添加到配置文件中;这要您用手工来完成。如果您希望在重新引导之后,前面所做的更改仍然有效,则必须维护这个配置文件。

注:不是所有的分发版都提供 sysctl 支持。如果您的特定系统属于这种情况,则可以用上面所描述的 echo 和重定向方法,将这些命令添加到启动脚本中,这样系统每次引导时,都会执行它们。

用于设置系统的命令
在 系统运行的同时更改其它非内核系统参数,而且在不重新引导系统的情况下使这些设置生效,这种做法是可能的。在 /etc/init.d 目录中列出了包含这些参数的文件,它们主要按服务、守护程序和服务器来分类。由于越来越多各方面的脚本可以罗列在这个目录下,所以这里不可能讨论所有各种 配置。不过,下面列举了一些示例,这些示例讨论了如何在不同的 Linux 分发版上操作 /etc/init.d 下的脚本。这里的示例可能很有用,其中讨论了更改守护程序,然后在不重新引导系统的情形下重新装入配置:
更改 Web 服务器配置,然后重新装入 Apache
除去不需要的 inetd 登录服务
操作网络设置
通过 NFS 导出新的文件系统
启动/停止防火墙

首先,常见的方法是,直接通过 /etc/init.d 中的脚本来操作系统服务。这些脚本用参数来操作它们所控制的服务;可以通过输入脚本名但不带任何参数这种方法来查看有哪些有效的选项。常见的参数有:
start:启动已停止的服务
stop:停止正在运行的服务
restart:停止正在运行的服务,然后再重启该服务;它将启动已停止的服务
reload:在不中断任何连接的情况下,重新装入服务配置
status:报告服务处于运行状态,还是停止状态

例如,下面这条命令将在不终止任何已连接的用户会话的情形下,重新装入 xinetd 配置(如果您更改了 /etc/xinetd.conf,那么这条命令很有用):

/etc/init.d/xinetd reload

Red Hat 提供了 service 这条命令,它可以为您操作服务。service 命令提供的功能与输入脚本名本身的功能一样。它的语法如下所示:

service script-name [parameter]

例如:

service xinetd reload

SuSE 也提供名为 rc 的命令。该命令类似于上面的 service 命令,但该命令与脚本名之间没有空格。它的语法如下所示:

rc{script-name} parameter

例如:

rcapache start

与 更改内核参数类似,一旦重新引导系统,则对这些服务的更改将会丢失。现在越来越多的分发版开始采用 chkconfig 命令,它管理在各种运行级别下(包括引导时)启动的服务。在撰写本文时,chkconfig 命令的语法会因 Linux 版本的不同而略有差异,不过如果输入不带任何参数的命令 chkconfig,则会显示一个如何使用该命令的列表。也可以通过 chkconfig(8) 的联机帮助页找到更多有关 chkconfig 的信息。
http://www-900.ibm.com/developerWorks/cn/linux/l-adfly/index.shtml'

Linux配置管理

By 明明

前言:
很久没有写过文章了,最近收到不少朋友来信,提及了有关优化配置和一些新的安全问题,在此我想和大家浅显讨论一下这些问题,有什么不准确和有更好的方式,请给我来信共同讨论提高。
在网上看到不少有关linux优化方面的好文章,在此我也不赘述这些文章了,我只想从我自己的体会来谈谈这方面的问题。
作为一个系统管理员,我下面说的都是基于服务器应用的linux来谈的,由于个人电脑上使用linux也许不是像服务器上一样,优先追求安全和稳定,因此个人电脑使用的朋友只做个参考吧
本文提及的系统,如没有特别声明,均采用redhat公司的redhat linux系统。

关于优化
说起优化,其实最好的优化就是提升硬件的配置,例如提高cpu的运算能力,提高内存的容量,个人认为如果你考虑升级硬件的话,建议优先提高内存的容量,因为一般服务器应用,对内存的消耗使用要求是最高的。当然这都是题外话了。
这里我们首要讨论的,是在同等硬件配置下(同一台服务器,不提升硬件的情况下)对你的系统进行优化。
作为系统管理员,我认为,首先我们要明确一个观点:在服务器上作任何操作,升级和修改任何配置文件或软件,都必须首要考虑安全性,不是越新的东西就越 好,这也是为什么linux管理感觉上和windows有所不同的地方,windows首先推荐大家去使用它的最新版本软件和操作系统,其实我个人认为这 是一种商业行为,作为从系统管理上来讲,这是很不好的,使用新的软件和系统可能带来新的问题,有些甚至是致命的。

因此,作为管理,我们还是应该考虑稳定的长期使用的软件版本来作为我们的版本,具体的好处我就不多说了。相信作为管理员的你应该知道的。

其实个人使用的linux最直接的一个优化就是升级内核,自己编译的内核是根据自己的系统编译而来,将得到最大的性能和最小的内核。

但是,服务器就不太一样了,当然我们也希望每一台服务器都是自己手工编译的内核,高效而精巧。但是实际和愿望是有差距的,试想一下,如果你管理100来台 linux主机,而每一台也许配置都不一样,那编译内核的一个过程将是一个浩大工程,而且从实际考虑,工作量大得难以想象。我想你也不会愿意做这种事情 吧。因此,个人建议,采用官方发布的内核升级包是很好的选择。

首先,我们对新安装的系统,将做一系列升级,包括软件和内核,这是很重要的步骤,(这方面的详细情况欢迎察看我另一篇关于升级方面的文章)。

在升级好所有软件后,基本的防火墙和配置都做好以后,我们开始优化一些细节配置,如果你是老系统,那么在作本问题及的一些操作和优化你系统之前,务必被备份所有数据到其他介质。

1、虚拟内存优化
首先查看虚拟内存的使用情况,使用命令

# free

查看当前系统的内存使用情况。
一般来说,linux的物理内存几乎是完全used。这个和windows非常大的区别,它的内存管理机制将系统内存充分利用,并非windows无论多大的内存都要去使用一些虚拟内存一样。这点需要注意。

Linux下面虚拟内存的默认配置通过命令

# cat /proc/sys/vm/freepages

可以查看,显示的三个数字是当前系统的:最小内存空白页、最低内存空白页和最高内存空白。
注意,这里系统使用虚拟内存的原则是:如果空白页数目低于最高空白页设置,则使用磁盘交换空间。当达到最低空白页设置时,使用内存交换(注:这个是我查看一些资料得来的,具体应用时还需要自己观察一下,不过这个不影响我们配置新的虚拟内存参数)。
内存一般以每页4k字节分配。最小内存空白页设置是系统中内存数量的2倍;最低内存空白页设置是内存数量的4倍;最高内存空白页设置是系统内存的6倍。这些值在系统启动时决定。
一般来讲在配置系统分配的虚拟内存配置上,我个人认为增大最高内存空白页是一种比较好的配置方式,以1G的内存配置为例:
可将原来的配置比例修改为:
2048 4096 6444
通过命令

# echo "2048 4096 6444" > /proc/sys/vm/freepages

因为增加了最高空白页配置,那么可以使内存更有效的利用。

2、硬盘优化
如果你是scsi硬盘或者是ide阵列,可以跳过这一节,这节介绍的参数调整只针对使用ide硬盘的服务器。

我们通过hdparm程序来设置IDE硬盘,
使用DMA和32位传输可以大幅提升系统性能。使用命令如下:

# /sbin/hdparm -c 1 /dev/hda

此命令将第一个IDE硬盘的PCI总线指定为32位,
使用 -c 0参数来禁用32位传输。

在硬盘上使用DMA,使用命令:

# /sbin/hdparm -d 1 /dev/hda

关闭DMA可以使用 -d 0的参数。

更改完成后,可以使用hdparm来检查修改后的结果,使用命令:

# /sbin/hdparm -t /dev/had

为了确保设置的结果不变,使用命令:
# /sbin/hdparm -k 1 /dev/hda

Hdparm命令的一些常用的其他参数功能

-g 显示硬盘的磁轨,磁头,磁区等参数。
-i 显示硬盘的硬件规格信息,这些信息是在开机时由硬盘本身所提供。
-I 直接读取硬盘所提供的硬件规格信息。
-p 设定硬盘的PIO模式。
-Tt 评估硬盘的读取效率和硬盘快取的读取效率。
-u 在硬盘存取时,允许其他中断要求同时执行。
-v 显示硬盘的相关设定。

3、其他优化
关闭不需要的服务,关于系统自动启动的服务,网上有很多资料,在此我就不赘述了;

关于安全
1、安全检查
作为一个系统管理员来说,定期对系统作一次全面的安全检查很重要的,最近遇到一些朋友来信说出现了一些莫名其妙的问题,例如最大的一个问题就是明显感觉网 络服务缓慢,这极有可能是被攻击的现象。实践证明,无论是那种系统,默认安装都是不安全的,实际不管你用windows也好,linux,bsd或其他什 么系统,默认安装的都有很多漏洞,那怎么才能成为安全的系统呢,这正是我们系统管理人员需要做的事情。配置配置再配置。任何系统,只要细心的配置,堵住已 知的漏洞,可以说这个系统是安全的,其实并非很多朋友说的那样,安装了系统,配置了防火墙,安装了杀毒软件,那么就安全了,其实如果对系统不作任何安全设 置,那就等于向黑客敞开一扇纸做的大门,数十分钟就能完全控制!
这并非骇人听闻。
作为linux系统,同样存在很多漏洞,黑可能利用这些漏洞控制你的整个系统,要防止这些问题,我们需要做以下步骤:
1、 升级系统中所有软件包的最新版本;
2、 设置较为强壮的防火墙;
3、 定期检查关键记录文件,配置杀毒软件
4、 多关心一下发布安全信息警告的网站,掌握一些最新的病毒和黑客程序的特点,这些都利于系统的正常运作。
这篇文章主要以优化为主,为了配合这一主题,安全部分我们只讨论一下日常的一些维护工作。
除了上面列出的4条是管理员必修之课外,对一些linux系统细节的维护也很重要。
包括:
1、 配置日志轮训工具,定期下载备份日志,是个非常好的习惯,这样不但能减少日志的消耗的磁盘空间,提高系统效率,更能及时发现问题,linux下有些很好的系统日志分析器,能直接提取日志中的特殊项目,省去了阅读日志的烦恼;
2、 使用命令lsof ?i ,netstat ?a ,ps ?e等命令,定期检查系统服务端口监听等情况,也可制作一个定期执行的脚本,将这些命令定期执行后发到邮箱中;
3、 定期检查root用户的history列表,last列表,vipw用户列表是否正常;
4、 定期备份文件,用tar命令就能很好的备份了,当然需要下载这些备份并转移介质;

如一点发现有任何特别的没见过的情况或端口,那么要引起足够的重视,切勿因小失大。

以上是我对linux系统安全和优化的一些浅显认识,希望大家都能安全高效的使用linux为你的工作生活带来方便。

80386 ASM程序设计基础–80386实模式下编程IV

主要介绍系统地址寄存器和控制寄存器以及在程序中实方式下与保护方式下的切换
80386处理器新增了一组控制寄存器CR0,CR1,CR2,CR3和一组系统地址寄存器GDTR, LDTR,IDTR,TR,它们全部都是32位的。CR0包含了指定处理器工作方式的控制位,CR1保留未使用,CR2和CR3由分页管理部件使用, CR0中的5~30位和CR3中的0~11位必须为0,分别介绍如下:

___________________________________________________________________________
|PG|0 |0 |0 |0 |0 |0 |0 |0 |0 |0 |0 |0 |0 |0 |0 |0 |0 |0 |0 |ET|TS|EM|MP|PE| CR0
|__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|_ |__|
| Reserved | CR1
|__________________________________________________________________________|
| 页故障线性地址 | CR2
|__________________________________________________________________________|
| 高20位页表的起始物理地址 |低12位为0 | CR3
|_____________________________________________________|____________________|

PE标记用于指定处理器的工作模式。PE=0,处理器处于实模式;PE=1,处理器处于保护模式
PG标记用于指定处理器是否启用分页管理机制。PG=0,禁用分页管理机制,此时由分段管理部件产生的 线性地址就是物理地址。;PG=1,启用分页管理机制,此时由分段管理部件产生的线性地址须再经过分页管理机制才能得到最终的物理地址。
MP,EM,TS,ET用于控制浮点协处理器的操作。
CR2和CR3控制寄存器由分页管理机制使用。CR2用于发生页异常时报告出错信息。当发生页故障时, 处理器会将当前的线性地址保存在CR2。CR3用于保存页表在内存中的起始物理地址,由于页表是对齐的,所以仅高20位有效,低12位必须为0。
全局描述符表GDT,局部描述符表LDT和中断描述符表IDT在保护模式下是特殊的段,也就是说处理器 将这些线性表当段一个特殊的段来处理,它包含了对段机制所用的重要数据。为了能够更快速地进位这些段,386处理器采用特殊的寄存器保存这种段的基地址和 界限,这种寄存器就是系统地址寄存。在80386下系统地址寄存器有:全局描述符表寄存器GDTR,局部描述符表寄存器LDTR,中断描述符表IDTR, 任务状态段寄存器TR。全局描述符表寄存器GDTR,长度为48位,其中高32位是基址,低16位含界限。由于GDT本身不可以由GDT内的描述符来描 述,所以处理使用GDTR寄存器为GDT这样的特殊段提供一个伪描述符,即是说:

| |
|________________| 全局描述符表寄存器GDTR
| | ________________________________
| GDT |______| | |
|________________|______| 32位基址 | 16界限 |
| | |___________________|___________|
| |
因为段选择子只用了13位来表描述表中的索引号,即是说最多可以有8192个描述符,而每个描述符是8 个字节。而在80386处理器下将全局描述表作为一个特殊的系统段,那么段的界限实际上就是8192*8,所以段的界限用16位就可以了。通常情况下,如 果GDT有N个描述符,那么GDT的段界限为N*8-1,这个伪描述符也就是全局描述符寄存器内容可以用结体定义成:
PreDesc STRUCT
BASE32 DD 0
LIMIT16 DW 0
PreDesc ENDS
局部描述表寄存器LDTR规定了当前任中使用的局部描述表LDT,LDTR类似于一个段寄存,它的长度 为32位,一个16位的寄存器和对程序员来讲不可见的高速缓冲存储器。每一个任务的局部描述符表作为一个特殊的系统段,它由定义在全局描述符表GDT中的 描述符来描述,前面已提到过一个任务只能有一张全局描述符表GDT和一张中断描述符表IDT,但可以有多张局部描述行表LDT,而每一张局部描述符表都由 定义在GDT中的描述符来确定。通常将描述LDT的选择子装入到LDTR,LDTR根据选择子从全局描述符表中取出对应的描述符,并把LDT的基址及界限 信息保存到对程序员来讲不可见的高速缓冲存储器,随后就可以对LDT进行访问。当前任务中的所有段都由GDT中的描述符来描述。
_________ ____________________________________________________
| |______| | | |
| LDTR |______| 32位基址 | 32位界限 |12位属性 |
|_______| |___________________|_____________________|_________|

中断描述符表和全局描述符表一样,长度为48位。32位段基址和16位界限。
如何从实式模式切换到保护模式下呢?通常来讲,要两个步骤:1.作好切换到保护模式下的准备;2.切换 到保护模式。主要准备工作就是建立全局描述符表,并使GDTR指向GDT,因为切换到保护模式下,至少要将代码段的选择子装入到CS中,看程序片段:

;定义好描述符的结构
DESCRIPTOR STRUCT
LIMIT DW 0;段界限
BASEL DW 0;段基址的低16位
BASEM DB 0;段基址的16~23位
ATTRIBUTES DW 0;段属性
BASEH DB 0;段基址的高8位,24~31
DESCRIPTOR ENDS
;定义好伪描述符
PDESC STRUCT
LIMIT DW 0
BASE DD 0
PDESC ENDS
;通常要定义一个段间跳转的宏,这样的话就可以保证在进入保护模式时将代码段的选择子装入到CS寄存器
JUMP MACRO selector,offset
DB 0EAH
DW offsetv;段偏移
DW selector;段选择子
ENDM
;打开A20地址线
PUSH AX
IN AL,92H
OR AL,2
OUT 92H,AL
POP AX
;关闭A20地址线
PUSH AX
IN AL,92H
AND AL,0FDH
OUT 92H,AL
POP AX
;切换到保护模式下,将CR0寄存中的第0位置1
MOV EAX,CR0
OR CR0,1
MOV CR0,EAX
其它的部分就要根据具体的应用来写, 下面的例子是如何在保护模下访问820000H单元开始的内容,看程序:
.386P
data segment use16
GDT LABEL BYTE;定义全局描述符表
DUMMY DESCRIPTOR;空描述符,它有特定义的含义,空描述符可以保证GDT中的第1个描述符永远不会被访问
CODE DESCRIPTOR;代码段的描述符
CODE_SEL=CODE-GDT;代码段描述符的选择子
DATAS DESCRIPTOR;源数据段描述符,即820000H
DATAS_SEL=DATAS-GDT;源数据段选择子
GDTLEN=$-GDT
VGDTR DESCRIPTOR
data ends

code segment use16
assume cs:code,ds:data
start:
mov ax,data
mov ds,ax
mov bx,16
mul bx;设置全局描述表GDT基址,因为现在还处在实模式下,所以段地址要左移4位
add ax,offset GDT
adc dx,0
mov word ptr VGDTR.BASE,ax;设置全局描述符表寄存器GDTR的内容
mov word ptr VGDTR.BASE+2,dx
;设置代码段描述符
mov ax,cs
mul bx
mov CODE.BASEL,ax
mov CODE.BASEM,dl
mov CODE.BASEH,dh
;以下部分你可以根据实际的应用来编写
.........
...........
;加载GDTR
LGDT QWORD PTR VGDTR
CLI;关中断
;打开A20地址线
;切换到保护模式
mov eax,cr0
or eax,1
mov cr0,eax
JUMP ,;清指令预取队列,真正进入保护模式
........
........
virutal:
;add your code here according to your needs
............
;回到实模式
;关闭A20地址线
STI;开中断
code ends
end start
上述的程序片段是随手写的,可根据需要自已加以调整,不过有点要说明。
a.通常来讲,从实模式下切换到保护模式下只要将CR0寄存器中的最低位设置为1就可以了。但是,此时CS的内容仍然是实模式下的内容,所以加了一条段间跳转指令JUMP
,,执行完这条指令就可以将代码段选择子CODE_SEL装入到段寄存器CS中,同时也可以刷新指令预取队列。
b.LGDT QWORD PTR VGDTR,该指令的功能是将VGDTR的内容装入到全局描述符表寄存器GDTR中。
c.上面的代码片段中并没有建立中断描述符表IDT,这样的话就要求整个程序必须运行在关中断情况下进行。
d.为了访问1M以上的存储单元,应该打开A20地址线,在WINDOWS下只需加载HIMEM.SYS就可以了。 能不能进入保护模式只与是否加载HIMEM.SYS有关,与处理器工作在实方式下还是在保护方式下无关。也就是说,只要加载HIMEM.SYS,就算处理 器当前处在实模式下,A20地址线关闭,处理器也一样可以进入保护模式。
下集预告:
80386ASM程序设计基础(十二)---任务切换
80386ASM程序设计基础(十三)---80386中断和异常
80386ASM程序设计基础(十四)---分页管理机制
80386ASM程序设计基础(十五)---V86模式
敬请关注,谢谢。

80386 ASM程序设计基础–80386实模式下编程III

主要介绍段描述符,段选择子
在保护模式下,段是实现虚拟地址到线性地址转换的基础。在保护方下,每个段有三个参数:段基址,段界 限,段属性。段基址规定了线性地址空间中段的开始地址,段基址长度为32位,所以任何一个段都可以从32位线性地址空间中的任何一个字节开始,这一点和实 式方式不同,实式方式下要求段的边界必须被16整除。段界限规定段的大小,段界限用20位表示,而且段界限可以是字节或4K为单位,这个称为段的粒度。当 段界限以字节为单位时,那么段的范围是1字节至1M字节;当段界限是以4K字节为单位时,那么段的范围是4K至4G。段的界限同时也是用来校验偏移地址的 合法性,比如说段A的基址为00123456H,段界限为1000H,如果段界限以字节为单位,那么段的范围是00123456H-00124456H; 如果段界限以4K字节为单位,那么段的范围是00123456H-00223456H。事实上,段的界限也可以用来校验偏移地址的合法性,上面的例子中界 限为1000H,那么偏移地址的范围就是0-1000H,如果偏移地址不在这个范围内那就会引起异常。需要说明的是,数据段有点特殊,因为数据段的偏移范 围不仅仅是由段界限来决定,还要由段的扩展方向(Extension Direction)来决定,因为要照顾到堆栈段(堆栈段是一种特殊的数据段,它是向低端地址扩展的),如果段界限为Limit,段的扩展方向为向高端地 址扩展的话,那么我们可以断定它是一普通的数据段,0-Limit是有效的偏移范围,而Limit以上属于无效的偏移范围;如果段界限为Limit,段的 扩展方向为向低端地址扩展的话,那么可以断定它是一堆栈段,此时0-Limit是无效的偏移范围,Limit以上则属于有效的偏移范围,正好和向高端地址 扩展的普通数据段相反。除了堆栈段以外,其它的段均是自然向高端扩展。
段基址,段界限及段属性这三个参数在保护模式下用描述符来描述,每个描述符的长度为8个字节,每个段都有一个对应的描述符。在保护模式下有三种描述符:存储段描述符,系统段描述符,门描述符。
A.存储段描述符:存储段是指程序直接执行的代码段和数据段,存储段描述符是用来描述存储段的,也可以说是用来描述代码和数据段的,它的长度为8个字节,该描述符结构示意图:

第7字节 第6字节 第5字节 第4字节 第3字节 第2字节 第1字节 第0字节
|--------|------------------|-----------------------------|-----------------|
|段基址的| | | |
|高8位 |Segment Attributes| 段基址的低24位 | 段界限的低16位 |
| 24~31 | 段属性,占用两 | 0~23 | 0~15 |
| | 个字节 | | |
|--------|------------------|-----------------------------|-----------------|
| |
| |
_________| |_____________________________
| 15 14 13 12 11 8 7 6 5 3 0|
|---|---|---|---|-------------|---|--- -|---|------------|
| G | D |0 |AVL|段界限的高4位| P | DPL |DT | TYPE |
|---|---|---|---|--- ---------|---|-----|---|------------|

段基址和段界限都被安排在描述符的两个域中,主要是来看段的属性:
a.G(第15位),这是段界限粒度,即是说段界限到底是以字节为单还是以4K字节为单位。G=0表示段界限是字节,G=1表示段界限为4K字节。
b.D(第14位),D是一个很特殊的位,在描述可执行段,向低扩展数据段或者由SS寄存器寻址的段。 在描述可执行段的描述符中,D位决定了指令使用的地址及操作数据默认的大小,D=1表示默认情况下使用32位地址及32位或8位操作数,这样的代码段称为 32位代码段;D=0表示默认情况下使用16位地址及16位操作数或8位操作数,这样的代码段称为16位代码段;在向低扩展的数据段中,D=1表示段的上 部界限为4G,D=0表示段的上部界限为64K;在描述由SS寄存器寻址的段中,该位决定使用隐式的堆栈访问指令使用何种堆栈指针寄存器。D=1表示使用 32位堆栈指针寄存器ESP,D=0表示使用16位堆栈指针寄存器SP,隐式的堆栈访问指令指的是那些指令中没有明显对SP或ESP进行操作的指令,比如 说PUSH,POP,PUSHA,POPA,PUSHAD,POPAD都属于隐式的堆栈访问指令。
c.0(第13位),这一位恒为0,为80386以后的处理器保留的。
d.AVL(第12位),软件可利用位,主要是为了保持和以后的处理兼容。
e.第11位到第8位是段界限的高4位。
f.P(第7位),存在位,P=1表示描述符对转换地址有效。P=0表示描述符对转换地址无效,如果使用该描述符将会引起异常。
g.DPL(Descriptor Privelege Level)描述符特权级,共2位,它规定了所述段的特权级别,用于特权检查,以决定是否能对该段进行访问。
h.DT(Descriptor Type)描述符的类型,DT=0表示存储段描述符,DT=0表示系统段描述符和门描述符。
i.TYPE,共4位,说明存储段的具体属性:
TYPE0:指示描述符是否被访问,用A标记,A=0表示描述符未被访问,A=1表示描述符已被访问。
TYPE1:根据TYPE3来确定。
TYPE2:根据TYPE3来确定。
TYPE3:指示描述符所描述的段是数据段还是代码段,用E标记。E=0表示是不可执行段,是数据段,对应的描述符也就是数据段描述符。E=1表示是可执行段,也就是代码段,对就的描述符也就是代码段描述符。
如果TYPE3=0,也就是说描述符是数据段描述符,那么TYPE1指示该数据段是否可写,用W标记。 W=0表示对应的数据段不可写,只读。W=1表示对应的数据段可写。TYPE2则指示数据段的扩展方向,用ED标记。ED=0表示向高端扩展,ED=1表 示向低端扩展。
如果TYPE3=1,也就是说描述符是代码段描述符,那么TYPE1指示该代码段是否可读,用符号R标 记。R=0表示对应的代码段不可读,只能执行,R=1表示对应的代码可读可执行。TYPE2则指示所描述的代码段是否是一致代码段,用C表示。C=0表示 代码段不是一致代码段,C=1表示是一致代码段。
TYPE3-TYPE0这四位可以列成一个表:
___________________________________________________________________________________
|0000 |只读 |
|_____|____________________________________________________________________________|
|0001 |只读,已访问 |
|_____|____________________________________________________________________________|
|0010 |可读,可写 |
|_____|____________________________________________________________________________|
|0011 |读写,已访问 |
|_____|____________________________________________________________________________|
|0100 |只读,向低扩展 |
|_____|____________________________________________________________________________|
|0101 |只读,向低扩展 |
|_____|____________________________________________________________________________|
|0110 |读/写,向低扩展 |
|_____|____________________________________________________________________________|
|0111 |读/写,向低扩展,已访问 |
|_____|____________________________________________________________________________|
|1000 |只执行 |
|_____|____________________________________________________________________________|
|1001 |只执行,已访问 |
|_____|____________________________________________________________________________|
|1010 |可执行,可读 |
|_____|____________________________________________________________________________|
|1011 |可执行,可读,已访问 |
|_____|____________________________________________________________________________|
|1100 |只执行,一致代码段 |
|_____|____________________________________________________________________________|
|1101 |只执行,一致代码段,已访问 |
|_____|____________________________________________________________________________|
|1110 |可执行,可读,一致代码段 |
|_____|____________________________________________________________________________|
|1111 |可执行,可读,一致代码段,已访问 |
|_____|____________________________________________________________________________|
存储段描述符的结构可以这样定义:
DESCRIPTOR STRUCT
Segment_LimitL16 DW 0;段界限的低16位
Segment_BaseL16 DW 0;段基址的低16位
Segment_BaseM8 DB 0;段基址的中间8位
Segment_BaseH8 DB 0;段基址的高8位
Segment_Attributes DW 0;段属性
DESCRIPTOR ENDS
一个任务有多个段,每个段都有一个描述符。因此在80386下,为了方便管理这些段描述符,将描述符组 成一个线性表,称之为描述符表。在80386下有三种描述符表:GDT(Global Descriptor Table),LDT(Local Descriptor Table),IDT(Interrupt Descriptor Table)。在整个系统中全局描述符表GDT和中断描述符表只有一张,局部描述符表可以由若干张。每个描述符表都形成一个特殊的16位数据段,这样的特 殊数据段最多可以有8192个描述符,具体使用哪一个段描述符,由段的选择子来确定。每个任务都有自已的局部描述符表LDT,它包含自已的代码段,数据 段,堆栈段,也包含该任务使用的一些门描述符。随着任务的切换,LDT也跟着切换。GDT包含每一个任务都可能或可以访问的段的描述符,通常包含描述操作 系统所用的代码段,数据段以及堆栈段的描述符,也包含描述任务LDT的描述符。在任务切换时,并不切换GDT。一个任务的整个虚拟地址空间可以分为相等的 两半,一半空间的描述符在全局描述符表GDT中,一半空的描述符在局部描述符表LDT中。由于全局描述符表和局部描述符表都可以包含最多为8192个描述 符,而每个描述符所描述的段的最大长度为4G,因此最大的虚拟地址空间为:8192*4G*2=64TB。
段选择子用来确定使用描述符表中的哪一个描述符。实式模式下逻辑地址由段地址*16再加上段内偏移地 址;保护模式下虚拟地址空间由段选择子和段内偏移来确定,和实式模式比较,段选择子代替了段值,实际上通过段选择子就可以确定了段基址。选择子的高13位 是描述符表中的索引号,用来确定描述符,因为是13位,所以说最多可以有2的13次方8192个描述符,索引号:0-8191。标记TI指示是从全局描述 符中读取描述符还是从局部描述符表中读取描述符。TI=0指示是从全局描述符表中读取描述符,TI=1指示从局部描述符表读取描述符。RPL表示请求特权 级,用于特权检查。假设段选择子为88H,则表示请求的特权级别是0,从全局描述表中读取描述表,描述符的索引号为11H。有一个特殊的选择子称为空选择 子,它的Index=0(即高13位为0),TI=0,RPL则可以为任意值。当用空选择子对存储器进行访问,会出现异常。空选择子对应于全局描述表中的 第0个描述符,因此全局描述符表中的第0个描述符总是不会被访问。如果TI=1,那么就不是空选择子,它指定的是当前局部描述符表中的第0个描述符。为了 更快地从段选择子中获得段的基本信息(段基址,段界限,段属性),从80386开始为每个段寄存器在硬件上配备了段描述符高速缓冲存储器,对我们写程序的 人来讲,它是不可编程的。有了这种高速缓冲寄存器后,每当将选择子装入段寄存器后,处理器将自动装入描述符表中相应的描述符,并将描述表的信息装入到高速 缓冲寄存器,这样可以加快访问速度,以下是段选择子的结构示意图:

15________________________________________________________________3__2__1_____0
| |TI | RPL |
|________________________________________________________________|___|________|

80386 ASM程序设计基础–80386实模式下编程II

虽然80386处理器要较以前的处理器的功能大大增强,但这些功能只能在保护模式下才能全部得到发挥。在实模式下最大寻址空间只有1M,但在保护模式最大 寻址空间可达4G,可以访问到所有的物理内存。同时由于引入虚拟内存的概念,在程序设计中可使用的地址空间为64TB。80386处理器采用了可扩充的分 段管理和可选的分页管理机制,这两个存储管理机制由MMU(Memory Management Unit)部件来实现。因此,如果在80386下进行实模式编程,这时的80386处理器相当于一功能更强大,运行速度更快的8086处理器。80386 提供对虚拟存储器的支持,虚拟存储器的理论基础就是:速度非常快的内存储器和海量的外存储器,所以它是一种软硬件结合的技术,它能够提供比物理内存大得多 的存储空间。
80386下的段具有三个属性:段基址,段界限,段属性,通常描述段的称作段描述符(Segment Descriptor),而描述符通常放在一个线性表中,这种线性表又分为:GDT(Global Descriptor Table),LDT(Local Descriptor Table),IDT(Interrupt Descriptor Table),通常用一个叫做选择子的东西去确定使用上述三个线性表中哪一个描述符。程序中使用的地址空间就是虚拟地址空间,上面已经说过80386下虚 拟地址空间可达到64TB(后面将解释为什么可以达到64TB),虚拟地址空间由一个选择子和段内偏移组成,这是因为通过段的选择子我们可以得到该段的描 述符,而在描述符中又说明了段的基址,段的界限及段的属性,再加上段的偏移就可以得到虚拟地址空间。不过请注意,这里并没有将段基址乘以16再加上偏移地 址,这是保护模式与实式模式的区别之一。很明显,任何数据都必须装入到物理内存才能够被存储器处理,所以二维的虚拟地址空间必须转换成一维的物理地址。同 时,由于每个任务都有自已的虚拟地址空间,为了防止多个并行任务将虚拟地址空间映射同一物理地址空间采用线性地址空间隔离虚拟地址和物理地址,线性地址空 间由一维的线性地址构成,线性地址空间与物理地址空间对等,线性地址为32位,可寻址空间为4GB(物理地址空间最大也可以达到4GB,址址为32位,所 以说线性地址空间与物理地址空间对等)。下面是80386虚拟地址空间与物理址空间的转换示意图:

|----------| |------------| |--------| |------------------| |--------|
| 虚拟地址 |------>|分段管理部件|------>|线性地址|---|--->|可选的分页管理部件|---|-->|物理地址|
|----|-----| |------------| |--------| | |------------------| | |--------|
|------|-------| | |
| | |---------------------------|
|----------| |---------|
| 选择子 | | 段内偏移|
|----------| |---------|

地址映射过程中,通过分段管理部件将虚拟地址空间转换成线性地址,这一步是必然存在的。如果在程序中启 用了分页管理机制,那么线性地址还要经过分页管理部件的处理才得到最后的物理地址。如果没有采用分页管理机制,那么得到的线性地址就是物理地址。分页管理 部件的主要的工作机制在于将线性地址和物理地址划分成大小相同的块,通过在建立两者之间的页表来建立对应关系。分段管理机制使用大小可变的存储块,使用分 段管理机制适合处理复杂系统的逻辑分段。分页管理机制使用固定大小的块,所以它适合管理物理存储器,分页管理机制能够更有效地使用虚拟地址空间。
80386支持多任务,因此对各个任务进行保护是非常必要的,对任务的保护可分为:同一任务内的保护,不同任务之间的保护。
a.同一任务内的保护,在同一任务内定义有四种特权级别(Previlege Level),将这些特权级别分配给段中的代码和数据,把最高的特权级别分配给最重要的数据和最可信任的代码,将较低级别的特权分给一般的代码和不重要的 数据。特权级别用0~3来表示,用数字0表示最高特权级别,用数字3表示最低特权级别,在比较特权级别时不使用大于或小于,而是使用外层或里层来比较,很 明显特权级别为0表示最里层,特别级别为3表示最外层。任何一个存储段(程序直接进行访问的代码段和数据段)都有一个特权级别,在一个程序试图访问这个存 储时,就会进行特权级别的比较,如果小于或等于(如果等于表明同级,小于则表明是内层)处该存储段的特权级别就可以对该存储段进行访问。任务在特定时刻下 的特权级别称为CPL(Current Previlege Level),看一简单的结构示意图:

|---------|-------|
| CodeA | DataA | 特权级别为0
|---------|-------|
|---------|-------|
| CodeB | DataB | 特权级别为1
|---------|-------|
|---------|-------|
| CodeC | DataC | 特权级别为2
|---------|-------|
|---------|-------|
| CodeD | DataD | 特权级别为3
|---------|-------|

CodeA可以访问DataA,CodeB,DataB,CodeC,DataC,CodeD,DataD
CodeB可以访问Datab,CodeC,DataC,CodeD,DataD,但不可以访问CodeA,DataA
CodeC可以访问DataC,CodeD,DataD,但不可以访问CodeA,DataA,CodeB,DataB
CodeD处在最外层,只能访问同级的DataD,不可以访问CodeA,DataA,CodeB,DataB,CodeC,DataC
通常应用程序放在最外层,但由于每个应用程序的虚拟地址空间不同,因此它们被隔离保护。这种特权级别的 典型用法就是:将操作系统的核心放在0层,操作系统的其余部分放在1级,2级留给中间软件使用,3级放应用程序,这样的安排的好处在于:操作系统的核心因 为放在0层,因此它可以访问任务中所有的存储段,而1级的部分操作系统可以访问除0级以外的所有存储段,应用程序只能访问自身的存储段。
b.不同任务间的保护,通过把每个任务放在不同的虚拟地址空间来实现隔离保护,虚拟地址到物理地址之间 的映射由每个任务中的映射函数来决定,随着任务切换,映射函数也跟着切换,这样可以保证任务A映射到物理内存中的区域与任务B映射到内存中的区域是不同 的,尽管有可能它们的虚拟地址空间相同,但它们最终在物理内存中的位置是不同的,从而起到了保护作用

80386 ASM程序设计基础–80386实模式下编程

80386实模式下编程
80386在实模式下是一个更快的8086,它不但可以进行32位操作,而且还可以进32位寻址,并且 还可以使用80386的扩展指令。不过,由于是在实模下,寻址的最大空间为1M。在一个段内,段的最大长度不超过64K,否则就会发生异常。
在8086下定义一个段的完整格式是:
段名 [定位类型] [组合类型] [‘类别’]
80386下定义一个段的完整格式是:
段名 [定位类型] [组合类型] [‘类别’] [属性类型]
说明:属性类型有两种:USE32和USE16,USE32表示32位段,USE16表示16位段。如 果你在程序中用到伪指令.386,那么默认的属性类型就是USE32(32位段),如果没有用伪指令指定CPU的类型,那么默认的属性类型就是 USE16,在实方式下只能使用16位段,即用USE16。
eg:
CSEG PARA PUBLIC USE32;定义一个32位的段
AA DW ?

BB DD ?
CC DB ?
DD DW ?
EE DW 0,0,0.....
CSEG ENDS
由于在80386中用到了66H操作前缀和67H地址前缀,因此尽管在实式模式下,只要设定的CPU类 型是80386,仍然可以进行32位操作,可以进行32位寻址,66H,67H这两个前缀无需程序员在程序中书写,汇编程序会自动加上的。只要在程序中对 32位操作数进行访问,或进行32位寻址,那么就会加上操作数前缀66H和地址前缀67H。相反,如果在32位段中对16位或8位的访问,汇编程序中也会 加上这两个前缀。
下面将给出一个例子程序,演示一下在80386的实模式下编程的方法与技巧(这是从网上down的一个程序,不是我写的,但我会作详细的解剖,并与8086下的程序设计作出比较):
用十进制,十六进制,二进制三种形式显示双字存储单元F000:1234中的内容
|------------------MAIN PROC------------|
| .386 |
| code segment para public 'code' use16 |
| assume cs:code |
| begin: |
| mov ax,0f000h |
| mov fs,ax |
| mov eax,fs:[1234H] |
| call todec |
| call newline |
| call tohex |
| mov al,'H' |
| call echo |
| call newline |
| call tobin |
| mov al,'B' |
| call echo |
| call newline |
| mov ah,4ch |
| int 21h |
|---------------------------------------|
;sub-function todec
todec proc near
pushad
mov ebx,10
xor cx,cx
dec1:
xor edx,edx
div ebx
push dx
inc cx
or eax,eax
jnz dec1
dec2:
pop ax
call toasc
call echo
loop dec2
popad
ret
todec endp

;sub-function tobin
tobin proc near
push eax
push ecx
push edx
bsr edx,eax
jnz bin1
xor dx,dx
bin1:
mov cl,31
sub cl,dl
shl eax,cl
mov cx,dx
inc cx
mov edx,eax
bin2:
rol edx,1
mov al,'0'
adc al,0
call echo
loop bin2
pop edx
pop ecx
pop eax
ret
tobin endp

;sub-function tohex
tohex proc near
countb=8
enter countb,0
movzx ebp,bp
mov ecx,countb
mov edx,eax
hex1:
mov al,dl
and al,0fh
mov [ebp-countb+ecx-1],al
ror edx,4
loop hex1
mov cx,countb
xor ebx,ebx
hex2:
cmp byte ptr [ebp-countb+ebx],0
jnz hex3
inc ebx
loop hex2
dec ebx
mov cx,1
hex3:
mov al,[ebp-countb+ebx]
inc ebx
call toasc
call echo
loop hex3
leave
ret
tohex endp

;sub-function toasc
toasc proc near
and al,0fh
cmp al,'0'
cmp al,'9'
seta dl
movzx dx,dl
imul dx,7
add al,dl
toasc1:ret
toasc endp

;sub-function newline
newline proc near
push dx
push ax
mov dl,0dh
mov ah,2
int 21
mov dl,0ah
int 21
pop ax
pop dx
ret
newline endp

echo proc near
push ax
push dx
mov dl,al
mov ah,2
int 21h
pop dx
pop ax
echo endp
剖析:
先来看主程序框架,下面就是MAIN PROC:
|------------------MAIN PROC-------------------------------|
|.386;定义处理器的类型为386表示可以使用所有80386指令 |
| code segment para public 'code' use16 |
| assume cs:code |
| begin: |
| mov ax,0f000h |
| mov fs,ax;将f000h装入段寄存器fs |
| mov eax,fs:[1234H];将1234H内存单元中的双字送给寄存器EAX|
| call todec;调用子过程todec |
| call newline;调用子过程newline进行回车换行 |
| mov eax,fs:[1234h]; |
| call tohex;调用子过程tohex |
| mov al,'H' |
| call echo;显示字符H |
| call newline; |
| mov eax,fs:[1234H] |
| call tobin;调用子过程tobin |
| mov al,'B' |
| call echo

| call newline |
| mov ah,4ch |
| int 21h |
|----------------------------------------------------------|
主程序中的内容一目了然,很简单。和8086下唯一不同的是就是要用伪指令定义CPU的类型,并且段寄存器的定义多了一个属性类型USE16,再就是32位操作,使用80386的指令,其它的和8086下没有什么区别。
重点是要分析几个过程,从网上down下来时,过程newline和toasc没有实现代码,因为这很 简单,所以上述toasc,newline,echo的过程体是由我写进去的,这两个过程体代码不多而且非常简单,就不作介绍了。重点介绍todec, tobin,tohex。
a.子过程todec,这个子过程的主要功能是将f000:1234双字单元的内容用十进制显示,下面就来看每一行代码:
|-----------------------------------------------------------|
|todec proc near |
| pushad |
| mov ebx,10 |
| xor cx,cx |
| dec1: |
| xor edx,edx |
| div ebx |
| push dx |
| inc cx |
| or eax,eax |
| jnz dec1 |
| dec2: |
| pop ax |
| call toasc |
| call echo |
| loop dec2 |
| popad |
| ret |
|todec endp |
|-----------------------------------------------------------|
分析:将一个数用十进制数来表示,要它对它进行除以10的运算,得到商和余数。再将商除以10,如此循 环直到商为0为止,在这个过程中得到的一系列的模(余数)就是十进制数系列。在主程序中,已经将f000:1234双字单元的内容放到EAX寄存器中,由 于后来要用十六进制数,二进制数显示,所以EAX寄存器的内容不允许改变,因此在子过程的一开始,要将EAX的内容先入栈,所以子过程的一开始就用 PUSHAD将8个32位通用寄存器的内容全部入栈。在标号dec1不断地进行除以10运算,将所得到的余数全部入栈,同时用cx进行计数。在标号 dec2中,逐个弹出在标号dec1中得到的余数,然后分别将它们显示出来,这样就可以将该存储单元中的内容用十进数表示,下面解释每一条指令的功能:
a1.pushad;将8个32位通用寄存器全部入栈
a2.xor cx,cx;cx清0
a3.mov ebx,10;10=>ebx
a4.xor edx,edx;edx清0
a5.div ebx;edx存放高32位,不过是0,EAX中存放低32位,即ffff:[1234]双字的内容;除法得到的商放在EAX,余数放在EDX
a6.push dx;将edx的低16位dx入栈
a7.inc cx;cx+1=>cx
a8.or eax,eax;对eax进行或操作,主要是用来判断eax是否为0,即判断商是否为0,从而判断是否应该结束标号为dec1的循环。
a9.jnz dec1
a10.pop ax;将放在堆栈中的余数逐个弹出到ax中
a11.call toasc;显示ax的内容
a12.call echo
a13.loop dec2;将所有的余数显示完毕
a14.popad;8个32位通用寄存器全部出栈
a15.ret

b.子过程tohex
PUSH BP
SP=>BP
SP<=SP-CNT1
|------------------------------------------------------------|
|tohex proc near |
| countb=8 |
| enter countb,0 |
| movzx ebp,bp |
| mov ecx,countb |
| mov edx,eax |
|hex1: |
| mov al,dl |
| and al,0fh |
| mov [ebp-countb+ecx-1],al |
| ror edx,4 |
| loop hex1 |
| mov cx,countb |
| xor ebx,ebx |
|hex2: |
| cmp byte ptr [ebp-countb+ebx],0 |
| jnz hex3 |
| inc ebx |
| loop hex2 |
| dec ebx |
| mov cx,1 |
|hex3: |
| mov al,[ebp-countb+ebx] |
| inc ebx |
| call toasc |
| call echo |
| loop hex3 |
| leave |
| ret |
|tohex endp |
|------------------------------------------------------------|
分析:该子过程的功能是将f000:1234双字单元的内容以16进制数显示出来,首先来考虑一下将一 个数以16进制数表示出来的算法,事实上在汇编语言中操作数一直都是以十六进制表示的。因此,在这个子过程中不可以像上一个子过程一样,通过不断的除法取 模得到结果。事实上,我们只需要将32位操作,以每半个字节(四位)的内容显示出来就可以了,有了这一编程思想,就很容易看懂上面的子过程。当然你们会 问,为什么要每次只显示半个字节而不显示一个字节呢?呵呵,十六进制的十六个数是从0000-1111,不就是半个字节了。所以要循环8次才可以显示出 32位的EAX,所以这里用ror指令来不断循环移位,每次右移4位放到dl的低4位中。这8个半字节分别放在[ebp-1]至[ebp-8]的存储单元 中。不过,存储的顺序是由低位到高位,如果就这样显示结果肯定显示反了。标号hex2,hex3的主要功能是用来判断f000:1234双字单元的内容是 否为0,如果为0,只需要将最后结果显示一个0即可,否则就显示出8位内容。下面是每条指令的功能:
b1.countb=8;伪指令定义一局部变量countb,其值为8
b2.enter countb,0;建立堆栈框架指令
b3.movzx ebp,bp;对bp进行零扩展
b4.mov ecx,countb;8=>ecx
b5.mov edx,eax;将eax=>edx
b6.mov al,dl
b7.and al,0fh;取低4位
b8.mov [ebp-countb+ecx-1],al;将8个半字节的内容逐一送到[ebp-1]至[ebp-8]的内存单元中
b9.ror edx,4;对edx进行循环右移,每次移动4位
b10.loop hex1
b11.mov cx,countb
b12.xor ebx,ebx;ebx清0
b13.cmp byte ptr [ebp-countb+ebx],0;下面的语句主要用来判断源操作数f000:1234的内容是否为0,如果是0,就在屏幕上只显示一个0
b14.jnz hex3
b15.inc ebx
b16.loop hex2
b17.dec ebx
b18.mov cx,1
b19.mov al,[ebp-countb+ebx];逐一显示[ebp-8]到[ebp-1]的内容。
b20.inc ebx
b21.call toasc
b22.call echo
b23.loop hex3
b24.leave;释放堆栈框架
b25.ret

c.子过程tobin
|---------------------------------------|
|tobin proc near |
| push eax |
| push ecx |
| push edx |
| bsr edx,eax |
| jnz bin1 |
| xor dx,dx |
|bin1: |
| mov cl,31 |
| sub cl,dl |
| shl eax,cl |

| mov cx,dx |
| inc cx
| mov edx,eax |
|bin2:

| rol edx,1
| mov al,'0'
| adc al,0
| call echo
| loop bin2
| pop edx |
| pop ecx
| pop eax
| ret
|tobin endp
|---------------------------------------|
分析:将一个数用二进制数显示出来,只需要用ROL指令就可以了。这里作者写的程序就是这个思路,在标 号bin1中主要判断f000:1234单元的内容是否为0,如果为0,那么只需要在屏幕上显示一个0就可以了。否则的话,就用ROL指令对源操作数移位 32位,从最高位31位到最低位逐一显示出来,程序设计思路很简单,没有什么复杂的算法,下面看每一条指令的含义:
c1.push eax;eax入栈
c2.push ecx;ecx入栈
c3.push edx;edx入栈
c4.bsr edx,eax;对eax进行扫描,并把第一个为1的位号送给edx
c5.jnz bin1;如果eax不为0,就跳到c7去执行
c6.xor dx,dx;如果eax为0,就将dx清0
c7.mov cl,31;从c7到c12主要用来设置计数器cx,如果eax=0,那么就设置cx=1,如果eax不等于0,那么就设置ecx=32
c8.sub cl,dl
c9.shl eax,cl
c10.mov cx,dx
c11.inc cx
c12.mov edx,eax
c13.rol edx,1;从c13到c15主要用来显示二进制数据,顺序是从最高位31位到最低位0位
c14.mov al,'0'
c15.adc al,0
c16.call echo
c17.loop bin2
c18.pop edx;edx出栈
c19.pop ecx;ecx出栈
c20.pop eax;eax出栈
c21.ret
在后续的篇幅里将主要介绍保护式下的段页管理机制及及如何在保护模下编程。

80386 ASM程序设计基础–高级语言支持,条件字节设置指令

高级语言支持,条件字节设置指令
AA.高级语言支持指令,开始于80186,主要是用来简化高级语言的某些特征,总共有3条指令:ENTER,LEAVE,BOUND
a.ENTER,LEAVE,建立与释放堆栈框架命令。在C语言中,栈不仅用来向函数传递入口参数,而 且在函数内部的局部变量也存放在栈中。为了准确地存取这些这些局变量和准确地获得入口参数,就需要建立堆栈框架,先看一个小程序:
//C Programming-Language
int sum(int x,int y)
{
int sum;
sum=x+y;
return sum;
}
//The corresponding ASM codes lists below
_sum proc near;注意C语言中函数参数的入栈方式是从右向左,即先是参数y入栈,再是x入栈,再是函数的返回地址入栈
push bp
mov bp,sp;建立堆栈框架
sub sp,2

mov ax,word ptr [bp+4];取参数x
add ax,word ptr [bp+6];加参数y
mov word ptr [bp-2],ax
mov ax,word ptr [bp-2]
mov sp,bp;释放栈框架
pop bp
ret
_sum endp
此时栈顶的示意图是:
|----------------------|
| BP |<====SP
|----------------------|
| 函数返回地址 |<====BP+2
|----------------------|
| 参数x |<====BP+4
|----------------------|
| 参数y |<====BP+6
|----------------------|
| ...... |<====BP+8
|----------------------|
| ........ |<====BP+n,n是一能被2整除的数
|----------------------|
如果用建立和释放堆栈框架指令,那么对应的汇编程序应该是:
_sum proc near
enter 2,0;建立栈框架
mov ax,word ptr [bp+4];取参数x
add ax,word ptr [bp+6];加参数y
mov word ptr [bp-2],ax
mov ax,word ptr [bp-2]
leave;释放栈框架
ret
_sum endp
b.建立栈框架指令ENTER,格式如下:ENTER CNT1,CNT2。其中CNT1表示框架的大小,即子程序中需要放在栈中局部变量的字节数;CNT2是立即数,表示子程序嵌套级别,即从调用框架复制到当前框架的指针数。在立即数CNT2为0时,ENTER指令的实过程是:
PUSH BP
SP=>BP
SP<=SP-CNT1
c.释放栈框架指令LEAVE,其具体实现过程:
8086:
BP=>SP
POP BP
80386:
EBP=>ESP
POP EBP
d.ENTER和LEAVE指令均不影响标志寄存器中的各标志位,同时LEAVE指令只负责释放栈框架,并不负责函数返回。因此,要在LEAVE指令后安排一条返回指令。

BB.条件字节设置指令
这是80386新增的一组指令集,将会在后面全部列表出来。条件字节设置指令的格式:
SETxx OPRD
xx是助记符的一部分,OPRD只能是8位的寄存器或存储单元。
eg:
SETO AL;表示当溢出标志位为1时,即OF=1,将AL置1,否则AL清0
SETNC CH;表示当CF=0时,将CH置1,否则将CH清0
SETNA BYTE PTR [100];表示当AF=0,将DS:[100]这一个字置1,否则将它清0
a.SETZ OPRD;等于0时(ZF=1),置OPRD为1,否则清0
b.SETE OPRD;同a
c.SETNZ OPRD;不等于0时(ZF=0),置OPRD为1,否则清0
d.SETNE OPRD;同c
e.SETS OPRD;为负数时(SF=1)置OPRD为1,否则清0
f.SETNS OPRD;同e正好相反(SF=0)
g.SETO OPRD;OF=1,置OPRD为1,否则清0
h.SETNO OPRD;同g正好相反
i.SETP OPRD;偶(PF=1)置1
j.SETPE OPRD;同i
k.SETNP OPRD;奇(PF=0)置1
l.SETPO OPRD;同k
m.SETB OPRD;低于置OPRD为1,否则清0,这是针对无符号数的
n.SETNAE OPRD;不高于即低于或等于时置OPRD为1,否则清0,这是针对无符号数的
o.SETC OPRD;CF=1,置OPRD为1,否则清0
p.SETNB OPRD;高于或等于时,置OPRD为1,否则清0,这是针对无符号数的
q.SETAE OPRD;高于时置OPRD为1,否则清0,这是针对无符号数的
r.SETNC OPRD;CF=0时,置OPRD为1,否则清0,这是针对无符号数的
s.SETBE OPRD;低于或等于时,置OPRD为1,否则清0,这是针对无符号数的,CF|ZF=1
t.SETNA OPRD;同s,这是针对无符号数的,CF|ZF=1
u.SETNBE OPRD;高于时置OPRD为1,否则清0,这是针对无符号数的,CF OR ZF=0
v.SETA OPRD;同u,这是针对无符号数的,CF OR ZF=0
w.SETL OPRD;小于时,置OPRD为1,否则清0,这是针对有符号数的
x.SETNGE OPRD;同w,这是针对有符号数的
y.SETNL OPRD;大于或等于时,置OPR为1,否则清0,这是针对有符号数的
z.SETGE OPRD;同y,这是针对有符号数的
a1.SETLE OPRD;小于或等于时,置OPRD为1,否则清0,这是针对有符号数的
a2.SETNG OPRD;同a1,这是针对有符号数的
a3.SETNLE;大于时,置OPRD为1,否则清0,这是针对有符号数的
a4.SETG;同a3,这是针对有符号数的

80386 ASM程序设计基础–控制转移指令,串操作指令

控制转移指令,串操作指令
80386控制转移指令包括:转移指令,循环指令,过程调用和返回指令。
A.转移指令包括无条件转移指令JMP和条件转移指令,无条件转移指令分为段内直接转移,段内间接转 移,段间直接转移,段间间接转移。由于80386有保护模式和实模式,在实模式下,段内转移的范围在-128~127,段间转移最大范围为64K。在保护 模式需要用48位指针,即CS:EIP(16位+32位)。条件转移指令有很多包括JCXZ,JECXZ,JBE,JAE,JA,JB等,其用法和 8086相似。

B.循环指令LOOP,LOOPZ,LOO0PE,LOOPNZ,LOOPNE,TASM支持助记符 LOOP,LOOPWE,LOOPWZ,LOOPWNZ,LOOPWNE,LOOPD,LOOPWD,LOOPDE,LOOPDNE,LOOPDNZ。以 CX作为计数器时,就可用LOOP,LOOPWE,LOOPWZ,LOOPWNZ,LOOPWNE;在以ECX作为计数器时,以LOOPD, LOOPDE,LOOPDZ,LOOPDNZ,LOOPDNE,下面的一段例子可以说明问题:
ABC PROC
MOV CX,100H
AA:
;ADD YOUR CODES HERE
LOOP AA
ABC END

C.过程调用和返回调用CALL,RET
这两个指令与8086的用法相同,但由于80386下有实模式和保护模式下。在实模式下,无论是段内调 用还是段间调用均采用32位指针,即CS:IP,它们的用法与8086下相同。在保护模式下,段间调用和段内调用均用48位指针,即ECS:IP。RET 用于返回,具体实现过程会比较复杂,在介绍完80386的地址的管理机制后会作介绍,先介绍一下以下CALL指令在8086中的用法:
a.段内直接转移,具体格式:CALL 过程名。此时CS不入栈,IP的内栈入栈,入栈后再将加上目的地址与CALL指令的下一条指令的偏移地址之差值就可以转移到目的地址,详细过程:
SP-2=>SP;将堆栈指针SP减2
(SP)<=IP;将IP进栈
IP+偏移地址之差;转到目的地址
b.段内间接转移,具体格式:CALL OPRD,那么在这里OPRD可以寄存器或内存单元,它的具体实现过程:
SP-2=>SP;将堆栈指针SP减2
(SP)<=IP;将IP进栈
IP<=(OPRD);转到目的地址
同a一样,CS不入栈
c.段间直接转移,具体格式:CALL 过程名 [FAR],此时CS,IP均要入栈,详细的实现过程:
SP-2=>SP;将堆栈指针减2
(SP)<=CS;将CS入栈
SP-2=>SP;将堆栈指针再减2
(SP)<=IP;将IP入栈
;装入新的CS,IP
IP<=过程入口的偏移地址
CS<=过程入口的段地址
d.段间间接转移,具体格式:CALL OPRD [FAR],此时CS,IP均要入栈,OPRD是32位,你知道在8086中没有32位寄存器。因此,这里的OPRD一定是存储单元,高16位是CS的值,低16位是IP值,详细的实现过程:
SP-2=>SP;将堆栈指针减2
(SP)<=CS;将CS入栈
SP-2=>SP;将堆栈指针再减2
(SP)<=IP;将IP入栈
;装入新的CS,IP
IP<=(OPRD+2,OPRD+3)
CS<=(OPRD,OPRD1)
e.段内返回
格式:RET。实际上它的实现过程:
(SP)=>IP;从当前栈顶弹出一个字,将它送给IP指令计数器
SP+2=>SP;SP
f.段间返回
格式:RET,实际上它的实现过程:
(SP)=>IP;IP出栈
SP+2=>SP;
(SP)=>CS;CS出栈
SP+2=>SP;

D.中断返回指令IRET
功能和用法与8086相同,这里顺便介绍一下8086的中断返回指令
IRET,具体的实现过程:
IP<=(SP);IP出栈
SP+2=>SP;
CS<=(SP);CS出栈
SP+2=>SP;
FLAGS<=(SP);标志寄存器出栈
SP+2=>SP;

E.串操作指令
80386在串操作指令方面增加了双字操作,在8086五条指令的基础上增加了INS,OUTS。
a.LOADSD,和8086的用法和功能相同,不过是对32位操作数操作。
b.STOSD,和8086的用法和功能相同,不过是对32位操作数操作。
c.CMPSD,和8086的用法和功能相同,不过是对32位操作数操作。
d.SCANSD,和8086的用法和功能相同,不过是对32位操作数操作。
e.MOVSD,和8086的用法和功能相同,不过是对32位操作数操作。
f.重复前缀REP,和8086的功能与用法相同,仍以CX为计数器,看下面的一小程序:
ROR ECX,2
REP MOVSD;以CX为计数器,每次传送双字
ROL ECX,1
REP MOVSW;以CX为计数器,每次传送一字
ROL ECX,1
REP MOVSB;以CX为计数器,每个传送一个字节
g.INSB,INSW,INSD,OUTSB,OUTSW,OUTSD
g1.INSB,串输入指令,以字节单位,该指令的功能是从DX指定的端口读入一个字节到ES:DI指定的内存单元中。
g2.INSW,串输入指令,以字单位,该指令的功能是从DX指定的端口读入一个字节到ES:DI指定的内存单元中。
g3.INSD,串输入指令,以双字单位,该指令的功能是从DX指定的端口读入一个字节到ES:DI指定的内存单元中。
g4.OUTSB, 串输出指令,以字节为单位,将DS:SI内存单元的内容送往DX指定的端口。
g5.OUTSW, 串输出指令,以字为单位,将DS:SI内存单元的内容送往DX指定的端口。
g6.OUTSD, 串输出指令,以双字为单位,将DS:SI内存单元的内容送往DX指定的端口。
g7.串输入和串输出指令不影响标志寄存器中的各标志位,串操作指令可以与REP一起使用

Pages: Prev 1 2 3 ...78 79 80 81 82 83 84 85 86 Next