蓝色步行者

每个人都有自己的梦想


  • 首页

  • 归档

  • 标签

  • 分类

  • 搜索

Cmake

发表于 2017-08-26 | 分类于 工具

PROJECT

格式:PROJECT(projectname [CXX] [C] [Java])
定义工程名称,指定工程支持的语言,支持的语言列表可以忽略,默认情况表示支持所有语言。这个指令隐式的定义了两个 cmake 变量:<projectname>_BINARY_DIR以及<projectname>_SOURCE_DIR。内部编译的话两个变量都是指向工程所在路径。工程名一般大写。

预定义变量

PROJECT_BINARY_DIR和PROJECT_SOURCE_DIR是cmake系统预定义变量,值分别跟<projectname>_BINARY_DIR以及<projectname>_SOURCE_DIR一致。

SET

格式:SET(VAR [VALUE] [CACHE TYPE DOCSTRING [FORCE]])
显式定义变量。引用变量的方式为${...},但有一些例外,比如在 IF 控制语句,变量是直接使用变量名引用,而不需要${}。

MESSAGE

格式:MESSAGE([SEND_ERROR | STATUS | FATAL_ERROR] "message to display" ...)
向终端输出用户定义的信息,包含了三种类型:
SEND_ERROR:产生错误,生成过程被跳过。
SATUS:输出前缀为—的信息。
FATAL_ERROR:立即终止所有 cmake 过程。

ADD_EXECUTABLE

格式:ADD_EXECUTABLE(<name> [WIN32] [MACOSX_BUNDLE] [EXCLUDE_FROM_ALL] source1 [source2 ...])
生成可执行文件,注意:工程名与可执行文件名没有任何关系。

基本语法规则

1、变量使用${}方式取值,但是在 IF 控制语句中是直接使用变量名。
2、指令(参数1 参数2…)
参数使用括弧括起,参数之间使用空格或分号分开。
3、指令是大小写无关的,参数和变量是大小写相关的。推荐全部使用大写指令。
4、参数可使用双引号。

内部编译/内部构建

编译方式:在工程目录中执行cmake .。
源文件、CMakeLists.txt都在工程目录中,生成的中间文件、Makefile等都在本目录中。生成一些无法通过使用make distclean自动删除的中间文件(因为无法跟踪)。

Learn Git Branching 学习笔记

发表于 2017-08-26 | 分类于 Git

Learn Git Branching

Learn Git Branching

基础

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
level intro1: git commit

level intro2: git branch; git checkout -b

level intro3: git merge <Commit>; git merge <Branch>;

level intro4: git rebase <Commit>; git rebase <Branch>

level rampup1: HEAD; git checkout <Branch>; git checkout <Commit>; commit in detached head

level rampup2: Relative Refs; git checkout <Commit>; git checkout HEAD^;

level rampup3: Relative Refs; git checkout HEAD~4; git branch -f <Branch> HEAD~3; git branch -f <Branch> <Commit>; git branch -f <Branch> <Branch>;

level rampup4: git reset; git revert <Commit>;

level move1: git cherry-pick <Commit1> <Commit2> <...>

level move2: git rebase -i <Commit>; git rebase -i <Branch>

level mixed1: git cherry-pick; git rebase -i;
用于:忽略debug/print的提交,只move最终的结果到主分支上

level mixed2: git rebase -i;(调整需要修改的commit到最前) git commit --amend;(修补提交,抛弃原有的提交结果,替换为新的提交) git rebase -i;(调整回原来的commit顺序)
用于:在更早之前的提交结果中做修改,并提交

level mixed3: git checkout;(跳到需要修改的提交结果C2的父节点C1) git cherry-pick;(move需要修改的提交结果C2) git commit --amend;(修补提交,抛弃原有的提交结果C2,替换为新的提交C2') git cherry-pick;(将原来C2之后的提交结果move到C2'之后)
用于:在更早之前的提交结果中做修改,并提交

level mixed4: git tag <Tag> <Commit>; 直接在tag上commit或checkout,实际上不是针对tag,而是针对对应的commit

level mixed5: git describe <ref>; 输出: <tag>_<numCommits>_g<hash>
用于:查看目标分支/commit在历史上最近的祖先tag

level advanced1: 将多个分支调整为顺序排列

level advanced2: ~和^都可以填写数字,merge的结果C1'是同时指向两个父节点C0和C1的,遇到C1'的时候向上checkout默认是按着第一个父节点C1向上移动HEAD,git checkout <branch>^2 可以使其移动到第二个父节点C0。以下命令:git checkout HEAD~;git checkout HEAD^2;git checkout HEAD~2;可以合并为git checkout HEAD~^2~2

level advanced3: 调整分支,使不同分支分别指向不同版本

1、git merge <Commit>; git merge <Branch>:指向分支1,将分支1和分支2的提交结果C1和C2合并为新的提交C3,该提交同时指向C1和C2,分支1指向C3,分支2仍指向C2。如果C1是C2的祖先,则结果为分支1指向C2(fast forwarding)。

2、git branch -f <Branch> <Commit>:可以直接将某个分支(不能是当前分支)指向任意一个commit点,而不需要先跳到该分支。

3、git revert <Commit>:生成一个新的提交来撤销某次提交,此次提交之前的commit都会被保留。

假设C1添加了文件f1,C2在C1的基础上添加了文件f2,C3是在C2的基础上git revert生成的新提交:如果git revert C2,那么在C3可以看到f1;如果git revert C1,那么在C3可以看到f1和f2,这是因为C2是基于C1的,虽然在C3看来是撤销了C1的提交,但是C2中仍然有f1文件。

假设C1添加了文件f1,C2添加了文件f2,C1’是在C1上执行git rebase master合并结果,C3是在C1’的基础上git revert生成的新提交:如果git revert C1',那么在C3可以看到f2;如果git revert C2,那么在C3可以看到f1。

4、git cherry-pick <Commit1> <Commit2> <...>:指向某分支1,将其他选中的提交结果move到分支1下。如果选中目标有分支2,move的是分支2对应的提交结果,分支2的指向不会被改变。如果选中的提交结果中是分支1(HEAD)的祖先节点,则abort,所有结果都不会被move。

5、git rebase <Commit>; git rebase <Branch>:指向某分支1,将分支1所有新工作move到分支2(或某个commit)下,如果目标是祖先节点,则不会move。

6、git rebase -i <Commit>; git rebase -i <Branch>:指向某分支1,将【分支1】到【分支1和分支2(或某个commit)的共同祖先(分支2/commit的祖先可以是自己)】之间分支1上的所有工作(不包括共同祖先本身)move到分支2(或某个commit)下,如果目标是祖先节点,也会move,可以调整和选择分支1上要move的工作。

如果涉及时间线合并,分支2(或某个commit)不是合并的时间线的一员,则将【分支1】到【分支1和分支2(或某个commit)的共同祖先(分支2/commit的祖先可以是自己)】之间【所有合并时间线的所有工作】(不包括共同祖先本身、合并节点)move到分支2(或某个commit)下,如果目标是祖先节点,也会move,可以调整和选择要move的工作。

执行git rebase -i b2之后可move的节点如下:

如果涉及时间线合并,分支2(或某个commit)是合并的时间线的一员,则将【分支1】到【分支1和分支2(或某个commit)的共同祖先(分支2/commit的祖先可以是自己)】之间【分支2(或某个commit)时间线之后的工作和其他合并时间线的所有工作】(不包括共同祖先本身、合并节点)move到分支2(或某个commit)下,如果目标是祖先节点,也会move,可以调整和选择要move的工作。

执行git rebase -i C3之后可move的节点如下:

7、level mixed2示意图:

8、level mixed3示意图:

进阶

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
level remote1: git clone; 

level remote2: git clone; git checkout origin/master

level remote3: git fetch;

level remote4: git pull;

level remote5: git fakeTeamwork; git fakeTeamwork master 3; 伪装远程更新

level remote6: git push;

level remote7:
1. git fetch; git rebase origin/master; git push;
2. git fetch; git merge origin/master; git push;
3. git pull --rebase; git push;
4. git pull; git push;
方法1等同于方法3;方法2等同于方法4,多了一个合并

level remoteAdvanced1: git rebase 处理多分支合并

level remoteAdvanced2: git merge 处理多分支合并
more rebase, less merge;
rebase: clean commit, but modify commit history;

level remoteAdvanced3:
1. git checkout -b newB origin/master;
2. git branch -u origin/master newB;
创新并新建分支newB,并设置其跟踪远端分支master

level remoteAdvanced4: git push <remote> <place>

level remoteAdvanced5: git push origin <source>:<destination>

level remoteAdvanced6: git fetch <remote> <place>; git fetch origin <source>:<destination>

level remoteAdvanced7: git push origin :<destination>; git fetch origin :<destination>

level remoteAdvanced8: git pull <remote> <place>; git pull origin <source>:<destination>

1、git clone:本地仓库会出现两种分支,一种是本地分支,一种是远端分支,远端分支格式为/。为所有远端的分支创建对应的本地远端分支,为远端当前的活跃分支创建对应的本地分支(一般为master),所以clone之后一般只看到一个分支。

2、git clone; git checkout origin/master:git clone之后执行git checkout origin/master会导致detached HEAD,因为远端分支只有在remote更新的时候才能更新。

3、git fetch:下载远端所有分支的新提交到本地仓库,更新所有远端分支指向,本地分支指向不变。

4、git pull:git fetch + git merge。指向分支1,下载分支1 track的远端分支(远端分支可同时被多个本地分支track,如果本地分支没有track远端分支,即使远端分支有同名的分支也不会被pull)的新提交到本地仓库,更新当前远端分支指向,merge当前分支和对应的远端分支为新提交结果。如果是detached HEAD,分开执行git fetch + git merge ,最终结果是下载更新并将HEAD fast forward到merge的对象,没有真正merge;如果是直接执行git pull,则失败。

5、git push:将当前分支所有提交结果更新到远端,并且更新本地的远端分支指向到最新提交。如果当前分支已更新到远端,而本地分支的远端分支没有指向本地分支(如本地不存在bar分支,但存在本地的远端分支origin/bar,git fetch origin bar:bar的时候创建并更新了本地分支bar的指向,但没有更新本地分支的远端分支origin/bar指向,则会造成上述现象),执行git push并不会更新本地远端分支指向。

6、git push <remote> <place>:
place指的是本地分支,如果该分支没有track远端分支,则会在远端创建对应的分支,并设置本地分支track到该远端分支。如果当前是detached HEAD,执行缺省的git push会失败,但可以执行不缺省的git push <remote> <place>。
由于远端分支可同时被本地多个分支track,如果本地分支place所track的远端分支版本更高,则push失败。

7、git push origin <source>:<destination>:将本地某个任意的提交结果source(不一定是最新的)以及之前的工作更新到远端分支;甚至于如果destination是远端不存在的分支,将在远端创建对应分支,并将指定的提交结果source以及之前的工作更新到远端,在本地创建对应的远端分支,并指向source位置。
可用于将本地两个分支的工作交换更新到远端分支,但如果远端分支的版本已经新于本地提交结果source,则push失败。git push origin <source>:<destination>会更新destination对应的本地远端分支指向到source。

上图中,执行git push origin b1:master失败

上图中,执行git push origin HEAD^1:foo、git push origin foo:master成功,结果如下:

8、git fetch <remote> <place>; git fetch origin <source>:<destination>:git fetch <remote> <place>只下载指定的远端分支工作,并更新本地的远端分支指向;git fetch origin <source>:<destination>不仅下载指定的远端分支工作,还更新对应的本地分支destination指向(指向fetch的最新结果),但不更新destination的远端分支指向,因此git fetch origin <source>:<destination>无法fetch到当前checkout的分支,这是为了不影响当前checkout分支的工作。git fetch origin <source>:<destination>中source是远端仓库某个提交结果,destination是本地分支,如果本地不存在destination分支,将创建该分支,但不会创建对应的远端分支,假如本地原来存在远端分支,该新建本地分支也没有track到远端分支。

9、git push origin :<destination>; git fetch origin :<destination>:git push origin :<destination>表示将本地“nothing”更新到远端分支,其结果是删除了远端分支和本地的远端分支(不删除本地分支)。git fetch origin :<destination>表示下载“nothing”到本地,更新对应的本地分支destination指向,但不更新本地的远端分支指向,其结果是在本地创建本地分支destination(指向当前HEAD指向的位置),但不创建destination的远端分支,如果本地已存在本地分支destination,则执行失败。

10、git pull <remote> <place>; git pull origin <source>:<destination>:git pull origin foo等同于git fetch origin foo+git merge o/foo,git pull origin bar~1:bugFix等同于git fetch origin bar~1:bugFix+git merge bugFix,注意前者将调整本地的远端分支到下载的最新提交结果,后者将调整本地分支到下载的最新提交结果,而merge的对象是最新的提交结果和当前checkout分支。
可用于解决以下情况:本地主分支和远端分支的工作完全不同,且远端有其他分支,该情况下在本地合并远端工作和本地工作。思路是:由于本地分支和远端分支的工作完全不同,将远端分支fetch到本地时使用新的分支指向它,就像是在本地把该分支当作完全崭新的分支来对待。如下:

执行git pull origin bar:foo+git pull origin master:side之后:

docker使用

发表于 2017-08-26 | 分类于 工具

CentOS 7中docker的安装和启动

若需要更新源

备份/etc/yum.repos.d/CentOS-Base.repo

1
mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.backup

下载源文件:

1
wget http://mirrors.163.com/.help/CentOS7-Base-163.repo

运行以下命令生成缓存:

1
2
yum clean all
yum makecache

安装docker

1
yum install docker

启动docker服务

安装完成后,使用下面的命令来启动docker服务,并将其设置为开机启动:
采用CentOS 7中支持的新式 systemd 语法,如下:

1
2
systemctl start docker.service
systemctl enable docker.service

ubuntu14.04安装

更新源

1
2
3
4
Precise 12.04 (LTS)	deb https://apt.dockerproject.org/repo ubuntu-precise main
Trusty 14.04 (LTS) deb https://apt.dockerproject.org/repo ubuntu-trusty main
Wily 15.10 deb https://apt.dockerproject.org/repo ubuntu-wily main
Xenial 16.04 (LTS) deb https://apt.dockerproject.org/repo ubuntu-xenial main
1
2
echo "deb https://apt.dockerproject.org/repo ubuntu-trusty main" > /etc/apt/sources.list.d/docker.list
apt-get update

安装docker

1
apt-get install docker-engine

启动

1
2
docker version
service docker start

使用registry容器

拉取镜像

1
docker pull daocloud.io/library/registry

启动registry

HOST_DIR为宿主机目录,存储镜像文件

1
docker run -d --restart always -p 5000:5000 -v <HOST_DIR>:/tmp/registry daocloud.io/registry

push docker镜像

在需要pull/push的机子上,centos下编辑/etc/sysconfig/docker,ubuntu下修改/etc/default/docker,在OPTIONS项后面加入–insecure-registry ip:5000,然后重启docker。

按照docker镜像命名规则为镜像打tag:

1
docker tag b477a2162de3 116.56.140.66:5000/jianzzz/red5:v1

push到私有docker-registry:

1
docker push 116.56.140.66:5000/jianzzz/red5:v1

docker中镜像的命名规则,如:registry.domain.com/mycom/base:latest,这是一个完整的image名称,各部分的作用:
registry.domain.com: image所在服务器地,如果是官方的hub部分忽略
mycom:namespace,被称为命名空间,或者说成是你镜像的一个分类
base:这个是镜像的具体名字
latest:这是此image的版本号,当然也可能是其它的,如1.1之类的

使用remote api

Centos docker配置文件位置:/etc/sysconfig/docker
Ubuntu docker配置文件位置:/etc/default/docker

修改配置文件中的内容如下:
OPTIONS='-H tcp://0.0.0.0:[docker_port] -H unix:///var/run/docker.sock --selinux-enabled'
将docker_port换成想要配置且未被占用的端口号,比如2375

ubuntu 14.04为:DOCKER_OPTS='-H tcp://0.0.0.0:2375 -H unix:///var/run/docker.sock --selinux-enabled'

重启docker服务

1
systemctl restart docker

查看服务启动

1
ps -ef | grep docker

开放防火墙端口

1
2
3
systemctl start firewalld
firewall-cmd --zone=public --add-port=2375/tcp --permanent
firewall-cmd --reload

或

1
iptables -I INPUT -p tcp --dport 2375 -j ACCEPT

非root用户直接使用docker

参考https://docs.docker.com/engine/installation/linux/centos/
创建docker组

1
sudo groupadd docker

将当前用户加入到docker组

1
sudo usermod -aG docker your_username

重启docker-daemon

1
sudo systemctl restart docker

log out并重新log in

docker run权限

有时候启动一个容器的时候可能会导致权限问题,导致容器一直restarting,比如启动mysql容器的时候,使用了volume,却发现启动不成功。
使用docker logs 容器id可以查看容器运行日志。

Centos7 可能出现Permission denied的问题。
原因:CentOS7中的安全模块selinux把权限禁掉了,至少有以下三种方式解决挂载的目录没有权限的问题:
1、在运行容器的时候,给容器加特权,及加上 –privileged 参数。使用该参数,container内的root拥有真正的root权限。该方法在centos7下有效,但在ubuntu14.04会出现docker run不成功的情况(ubuntu14.04倒不会出现Permission denied的问题)。
2、关闭selinux:
查看SELinux状态命令:/usr/sbin/sestatus -v 或 getenforce
临时关闭:
setenforce 0 #设置SELinux 成为permissive模式
setenforce 1 #设置SELinux 成为enforcing模式
永久关闭,需要重启机器:修改/etc/selinux/config文件,将SELINUX=enforcing改为SELINUX=disabled,重启机器。
3、添加selinux规则,改变要挂载的目录的安全性文本。
更改安全性文本的格式如下
chcon [-R] [-t type] [-u user] [-r role] 文件或者目录

Docker Volume权限管理可参考: https://yq.aliyun.com/articles/53990

dockerfile中entrypoint和cmd

1、ENTRYPOINT,表示镜像在初始化时需要执行的命令,不可被重写覆盖,需谨记。
2、CMD,表示镜像运行默认参数,可被重写覆盖。
3、ENTRYPOINT/CMD都只能在文件中存在一次,并且最后一个生效 多个存在,只有最后一个生效,其它无效!
4、需要初始化运行多个命令,彼此之间可以使用 && 隔开,但最后一个须要为无限运行的命令,需切记!

参考 Dockerfile Best Practices和论docker中 CMD 与 ENTRYPOINT 的区别

cmd的两种格式

entrypoint类似

1
2
3
CMD /bin/echo
# or
CMD ["/bin/echo"]

格式1不是数组,命令在执行前会被加上/bin/sh -c;格式2是数组,执行效果如愿。

给entrypoint传递参数

docker run期间可以给entrypoint传递参数。CMD 和 ENTRYPOINT 放在一起效果更佳:

1
2
3
ENTRYPOINT ["/usr/bin/mysql"]

CMD ["--help"]

一旦设置了entrypoint,docker run期间传递的所有参数将作为ENTRYPOINT(”/usr/bin/mysql”)的参数。而CMD默认被设为["--help"],一旦docker run期间没有传递任何参数,则docker run会输出help的信息。

基于mysql镜像构建新镜像

https://github.com/docker-library/mysql/blob/c207cc19a272a6bfe1916c964ed8df47f18479e7/5.7/Dockerfile
查看docker hub上mysql的Dockerfile:

1
2
3
4
ENTRYPOINT ["docker-entrypoint.sh"]

EXPOSE 3306
CMD ["mysqld"]

可以看出该Dockerfile已经占据了ENTRYPOINT和CMD,且”mysqld”作为”docker-entrypoint.sh”的默认参数。假设我们扩展该镜像,加上vnc等服务,则可以将mysql启动放置到supervisor中,并且在新的dockerfile中直接运行supervisor。请注意在supervisor中启动mysql并不是command=docker-entrypoint.sh或command=mysqld,而是command=docker-entrypoint.sh mysqld。

Dockerfile

vnc.sh

supervisord.conf

上述方案的缺点是:假如我们使用该镜像启动了一个容器,并且启动另一个容器连接该镜像,然后执行数据库导入语句,如docker run -v /home/ubuntu/Desktop/web/db/mysql_5_7_16_vnc_latest_1482889787/data:/tmp/data:rw --link mysql_5_7_16_vnc_latest_1482889787:mysql --rm mysql-5.7.16-vnc:latest /bin/sh -c 'mysql -h"$MYSQL_PORT_3306_TCP_ADDR" -P"$MYSQL_PORT_3306_TCP_PORT" -uroot -p"$MYSQL_ENV_MYSQL_ROOT_PASSWORD" -e "DROP DATABASE IF EXISTS expand;CREATE DATABASE expand DEFAULT CHARSET utf8 COLLATE utf8_general_ci;use expand;source tmp/data/system-expand.sql;"'。关于第二个容器,因为它要执行mysql语句,因此需要mysql服务,一般选择跟第一个容器相同的mysql镜像来启动第二个连接容器。但由于我们将supervisor作为ENTRYPOINT,因此可能会出现以下错误:

出错原因是docker run的参数被当作supervisor的参数了,因此我们继续修改上述文件,将supervisor放到脚本中执行:
Dockerfile

vnc.sh

supervisord.conf

继续完善:上述方案仍有缺陷,连接容器是基于vnc-mysql镜像启动的,但是并不需要vnc等相关服务,因此应该添加一个环境变量控制是否启动vnc。另外,vnc.sh会wait等待supervisor进程,因此为了使得docker run过程指定的/bin/sh -c "..."能够生效,应该在wait之前添加exec "$@"。
Dockerfile

vnc.sh

supervisord.conf

最终的docker run实例:

docker run -v /home/ubuntu/Desktop/web/db/mysql_5_7_16_vnc_latest_1482889787/data:/tmp/data:rw --link mysql_5_7_16_vnc_latest_1482889787:mysql -e MYSQL_ROOT_PASSWORD=any_noused -e RUN_VNC=no --rm mysql-5.7.16-vnc:latest /bin/sh -c 'mysql -h"$MYSQL_PORT_3306_TCP_ADDR" -P"$MYSQL_PORT_3306_TCP_PORT" -uroot -p"$MYSQL_ENV_MYSQL_ROOT_PASSWORD" -e "DROP DATABASE IF EXISTS expand;CREATE DATABASE expand DEFAULT CHARSET utf8 COLLATE utf8_general_ci;use expand;source tmp/data/system-expand.sql;"'

奇怪的是,原本不使用supervisor管理mysqld的时候,连接容器是不需要指定MYSQL_ROOT_PASSWORD的(奇怪于为什么不需要,因为mysql容器的启动是需要指定MYSQL_ROOT_PASSWORD, MYSQL_ALLOW_EMPTY_PASSWORD,MYSQL_RANDOM_ROOT_PASSWORD其中之一的),而使用supervisor之后就需要指定了,虽然这个密码在这里是没有实际作用的。

孤儿进程和僵尸进程

发表于 2017-08-26 | 分类于 计算机原理

孤儿进程

一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。因此孤儿进程并不会有什么危害。

僵尸进程

一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构,直到父进程通过wait / waitpid来取时才释放。如果进程不调用wait / waitpid的话, 那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免。

僵尸进程解决办法

通过信号机制

子进程退出时向父进程发送SIGCHILD信号,父进程处理SIGCHILD信号。在信号处理函数中调用wait进行处理僵尸进程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
#include <signal.h>

static void sig_child(int signo);

int main()
{
pid_t pid;
//创建捕捉子进程退出信号
signal(SIGCHLD,sig_child);
pid = fork();
if (pid < 0)
{
perror("fork error:");
exit(1);
}
else if (pid == 0)
{
printf("I am child process,pid id %d.I am exiting.\n",getpid());
exit(0);
}
printf("I am father process.I will sleep two seconds\n");
//等待子进程先退出
sleep(2);
//输出进程信息
system("ps -o pid,ppid,state,tty,command");
printf("father process is exiting.\n");
return 0;
}

static void sig_child(int signo)
{
pid_t pid;
int stat;
//处理僵尸进程
while ((pid = waitpid(-1, &stat, WNOHANG)) >0)
printf("child %d terminated.\n", pid);
}

fork两次

可参考《Unix 环境高级编程》8.6节。原理是将子进程成为孤儿进程,从而其的父进程变为init进程,通过init进程可以处理僵尸进程。

以下是fork两次的原因:
当我们只fork()一次后,存在父进程和子进程。这时有两种方法来避免产生僵尸进程:
       确保父进程先结束,子进程则自动托管到init进程(pid = 1)。
       父进程调用waitpid()等函数来接收子进程退出状态。
考虑子进程先于父进程结束的情况:
       若父进程未处理子进程退出状态,在父进程退出前,子进程一直处于僵尸进程状态。
       若父进程调用waitpid()(应使用阻塞调用确保子进程先于父进程结束)来等待子进程结束,将会使父进程在调用waitpid()后进入睡眠状态,只有子进程结束父进程的waitpid()才会返回。 如果存在子进程结束,但父进程还未执行到waitpid()的情况,那么这段时期子进程也将处于僵尸进程状态。

由此可以看出,在子进程先于父进程结束的情况下,即使父进程调用waitpid(),但子进程结束前父进程还未执行waitpid()的话,子进程仍有机会成为僵尸进程。那么如何使父进程更方便地创建不会成为僵尸进程的子进程呢?这就要用两次fork()了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>

int main()
{
pid_t pid;
//创建第一个子进程
pid = fork();
if (pid < 0)
{
perror("fork error:");
exit(1);
}
//第一个子进程
else if (pid == 0)
{
//子进程再创建子进程
printf("I am the first child process.pid:%d\tppid:%d\n",getpid(),getppid());
pid = fork();
if (pid < 0)
{
perror("fork error:");
exit(1);
}
//第一个子进程退出
else if (pid >0)
{
printf("first procee is exited.\n");
exit(0);
}
//第二个子进程
//睡眠3s保证第一个子进程退出,这样第二个子进程的父亲就是init进程里
sleep(3);
printf("I am the second child process.pid: %d\tppid:%d\n",getpid(),getppid());
exit(0);
}
//父进程处理第一个子进程退出
if (waitpid(pid, NULL, 0) != pid)
{
perror("waitepid error:");
exit(1);
}
exit(0);
return 0;
}

父进程一次fork()后产生一个子进程随后立即执行waitpid(子进程pid, NULL, 0)来等待子进程结束,然后子进程fork()后产生孙子进程随后立即exit(0)。这样子进程顺利终止(父进程仅仅给子进程收尸,并不需要子进程的返回值),然后父进程继续执行。这时的孙子进程由于失去了它的父进程(即是父进程的子进程),将被转交给init进程托管。于是父进程与孙子进程无继承关系了,它们的父进程均为init,init进程在其子进程结束时会自动收尸,这样也就不会产生僵尸进程了。

注:以上总结引用自孤儿进程与僵尸进程和为何要fork()两次来避免产生僵尸进程

计算机启动

发表于 2017-08-26 | 分类于 计算机原理

计算机启动过程有哪些阶段

第一阶段:BIOS
第二阶段:MBR
第三阶段:硬盘启动
第四阶段:操作系统

第一阶段:BIOS:控制权为BIOS

概念

BIOS:BIOS是一组固化到计算机内主板上一个ROM芯片上的程序,它保存着计算机最重要的基本输入输出的程序、开机后自检程序和系统自启动程序,它可从CMOS中读写系统设置的具体信息。
CMOS:CMOS 是电脑主机板上一块特殊的RAM芯片,是系统参数存放的地方,而BIOS中系统设置程序是完成参数设置的手段。CMOS芯片由主板上的钮扣电池供电,即使系统断电,参数也不会丢失。
实模式:最早期的8086 CPU(16位)只有一种工作方式,那就是实模式,而且数据总线为16位(一次最多能取2^16=64KB数据,实模式下每个段最大只有64KB),地址总线为20位(寻址的能力是2^20=1MB,实模式下CPU的最大寻址能力),实模式下所有寄存器都是16位。8086处理器在实模式下的存储器寻址方式由段寄存器的内容乘以16作为基地址,加上段内的偏移地址形成最终内存中实际的物理地址。
这里回答一个问题:在8086/8088中,只有20根地址总线,所以可以访问的地址是2^20=1M,但由于8086/8088是16位地址模式,能够表示的地址范围是0-64K,所以为了在8086/8088下能够访问1M内存(16位寄存器怎么表示20位的寻址空间?),Intel用了分段的方法 —— segment:offset. 实际的地址是segment << 4 + offset。
保护模式:从80286开始就有了保护模式,从80386开始CPU数据总线和地址总线均为32位,而且寄存器都是32位。但80386以及现在的奔腾、酷睿等等CPU为了向前兼容都保留了实模式,现代操作系统在刚加电时首先运行在实模式下,然后再切换到保护模式下运行。

BIOS映射到内存

boot block: 当启动中计算机的时候,一开始BIOS并不会马上把Flash ROM中的内容copy到shadow memory,CPU会执行一部分代码,我们习惯把这一段代码叫做Boot Block。之所以要把Flash ROM中的内容loaded到memory,是因为当对CPU,memory进行完initialized,由于ROM的执行速度远比RAM要低,才会把BIOS shadow到memory中去,这样可以reduce POST的时间。所以我们在OS下会发现BIOS的content会shadow到memory的最顶端,举个例子,如果你是用的32bit的XP(support 4G),你就可以在FFF00000-FFFFFFFF(对1M的BIOS而言,如果是512K地址则是FFF7FFFF-FFFFFFFF)这段memory空间找到你的BIOS的content。

注:以下内容参考并摘抄自《CPU rest》
i386 CPU取的第一条指令地址与所处模式: 当CPU 抓到第一条指令,并执行的时候,如果说处于real mode,那么FFFFFFF0h的地址,早已超出了real mode CPU的寻址范围。也许很多人都讨论过这个问题。首先,可以确定的是:
此时CPU并不处于protect mode。CPU进入protect mode的标志是CR0的PE bit置1,而当CPU执行第一条指令时,此Flag bit并未置1。如果说此时CPU处于real mode, 那么此时CS=F000h, IP=FFF0h,(CS和IP的初始值应当是8086/8088-1M物理地址空间-时代编址BIOS第一条指令地址遗留下来的) 按照real mode的寻址方式, 此时形成的第一条指令的地址应该是 CS:IP=F000h 10h+FFF0h =FFFF0h, 而不是FFFFFFF0h。(有文档说, CPU第一条指令会从 FFFFFFF0h抓,而有文档说是从FFFF0抓, 那到底从那里抓呢?一切以 Intel的说法为准,Intel 说从FFFFFFF0h抓,那就是从FFFFFFF0h抓。)
CPU寻址和所在的模式无关,它只会使用内部的base address寄存器加上offset,实模式下base address是段寄存器×16,保护模式下就是加载段选择子的内容。自从386以来,CS不再是单个的段寄存器,而是包括Segment selector,segment base,和segment limit 3个register的一组寄存器。Segment Base决定着基地址的值。当 CPU 处于实模式段寻址方式时,如果段寄存器装入F000h, 那么 CPU 会自动将F000h
10h = F0000h装入Segment Base,那么最终形成的地址就是F0000h+IP, 即Segment Base+IP形成地址。然而实际情况是,CPU reset过后,虽然此时CPU的确处于段寻址模式,Segment Selector的值为F000h,但是Segment Base register的值却并没有按照段寻址模式的方式装入,而是被设置为Segment Base=FFFF0000h,按照Segment Base+IP的方式,最终形成的地址为FFFF0000h + FFF0h = FFFFFFF0h。这就是为何第一条指令会从 FFFFFFF0h抓,而不是FFFF0h抓的原因。
那么,其实我们可以说,CPU执行第一条指令时,它处于一种不普通的real mode中。
(段寻址模式:在寻址一个内存具体单元时,由一个基址再加上某些寄存器提供的16位偏移量来形成实际的20位物理地址。这个基址就是CPU中的段寄存器。在形成20位物理地址时,段寄存器的16位的数会自动左移4位,然后与16位偏移量相加,即可形成所需的内存地址。这种寻址方式的实质还是基址寻址。)

地址映射:在CPU读到BIOSCode之前,一切工作都是由HW完成,其中南桥做了一些很重要的幕后工作。在CPUreset和BIST之后,CPU将会去读取第一条指令。对于传统的 CPU + 北桥 + 南桥类型的platform来说,CPU的request通过FSB到达北桥,北桥将这个request透过ESI送到南桥。而对于最新的chipset来说,北桥和CPU封装在一颗Chip里面,所以会看到这个request通过DMI/QPI被送到南桥。Request到达南桥后,南桥根据配置决定将request route 到SPI或者LPC。南桥自己有一张特殊的address mapping table,存在两个这样的区域,第一个是从地址空间4G向下,大小从4MB到16MB不等的一个区域,以4MB为例,地址空间从FFFC00000h~FFFFFFFFh,称之为Range4G。CPU reset后会从FFFFFFF0h抓第一条指令,这第一条指令正好处于南桥的4G范围内,能享受南桥的special service。第二个受到南桥关照的地址空间,一般是从1MB向下128KB的范围,即ESegment和FSegment,从E0000~FFFFF,称之为Legacy Range,也就是说,不仅FFFC00000h~FFFFFFFFh之间的request能够被route到SPI/LPC,E0000~FFFFF之间的request也同样会被route到SPI/LPC。
CPU reset的时候,内存还没初始化,所以不可能从内存XXXX的位置读取第一条指令。CPU从来不会直接从内存中读数据,CPU的读写,都是针对自己的地址空间而言。假设一个 CPU,其地址空间大小是4GB,这个系统拥有一根1GB的内存。内存初始化过后,其地址被映射到0~3FFFFFFFh的地址空间,假设CPU要访问一个地址10000h,CPU的request首先针对自己的地址空间,然后 CPU的硬件会将这个请求转化为一个bus的请求,比如 FSB or PCI,然后内存控制器从bus上收到这样一个请求后,会确认这个请求是否在内存的地址空间,如果是在内存映射的空间内,则将其重定向到内存中,然后就由内存回复这个请求。 如果CPU访问一个地址8FFFFFFFh,当这个请求到达bus被内存控制器收到后,经过比较,这个地址不在内存映射的范围内,所以内存控制器将不处理这个请求, 然后这个请求可能最终通过北桥,到达南桥。当CPU reset时,其第一条指令请求的地址为FFFFFFF0h,此时内存并未初始化,这个请求会从bus上被转发到南桥,南桥最终将其重定向到LPC/SPI的ROM上,所以boot block阶段,所有的代码都是从ROM上读取的。这也是从概念上区分boot block/PEI和POST/DXE阶段的方法。那么,如果内存初始化过后,CPU读取FFFFFFF0h的request是否有可能被内存接收到呢?No,在内存控制器初始化的时候,会设置一些参数,保留几段地址空间的区域给flash rom、PCI设备分配使用,它确保不会将内存映射到这些地址空间上。所以内存初始化过后,地址请求FFFFFFF0h还是会被北桥转发出去,不会decode到内存。但是,如果一个PCI设备,在南桥之前收到了这个请求,并且解码了这个地址,回复了CPU的请求,那么将不会从南桥的LPC/SPI boot。

注:以下内容参考自OldLinux’s Archiver上关于内存的讨论。
8086时代 - 1M物理地址空间:机器加电成功后,CPU自设定为初试状态,开始准备运行(以实模式),此时RAM是空的。不同BIOS厂商生产的BIOS代码量不一样,因此习惯将BIOS的第一条指令编码到特定地址上(CS:IP = 0xF000:0xFFF0处),CPU则被预编程查找这个固定的位置。该位置放置一个jmp指令以跳转到真正执行的代码位置,通过jmp指定位置来适配不同数量的BIOS代码。事实上CS:IP = 0xF000:0xFFF0离实模式的极限地址0xF000:0xFFFF只有16个字节了,也就安排得开一个跳转指令和其他几条简短指令。而我们实际究竟需要使用多长的冷启动代码由jmp 0xF000:0xXXXX中的偏移0xXXXX来把握,如果使用得多,0xXXXX就小,使用得少,0xXXXX就大。BIOS冷启动代码编址到1M顶部保证启动代码尽量靠后,而不浪费多余的地址空间,由于地址空间安排在最后,也不会把整个地址空间隔离成两段。
i386时代:我们说的bios rom,当然这包括很多bios的rom,如视频bios rom和SCSI卡bios rom等,当然还包括我们传统意义上的包含冷启动程序代码以及一系列原始中断服务程序的bios的rom,由于技术的发展,这一部分rom现在变得非常大,如现在一般的传统bios rom就达2M以上,而视频bios rom等都更大。以前习惯上把这些rom的地址空间安排在640k以上1M以下的空间里(也就是说640k~1M这一段地址指向rom而不是指向ram),但这一点空间是明显安排不下现在的rom的,又为了不让这一段rom的地址将ram的地址隔离成两段,一般都将这段rom的地址空间安排在cpu所能访问到的地址的最高端。因此,为了保持对8086/8088的兼容性,所以intel采用了一个变通的方法,具体就是:
1)CPU复位后得到机器执行的第一条指令地址0xFFFFFFF0(cpu取指令时,由于是从物理上使A20~A31地址线为1,所以当cs为f000,指令偏移为FFF0时,形成物理地址FFFF0,再加上A20~A31都为1,实际得到的物理地址是:FFFFFFF0)。执行一条跳转指令jmp f000:xxxx跳转到启动代码的开始处。(段内跳转,跳转的位置仍然是rom的位置)
2)在实模式下,intel把BIOS中的冷启动代码映射到32位cpu所能寻址的4G地址空间的最后64k,把A20~A31地址线全置为1,把CS = 0xf000,这样,cpu所得到的物理地址就是0xffff0000 - 0xffffffff,即4G地址空间的最后64K。(猜测映射过程并没有复制数据到4G顶部,也就是说,可能被映射的物理地址空间没有数据但仍然无法被使用!)
3)通过执行启动代码(或者别的方法)把BIOS中的ROM内容关于实模式的代码和数据拷贝到物理地址640K~1Mb处,这样使得在实模式下,cpu也能通过动态映射访问BIOS中的部分内容。
4)执行一个段间跳转,屏蔽掉地址的A20~A31线(intel规定任何一个段间转移指令可以置触发器的输出为0),从而来到低地址空间,接着执行从rom bios拷贝过来的代码。
(内存控制器控制CPU对BIOS中ROM内容的访问到底转发到RAM还是ROM,而4G处则一直映射着全部的BIOS数据。按照《CPU rest》的分析,内存控制器初始化的时候,会设置一些参数,保留几段地址空间的区域给flash rom、 PCI设备分配使用,它确保不会将内存映射到这些地址空间上,那么复制BIOS的数据到4G处将没有多大的意义,因为也读不了,而且实模式复制数据到4G又得采取特殊的方式。)

BIOS硬件自检

注:以下内容参考自计算机是如何启动的
BIOS程序首先检查,计算机硬件能否满足运行的基本条件,这叫做”硬件自检”(Power-On Self-Test),缩写为POST。
如果硬件出现问题,主板会发出不同含义的蜂鸣,启动中止。如果没有问题,屏幕就会显示出CPU、内存、硬盘等信息。

启动顺序

注:以下内容参考自维基百科-主引导记录和计算机是如何启动的
当BIOS检查到硬件正常并与CMOS中的设置相符后,BIOS需要知道,”下一阶段的启动程序”具体存放在哪一个设备。也就是说,BIOS需要有一个外部储存设备的排序,排在前面的设备就是优先转交控制权的设备。这种排序叫做”启动顺序”(Boot Sequence)。按照CMOS中对启动设备的设置顺序检测可用的启动设备。BIOS将相应启动设备的第一个扇区(也就是MBR扇区)读入内存地址为0000:7C00H处,然后检查0000:7CFEH-0000:7CFFH(MBR的结束标志位)是否等于55AAH,若不等于则转去尝试其他启动设备(往往还对磁盘是否有写保护、主引导扇区中是否存在活动分区等进行检查),如果没有启动设备满足要求则显示”NO ROM BASIC”然后死机。当检测到有启动设备满足要求后,BIOS将控制权交给相应启动设备。根据MBR中的引导代码启动某个分区的引导程序。

第二阶段:MBR:控制权为满足要求的启动设备

注:以下内容参考自维基百科-主引导记录和计算机是如何启动的

概念

主引导记录:主引导记录(Master Boot Record,缩写:MBR),又叫做主引导扇区,是计算机开机后访问硬盘时所必须要读取的首个扇区,它在硬盘上的三维地址为(柱面,磁头,扇区)=(0,0,1)。在深入讨论主引导扇区内部结构的时候,有时也将其开头的446字节内容特指为“主引导记录”(MBR),其后是4个16字节的“磁盘分区表”(DPT),以及2字节的结束标志(55AA)。因此,在使用“主引导记录”(MBR)这个术语的时候,需要根据具体情况判断其到底是指整个主引导扇区,还是主引导扇区的前446字节。(这512个字节的最后两个字节是0x55和0xAA,表明这个设备可以用于启动;如果不是,表明设备不能用于启动,控制权于是被转交给”启动顺序”中的下一个设备。)
BIOS POST之后call INT 19h,主引导记录本身就是中断信号INT 19h的处理程序。

MBR结构

启动代码

主引导记录最开头是引导代码。其中的硬盘引导程序的主要作用是检查分区表是否正确并且在系统硬件完成自检以后将控制权交给硬盘上的引导程序(如GNU GRUB)。它不依赖任何操作系统,而且启动代码也是可以改变的,从而能够实现多系统引导。
随着计算机操作系统越来越复杂,位于主引导记录的空间已经放不下引导操作系统的代码,于是就有了第二阶段的引导程序,而MBR中代码的功能也从直接引导操作系统变为了引导第二阶段的引导程序。例如在普通的个人电脑上,引导程序通常分为两部分:第一阶段引导程序(Boot Loader)位于主引导记录(MBR),用以引导位于某个分区上的第二阶段引导程序(Boot Loader),如NTLDR、BOOTMGR和GNU GRUB等。

分区表

考虑到每个区可以安装不同的操作系统,”主引导记录”因此必须知道将控制权转交给哪个区。分区表的长度只有64个字节,里面又分成四项,每项16个字节。所以,一个硬盘最多只能分四个一级分区,又叫做”主分区”。每个主分区的16个字节,由6个部分组成:

1
2
3
4
5
6
(1) 第1个字节:如果为0x80,就表示该主分区是激活分区,控制权要转交给这个分区。四个主分区里面只能有一个是激活的。
(2) 第2-4个字节:主分区第一个扇区的物理位置。
(3) 第5个字节:主分区类型。
(4) 第6-8个字节:主分区最后一个扇区的物理位置。
(5) 第9-12字节:该主分区第一个扇区的逻辑地址。
(6) 第13-16字节:主分区的扇区总数。

最后的四个字节(”主分区的扇区总数”),决定了这个主分区的长度。也就是说,一个主分区的扇区总数最多不超过2的32次方。如果每个扇区为512个字节,就意味着单个分区最大不超过2TB。再考虑到扇区的逻辑地址也是32位,所以单个硬盘可利用的空间最大也不超过2TB。如果想使用更大的硬盘,只有2个方法:一是提高每个扇区的字节数,二是增加扇区总数。

下面是一个例子:
如果某一分区在硬盘分区表的信息如下

1
80 01 01 00 0B FE BF FC 3F 00 00 00 7E 86 BB 00

则我们可以看到,最前面的”80”是一个分区的激活标志,表示系统可引导[1];”01 01 00”表示分区开始的磁头号为1,开始的扇区号为1,开始的柱面号为0;”0B”表示分区的系统类型是FAT32,其他比较常用的有04(FAT16)、07(NTFS);”FE BF FC”表示分区结束的磁头号为254,分区结束的扇区号为63、分区结束的柱面号为764;”3F 00 00 00”表示首扇区的相对扇区号为63(小端序);”7E 86 BB 00”表示总扇区数为12289662(小端序)。

主引导记录的内存地址为0x7C00的原因

注:以下内容参考自Why BIOS loads MBR into 0x7C00 in x86
0x7C00这个地址来自IBM PC 5150 BIOS Developer Team,IBM PC 5150使用Intel 8088处理器,是现代x86(32bit) IBM PC/AT兼容机的祖先。DOS 1.0要求至少32KiB的内存空间,所以BIOS团队不再考虑16KB的启动情况。BIOS开发团队基于以下原因设计了0x7C00地址:
1、他们想在32KiB空间内留出尽可能多的空间来加载操作系统本身。
2、8086/8088使用0x0 - 0x3FF存储中断向量,BIOS数据存在它后面。
3、启动扇区大小是512字节,启动程序的stack/data区域需要512字节。
4、操作系统加载后,主引导记录就没有用处了,此后它所在的内存地址可以被操作系统重新利用。
5、所以,32KiB的最后1024B被选来加载MBR。计算过程为 0x7FFF - 512 - 512 + 1 = 0x7C00。
操作系统加载后,内存布局如下:

第三阶段:硬盘启动:控制权为硬盘上的启动程序

注:以下内容参考自计算机是如何启动的

情况A:卷引导记录

四个主分区里面,只有一个是激活的。MBR的引导代码读取激活分区的第一个扇区,叫做”卷引导记录”(Volume boot record,缩写为VBR),并将控制权交给它。
“卷引导记录”包含了操作系统引导代码,它的主要作用是,告诉计算机,操作系统在这个分区里的位置。然后,计算机就会加载操作系统了。
NTFS文件系统的卷引导记录,该文件始终位于卷的第一个簇,其中包含引导代码(用于定位并启动NTLDR/BOOTMGR,NTLDR–NT loader的缩写,是微软的Windows NT系列操作系统的引导程序,包括Windows XP和Windows Server 2003)、BIOS参数块(其中包含卷序列号),以及$MFT和$MFTMirr所在的簇编号。

情况B:扩展分区和逻辑分区

随着硬盘越来越大,四个主分区已经不够了,需要更多的分区。但是,分区表只有四项,因此规定有且仅有一个区可以被定义成”扩展分区”(Extended partition)。
所谓”扩展分区”,就是指这个区里面又分成多个区。这种分区里面的分区,就叫做”逻辑分区”(logical partition)。在MBR分区表中最多4个主分区或者3个主分区+1个扩展分区,也就是说扩展分区只能有一个,然后可以再细分为多个逻辑分区。
计算机先读取扩展分区的第一个扇区,叫做”扩展引导记录”(Extended boot record,缩写为EBR)。和MBR结构类似,它里面也包含一张64字节的分区表,但是最多只有两项(也就是两个逻辑分区),其分区表的第一项指向该逻辑分区本身的引导扇区,第二项指向下一个逻辑驱动器的EBR,分区表第三、第四项没有用到。
计算机接着读取第二个逻辑分区的第一个扇区,再从里面的分区表中找到第三个逻辑分区的位置,以此类推,直到某个逻辑分区的分区表只包含它自身为止(即只有一个分区项)。因此,扩展分区可以包含无数个逻辑分区。
但是,似乎很少通过这种方式启动操作系统。如果操作系统确实安装在扩展分区,一般采用下一种方式启动。

情况C:启动管理器

注:以下内容参考自GNU GRUB
计算机启动后,BIOS将寻找第一个可启动的设备(通常为硬盘),而后从MBR中载入启动程序,然后把控制交给这段代码,情况C控制权给了GNU GRUB。MBR位于硬盘的前512字节内。

GNU GRUB在MBR分区表的硬盘上

GRUB第一版
GRUB的步骤1包含在MBR中。由于受MBR的大小限制,步骤一所做的几乎只是装载GRUB的下一步骤(存放在硬盘的其它位置)。步骤1既可以直接装载步骤2,也可以装载步骤1.5:GRUB的步骤1.5包含在MBR后面的30千字节中。步骤1.5载入步骤2。
当步骤2启动后,它将呈现一个界面来让用户选择启动的操作系统。这步通常采用的是图形菜单的形式,如果图形方式不可用或者用户需要更高级的控制,可以使用GRUB的命令行提示,通过它,用户可以手工指定启动参数。GRUB还可以设置超时后自动从某一个内核启动。

GRUB第二版本
与GRUB第一版相似的是,boot.img像步骤1一样在MBR或在启动分区中,但是,它可以从任何LBA48地址的一个扇区中读取,它(boot.img)将读取core.img(产生于diskboot.img)的第一个扇区以用来后面读取core.img的剩余部分。core.img正常情况下跟步骤1.5储存在同一地方并且有着同样的问题,可是,当它被移动到一个文件系统或一个纯粹的分区时会比在步骤1.5移动或删除引起更少的麻烦。一旦完成读取,core.img会读取默认的配置文件和其他需要的模块。
GRUB配置文件的文件名和位置随系统的不同而不同;如在Debian(GRUB Legacy)和OpenSUSE中,这个文件为/boot/grub/menu.lst,而在Fedora和Gentoo中为/boot/grub/grub.conf。Fedora、Gentoo Linux和Debian(GRUB 2)使用/boot/grub/grub.conf。 Fedora为了兼容文件系统层次结构标准提供了一个从/etc/grub.conf到/boot/grub/grub.conf的符号链接。
当GRUB启动后
一旦选择了启动选项,GRUB把选择的内核载入内存并把控制交给内核。在此步骤中,对于Windows之类不支持多启动标准的操作系统,GRUB也可以通过链式启动把控制传给其它启动器。在这种情况下,其它操作系统的启动程序被GRUB保存了下来;与内核不同,其它操作系统如同直接自MBR启动。类似Windows的启动菜单,也许是另一个启动管理器,它允许在多个不支持多启动的操作系统中做进一步的选择。(在已有Windows的系统上面,或者包含多个Windows版本的系统上安装现代的Linux而不修改原操作系统,即属于这类情况。)

GNU GRUB在GPT分区表的硬盘上

第四阶段:操作系统:控制权为操作系统内核

注:以下内容参考自计算机是如何启动的
操作系统的内核首先被载入内存。
以Linux系统为例,先载入/boot目录下面的kernel。内核加载成功后,第一个运行的程序是/sbin/init。它根据配置文件(Debian系统是/etc/initab)产生init进程。这是Linux启动后的第一个进程,pid进程编号为1,其他进程都是它的后代。
然后,init线程加载系统的各个模块,比如窗口程序和网络程序,直至执行/bin/login程序,跳出登录界面,等待用户输入用户名和密码。
至此,全部启动过程完成。

内存管理之地址转换和保护机制

发表于 2017-08-26 | 分类于 计算机原理

注:以下分析引用自 80386 保护模式简介

保护模式简介

Intel 推出 x86 架构已近 30 年,刚开始推出的 8086 处理器是一款 16 位的处理器,它标志着 x86 架构的诞生,这种 16 位处理器数据总线是 16 位的,而地址总线是 20 位的,最多可以寻址 1MB 的地址空间。之后的 80286 处理器也是 16 位,但是地址总线有 24 位,而且从 80286开始 CPU 演变出两种工作模式:实模式和保护模式;而 80386 则是 Intel 推出的 80x86 系列中的第一款 32 位处理器,它的数据总线与地址总线都是 32 位,可以寻址 4G 的地址空间; AMD 公司随后在 2000 年又在 x86 架构的基础上推出了 x86-64 处理器架构, AMD 的处理器可以兼容 32 位的指令集,所以它既是 64 位的又是 32 位的。

在 x86 架构中, 16 位的处理器与 32 位处理器所对应的寄存器是有所不同的。像 8086 寄存器组就分为通用寄存器、专用寄存器和段寄存器三类总共 15 个,其中通用寄存器有 AX、BX、 CX、 DX、 SP、 BP、 DI 及 SI,专用寄存器包括 IP、 SP 和 FLAGS 三个 16 位寄存器,而段寄存器则有 CS、 DS、 SS、 ES,这些寄存器都是 16 位的。 32 位 x86 架构对应的寄存器则共有 34 个,其中包括 EAX、 EBX、 ECX、 EDX、 ESI、 EDI、 EBP、 ESP 8 个 32 位的通用寄存器; 6 个 16 位的段寄存器 CS、 DS、 SS、 ES、 FS、 GS,相比 8086 增加了 FS 和 GS;GDTR、 LDTR、 IDTR 和 TR 四个系统地址寄存器; EFLAGS、 EIP、 CR0—CR3 6 个状态和控制寄存器,在这里标志寄存器 EFLAGS 与指令指针寄存器 EIP 都从 16 位进化到了 32 位;还有就是增加了一些调试寄存器、段描述符寄存器以及测试寄存器。

保护模式(Protected Mode) 是一种和 80286 系列及之后的 x86 兼容 CPU 操作模式。保护模式有一些新的特色,设计用来增强多功能和系统稳定度,比如内存保护、分页、系统以及硬件支持的虚拟内存。大部分的现今 x86 操作系统都在保护模式下运行,包含 Linux、 FreeBSD、以及 微软 Windows 2.0 和之后版本。需要指出的是,保护模式在增加这些新特性的同时,也带来了系统软件设计的复杂性。

在 8086 时代, CPU 中设置了四个段寄存器: CS、 DS、 SS 和 ES,分别用于可执行代码段、数据段以及堆栈段。每个段寄存器都是 16 位的,对应于地址总线中的高 16 位。每条“访存”指令中的内部地址也都是 16 位的,但是在送到地址总线之前,CPU 内部会自动地把它与某个段寄存器中的内容相加。因为段寄存器中的内容对应于 20 位地址总线中的高 16 位,所以相加时实际上是地址总线中的高 16 位与段寄存器中的 16 位相加,而低 4 位保留不变,这样就形成一个 20 位的实际地址,也就实现了从 16 位内存地址到 20 位实际地址的转换,或者叫 “映射”。

到了 80286 时代,它的地址总线位数增加到了 24 位,因此可以访问到 16MB 的内存空间。更重要的是从此开始引进了一个全新理念——保护模式。这种模式下内存段的访问受到了限制。访问内存时不能直接从段寄存器中获得段的起始地址了,而需要经过额外转换和检查。为了和 8086 兼容, 80286 内存寻址可以有两种方式,一种是先进的保护模式,另一种是老式的 8086 方式,被称为实模式。 Intel 选择了在段寄存器的基础上构筑保护模式,并且保留 16 位的段寄存器。不同的是,在保护模式下,段范围不再受限于 64K,可以达到 16MB(或者 80386 的 4GB)。

寻址方式的变化

实模式下段的管理

实模式采用 16 位寻址模式,在该模式中,最大寻址空间为 1MB,最大分段为 64KB。由于处理器的设计需要考虑到向下兼容的问题,实模式也是我们今天接触到的大多数计算机在启动后处于的寻址模式。

8086 处理器地址总线扩展到 20 位,但算术逻辑运算单元( ALU)宽度即数据总线却只有 16 位,也就是说直接参与运算的数值都是 16 位的。为支持 1MB 寻址空间, 8086 在实模式下引入了分段的方法。在处理器中设置了四个 16 位的段寄存器: CS、 DS、 SS、 ES,对应于地址总线中的高 16 位。寻址时,采用以下公式计算实际访问的物理内存地址:实际物理地址 = (段寄存器 << 4) + 偏移地址。这样,便实现了 16 位内存地址到 20 位物理地址的转换。

我们回顾一下实模式下程序的运行。程序运行的实质就是指令的执行,显然 CPU 是指令得以执行的硬件保障,而 CPU 是如何知道指令在什么地方呢? 80x86 系列是使用 CS 寄存器配合 IP 寄存器的组合来通知 CPU 指令在内存中的位置。 程序指令在执行过程中一般还需要有各种数据, 80x86 系列有 DS、 ES、 FS、 GS、 SS 等用于指示不同用途的数据段在内存中的位置。程序可能需要调用系统的服务子程序, 80x86 系列使用中断机制来实现系统服务。总的来说,这些就是实模式下一个程序运行所需的主要内容。

我们再来回顾一下实模式下的寻址方式。寻址方式一共有以下 8 种:

1
2
3
4
5
6
7
8
1. 立即数寻址 例如: MOV AX, 1234H  
2. 寄存器寻址 例如: MOV AX, BX
3. 直接寻址 例如: MOV AX, [1234H]
4. 寄存器间接寻址 例如: MOV AX, [BX]
5. 基址寻址 例如: MOV AX, [BX+100H]
6. 变址寻址 例如: MOV AX, [SI+100H]
7. 基址加变址寻址 例如: MOV AX, [BX+SI]
8. 带位移的基址加变址寻址 例如: MOV AX, [BX+SI+100H]

纵然有这么多种的寻址方式,但实际上实模式的寻址本质上都是段基址左移 4 位加上偏移得到物理地址,如下图所示:

保护模式下段的管理

在保护模式下,分段机制是利用一个称作段选择子的偏移量到全局描述符表中找到需要的段描述符,而这个段描述符中就存放着段的在线性地址空间的位置,然后再加上偏移地址量便得到了最后的线性地址。
需要指出的是,在 32 位平台上,段基址和偏移址都是 32 位的,地址计算不再需要将段首地址左移 4 位了,直接相加即可,如果发生溢出的情况,则将溢出位舍弃。

80386 转换逻辑地址(程序员观点的地址)到物理地址分以下两步:
1、 分段地址转换,这一步中把逻辑地址(由段选择子和段偏移组成) 转换为线性地址。
2、 分页地址转换,这一步中把线性地址转换为物理地址。这一步是可选的,由系统软件设计者决定是否需要。

分段地址转换

我们可以这样理解这个寻址过程,首先有一个结构体类型(称为段描述符, Descriptor),它有三个成员变量:段基址、段界限、段属性,在内存中存在一个数组(称为全局描述符表, Global Descriptor Table)维护一组这样的结构体。段选择子( Selector)中存储的是对应的结构体在该数组中的下标,也就是索引,通过该索引从数组中找到对应的结构体,从而得到段基址,然后加上偏移量,得到最后的线性地址。为了这样的转换, 处理器用到了以下的数据结构:描述符( Descriptors)、描述符表(Descriptor tables)、选择子(Selectors)、段寄存器(Segment Registers)。

一般保护模式段式寻址可用 xxxx: yyyyyyyy 表示。其中 xxxx 表示索引,也就是段选择子,是 16 位的; yyyyyyyy 是偏移量,是 32 位的。到哪里去寻找全局描述符表呢? 80386 以及以后的处理器专门设计了一个寄存器 GDTR( Global Descriptor Table Register),专门用于存储全局描述符表在内存中存放的位置,当发生内存寻址与定位的时候,处理器通过该寄存器找到全局描述符表,并通过 xxxx 找到对应的描述符,进而得到该段的起始地址,并加上 yyyyyyyy 得到最终的物理地址。这个过程可以用下图来描述:

GDTR 寄存器

GDTR 寄存器有 48 位,其中有 32 位记录描述符表的物理地址, 16 位记录全局描述符表的长度(该表占据的物理内存字节数),如下图所示。

段选择符

段选择符为16位,它不直接指向段,而是通过指向的段描述符。选择子可以做为指针变量的一部分,从而对应用程序员是可见的,但是一般是由连接加载器来设置的。段选择符的结构如下图所示。

索引(Index),在描述符表中从8192个描述符中选择一个描述符。 处理器自动将这个索引值乘以8(描述符的长度),再加上描述符表的基址来索引描述符表,从而选出一个合适的描述符。索引值为 13 位(段选择符可寻找 2^13 个段描述符,每个段描述符占 2^3 字节,所有段选择符共可占 2^16 字节,而 GDTR 使用 1 6位记录段描述符的长度,刚好对应),所以在保护模式下最多可以表示 2^13=8192 个段描述符,而 TI 又分 GDT 和 LDT ,所以一共可以表示 81922=16384 个段描述符,每个段描述符可以指定一个具体的段信息,所以一共可以表示 16384 个段。而段内偏移地址为 32 位值,所以一个段最大可达 4GB ,这样 16384\4GB=64TB ,这就是所谓的 64TB 最大寻址能力。
TI 是表指示位,0代表应该访问全局描述符表(GDT),1代表应该访问局部描述符表。
RPL 表示请求特权级。保护机制使用该位。
注意:如此看来,段选择子的13位索引部分表示的是第几个段描述符,如果是第1个,则%cs应该为0x1(从0算起)。但这是不对的,因为低三位是RPL和TI,所以如果是GDT表的第0项且RPL为0,则索引第1个段描述符的段选择子的值应该是0x8。

段描述符

再来看看段描述符,段描述符实际上是一个占据 64 位内存( 8 个字节)的结构体,是处理器用来把逻辑地址映射为线性地址的必要数据结构。描述符是由编译器、连接器、加载器、或者是操作系统生成的,不能由应用程序员生成。下图显示了两种常用的描述符的格式。所有的段描述符都是这两种格式当中的一种:

另一种表示则如下所示:

下面我们将结合上图详细介绍一下段描述符中的段属性,请对应上述两种图进行思考。
一个 64 位的段描述符包含了段基址、段界限以及段属性。在描述符中,段基址占 32 位,段限长占 20 位,属性占 12 位。
由上图可知,段基址为 2, 3, 4, 7 字节,共 32 位。段限长为 0, 1 以及 6 字节的低四位,共 20 位,段限长即段最大长度,与粒度位 G 共同确定。粒度位 G(Granularity bit)决定了界限值被处理器解析的方式。
G:粒度位, G = 0 时,粒度为 1B,界限值被解析为以 1 字节为一个单元,描述符中的 20 位段限长为实际段限长,最大限长为 1MB( 0-FFFFFh)。 G = 1 时,粒度为 1 页(4KB),界限值以 4K 为一个单元,界限值在使用之前处理器将会把它先左移 12 位,低 12 位则自动插入 0,段大小可以高达 4G(这又恰好对应于寻址时偏移地址为 32 位的情况)。
D/B:对于不同类型段含义不同。在可执行代码段中,这一位叫做 D 位, D = 1 使用 32 位地址和 32/8 位操作数, D = 0 使用 16 位地址和 16/8 位操作数。在数据段中,这一位叫做 B 位, B = 1 段的上界为 4GB, B = 0 段的上界为 64KB。在堆栈段中,这一位叫做 B 位, B = 1 使用 32 位操作数,堆栈指针用 ESP, B = 0 使用 16 位操作数,堆栈指针用 SP。
AVL: Available and Reserved Bit,通常设为 0。
P:段存在位, P = 1 表示段在内存中。如果这一位为0,则此描述符为非法的,不能被用来实现地址转换。如果一个非法描述符被加载进一个段寄存器,处理器会立即产生异常。操作系统可以任意的使用被标识为可用(AVAILABLE)的位。一个实现基于段的虚拟内存的操作系统可以在以下情况下来清除存在位:1、当这个段的线性地址空间并没有完全被分页系统映射到物理地址空间时。2、当段根本没有在内存里时。
DPL:描述符特权级,取值 0 ~ 3 共 4 级。 0 特权级为最高,而 3 特权级为最低,表示访问该段时 CPU 所需处于的最低特权级,我们在后面会详细讨论特权级的问题。
S:描述符类型标志, S = 1 表示代码段或者数据段; S = 0 表示系统段(TSS、LDT)和门描述符。
TYPE:描述符类型,和 S 结合使用,可以表示的描述符类型有:代码段、数据段、 TSS、LDT、中断门( Interrupt Gate)、陷阱门( Trap Gate)、调用门( Call Gate)、任务门( Task Gate)。

  • 其中,根据描述符类型标志 S 和 TYPE 可以确定描述符的类型。
  • 当 S = 1 时,Type 的最后一位代表已访问位(Accessed bit)。当处理器访问该段时,将自动设置访问位。也就是说,当一个指向该段描述符的选择子被加载进一个段寄存器时或者当被一条选择子测试指令使用时。在段级基础上实现虚拟内存的操作系统可能会周期性的测试和清除该位,从而监视一个段的使用情况。
  • 当 S = 1 时, TYPE < 8 时,为数据段描述符。数据段都是可读的,不一定可写。如下图所示:
  • 当 S = 1 时, TYPE ≥ 8 时,为代码段描述符。代码段都是可执行的,一定不可写。如下图所示:

    当 S=1 时,TYPE中的4个二进制位情况:

    1
    2
    3
    4
    5
    6
         3        2       1       0
    执行位 一致位 读写位 访问位
    执行位:置1时表示可执行,置0时表示不可执行;
    一致位:置1时表示一致码段,置0时表示非一致码段;
    读写位:置1时表示可读可写,置0时表示只读;
    访问位:置1时表示已访问,置0时表示未访问。
  • S = 0 时,描述符可能为 TSS、 LDT 和 4 种门描述符。如下图所示:

段寄存器

80386把描述符的信息存储在段寄存器里,以便不用每次内存访问都去访问内存中的描述符表。
如下图所示,每一个段寄存器都有一个可见部分和一个不可见部分。这些段寄存器的可见部分被程序员当作一个16位的寄存器来使用。不可见的部分则只能由处理器来操纵。

加载这些寄存器的操作和一般的加载指令是一样的,这些指令分为两类:
1、直接的加载指令,例如, MOV, POP, LDS, LSS, LGS, LFS。 这些指令显示的访问这些段寄存器。
2、隐式的加载指令,例如, far CALL和JMP。这些指令隐式的访问CS 段寄存器,给它加载一个新的值。
使用这些指令,程序将用一个16位的选择子加载段寄存器的可见部分。 处理器自动将基址、界限、类型和其它信息从描述符表中加载到段选择子的不可见部分。因为很多数据访问指令访问的数据段选择子已经加载到段寄存器中,所以处理器可以直接把段相关的基址加上指令提供的偏移部分,而且不会有额外的加法开销。

GDT简介

注:以下分析引用自 全局描述符表(GDT)-《x86汇编语言:从实模式到保护模式》

同实模式一样,在保护模式下,对内存的访问仍然使用段地址加偏移地址。但是,在保护模式下,在每个段能够访问之前,必须先登记。这就好比像C语言中,“对变量的使用必须先定义”一样。

每个段在能够使用之前,都要为这个段建立一个描述符。每个描述符占8个字节,这些描述符集中存放在内存的某个区域,一个挨着一个,就构成了一张“表”。

80x86中有两种描述符表:
全局描述符表(Global Descriptor Table, 简称GDT)
局部描述符表(Local Descriptor Table,简称LDT)
一个描述符表仅仅是一个包含了很多描述符的8字节内存数组而以。描述符表是长度是可变的,最多可包含高达8192(2^13)个描述符。处理器用GDTR和LDTR来定位内存中的全局描述符表和当前的局部描述符表。这些寄存器存储了这些表的线性地址的基址和段长界限。指令LGDT和SGDT是用业访问全局描述符表寄存器的,而指令LLDT和SLDT则是用来访问局部描述符表寄存器的。

在进入保护模式之前,必须要定义GDT,也就是说,我们要在内存中构建出一张表。

需要说明的是:在整个系统中,全局描述符表GDT只有一张(一个处理器对应一个GDT);GDT可以被放在内存的任何位置,但CPU必须知道GDT的入口。

CPU如何知道GDT的入口呢?在处理器内部,有一个48位的寄存器,名叫GDTR,也就是全局描述符表寄存器。其结构如下图:

该寄存器分为2部分:
32位的线性基地址:GDT在内存中的起始线性地址(我们还没有涉及到分页,所以这里的线性地址等同于物理地址,下同,以后同);
16位的表界限:在数值上等于表的大小(总字节数)减去1;
注意:在处理器刚上电的时候,基地址默认为0,表界限默认为0xFFFF; 在保护模式初始化过程中,必须给GDTR加载一个新值。

因为表界限是16位的,最大值是0xFFFF,也就是十进制的65535,那么表的大小就是65535+1=65536.又因为一个描述符占用8个字节,所以65536字节相当于8192个描述符(65536/8=8192).故理论上最多可以定义8192个描述符。实际上,不一定这么多,具体多少根据需要而定。

理论上,GDT可以放在内存中的任何地方。但是,我们必须在进入保护模式之前就定义GDT,所以GDT一般都定义在1MB以下的内存范围中。当然,允许在进入保护模式后换个位置重新定义GDT。

接下来我们来分析一下MIT 6.828 jos中boot.S 中关于GDT的信息:


这是一个最简单版本的段描述符,仅仅把内存分为数据段(data seg)和代码段(code seg)。
其中 SEG_NULL 的定义为:

1
2
3
#define SEG_NULL \
.word 0, 0; \
.byte 0, 0, 0, 0

它的作用就是定义连续 8 个值为 0 的字节,这就表示一个空的 GDT 表项。处理器规定,GDT 中的第一个描述符必须是空描述符。由于全局描述符表的第一项是不被处理器使用的,所以当一个选择子的索引(Index)部分和表指示位(Table Indicator)都为0的时候(也就是说,选择子指向全局描述符表的第一项时),可以当做一个空的选择子。很多时候寄存器和内存单元的初始值都会为 0,或者程序设计无意中用全 0 的索引来选择描述符,如果第一个描述符不为0,很有可能经常错误选中描述符。当一个段寄存器被加载一个空选择子时,处理器并不会产生一个异常。但是,当用一个空选择子去访问内存时,则会产生异常。这个特点可以用来初始化不用的段寄存器,以防偶然性的非法访问。
而 SEG(type,base,lim)的定义为:

1
2
3
4
#define SEG(type,base,lim) \
.word (((lim) >> 12) & 0xffff), ((base) & 0xffff); \
.byte (((base) >> 16) & 0xff), (0x90 | (type)), \
(0xC0 | (((lim) >> 28) & 0xf)), (((base) >> 24) & 0xff)

在这里, type 表示段属性, base 表示段基址,而 lim 则表示段长的界限,给出这三个参数就可以用这个宏来定义一个 GDT 表项。在这里段属性的参数也是一般式通过宏的形式给出的,下表给出了常用的一些宏,这些宏每个都代表表项中的一个 bit 位,同时也代表一种段的属性。

1
2
3
4
5
6
7
宏         值       属性
STA_X 0x8 可执行的段
STA_E 0x4 向下扩展(该属性仅限于非可执行段)
STA_C 0x4 一致性的代码段(仅限于可执行段)
STA_W 0x2 可写(仅限于非可执行段)
STA_R 0x2 可读(仅限于可执行段)
STA_A 0x1 可访问的

分页地址转换

在地址转换的第二个阶段,80386 将线性地址转换为实物理地址。这个阶段实现了基于页的虚拟内存和页级保护机制。
分页地址转换过程是可选的。只有当CR0中的PG位置位时才会产生效果。 这个位的设置一般来说是由操作系统在系统初始化的过程中设置的。如果操作系统想要实现能运行多个虚拟8086任务、基于页级的保护、基于页级的虚拟内存的话,PG位是必需置位的。

处理器的控制寄存器的三个标识控制着分页:
1.PG (paging) flag。CR0寄存器的第31位。从Intel386处理器开始的所有IA-32处理器均可用。该位允许页转换机制。
2.PSE (page size extensions) flag。CR4的第4位。当PSE位复位时,页长度为4KB,此时存在二级页表机制;当PSE置位时,页长度为4MB或2MB(PAE置位时)。
3.PAE (physical address extension) flag。CR4的第5位。当该位置位时,提供了一种机制将物理地址扩展到36位。只有允许分页时才能使用物理地址扩展。

处理器用于转换线性地址为物理地址的信息包括四个数据结构:
1.页目录:32位页目录项的数组,占据一页(4K)空间,最多有1024个页目录项。页目录项的PS位表明页目录项指向的是页表(其页表项指向一个4K的页,此时PS为0),还是直接指向4M大小的页(此时PSE和PS为1),还是直接指向2M大小的页(此时PAE和PS为1)。
2.页表:32位页表项的数组,占据一页(4K)空间,最多有1024个页表项。当页长为4MB或2MB时,页表将不被使用。
3.页:4KB、2MB、4MB的平滑地址空间。
4.页目录指针表:包含4个64位条目项的数组,每一个指向一个页目录。该数据结构只有当允许物理地址扩展时才被使用。

线性地址和页表

线性地址通过使用一个页表,表内的一个页,和一个页内的偏移来映射到实物理地址外。格式如下:

下图显示处理器如何将线性地址中的DIR,PAGE和OFFSET字段转换为实物理地址上的,这个过程使用了两级页表。

高一级的页表称为页目录,本身占据一个页,每个低一级的页表也是占据一个页,其中每个页表项占4个字节,一个页目录/页表共有1K项。首先根据cr3寄存器获取到页目录的地址(jos系统中的页目录将存储在0x113000地址上,即在内核代码存储空间内),然后使用10-bit的DIR索引得到一个页目录项,该项存储了某个二级页表的信息;接着,使用10-bit的PAGE字段索引该二级页表,得到一个页表项,该项存储了某个物理页的信息;再使用OFFSET部分来索引该物理页桢,最终访问所需要的数据。

在寻址一个内存页时,使用了两级的页表。高一级的页表也被叫作页目录。页目录可最多寻址1K个二级页表。一个二级页表最多可寻址1K个页面。所以,一个页目录最多可寻址1M个页面。因为每个页面有4K(2^12)字节大小。所以一个页目录可寻址整个80386的实物理地址空间(2^20 * 2^12 = 2^32)。

页表项

两级页表项都有相同的格式,页表项的格式如下:

页桢地址(Page Frame Address):页桢地址指出了一个实物理页的开始地址。因为实物理页的地址是以4K为边界的,所以地址的低12位总是为0,所以二级页表使用20位页帧地址即可表示要访问的物理页的起始地址,该物理页包含了要访问的指令操作数。二级页表本身也是占据一个页,所以页目录使用20位页帧地址即可表示二级页表的起始地址。
存在位(Present Bit):存在位决定了一个页表项是否可以用作地址转换过程,如果P=1则可以用该页表项。当任何一级页表项的P=0时,该项都不可以用作地址转换过程,这时,该项的其它位可以被软件使用,它们中的任何一位都不会被硬件使用。当任何一级页表项的P=0时,而软件又试图用它来访问内存时,处理器将会引发一个异常。在支持页级虚拟内存的软件系里,缺页异常处理子程序可以将所需的页面调入物理内存。引起缺页异常的指令是可以重起的。注意,没有页目录自身的存在位。当任务挂起时,该任务的页目录是可以不存在的,但是操作系必须在一个任务被重运行前确保该任务的CR3映象(保存在TSS里)指示的页面(即页目录表)在内存中。下图显示了当P=0时的页表项格式:

已访问位和脏位(Accessed and Dirty Bits):这些位提供了两级页表的数据使用情况信息。除了页目录表的脏位(Dirty bit),所有的这些位都由硬件自动置位,但是处理器绝对不会复位它们。在一个页面被读或写之前,处理器将自动将两级页表的这些相关的位置1。当向一个地址写入时,处理器将会把相关的二级页表的脏位(Dirty bit)置为1。页目录表项的脏位没有作定义。当系统内存紧张时,一个支持页级虚拟内存的操作系统可以使用这些位来决定将要换出哪些物理页面。操作系统应该自已负责测试和清除这些相关位。
读/写位,用户/特权用户位(Read / Write and User / Supervisor Bits):这些位并不是用于地址转换过程的,它们是用来实现页级保护机制的,这些保护机制是在地址转换过程的同时实施的。

页地址转换缓存(Page Translation Cache)

为了获得最大的地址转换效率,处理器把最近使用的页表数据存储在一个芯片内的缓存中。只有当所要的地址转换信息没有在缓存中时,才有访问两级页表的必要。应用程序员是感觉不到页地址转换缓存的存在的,但系统程序员知来说不是。当页表内容改变时,操作系统程序员必须清除缓存。页地址转换缓存可以用以下两种方法清除:
1、 通过MOV指令重新加载CR3寄存器,例如,MOV CR3,EAX。
2、 通过任务切换到一个TSS,该TSS保存了一个不同的CR3映象。

保护模式完整的权限检查

特权级是保护模式下一个重要的概念,CPL,RPL和DPL是其中的核心概念。
CPL(CS.RPL)是当前进程的权限级别(Current Privilege Level),是当前正在执行的代码所在的段的特权级,存在于cs寄存器的低两位。
RPL是段选择子里面的bit 0和bit 1位组合所得的值,说明的是进程对段访问的请求权限(Request Privilege Level),是对于段选择子而言的。每个段选择子有自己的RPL,它说明的是进程对段访问的请求权限。RPL对每个段来说不是固定的,两次访问同一段时的RPL可以不同。RPL可能会削弱CPL的作用,例如当前CPL=0的进程要访问一个数据段,它把段选择符中的RPL设为3,这样它对该段仍然只有特权为3的访问权限。
DPL存储在段描述符中,规定访问该段的权限级别(Descriptor Privilege Level),每个段的DPL固定的、静态不变的。当进程访问一个段时,需要进程特权级检查,一般要求DPL >= max {CPL, RPL}。
采用RPL的原因是:假设当前进程的CPL是0,将访问DPL为3的段,如果进程对目标段的RPL > DPL,则当前进程仍然无法访问该段。因此控制RPL即可实现安全访问,即使线程的权限级别CPL很高。

对数据段和堆栈段访问时的特权级控制

程序访问数据段或堆栈段要遵循一个准则:只有相同或更高特权级的代码才能访问相应的数据段。即:访问数据段或堆栈段的程序的CPL≤待访问的数据段或堆栈段的DPL,同时选择子的RPL≤待访问的数据段或堆栈段的DPL。RPL可能会削弱CPL的作用,访问数据段或堆栈段时,默认用CPL和RPL中的最小特权(值最大)去访问数据段,所以max {CPL, RPL} ≤ DPL,否则访问失败。

对代码段访问的特权级控制(代码执行权的特权转移)

程序通过JMP或Call跳转时,根据目标代码段的不同对现有代码段的特权级要求有所不同,但最终结果是:一旦允许跳转,跳转后特权级别CPL不会发生变化。
首先我们来看看目标代码段有什么不同。

一致代码段和非一致代码段

一致代码段和非一致代码段的物理区分主要是根据代码段描述符里的TYPE来决定的。
一致代码段:简单理解,就是操作系统拿出来被共享的代码段,可以被低特权级的用户直接调用访问的代码。通常这些共享代码,是“不访问”受保护的资源和某些类型异常处理。比如一些数学计算函数库,为纯粹的数学运算计算,被作为一致代码段。
一致代码段的限制作用:
1、特权级高的程序不允许访问特权级低的程序:核心态不允许调用用户态的程序。
2、特权级低的程序可以访问到特权级高的程序。但是特权级不会改变:用户态还是用户态。
非一致代码段:为了避免低特权级的访问而被操作系统保护起来的系统代码。
非一致代码段的限制作用:
1、只允许同级间访问。
2、绝对禁止不同级访问:核心态不允许调用用户态的程序,用户态程序也不能访问核心态程序。
由此可知,跳转到一致代码段或非一致代码段,特权级限制是不同的。跳转又分直接跳转和通过调用门的跳转。具体是取决于目标段描述符的AR byte,有以下情况:CONFORMING-CODE-SEGMENT、NONCONFORMING-CODE-SEGMENT、CALL-GATE、TASK-GATE、TASK-STATE-SEGMENT。

以下跳转的内容可参考 https://pdos.csail.mit.edu/6.828/2014/readings/i386/CALL.htm 和https://pdos.csail.mit.edu/6.828/2014/readings/i386/JMP.htm 。

代码间跳转的特权级限制:直接跳转

普通转跳不经过Gate,JMP或Call后跟着48位全指针(16位段选择子+32位地址偏移),且其中的段选择子指向代码段描述符,这样的跳转称为直接(普通)跳转。普通跳转不能使特权级发生跃迁,即不会引起CPL的变化。

如果目标代码段是一致代码段:
要求:当前代码段CPL >= 目标代码段描述符DPL ,RPL不检查。适用于JMP和CALL。
结果:Load CS with new code segment selector。转跳后程序的CPL = 转跳前程序的CPL。
说明:CPL没有发生变化,纵使它执行了特权级DPL较高的代码。若访问时不满足要求,则发生异常。

如果目标代码段是非一致代码段:
要求:当前代码段CPL = 目标代码段描述符DPL AND 当前代码段CPL >= 目标代码段选择子RPL。此时目标代码段选择子RPL <= 目标代码段描述符DPL。适用于JMP和CALL。
结果:Load CS with new code segment selector; Set RPL field of CS register to CPL。转跳后程序的CPL = 目标代码段描述符DPL = 转跳前程序的CPL。
说明:因为前提是CPL=DPL,所以转跳后程序的CPL = 目标代码段描述符DPL不会改变CPL的值,特权级(CPL)也没有发生变化。如果访问时不满足前提CPL=DPL,则引发异常。

代码间跳转的特权级限制:通过调用门CALL-GATE的跳转

当段间转移指令JMP和段间转移指令CALL后跟着的目标段选择子指向一个调用门描述符时,该跳转就是利用调用门的跳转。这时如果选择子后跟着32位的地址偏移,也不会被cpu使用,因为调用门描述符已经记录了目标代码的偏移。使用调门进行的跳转比普通跳转多一个步骤,即在访问调用门描述符时要将描述符当作一个数据段来检查访问权限,具体是:当前代码段CPL <= 门描述符DPL;调用门选择子RPL <= 门描述符DPL。只有满足了以上条件,CPU才会进一步从调用门描述符中读取目标代码段的选择子和地址偏移。
从调用门中读取到目标代码的段选择子和地址偏移后(不同于普通跳转一开始就得到了目标代码的段选择子和地址偏移),CPU会将读到的目标代码段选择子中的RPL清0,即忽略了调用门中代码段选择子的RPL的作用。完成这一步后,CPU开始对当前程序的CPL,目标代码段选择子的RPL(事实上它被清0后总能满足要求)以及由目标代码选择子指示的目标代码段描述符中的DPL进行特权级检查,并根据情况进行跳转。

如果目标代码段是一致代码段:
要求:当前代码段CPL >= 目标代码段描述符DPL,RPL被清0,不检查,永远满足RPL <= DPL。适用于JMP和CALL。
结果:Load CS register with new code-segment descriptor;Set RPL of CS to CPL。转跳后程序的CPL = 转跳前程序的CPL。(CALL指令是SAME-PRIVILEGE)。

如果目标代码段是非一致代码段:
JMP指令:
要求:当前代码段CPL = 目标代码段描述符DPL,(RPL被清0,不检查)。
结果:Load CS register with new code-segment descriptor;Set RPL of CS to CPL。转跳后程序的CPL = 目标代码段描述符DPL = 转跳前程序的CPL。
CALL指令:
要求:当前代码段CPL >= 目标代码段描述符DPL,(RPL被清0,不检查)。
结果:如果当前代码段CPL > 目标代码段描述符DPL:(MORE-PRIVILEGE)Load CS descriptor;Set CPL to stack segment DPL;Set RPL of CS to CPL。
如果当前代码段CPL = 目标代码段描述符DPL:(SAME-PRIVILEGE)Load code segment descriptor into CS register,Set RPL of CS to CPL。转跳后程序的CPL = 目标代码段描述符DPL。

当条件CPL=DPL时,程序跳转后CPL=DPL,特权级不发生跃迁;当CPL>DPL时,程序跳转后CPL=DPL,特权级发生跃迁,这是我们当目前位置唯一见到的使程序当前执行忧先级(CPL)发生变化的跳转方法,即用CALL指令+调用门方式跳转,且目标代码段是非一致代码段。

代码间跳转的特权级限制:通过任务门TASK-GATE的跳转

当前代码段CPL <= 门描述符DPL;调用门选择子RPL <= 门描述符DPL,然后测试任务门给出的指向TSS的选择子,TSS描述符AR byte必须指向可用的TSS。切换到TSS的任务。

代码间跳转的特权级限制:通过TASK-STATE-SEGMENT的跳转

当前代码段CPL <= TSS描述符DPL;TSS选择子RPL <= TSS描述符DPL,TSS描述符AR byte必须指向可用的TSS。切换到TSS的任务。

jos系统保护机制

接下来我们来看一下MIT 6.828 jos中关于CPL、RPL、DPL的相关设置。
创建用户进程结构的时候,会设置env的trapframe相关寄存器值,其中,ds、es、ss、cs的低2位代表RPL,RPL说明了进程对该段选择子对应的段描述符描述的段的访问权限。trapframe相关寄存器值会被加载到相应寄存器的可视部分(选择子)。

执行程序过程中可能产生中断,中断会经由中断门或陷阱门,中断门或陷阱门的门描述符描述了中断处理程序的代码段选择子和偏移位置等信息。在这里,代码段选择子sel就是内核代码段选择子,权限是TI为0,RPL为0,所以给sel传递参数GD_KT即可(或者是GD_KT | 0更明显)。中断门本身也有DPL,约束了中断或异常的特权级别。如果触发中断的程序CPL>中断门的DPL,则不能调用相应处理函数。所以系统调用、断点需要设置dpl为3才能被用户程序调用。
如果可以调用中断处理函数,则调用之前处理器会将中断门sel部分加载到cs段选择器中(以及对应的cs隐藏部分),在这里是GD_KT,在GDT中查找到对应的段描述符,并加上偏移地址得出处理程序地址(中断处理程序定义在内核中,所以偏移地址也就是中断处理程序的入口地址)。

执行中断程序时,由代码段选择子的CPL/RPL和段描述符DPL决定是否能够访问内核代码段,段描述符DPL在GDT表的初始化过程中已经设置好了。

Intel 32 位处理器的工作模式

发表于 2017-08-26 | 分类于 计算机原理

注:以下内容引用自 16位模式/32位模式下PUSH指令探究-《x86汇编语言:从实模式到保护模式》读书笔记16

如上图所示,Intel 32 位处理器有3种工作模式。
(1)实模式:工作方式相当于一个8086
(2)保护模式:提供支持多任务环境的工作方式,建立保护机制
(3)虚拟8086模式:这种方式可以使用户在保护模式下运行8086程序(比如cmd打开的console窗口,就是工作在虚拟8086模式)

最早期的8086 CPU(16位)只有一种工作方式,那就是实模式,而且数据总线为16位(一次最多能取2^16=64KB数据,实模式下每个段最大只有64KB),地址总线为20位(寻址的能力是2^20=1MB,实模式下CPU的最大寻址能力),实模式下所有寄存器都是16位。
从 80286 开始就有了保护模式,从 80386 开始 CPU 数据总线和地址总线均为 32 位,而且寄存器都是 32 位。但 80386 以及现在的奔腾、酷睿等等 CPU 为了向前兼容都保留了实模式。

有几点需要特别说明:
(1)保护模式可分为16位和32位的,由段描述符中的D标志指明。对于32位代码段和数据段,这个标志总是设为1;对于16位代码和数据段,这个标志被设置为0.
D=1:默认使用32位地址和32位或8位的操作数。
D=0:默认使用16位地址和16位或8位的操作数。(主要是为了能够在32位处理器上运行16位保护模式的程序)
指令前缀0x66用来选择非默认值的操作数大小,0x67用来选择非默认值的地址大小。
(2)在实模式下,也可以使用32位的寄存器,比如

1
2
mov eax,ecx
mov ebx,0x12345678

(3)在书中,把实模式和16位的保护模式统称为“16位模式”;把32位保护模式称为“32位模式”。
(4)32位处理器可以执行16位的程序,包括实模式和16位保护模式。
(5)当处理器在16位模式下运行时,可以使用32位的寄存器,执行32位运算。
(6)在16位模式下,数据的大小是8位或者16位的;控制转移和内存访问时,偏移量也是16位的。
(7)32位保护模式兼容80286的16位保护模式。
(8)在16位模式下,处理器把所有指令都看成是16位的。

结合(5)和(8),我们发现一个问题:当处理器运行16位模式下,既然把所有指令都看成16位的,那么怎么使用32位的寄存器,执行32位的运算呢?答案是利用指令前缀0x66和0x67。前面已经说过,指令前缀0x66用来选择非默认值的操作数大小,0x67用来选择非默认值的地址大小。

比如说,指令码0x40在16位模式下对应的指令是

1
inc ax

如果加上前缀0x66,也就是指令码66 40,当处理器在16位模式下运行,66 40对应的指令是

1
inc eax

同理,如果处理器运行在32位模式下,处理器认为指令是32位的,如果加了0x66,那么就表示指令的操作数是16位的。

大端和小端

发表于 2017-08-26 | 分类于 计算机原理

理解字节序

大端和小端的问题

对于整型、长整型等数据类型,Big endian 认为第一个字节是最高位字节(按照从低地址到高地址的顺序存放数据的高位字节到低位字节);而 Little endian 则相反,它认为第一个字节是最低位字节(按照从低地址到高地址的顺序存放数据的低位字节到高位字节)。

写入

如果我们将0x1234abcd 写入到以 0x0000 开始的内存中,则Little endian 和 Big endian 模式的存放结果如下:

1
2
3
地址           0x0000         0x0001        0x0002          0x0003   
big-endian 0x12 0x34 0xab 0xcd
little-endian 0xcd 0xab 0x34 0x12

读取

假设从内存地址 0x0000 开始有以下数据:

1
2
0x0000         0x0001       0x0002       0x0003  
0x12 0x34 0xab 0xcd

如果我们去读取一个地址为 0x0000 的四个字节变量,若字节序为 big-endian,则读出结果为 0x1234abcd;若字节序为 little-endian,则读出结果为 0xcdab3412。

常见情况

一般来说,x86 系列 CPU 都是 little-endian 的字节序,PowerPC 通常是 big-endian,网络字节顺序也是 big-endian,还有的 CPU 能通过跳线来设置 CPU 工作于 Little endian 还是 Big endian 模式。

x86 gcc

对于 x86 gcc 而言,

1
2
unsigned int i = 0x00646c72;
printf("H%x Wo%s", 57616, &i);

小端模式下:十进制的 57616 是 0xe110,由低地址到高地址的存储是 0x10 0xe1, 0x00646c72 由低地址到高地址的存储是 0x72 0x6c 0x64 0x00;读取的时候,57616 按照小端法正常读取,&i 被分成四部分读取输出,分别是 0x72 0x6c 0x64 0x00,0x00 代表就是空字符,即标识字符串的结束。
大端模式下:十进制的 57616 是 0xe110,由低地址到高地址的存储是 0xe1 0x10, 0x00646c72 由低地址到高地址的存储是 0x00 0x64 0x6c 0x72;读取的时候,57616 按照大端法正常读取,&i 被分成四部分读取输出,分别是 0x00 0x64 0x6c 0x72。因此,大端模式下,想要正常输出的话,需要将 i 修改为 0x726c6400,而 57616 无需做修改。

32 位数大小端互转程序

uint32_t reversebytes_uint32t(uint32_t value){  
    return (value & 0x000000FFU) << 24 | (value & 0x0000FF00U) << 8 |   
        (value & 0x00FF0000U) >> 8 | (value & 0xFF000000U) >> 24;   
}  

异常和中断

发表于 2017-08-26 | 分类于 计算机原理

以下内容摘抄自《80386 Programmer’s Manual》

《Chapter 9, Exceptions and Interrupts》

中断和异常是特殊类型的控制转移,它们的表现有时候像不受计算机程序支配的CALL。中断和异常改变了正常程序流,以处理外部事件或报告错误、异常情况。中断和异常的区别是,中断interrupts用于处理处理器外部的异步事件,异常exceptions用于处理处理器在执行指令时检测到的情况。
外部中断的两个来源:
1、可屏蔽中断Maskable interrupts,通过INTR pin来发送信号。
2、不可屏蔽中断Nonmaskable interrupts,通过NMI (Non-Maskable Interrupt) pin来发送信号。
异常的两个来源:
1、处理器检测。进一步分为故障faults、陷阱traps和中止aborts。
2、编程。指令INTO、INT 3、INT n、BOUND可以引发异常。这些指令通常被称为“软件中断”,但处理器把它们当作异常处理。

《9.1 Identifying Interrupts》

每一个不同类型的中断或异常都有一个处理器可识别的数。处理器给不可屏蔽中断和异常分配了0-31的标识符。80386目前没有使用所有的数字,在这个范围内未赋值的标识符保留了未来扩张的可能性。可屏蔽中断的标识符是由外部中断控制器决定的,如英特尔的8259A可编程中断控制器,并在处理器的中断应答序列interrupt-acknowledge sequence过程中与处理器通信。具体的分配如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Identifier   Description

0 Divide error
1 Debug exceptions
2 Nonmaskable interrupt
3 Breakpoint (one-byte INT 3 instruction)
4 Overflow (INTO instruction)
5 Bounds check (BOUND instruction)
6 Invalid opcode
7 Coprocessor not available
8 Double fault
9 (reserved)
10 Invalid TSS
11 Segment not present
12 Stack exception
13 General protection
14 Page fault
15 (reserved)
16 Coprecessor error
17-31 (reserved)
32-255 Available for external interrupts via INTR pin

根据报告的方式以及是否支持重新启动导致异常的指令,异常被分为故障、陷阱、终止。
故障:
故障是这样一种异常:要么在指令开始执行之前被检测到,要么在指令执行期间被检测到。如果在指令执行期间被检测到,机器恢复到能允许重启导致故障的指令的状态,然后报告故障。
陷阱:
陷阱是这样一种异常:在检测到异常的指令之后的指令边界立即被报告。
终止:
终止是这样一种异常:它既不允许获取引起异常的指令的精确位置,也不允许重启导致异常的程序。终止用于报告严重的错误,比如硬件错误和不一致、系统表的非法值。

《9.2 Enabling and Disabling Interrupts》

处理器只在一条指令结束及下一条指令开始之际处理异常和中断。在指令边界,处理器通过某些条件和标识设置禁止某些异常和中断。

《9.2.1 NMI Masks Further NMIs》

如果正在执行一个不可屏蔽中断的处理程序,处理器将忽略其他来自NMI pin的中断信号,直至执行IRET指令。

《9.2.2 IF Masks INTR》

允许中断标志位IF(interrupt-enable flag)控制着是否接受经由INTR pin的外部中断信号。当IF=0,禁止INTR中断;当IF=1,允许INTR中断。处理器接收到RESET信号后,将清除IF和其他标志位。
显式改变IF:CLI(Clear Interrupt-Enable Flag)和STI(Set Interrupt-Enable Flag)显式改变IF(bit 9 in the flag register)。只有CPL < = IOPL时才允许执行CLI和STI,否则将会发生保护异常。
隐式改变IF:1、指令PUSHF将会在栈上存储所有的标识,包括IF。2、任务切换和指令POPF、IRET将加载标志寄存器,会修改IF。3、中断门interrupt gates自动重置IF,禁止中断。

《9.2.3 RF Masks Debug Faults》

EFLAGS的RF位控制识别出调试错误,给定一条指令,无论它被重启多少次,RF使得最多只有一次调试错误出现。

《9.2.4 MOV or POP to SS Masks Some Interrupts and Exceptions》

软件经常需要使用成对的指令来改变堆栈段,比如MOV SS, AX、MOV ESP, StackTop。如果SS已经改变而ESP还未收到相应的改变的时候处理异常或中断,中断或异常处理程序执行期间栈指针SS:ESP是不一致的。为了防止这种情况的发生,80386在执行MOV SS和POP SS指令之后,在下一条指令的指令边界内禁止NMI、INTR、debug exceptions、single-step traps。但是页错误和保护错误仍可能发生,若使用80386 LSS指令,则不会出现这些问题。

《9.3 Priority Among Simultaneous Interrupts and Exceptions》

如果在指令边界有多个中断或异常在等待,处理器一次只会处理其中一个。该中断或异常的分类需具有最高优先级。处理器将控制转移到中断处理程序的第一个指令。低优先级的异常被丢弃,低优先级的中断保持等待。在中断处理程序返回控制权的时候,被丢弃的异常将被重新发现。
同时发生的异常和中断的优先级:

1
2
3
4
5
6
7
8
Priority   Class of Interrupt or Exception

HIGHEST Faults except debug faults
Trap instructions INTO, INT n, INT 3
Debug traps for this instruction
Debug faults for next instruction
NMI interrupt
LOWEST INTR interrupt

《9.4 Interrupt Descriptor Table》

中断描述符表(Interrupt Descriptor Table,IDT)将每个异常或中断向量分别与它们的处理过程联系起来。与GDT和LDT表类似,IDT也是由8字节长描述符组成的一个数组。与GDT和LDT表不同的是,IDT的第一项可以包含一个描述符。为了构成IDT表中的一个索引值,处理器把异常或中断的向量号乘以8。因为最多只有256个中断或异常向量,所以IDT无需包含多于256个描述符。IDT中可以含有少于256个描述符,因为只有可能发生的异常或中断才需要描述符。不过IDT中所有空描述符项应该设置其存在位(标志)为0。
IDT表可以驻留在线性地址空间的任何地方,处理器使用IDTR寄存器来定位IDT表的位置。这个寄存器中含有IDT表32位的基地址和16位的长度(限长)值。IDT表基地址应该对齐在8字节边界上以提高处理器的访问效率。限长值是以字节为单位的IDT表的长度。
LIDT和SIDT指令分别用于加载和保存IDTR寄存器的内容,两者都有一个显式的操作数:内存中一个6字节区域的地址。LIDT指令用于把内存中的限长值和基地址操作数加载到IDTR寄存器中。该指令仅能由当前特权级CPL是0的代码执行,通常被用于创建IDT时的操作系统初始化代码中。SIDT指令用于把IDTR中的基地址和限长内容复制到内存中。该指令可在任何特权级上执行。如果中断或异常向量引用的描述符超过了IDT的界限,处理器会产生一个一般保护性异常。
中断描述符寄存器和中断寄存器表:

LIDT和SIDT的伪描述符格式:

《9.5 IDT Descriptors》

IDT可能包括3种描述符:Task gates、Interrupt gates、Trap gates。

参考于http://stackoverflow.com/questions/3425085/%20the-difference-between-call-gate-interrupt-gate-trap-gate,对各种门描述符进行分析。
通用寄存器EFLAGS保存的是CPU的执行状态和控制信息,其中只需要关注两个寄存器:IF和TF。

TF(Trap Flag):跟踪标志。置1则开启单步执行调试模式,置0则关闭。在单步执行模式下,处理器在每条指令后产生一个调试异常,这样在每条指令执行后都可以查看执行程序的状态。
IF(Interrupt enable):中断许可标志。控制处理器对可屏蔽硬件中断请求的响应。置1则开启可屏蔽硬件中断响应,置0则关闭。IF标志不影响异常和不可屏蔽终端NMI的产生。

gate用于实现从一段代码跳转到另一段代码(可能存在不同的代码段、不同的特权级)时的保护机制问题。一般而言,存在四种gate:
1、Task gate。只能存在GDT或IDT中并被int指令调用。因特尔工程师设计TSS(Task State Segment)主要是通过保存任务的寄存器状态来应用于硬件任务切换。触发硬件任务切换有两种方式,一种是使用TSS本身,一种是使用Task gate。可以通过call和jmp指令导致任务切换。Task gate设计的目的可能是用于在中断到来时触发硬件任务切换,因为跳转到TSS的选择器并不能触发硬件任务切换。现实情况下,由于使用不方便和性能不佳的原因,Task gate和硬件上下文切换功能基本不被使用。例如考虑到TSS只能存在GDT中,而GDT的长度不能大于8192,这就意味着从硬件的角度来说任务数不能超过8k。
2、Trap gate。只能存到IDT中并被int指令调用。Trap gate只是将控制权传递给陷阱门描述符指定的拥有更多特权的段的地址。Trap gate的用处在于:system call的实现;异常处理的实现;中断处理的实现(on machines with APIC)。
3、Interrupt gate。只能存到IDT中并被int指令调用。类似于Trap gate但会清空IF标志。
4、Call gate。可被存到GDT和LDT中并被call和jmp指令调用。类似于Trap gate,Call gate可以从用户模式的任务栈传递参数到内核模式的任务栈。传递的参数数量在Call gate的描述符中指定。Call的使用场景不多,因为:可以使用Trap gate代替;不轻便,如果另一个处理器没有类似的功能,则操作系统移植到另一个处理器的时候为了支持Call gate必须编写更多的代码;不灵活,栈之间可以传递的参数是有限的;性能不优。

Interrupt gate和Trap gate用来专门处理处理器异常或者中断,Task gate和Call gate一般用来处理用户的软件切换。
Interrupt gate和Trap gate的区别:一般情况下,Interrupt gate用于处理意外发生的错误,Trap gate用于处理软件中断和异常,比如页错误、调试中断、除0错误等。Interrupt gate会修改IF,即不再响应接下来的中断了。比如操作系统捕获了一个硬件中断正在处理,又来了另一个。如果用Trap gate,那么第一个处理就被打断了,这样会造成数据崩溃,所以必须屏蔽掉第二个,使一次硬件操作成为原子操作(NMI另说)。还有一个就是断点异常的处理必须暴露给用户程序进行调用,使用Trap gate处理。
Call gate和Task gate的区别:task是一个具体可运行的单位,可以运行、挂起、重启等,task的状态保存在TSS中。可以通过call或jmp指令具体调用一个task程序。Call gate和Task gate都可以用来切换到一个task,但是Task gate的寻址需要经过一个TSS找到code selector(具体见task gate descriptor),这比Call gate麻烦,但是带来的好处是:1、切换的时候原来task的上下文环境被自动保存到TSS。2、如果使用Task gate来处理中断例程,可以使程序和其他例程分开,使其具有独立的地址空间等。

以上四种切换机制的细节如下:

《9.6 Interrupt Tasks and Interrupt Procedures》

类似于CALL指令可以调用程序procedure或任务task,中断或异常可以调用一个中断处理程序handler,该handler可能是程序procedure或任务task。当响应中断或异常的时候,处理器使用中断或异常标志在IDT中索引描述符。如果处理器索引到一个中断门或陷阱门,它调用处理程序的方式类似于CALL指令调用一个调用门call gate。如果处理器索引到一个任务门,类似于CALL指令调用一个任务门task gate,它将导致一个任务切换。

《9.6.1 Interrupt Procedures》

中断门或陷阱门间接指向一个处理程序,该程序将在当前执行任务的上下文中被执行。中断门或陷阱门的选择器指向了GDT或当前LDT的一个可执行段描述符。中断门或陷阱门的偏移部分指向了中断或异常处理程序的起始位置。如下所示:

80386调用一个中断或异常处理程序的方式大致类似于CALL一个程序,以下部分将解释两者差异之处。

《9.6.1.1 Stack of Interrupt Procedure》

就像CALL指令导致控制转移一样,中断或异常处理程序的控制转移使用了栈存储了返回原先程序需要的信息。一个中断将在指针指向中断指令之前将EFLAGS进栈,如下图所示。某些异常会导致error code进栈,异常处理函数可以通过error code判断是什么异常。

《9.6.1.2 Returning from an Interrupt Procedure》

中断程序离开程序的方法也不用于普通程序,它将使用IRET指令离开。IRET类似于RET,除了IRET会将ESP额外增量四个字节(因为flags在栈上),并将保存的flags赋值到EFLAGS寄存器中。仅当CPL=0时,EFLAGS的IOPL部分将会被改变;仅当CPL <= IOPL时,EFLAGS的IF部分才会被改变。

《9.6.1.3 Flags Usage by Interrupt Procedure》

通过中断门或陷阱门的中断在当前TF(the trap flag)作为EFLAGS的一部分被保存到栈后,将清零TF。通过这个动作处理器可以防止使用单步调试活动影响中断响应。随后IRET指令恢复EFLAGS在栈上的值,也恢复了TF。
中断门和陷阱门的不同在于对IF(the interrupt-enable flag)的影响。经由中断门的中断将重置IF,防止其他中断(当IF=1,允许INTR中断)干扰当前的中断处理程序,随后IRET指令恢复EFLAGS在栈上的值。经由陷阱门的中断将不改变IF。

《9.6.1.4 Protection in Interrupt Procedures》

管理中断程序的特权规则类似于程序调用:CPU不允许一个中断转移控制到一个比当前权限更少特权的程序。试图违反该规则将会导致一般保护异常。由于中断的发生一般不可预测,特权规则可以有效地在可执行的中断或异常处理程序的特权级别上强加限制。以下策略确保特权规则不被违反:1、将处理程序安置在一个conforming segment,这种策略用于处理某些异常(如divide error),该程序只能使用栈上可被使用的数据,如果它需要数据段的数据,数据段必须有特权级别3,从而使其不受保护。2、将处理程序安置在特权级别为0的栈上。

《9.6.2 Interrupt Tasks》

IDT的任务门间接指向一个任务,任务门的选择器指向GDT的TSS描述符。如下图所示:

经由任务门的中断或陷阱的结果是出现一个任务切换。使用一个单独任务来处理中断有两个优点:1、整个上下文将被自动保存。2、通过LDT或页目录给予处理程序单独的地址空间,使其独立于其他任务。
中断任务通过IRET返回被中断的任务。
如果导致任务切换的异常有一个错误代码,处理器会自动把错误代码放入栈中,对应于中断任务执行的第一条指令的特权级别。
当80386操作系统使用中断任务时,实际上有两个调度器:软件调度器(操作系统的一部分)和硬件调度器(处理器的中断机制的一部分)。软件调度器的设计应该考虑一种情况:在启用中断时,硬件调度器随时可能派遣一个中断任务。

《9.7 Error Code》

当异常涉及到特定的段,处理器将异常处理程序的错误码放入栈中(无论是程序或任务)。错误码的格式如下:

类似于选择器的格式,但是它没有RPL,而是包含两个one-bit项:1、如果程序的外部事件导致异常,处理器设置EXT位;2、如果错误码的索引部分指向了IDT的门描述符,处理器设置I-bit (IDT-bit)。
如果没有设置I-bit,TI值为0表示错误码指向GDT,TI值为1表示指向LDT。剩下的14 bits(包括TI)是段选择符的高14位。在某些情况下,栈上的错误码为空,即低位的word为空。

《9.8 Exception Conditions》

下面详细描述每一个可能的异常情况。每个描述将异常按故障、陷阱、中止分类。这种分类为系统重启发生异常的程序提供了必要信息。
故障faults:当故障被报告时,导致故障的指令的CS和EIP值将被保存。
陷阱traps:当陷阱被报告时,导致陷阱的指令的下一条动态指令的CS和EIP值将被保存。如果是在指令改变程序流期间发现陷阱,CS和EIP的值会反映程序流的变更。例如,执行JMP指令时发现陷阱,CS和EIP的值指向JMP的目标。
终止aborts:终止是这样一种异常:它既不允许获取引起异常的指令的精确位置,也不允许重启导致异常的程序。终止用于报告严重的错误,比如硬件错误和不一致、系统表的非法值。

《9.8.1 Interrupt 0 – Divide Error》

divide-error属于故障fault,在执行DIV或IDIV指令时,如果除数为0则出现该故障。

《9.8.2 Interrupt 1 – Debug Exceptions》

处理器触发调试异常的情况有很多种,该异常是故障还是陷阱取决于具体的情况:
指令地址断点 故障fault
数据地址断点 陷阱trap
一般检测 故障fault
单步调试single-step 陷阱trap
任务切换断点 陷阱trap
处理器不会将该异常的错误码进栈,异常处理程序可以通过检查调试寄存器来确定是哪些条件引起异常。

《9.8.3 Interrupt 3 – Breakpoint》

INT 3指令导致该陷阱trap。INT 3指令只有一个字节长,这使得在一个可执行段将一个操作码替换为断点操作码变得很容易。操作系统或调试子系统可以为可执行段使用一个数据段别名,在任何方便捕获正常执行的地方放置INT 3,这样可以执行一些特殊处理,如显示寄存器变量。
CS:EIP保存的值指向了断点的后续字节。如果调试器用另外的有效操作符替代了断点,则应该在返回前将保存的EIP值减一。

《9.8.4 Interrupt 4 – Overflow》

当处理器遇到INTO指令并且设置了溢出标志OF时,引发该陷阱trap。由于有符号算术和无符号算术都使用相同的运算指令,处理器无法区分,因此不会自动引起溢出异常。因此,当在结果被解释为有符号数并且会越界的时候,处理器将设置OF位。当计算有符号操作数时,程序员和编译器要么直接测试OF位,要么使用INTO指令。

《9.8.5 Interrupt 5 – Bounds Check》

当处理器在执行BOUND指令并发现操作数超过指定的限制的时候,引发该故障fault。程序可以使用BOUND指令来检查定义在内存块中的有符号数组索引和有符号限长。

《9.8.6 Interrupt 6 – Invalid Opcode》

如果在可执行单元中发现了无效的操作码,引发该故障fault。只有当试图执行该非法操作码的时候才会引发故障。该故障的错误码没有入栈。可以在相同的任务里处理该异常。
对于给定的操作码,如果操作数的类型是无效的也会引发该异常。比如段内JMP引用一个寄存器操作数,或者LES指令涉及寄存器源操作数

《9.8.7 Interrupt 7 – Coprocessor Not Available》

以下两种情况会引发该异常:
处理器遇到ESC(逃避escape)指令,且设置了CR0(control register zero)的EM(模拟emulate)位。
处理器遇到WAIT指令或ESC指令,且设置了CR0的MP(监控协处理器monitor coprocessor)和TS(任务切换task switched)位。

《9.8.8 Interrupt 8 – Double Fault》

通常情况下,当处理器试图调用异常处理程序处理之前的例外时又检测到异常,可以连续处理这两个异常。如果处理器不能串行处理它们,将会发出一个double-fault异常。为了确定何时将两个故障标识为double-fault,80386将异常分为三类:benign exceptions、contributory exceptions、page faults。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Table 9-3. Double-Fault Detection Classes

Class ID Description

1 Debug exceptions
2 NMI
3 Breakpoint
Benign 4 Overflow
Exceptions 5 Bounds check
6 Invalid opcode
7 Coprocessor not available
16 Coprocessor error

0 Divide error
9 Coprocessor Segment Overrun
Contributory 10 Invalid TSS
Exceptions 11 Segment not present
12 Stack exception
13 General protection

Page Faults 14 Page fault

以下说明哪些异常组合导致double-fault而哪些不会:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Table 9-4. Double-Fault Definition
-----------------------------------------------------------------
| SECOND EXCEPTION
|
| Benign | Contributory | Page
| Exception | Exception | Fault
-----------------------------------------------------------------
Benign | OK | OK | OK
Exception | | |
------------------------------------------------------
FIRST Contributory | OK | DOUBLE | OK
EXCEPTION Exception | | |
------------------------------------------------------
Page | | |
Fault | OK | DOUBLE | DOUBLE
-----------------------------------------------------------------

处理器总会将double-fault的错误码入栈,但是错误码总为0。可能不会重新启动引发故障的指令。如果试图调用double-fault处理程序的过程中发生任何异常,将关闭处理器。

《9.8.9 Interrupt 9 – Coprocessor Segment Overrun》

在保护模式下,如果在传递协处理器操作数的中间部分到NPX的过程中80386检测到页面或段非法,将引发该异常。该异常是可以避免的。

《9.8.10 Interrupt 10 – Invalid TSS》

当任务切换期间新TSS是非法的将引发该异常。以下情况下TSS被认为是非法的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Table 9-5. Conditions That Invalidate the TSS

Error Code Condition

TSS id + EXT The limit in the TSS descriptor is less than 103
LTD id + EXT Invalid LDT selector or LDT not present
SS id + EXT Stack segment selector is outside table limit
SS id + EXT Stack segment is not a writable segment
SS id + EXT Stack segment DPL does not match new CPL
SS id + EXT Stack segment selector RPL < > CPL
CS id + EXT Code segment selector is outside table limit
CS id + EXT Code segment selector does not refer to code
segment
CS id + EXT DPL of non-conforming code segment < > new CPL
CS id + EXT DPL of conforming code segment > new CPL
DS/ES/FS/GS id + EXT DS, ES, FS, or GS segment selector is outside
table limits
DS/ES/FS/GS id + EXT DS, ES, FS, or GS is not readable segment

该故障fault的错误码将入栈,用于帮助识别故障的原因。EXT位表示该异常是否由不受程序控制的外部情况所引发,比如经由任务门的外部中断引起非法TSS切换。
该故障可能出现在原始任务的上下文或新任务的上下文。异常停留在原始任务的上下文中直到处理器完全验证新TSS的存在。一旦新TSS被验证存在,任务切换则是完整的:也就是说,TR位已经被更新,并且如果是由CALL或中断导致该切换,新TSS反向链接旧TSS。此后处理器发现的任何错误都将在新任务的上下文中进行处理。
为了确保有合适的TSS,Interrupt 10的处理程序必须是经由任务门调用的一个任务。

《9.8.11 Interrupt 11 – Segment Not Present》

当处理器发现描述符的存在位为0时引发该异常。处理器在以下任何一种情况下可以引发该故障fault:
尝试加载CS、DS、ES、FS或GS寄存器;加载SS寄存器但导致了栈错误。
尝试通过LLDT指令加载LDT寄存器;在任务切换期间加载LDT寄存器,但导致”invalid TSS”异常。
尝试使用被标记为不存在的门描述符。
该故障可以重启。如果异常处理程序设置段存在并返回,被中断的程序将恢复执行。

如果在任务切换过程中发生了not-present异常,可能未完成任务切换的所有步骤。在任务切换时,处理器首先载入所有段寄存器,然后检查他们的有效性。一旦发现某个not-present异常,剩下的寄存器将不会被检查,因此不能用它们来引用内存。not-present异常处理程序在试图恢复新任务之前需要检查所有段寄存器,否则后续可能引发一般保护错误,使得诊断更加困难。有三种方法可以处理这种情况:
1、使用新任务来处理not-present故障。任务切换为被中断程序时,会导致处理器从TSS中加载所有寄存器并检查。
2、PUSH和POP所有段寄存器。POP指令会导致处理器检查所有段寄存器的新内容。
3、检查每个段寄存器在TSS中的镜像,模拟测试处理器加载它们的情况。

该异常会将错误码入栈。如果外部事件引用了不存在段并引发中断,错误码的EXT位将被置位。如果错误码指向IDT项,I位将被置位,比如INT指令引用了一个不存在的门。
操作系统经常使用“segment not present”在段级别上实现虚拟内存。然而,门描述符not-present通常并不表示段不存在,因为门不一定对应于段。Not-present门可能只是操作系统为了某些特殊意义用于触发异常。

《9.8.12 Interrupt 12 – Stack Exception》

以下两种情况一般会触发栈故障fault:
引用SS寄存器的任何操作由于违反限制的结果。包括POP、PUSH、ENTER、LEAVE,以及其他隐式使用SS的内存引用操作,如MOV AX, [BP+6]。当栈空间太小不满足局部变量空间时,ENTER引发该故障。
描述符有效但被标记为不存在的情况下尝试加载SS寄存器,引发该故障。这种情况可能发生在任务切换、interlevel CALL、interlevel return、LSS指令,或者针对SS的MOV或POP指令。

当处理器检测到栈异常时,将错误码入栈。如果是由于not-present栈段,或由于interlevel CALL期间新栈溢出而引发异常,错误码将包含出问题的段选择器,处理器可以通过测试描述符的存在位来判断发生哪些异常;其他情况下错误码为0。
在所有情况下,导致该故障的指令都可被重启。保存到异常处理程序栈上的返回指针指向了需要被重启的指令。这个指令通常是导致该故障的指令,但如果是由于在任务切换时加载一个不存在的栈段描述符而引发了栈异常,该指令将是新任务的第一条指令。
如果在任务切换过程中发生了栈异常,段寄存器不能用于引用内存。在任务切换时,处理器在检查描述符之前首先载入所有段寄存器,然后检查段寄存器的有效性。一旦发现栈异常,剩下的寄存器将不会被检查,因此不能用CS, SS, DS, ES, FS, GS来引用内存。栈异常处理程序在试图恢复新任务之前需要检查所有段寄存器,否则后续可能引发一般保护错误,使得诊断更加困难。

《9.8.13 Interrupt 13 – General Protection Exception》

所有不会引发另一个异常的保护违反会引发一般保护异常。包括但不限于:
1、使用CS, DS, ES, FS, GS的时候超过限制。
2、引用一个描述符表的时候超过段长限制。
3、控制转移到一个不可执行的段。
4、写入只读数据段或代码段。
5、读取只执行段。
6、以一个只读描述符来加载SS。除非段寄存器是在任务切换期间来源于TSS,这种情况会引发TSS异常。
7、以系统段的描述符来加载SS, DS, ES, FS, GS。
8、以可执行但不可读的段的描述符来加载DS, ES, FS, GS。
9、以可执行段的描述符来加载SS。
10、当段寄存器包含一个空选择器时通过DS, ES, FS, GS访问内存。
11、切换到一个繁忙的任务。
12、违反特权规则。
13、加载CR0时 PG=1 且 PE=0。
14、经由中断门或陷阱门的中断或异常从V86模式到除0以外的特权级别。
15、超过15个字节的指令长度限制。仅当冗余前缀被放置在一条指令之前才会发生。

一般保护异常属于故障fault。在处理一般保护异常时,处理器将错误码保存到异常处理程序的栈上。如果因为加载描述符而导致异常,错误码将包含一个指向描述符的选择器;否则错误码为空。错误码的选择器可能来自于:1、选择器来源于指令的一个操作数。2、一个门的选择器是指令的操作数。3、选择器来源于任务切换时的TSS。

《9.8.14 Interrupt 14 – Page Fault》

允许分页的情况下,处理器在将线性地址转换为物理地址的过程中如果发现以下情况将会引发页错误:1、地址转换涉及到的页目录项或页表项的存在位为0;2、当前程序没有权限访问该物理页。
处理器为异常处理程序提供两种信息用于诊断和恢复:
1、保存到栈上的错误码,该错误码的格式不同于其他异常的错误码。如下所示:

错误码告知异常处理程序三件事:
1.1、异常是由于页面不存在还是违反访问权限所引起的。
1.2、发生异常的时间点上处理器是在用户级别还是supervisor级别。
1.3、引起异常的内存访问是写操作还是读操作。
2、CR2(control register two)。处理器将因访问内存而引起异常的线性地址保存到CR2中,异常处理程序可以根据该地址定位到页目录项和页表项。如果在页面错误处理程序执行期间出现了另一个页面错误,异常处理程序应该将CR2保存到栈上。如下所示:

《9.8.14.1 Page Fault During Task Switch》

任务切换期间,处理器可能访问四个部分:
1、在原先任务的TSS上保存原先任务的状态。
2、读取GDT,定位新任务的TSS描述符。
3、读取新任务的TSS,检查来源于TSS的段描述符的类型。
4、可能读取新任务的LDT,用于验证保存到新TSS上的段寄存器。
上述任何一种情况都可能导致页错误。后两种情况的页错误发生在新任务的上下文中。指令指针指的是新任务的下一个指令,而不是导致任务切换的指令。
如果操作系统允许在任务切换期间发生页错误,页错误处理程序应该经由任务门调用。

《9.8.14.2 Page Fault with Inconsistent Stack Pointer》

应该特别注意的是,页错误不应该导致处理器使用了无效的栈指针(SS:ESP)。8086系列早期处理器经常使用一对指令来切换到新栈,如:MOV SS, AX; MOV SP, StackTop。由于第二条指令会访问到内存,在SS改变而SP还没有改变的时候,可能会出现页错误。这种时候栈指针SS:SP(32位程序是SS:ESP)的两部分是不一致的。
如果页错误处理程序导致切换到定义良好的栈(即处理程序是一个任务或有更高特权的程序),处理器将不会使用不一致的栈指针。然而,如果页错误处理程序是经由中断门或陷阱门调用的,并且是在与页错误处理程序一样的特权级别下发生页错误,处理器仍然会尝试使用当前栈指针指向的栈(非法)。
系统实现分页后,在处理页错误时,应该使用LSS指令来初始化新的栈。如果页错误处理程序在特权级别0上执行(通常情况下),页错误问题的范围就被限制在特权级别为0的代码上,通常这是操作系统的内核代码。

《9.8.15 Interrupt 16 – Coprocessor Error》

如果在80386的ERROR# input pin上发现80287或80387信号时,80386引发该异常。80386只在开始执行ESC指令的时候,和遇到WAIT指令的时候(MSW的EM位为0),才会测试该pin。

《9.9 Exception Summary》

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
Table 9-6 summarizes the exceptions recognized by the 386

Description Interrupt Return Address Exception Type Function That
Number Points to Can Generate
Faulting the Exception
Instruction
-----------------------------------------------------------------------
Divide error 0 YES FAULT DIV, IDIV
-----------------------------------------------------------------------
Debug 1 Some debug Any instruction
exceptions exceptions are traps and some
are faults. The exception handler
can determine which has occurred
by examining DR6.
-----------------------------------------------------------------------
Breakpoint 3 NO TRAP One-byte INT 3
-----------------------------------------------------------------------
Overflow 4 NO TRAP INTO
-----------------------------------------------------------------------
Bounds check 5 YES FAULT BOUND
-----------------------------------------------------------------------
Invalid opcode 6 YES FAULT Any illegal
instruction
-----------------------------------------------------------------------
Coprocessor 7 YES FAULT ESC, WAIT
not available
-----------------------------------------------------------------------
Double fault 8 YES ABORT Any instruction
that can generate
an exception
-----------------------------------------------------------------------
Coprocessor 9 NO ABORT Any operand of an
Segment Overrun ESC instruction
that wraps around
the end of a
segment
-----------------------------------------------------------------------
Invalid TSS 10 YES FAULT JMP, CALL, IRET,
any interrupt
An invalid-TSS fault is not restartable if it occurs during the
processing of an external interrupt.
-----------------------------------------------------------------------
Segment not 11 YES FAULT Any segment-
present register modifier
-----------------------------------------------------------------------
Stack 12 YES FAULT Any memory
exception reference thru SS
-----------------------------------------------------------------------
General 13 YES FAULT/ Any memory
Protection ABORT reference or
code fetch
All GP faults are restartable. If the fault occurs while attempting to
vector to the handler for an external interrupt, the interrupted program is restartable, but the interrupt may be lost.
-----------------------------------------------------------------------
Page fault 14 YES FAULT Any memory
reference or
code fetch
-----------------------------------------------------------------------
Coprocessor 16 YES FAULT ESC, WAIT
error
Coprocessor errors are reported as a fault on the first ESC or WAIT
instruction executed after the ESC instruction that caused the error.
-----------------------------------------------------------------------
Two-byte 0-255 NO TRAP INT n
SW Interrupt

《9.10 Error Code Summary》

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Table 9-7 summarizes the error information that is available with each 
exception.

Description Interrupt Number Error Code
-----------------------------------------------------------------------
Divide error 0 No
Debug exceptions 1 No
Breakpoint 3 No
Overflow 4 No
Bounds check 5 No
Invalid opcode 6 No
Coprocessor not available 7 No
System error 8 Yes (always 0)
Coprocessor Segment Overrun 9 No
Invalid TSS 10 Yes
Segment not present 11 Yes
Stack exception 12 Yes
General protection fault 13 Yes
Page fault 14 Yes
Coprocessor error 16 No
Two-byte SW interrupt 0-255 No

系统预留中断类型:

远程登陆centos/ubuntu桌面

发表于 2017-08-26 | 分类于 Linux应用

ubuntu可参考

https://help.ubuntu.com/community/VNC/Servers#Start\_TigerVNC\_vncserver\_at\_boot

查看是否安装桌面

sudo yum grouplist

若没有以上两项,安装
sudo yum groupinstall -y "GNOME Desktop" "Graphical Administration Tools"

安装vnc-server

yum install tigervnc-server -y

ubuntu14.04:
wget https://bintray.com/artifact/download/tigervnc/stable/ubuntu-14.04LTS/amd64/tigervncserver_1.6.0-3ubuntu1_amd64.deb
sudo dpkg -i tigervncserver_1.6.0-3ubuntu1_amd64.deb
如果发现依赖错误:sudo apt-get install -f

vnc运行机制

Linux下的VNC可以同时启动多个vncserver,各个vncserver之间用显示编号(display number)来区分,每个vncserver服务监听3个端口,它们分别是:
1、5800+显示编号: VNC的httpd监听端口,如果VNC客户端为IE,Firefox等非vncviewer时必须开放。
2、5900+显示编号: VNC服务端与客户端通信的真正端口,必须无条件开放。
3、6000+显示编号: X监听端口,可选。

显示编号、开放的端口分别由/etc/sysconfig/vncservers(ubuntu 14.04为 /etc/default/vncservers)文件中的VNCSERVERS和VNCSERVERARGS控制。
VNCSERVERS的设置方式为:
VNCSERVERS="显示编号1:用户名1 …"
如:VNCSERVERS="1:root 2:aiezu"
VNCSERVERARGS的设置方式为:
VNCSERVERARGS[显示编号1]="参数一 参数值一 参数二 参数值二 ……"
如:VNCSERVERARGS[2]="-geometry 800x600 -nohttpd"

VNCSERVERARGS的详细参数有:
-geometry 桌面分辨率,默认1024x768;
-nohttpd 不监听HTTP端口(58xx端口);
-nolisten tcp 不监听X端口(60xx端口);
-localhost 只允许从本机访问;
-AlwaysShared 默认只同时允许一个vncviewer连接,此参数允许同时连多个vncviewer;
-SecurityTypes None 登录不需要密码认证VncAuth默认值,要密码认证。

配置vnc-server:centos7方式1

(可省略)
假设登录用户为vnc,复制一份通用的VNC服务文件来为用户vnc创建一个VNC服务配置
sudo cp /lib/systemd/system/vncserver@.service /etc/systemd/system/vncserver@:1.service
sudo vi /etc/systemd/system/vncserver@:1.service

打开文件后找到以下内容,替换为用户vnc。
ExecStart=/sbin/runuser -l <USER> -c "/usr/bin/vncserver %i"
PIDFile=/home/<USER>/.vnc/%H%i.pid

重新加载服务来使新的VNC配置生效
sudo systemctl daemon-reload

配置vnc-server:方式2

(可省略)
sudo vi /etc/sysconfig/vncserver
ubuntu 14.04:sudo vi /etc/default/vncservers
添加以下内容:

1
2
VNCSERVERS="1:vnc"
VNCSERVERARGS[1]="-geometry 1600:900 -nolisten tcp"

重新加载服务来使新的VNC配置生效
sudo systemctl daemon-reload
ubuntu 14.04:sudo service vncserver restart

开机启动

在启动时自动启动VNC服务
sudo systemctl enable vncserver@:1.service

ubuntu14.04: enable the service at boot with:
sudo update-rc.d vncserver defaults

设置VNC用户密码

切换到vnc用户,并运行vncpasswd命令,输入远程连接密码

运行vncserver命令,显示的序号是在5900基础上加上虚拟机中的VNC序号。

运行上面命令后,会在用户根目录($HOME)下的”.vnc”文件夹下生成一系列文件。其中passwd为vnc用户密码文件,由vncpasswd生成。其他的都由vnc初次启动时生成,xstartup为VNC客户端连接时启动的脚本。

重新加载服务来使新的VNC配置生效
sudo systemctl daemon-reload
ubuntu 14.04:sudo service vncserver restart

开启VNC端口

开放的端口为5900+VNC序号
sudo iptables -I INPUT -p tcp --dport 5901 -j ACCEPT

查看vnc在服务器上的端口号

netstat -lp|grep -i vnc

客户端访问

下载vnc viewer,server框中输入ip:1(1代表上面配置的远程用户代号,配置文件中可以配置多个远程用户)或者ip:port,输入VNC密码,连接到远程桌面。

ubuntu没有显示菜单栏

修改~/.vnc/xstartup,将其内容替换为以下内容,确保软件都安装过。

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/sh
[ -x /etc/vnc/xstartup ] && exec /etc/vnc/xstartup
[ -r $HOME/.Xresources ] && xrdb $HOME/.Xresources
xsetroot -solid grey
vncconfig -iconic &
x-terminal-emulator -geometry 80x24+10+10 -ls -title "$VNCDESKTOP Desktop" &

gnome-session --session=gnome-flashback &
gnome-panel &
gnome-settings-daemon &
metacity &
nautilus &
vncconfig -nowin &i

session参数可在/usr/share/gnome-session/sessions/中查看。

vnc加密

从设计上说,VNC使用的远程帧缓存(RFB)并不是一种安全的协议,VNC客户端直接连接到VNC服务器上并不明智。任何敏感信息在VNC流量中被轻易地泄露。因此,建议使用SSH隧道来加密VNC流量。
在运行VNC客户端的本机上,使用下面的命令来创建一个连接到远程VPS的SSH通道。当被要输入SSH密码时,输入用户的密码。
ssh vnc@<VPS-IP-address> -L 5901:127.0.0.1:5901
一旦SSH通道建立,远程VNC流量就会通过ssh通道路由并发送到127.0.0.1:5901。现在启动VNC客户端(比如:vinagre),来连接到127.0.0.1:5901。

ubuntu 14.04重启服务:sudo service vncserver restart

123…7
zoro

zoro

如果我后退的话,我曾重视的誓言和约定就会全部消失,然后,再也不会回来这个地方了

65 日志
12 分类
18 标签
© 2020 zoro
由 Hexo 强力驱动
|
主题 — NexT.Gemini