大家在使用CS1时,固件更新时遇到问题过什么问题吗?可以来交

楼主邀你扫码
参与上面帖子讨论
发表于:12-04-07 13:23
被系统删除于: 14:16:18
你尚未登录或可能已退出账号:(请先或者
【敬请阅读】
亲爱的网友们,、有更新哦!
请您务必审慎阅读、充分理解各条款内容,特别是免除或者限制责任的条款、法律适用和争议解决条款。免除或者限制责任将以粗体标识,您应重点阅读。
【特别提示】
如您继续使用我们的服务,表示您已充分阅读、理解并接受《西祠站规》、《西祠胡同用户隐私保护政策》的全部内容。阅读《西祠站规》、《西祠胡同用户隐私保护政策》的过程中,如果您有任何疑问,可向平台客服咨询。如您不同意《西祠站规》、《西祠胡同用户隐私保护政策》的任何条款,可立即停止使用服务。
南京西祠信息技术股份有限公司
我已阅读并同意、中的全部内容!您需要通过验证再能继续浏览 3秒后开始验证
丨 粤ICP备号-10 丨 新三板上市公司威锋科技(836555)
增值电信业务经营许可证:
Powered by Discuz!
(C) Joyslink Inc. All rights reserved 保留所有权利& 问一个有关段保护的问题
声明: 本页内容为的内容镜像,文章的版权以及其他所有的相关权利属于和相应文章的作者,如果转载,请注明文章来源及相关版权信息。
(finished)
(finished)
(finished)
问一个有关段保护的问题
[ | 204 byte(s)]
[ | 1,133 byte(s)]
[ | 1,229 byte(s)]
[ | 170 byte(s)]
[ | 300 byte(s)]
[ | 356 byte(s)]
[ | 43 byte(s)]
[ | 389 byte(s)]
[ | 119 byte(s)]
[ | 46 byte(s)]
[ | 593 byte(s)]
[ | 709 byte(s)]
[ | 887 byte(s)]
[ | 646 byte(s)]
[ | 497 byte(s)]
[ | 340 byte(s)]
[ | 257 byte(s)]
[ | 42 byte(s)]
[ | 58 byte(s)]
[ | 43 byte(s)]
[ | 52 byte(s)]
[ | 307 byte(s)]
[ | 1,570 byte(s)]
[ | 145 byte(s)]
[ | 96 byte(s)]
[ | 110 byte(s)]
[ | 74 byte(s)]
[ | 164 byte(s)]
[ | 337 byte(s)]
[ | 208 byte(s)]
[ | 356 byte(s)]
[ | 54,116 byte(s)]
[ | 58,279 byte(s)]
[ | 3,394 byte(s)]
[ | 47 byte(s)]
一直不明白的一个问题:
在linux中,数据段与代码段使用的是同一个地址空间,而代码段与数据段的性质是不一样的(代码段可读,可执行; 数据段可读写),那么在她们共同决定的这一个地址空间的性质究竟是什么样子的????
不在同一页吧
不要再想段了 很少有CPU知道什么段了吧
读了这么多年的书 还是觉得幼儿园好混
内核中同一个地址怎么会不在同一页?
某一确定时刻某个确定的线性地址不会同时属于两个段的吧
是不是说, cpu 取指令是是代码段属性, 而取数据时是数据段属性?
这样的话,是不是意味着可以修改代码段?
这样的操作是允许的:
*(int*)(&f) = 0x0;
建议看一下intel 386手册,你就明白了。
by the way,可以看一下同济大学赵博士写的那本关于linux 0.11代码的注释,非常好。
谢谢,赵炯那本书还是很久以前看的了,忘的差不多了.
不好意思,也许是我的领悟能力比较差,没有从赵炯的书中和i386的手册中得到解答
能否指教一下??
每个页都有自己的权限属性。
你虽然看到代码和数据都是一个段,但是地址不同啊,他们是不同的页。
确实属于不同的页,但是内核并没有区别这些不同的页啊,内核页的属性都是一样的呀
纠正自己一下。
数据段和代码段还是不同的,虽然他们映射空间是一致的,但还是定义了两个段。
你打印一下CS、DS的值就知道。CS=0x23, DS=SS=0x2B.
cs和ds当然都得定义,但是她们所用的空间是一样的
我的问题是这个样子的:
前者要求这个地址空间可读可执行
后者要求这个地址空间可读可写
那么这个地址空间究竟是可读可执行呢还是可读可写呢?还是可读可写可执行呢?
ps: "这个地址空间" 指的是内核所使用的地址空间,即 0xc0000000----0xffffffff
你最好把x86体系结构看一下,你的用词很混乱。总之,架设咱们在linux内核下这个范畴下讨论的话,段这个词就没意义了。你说的cs,ds的值已经跟分页有关了,参考head.S这个文件去看吧,看了这个源代码你的问题会全都解释清楚的
解释清楚了才是真的
不要说这种看什么什么的
想清楚了再回帖,要不然就不要回帖
浮躁之风不可有啊
知道exec-shield吗? 她里面用到的 exec-limt 就是用到了段保护技术,怎么说段没有意义呢?
当然对于amd64,ia64她们的页描述中有nx属性,这个先不谈
ps: 至于那个head.s文件,我敢说,我读过的次数绝对不比阁下少
好吧,我把我以前非典时自娱自乐写的文章,贴一下吧(原本想发在网上的,但后来也没时间再修正,就老土了),希望对你能有点帮助。因为你说head.s的部分看的很熟了,那我就把之前的部分贴给你吧,这样前后一联系也许你就明白了。我也很久不再管x86了,所以有些东西忘光了,错误肯定有的。大家一起讨论吧^^另外下面的版本说的混乱,起初是写的2.4.21,后来发现2.6的也差不多,所以就没再改了
Linux引导实录1
Linux作为现代操作系统,已经能够很好的屏蔽硬件的底层细节,而操作系统的所有高级概念又都是由底层抽象而来的,所以了解最原始的引导阶段是了解整个Linux的最好起点。毕竟这个阶段是以后所有的基础,它为Linux后续的操作做好了准备。
我以从软盘引导Linux为例,结合源代码进行讲解。毕竟软盘引导是最简单的,以后有机会我会给大家讲解高级的引导程序代码(我个人比较喜欢grub^^)。作为背景知识,希望大家了解Inter i386的体系结构,汇编语言(AT&t语法),C语言等等基本知识。没有这些的话,你就全当我在说天书啦^^.在正式阅读源代码之前,我们要了解一下内核是如何编译的,内核镜像文件是怎么生成的,了解这些有助于我们今后的研究.我以kernel-2.4.21为例进行讲解.由于内核引导与cpu体系机构密不可分,所以以x86体系为例.你最好有一份内核源代码的副本,它就放在/usr/src/linux/中,如果没有的话可以到www.kernel.org去下载一份.(以后所有对内核源代码树的路径引用全部以/usr/src/linux/为根)
先来复习一下内核的编译过程.我们知道编译内核时需要使用make命令,而make命令依赖Makefile这个文件.在linux源代码书中几乎每个目录下都有一个Makefile,也许你会疑问这有必要吗.往下看你就明白了.
我们以2.6.0-test7为例进行讲解.另外为了表述方便,"根目录"一词特指你的linux内核源代码所在的目录.建议去阅读make文档来获得一些make的背景知识.
linux源代码树中的Makefile有5种类型:
1 根目录下的Makefile
它是整个make的核心,也是最重要的Makefile.它需要.config文件中的一些内容,主要负责生成源代码树下的vmlinux和各种module.
由于make的特性,它会从根目录开始递归向下编译.
内核选项配置文件,在配置完内核时才会在根目录下出现这个文件.一般通过"make menuconfig","make xconfig","make
gconfig"或"make oldconfig这4种之一的方式得到.它被包含在根目录下的Makefile中.
3.arch/*/Makefile
与特定体系结构相关的Makefile,也包含在根目录下的Makefile中.它为根目录下的Makefile提供了特定体系的特定信息.简称为"体系M
-akefile".
4.kbuild Makefiles
存在于每个子目录下,大约有500个左右. 它们才是真正的底层的执行单元,它们负责编译各种源文件或目标.它们接受来自上层make传
递下来的信息,并根据这些信息来决定如何编译.
5 scripts/Makefile.*
定义了一般的make规则,这些规则主要用于kbuild Makefiles.
在分析Makefile之前,我们先看一下kbuild makefile的一些简单样例:
例如在makefile中有这么一行:
obj-y += foo.o
它表示在当前目录中应该有一个叫foo.o的目标文件.如果obj-y变量以前值为"helo.o",那么现在的值就是"helo.o foo.o".如果当前目录中没有foo.o,就通过编译foo.c或者foo.S来获得,这是由内置规则决定的.
如果foo.o作为模块编译,我们使用变量obj-m来表示,看这个例子:
obj-$(CONFIG_FOO) += foo.o
CONFIG_FOO这个变量的值是从.config中获得的.该文件的格式是每行一个变量定义:左边是变量名,然后是等号,最右边是y(表示直接编译到内核vmlinux中, 通过ld -r将这些目标文件合到一起)或m(表示作为可装载模块编译).如果CONFIG_FOO既不是y也不是m,那么CONFIG_FOO值为空,就会产生obj-这个变量,而我们只使用obj-m和obj-y变量,所以不会产生其它的影响.
再看一个obj-m的例子:
obj-$(CONFIG_ISDN) += isdn.o
isdn-objs := isdn_net_lib.o isdn_v110.o isdn_common.o
isdn-obj实际上是格式为&模块名&-objs这样一类的变量.在这个例子中,模块名是isdn,实际就是isdn.o文件.而isdn.o由isdn_net_lib.o isdn_v110.o isdn_common.o组成,通过ld -r来构建isdn.o. 注意:CONFIG_ISDN=y时也有这种特殊的编译方法.
所以obj-m变量用于创建一个lkm,而obj-y用于把特定目录中的目标文件合并起来创建vmlinux.
使用这样的方法,以递归的方式遍历所有的要使用的子目录,可以获得所需的make目标.
我们通过一个完整的内核编译过程来熟悉一下make流程,最终了解一个真正的,可以启动的内核是如何组织和建立的.
1.make mrproper
该命令确保源代码目录下没有不正确的.o, .a, .S文件以及文件的互相依赖。由于我们使用刚下载的完整的源程序包进行编译,所以本步可以省略。而如果你多次使用了这些源程序编译内核,那么最好要先运行一下这个命令。
2.make O=/home/name/build/kernel menuconfig
make O=/home/name/build/kernel
sudo make O=/home/name/build/kernel install_modules install
2.6的内核编译方式与以前版本有很大不同,我们称之为kbuild.kbuild允许你把编译好的内核放到你指定的目录,不用像以前那样还要手动移动编译好的内核.使用O=(大写字母O)来指定输出路径.注意:涉及内核的编译操作时,当前工作目录必须是内核源代码树的根目录.比如在我的机器上:
make O=/boot menuconfig
另一种方法是使用KBUILD_OUTPUT环境变量,比如:
export KBUILD_OUTPUT=/boot
注意O=的优先级高于KBUILD_OUTPUT.
3.make bzImage
这是生成内核最重要的步骤,所以着重讲解.
引自arch/i386/boot/Makefile:
46 $(obj)/zImage $(obj)/bzImage: $(obj)/bootsect $(obj)/setup
$(obj)/vmlinux.bin $(obj)/tools/build FORCE
$(call if_changed,image)
@echo 'Kernel: $@ is ready'
现在我们看一下内核镜像文件是如何生成的.详见arch/i386/boot/tools/build.c
27 #include &stdio.h&
28 #include &string.h&
29 #include &stdlib.h&
30 #include &stdarg.h&
31 #include &sys/types.h&
32 #include &sys/stat.h&
33 #include &sys/sysmacros.h&
34 #include &unistd.h&
35 #include &fcntl.h&
36 #include &asm/boot.h&
39 typedef
40 typedef unsigned long u32;
42 #define DEFAULT_MAJOR_ROOT 0
43 #define DEFAULT_MINOR_ROOT 0
45 /* Minimal number of setup sectors (see also bootsect.S) */
46 #define SETUP_SECTS 4
48 byte buf[1024];
50 int is_big_
52 void die(const char * str, ...)
va_start(args, str);
vfprintf(stderr, str, args);
', stderr);
61 void file_open(const char *name)
if ((fd = open(name, O_RDONLY, 0)) & 0)
die("Unable to open `%s': %m", name);
67 void usage(void)
die("Usage: build [-b] bootsect setup system [rootdev] [& image]");
72 int main(int argc, char ** argv)
unsigned int i, c, sz, setup_
byte major_root, minor_
if (argc & 2 && !strcmp(argv[1], "-b"))
is_big_kernel = 1;
argc--, argv++;
if ((argc & 4) || (argc & 5))
if (argc & 4) {
if (!strcmp(argv[4], "CURRENT")) {
if (stat("/", &sb)) {
perror("/");
die("Couldn't stat /");
major_root = major(sb.st_dev);
minor_root = minor(sb.st_dev);
} else if (strcmp(argv[4], "FLOPPY")) {
if (stat(argv[4], &sb)) {
perror(argv[4]);
die("Couldn't stat root device.");
major_root = major(sb.st_rdev);
minor_root = minor(sb.st_rdev);
major_root = 0;
minor_root = 0;
major_root = DEFAULT_MAJOR_ROOT;
minor_root = DEFAULT_MINOR_ROOT;
fprintf(stderr, "Root device is (%d, %d)
", major_root, minor_root);
#define MAJOR(dev)
((dev)&&8)
#define MINOR(dev)
((dev) & 0xff)
79-83:如果第二个参数是'-b',表示我们使用大内核(后面将详细解释).注意,为了统一大内核和小内核的参数个数,82语句起到了协调作用.这样的话argc至少有4个,至多5个,小内核:build bootsect setup system [rootdev];大内核:-b bootsect setup system [rootdev].注意[& image]并不是一个参数,它表示标准输出重定向(文件描述符1).因为我们要把bootsect,setup和内核连接在一起组成内核镜像,所以如果不重定向到内核镜像文件的话就毫无意义了.
86:判断是否提供了rootdev参数.
87-91:如果提供了rootdev参数且rootdev参数是"CURRENT",则通过stat()返回一个与根目录文件相关的一个信息结构.我们经常使用的ls命令就使用这个函数.
92-93:st_rdev是dev_t类型,实际上是unsigned short.高8位代表主设备号,低8位代表从设备号.major_root和minor_root就是根文件系统所在的磁盘的主从设备号.
94-100:如果rootdev参数不是"FLOPPY",就通过stat()获得你提供的rootdev的文件信息.然后从中提取出主从设备号.
101-104:如果rootdev参数是"FLOPPY",主从设备号就都为0.
105-108:如果没有提供rootdev参数,主从设备号就为默认值:0,表示从软盘启动.
109:显示根设备的主从设备号.
file_open(argv[1]);
i = read(fd, buf, sizeof(buf));
fprintf(stderr,"Boot sector %d bytes.
if (i != 512)
die("Boot block must be exactly 512 bytes");
if (buf[510] != 0x55 || buf[511] != 0xaa)
die("Boot block hasn't got boot flag (0xAA55)");
buf[508] = minor_
buf[509] = major_
if (write(1, buf, 512) != 512)
die("Write call failed");
close (fd);
相关数据:(引自arch/asm-i386/boot/bootsect.S)
416 .org 497
417 setup_sects:
.byte SETUPSECTS
418 root_flags:
.word ROOT_RDONLY
419 syssize:
.word SYSSIZE
420 swap_dev:
.word SWAP_DEV
421 ram_size:
.word RAMDISK
422 vid_mode:
.word SVGA_MODE
423 root_dev:
.word ROOT_DEV
424 boot_flag:
.word 0xAA55
111-113:读取已经编译好的bootsect,然后显示读到的字节数.
114-115:如果不是512字节,显示出错信息,因为bootsect必须是一个扇区512字节大小!!
116-117:检查bootsect的最后两个字节的魔数,倒数第2个字节应该是55,最后一个字节应该是AA(因为x86是LSB的缘故).如果有一个不符合的话就打印出错信息然后退出.
118-119:将从设备号写入内核镜像的第508字节,主设备号写入内核镜像的第509字节.
120-122:对bootsect的修改全部完成,把修改好的bootsect写回到内核镜像文件.
file_open(argv[2]);
/* Copy the setup code */
for (i=0 ; (c=read(fd, buf, sizeof(buf)))&0 ; i+=c )
if (write(1, buf, c) != c)
die("Write call failed");
if (c != 0)
die("read-error on `setup'");
close (fd);
124-130:读取已经编译好的setup.i是setup的长度.
setup_sectors = (i + 511) / 512;
/* Pad unused space with zeros */
/* for compatibility with ancient versions of LILO. */
if (setup_sectors & SETUP_SECTS)
setup_sectors = SETUP_SECTS;
fprintf(stderr, "Setup is %d bytes.
memset(buf, 0, sizeof(buf));
while (i & setup_sectors * 512) {
c = setup_sectors * 512 -
if (c & sizeof(buf))
c = sizeof(buf);
if (write(1, buf, c) != c)
die("Write call failed");
132:setup_sectors存储setup所占用的扇区数,所以应该把i向上舍入.
134-135:setup_sectors不能小于默认值(4).
136-137:显示setup的长度,然后把缓冲区清0.
138-145:大多数情况setup不会是512字节的整数倍,所以我们计算一下向上舍入时那多出来的字节数.比如:我的setup是4.8K,那么i=4.8K,setup_sectors=10,所以c=0.2K.然后用0填充那多余的部分,这样i就是个与512字节对齐的数了.
file_open(argv[3]);
if (fstat (fd, &sb))
die("Unable to stat `%s': %m", argv[3]);
sz = sb.st_
fprintf (stderr, "System is %d kB
", sz/1024);
sys_size = (sz + 15) / 16;
/* 0x28000*16 = 2.5 MB, conservative estimate for the current maximum */
if (sys_size & (is_big_kernel ? 0x28000 : DEF_SYSSIZE))
die("System is too big. Try using %smodules.",
is_big_kernel ? "" : "bzImage or ");
if (sys_size & 0xefff)
fprintf(stderr,"warning: kernel is too big for standalone boot "
"from floppy
while (sz & 0) {
l = (sz & sizeof(buf)) ? sizeof(buf) :
if ((n=read(fd, buf, l)) != l) {
if (n & 0)
die("Error reading %s: %m", argv[3]);
die("%s: Unexpected EOF", argv[3]);
if (write(1, buf, l) != l)
die("Write failed");
close(fd);
147-149:获得已编译好的system内核的文件信息.
150-151:显示system的大小,以KB为单位.
152-159:以16为一个块向上舍入.因为内核大小要存放在一个叫syssize(后面见到)的地方,而syssize要在实模式下用到.众所周知实模式下通常只能使用最大为16位的操作数,虽然可以使用特殊方法使用32位操作数(在setup的分析中将会见到),但是毕竟太麻烦了,我们尽量保持简单.syssize的作用是决定我们的内核是"大"内核还是"小"内核,而这个阀值就是508K,再除以16就是0x7f00.即便是最大的小内核,其大小也可以在实模式下被访问到了.当然,大内核也可以放在软盘中,157就告诉我们了.不过软盘一般只有1.44M,所以也不能使用大于960K的内核.因为bootsect和setup还要占用一部分空间.DEF_SYSSIZE就定义为0x7f00,而大内核如果超过了2.5M,就太大了,要使用特殊方法,这里我们就不讨论了.
160-174:把system内核写到镜像文件中,这样我们的内核镜像就拼接好了.
if (lseek(1, 497, SEEK_SET) != 497)
/* Write sizes to the bootsector */
die("Output: seek failed");
buf[0] = setup_
if (write(1, buf, 1) != 1)
die("Write of setup sector count failed");
if (lseek(1, 500, SEEK_SET) != 500)
die("Output: seek failed");
buf[0] = (sys_size & 0xff);
buf[1] = ((sys_size && 8) & 0xff);
if (write(1, buf, 2) != 2)
die("Write of image length failed");
/* Everything is OK */
416 .org 497
417 setup_sects:
.byte SETUPSECTS
418 root_flags:
.word ROOT_RDONLY
419 syssize:
.word SYSSIZE
176-180:把setup_sectors写到bootsect中的第497字节处.把sys_size的低字节写到第500字节,sys_size的高字节写到第501字节.
Linux引导实录2-实模式的初始化
作为本篇的学习重点,我们来分析Linux源代码树的arch/asm-i386/boot/bootsect.S文件,以后管他叫bootsect。这个文件就是引导装载程序中的一种,大家比较熟悉的LILO,GRUB等等也属于引导装载程序。他们一般驻留在MBR或者操作系统所在分区的引导扇区(一定是该分区的第一个扇区)。结合我们的情况,bootsect就在软盘的0面,0磁道,1号扇区,大小是512字节。
无论多么高超的设计,bootsect毕竟只有512字节,对于庞大的操作系统内核来说,它还不足以完成所有的准备和初始化工作。所以我们还需要一个叫setup(arch/i386/boot/setup.S)的代码。我们的setup从软盘的0面,0磁道,2号扇区开始,大小视具体情况而定。(我们的情况是4.8K左右)
好了,废话就这么多了,接下来我们步入正题。
当你开机加电时,intel系列的cpu处于实模式状态,整个系统最大寻址空间为1M。这时,一个硬件电路把cpu内的主要寄存器设置成固定的值:ds=es=fs=gs=ss=0,cs=0xf000,eip=0x0000fff0,cs:ip指向0x000ffff0(这个地址一定在ROM BIOS中).而此处的则是一个jump指令,跳转到ROM BIOS中的另一个位置,开始执行一系列的动作(见下面). BIOS启动要经过以下几个步骤:
?对计算机硬件进行检测。这就是我们常说的POST-上电自检。此时,屏幕上会显示一些自检信息。此过程必须关闭中断。
?初始化硬件设备。因为刚刚上电时,所有的设备本身处于未知状态,BIOS必须对硬件进行初始化,以防止不必要的硬件冲突和故障。
?初始化中断向量表。这个表定位在物理地址0。
?执行BIOS功能调用int 0x19。根据%dl找到引导设备:系统可能要访问硬盘,光驱,软盘或者其他的引导介质(我们现在只讨论引导介质是软盘的情况)。
?加载引导扇区。把软盘的0面,0磁道,1扇区内容加载到物理地址为0x7c00的位置,然后跳转到这个位置,开始执行代码。此时,系统控制权交给了引导程序。我们的Linux之旅就此开始.
在分析bootsect.S的源代码之前,请大家看一下这张图:
Protected-mode kernel
| 大内核,解压缩后的大小内核都放在这里
100000 +-------------------------+
Reserved for GRAM&BIOS | 视频RAM和BIOS使用(不可用)
0A0000 +-------------------------+
Reserved for BIOS
| BIOS EBDA使用(不可用)
09A000 +-------------------------+
Stack/heap/cmdline
| 用于内核堆栈,命令行(lilo,grub支持命令行)
098000 +-------------------------+
Kernel setup
| setup代码
090200 +-------------------------+
Kernel boot sector
| 引导扇区代码
090000 +-------------------------+
Small kernel
| 小内核或搬运大内核的缓冲区
010000 +-------------------------+
Boot loader
| &- 引导扇区由BIOS加载到7C00
001000 +-------------------------+
Reserved for MBR/BIOS
| (不可用)
000800 +-------------------------+
Typically used by MBR
| (不可用)
000600 +-------------------------+
BIOS use only
| (不可用)
000000 +-------------------------+
这张引导阶段的内存布局一定要记住啊,很重要di~~~~~
我们现在来看bootsect.S的源代码:(为了方便,最左面的数字表示行号。被略过的行号是注释,感兴趣的话请自行查阅)
32 #include &asm/boot.h&
34 SETUPSECTS
/* default nr of setup-sectors */
35 BOOTSEG
/* original address of boot-sector */
36 INITSEG
= DEF_INITSEG
/* we move boot here - out of the way */
37 SETUPSEG
= DEF_SETUPSEG
/* setup starts here */
= DEF_SYSSEG
/* system loaded at 0x1) */
39 SYSSIZE
= DEF_SYSSIZE
/* system size: # of 16-byte clicks */
/* to be loaded */
41 ROOT_DEV
/* ROOT_DEV is now written by "build" */
42 SWAP_DEV
/* SWAP_DEV is now written by "build" */
44 #ifndef SVGA_MODE
45 #define SVGA_MODE ASK_VGA
48 #ifndef RAMDISK
49 #define RAMDISK 0
52 #ifndef ROOT_RDONLY
53 #define ROOT_RDONLY 1
56 .code16
59 .global _start
60 _start:
62 # First things first. Move ourself from 0x7C00 -& 0x90000 and jump there.
$BOOTSEG, %ax
# %ds = BOOTSEG
$INITSEG, %ax
# %ax = %es = INITSEG
$INITSEG, $go
64-74:这里BOOTSEG=0x07C0,就是bootsect一开始被BIOS加载到的地址;而INITSEG=0x9000。这样就理解了:bootsect把自己从 0x07c0:0x0000搬运到0x0(如上图)然后一个远程转移指令,跳到go。此时cs已经是0x9000了,我们今后的工作就在这里开始了.而0x7c00:0x0000的引导扇区代码bootsect在以后就不再用了。
$0x4000-12, %di
# 0x4000 is an arbitrary value &=
# length of bootsect + length of
# 12 is disk parm size.
# %ax and %es already contain INITSEG
# put stack at INITSEG:0x4000-12.
82-88:到现在为止,我们更新了所有的段寄存器:cs = ds = es = ss = 0x9000。这里有两点要注意:第一,根据注释,0x4000(16K)要大于bootsect(0.5K)+setup长度+bootsect与setup执行过程中的堆栈大小,而setup代码的长度是在编译过程中决定的,默认是2K。我的情况是4.8K。第二:为什么要减12个字节呢?注释说它是磁盘参数.看下面:
$0x78, %bx
# %fs:%bx is parameter table address
%fs:(%bx), %si
# %ds:%si is source
# copy 12 bytes
# %di = 0x4000-12.
# don't worry about cld
# already done above
$36, 0x4(%di)
# patch sector count
%di, %fs:(%bx)
%es, %fs:2(%bx)
Linux支持多种类型的软盘进行引导,除了我们常见的3.5",还有很多种类,见下表:
软盘类型 容量 扇区数/道 磁道数/面 字节数/扇区
512 《-我们使用的是这种
在中断向量表中有一个软盘参数表指针,用于后面的读取扇区的操作。很遗憾,很多BIOS并不支持大于默认值的设定。这意味着每次读取软盘最多只能读取7个扇区大小。而我们却希望每次能读取一个磁道上的所有扇区,这样可以大大的提高读取效率(因为谁都希望这个引导过程能尽快结束)。因此:从上表可见,为了支持扇区数/道最大的3.5"2.88M软盘,我们只能放弃默认的软盘参数表,再建立一个新的软盘参数表以允许最大读取扇区数为36。这个原始的软盘参数表的地址保存在0x8,可以理解为是一个"指针"。所以我们根据这个地址就能找到软盘参数表,然后把它拷到上面的那12字节空档里,再修改其中的参数,使其支持36扇区数/道,最后修改原来的软盘参数表"指针",使其指向新的软盘参数表。105-117就是干上述工作的。
$disksizes, %si
# table of sizes to try
127 probe_loop:
# extend to word
%ax, sectors
$disksizes+4, %si
got_sectors
# If all else fails, try 9
# %cx = track and sector
# drive 0, head 0
$0x0200, %bx
# address = 512, in INITSEG (%es = %cs)
$0x0201, %ax
# service 2, 1 sector
probe_loop
# try next value
相关数据:
sectors: .word 0
disksizes: .byte 36, 18, 15, 9
#对应上面表格中的扇区数/道
好像没有哪个BIOS功能调用能让我们知道软盘的扇区数/道,但是我们只有据此才能知道软盘类型。既然没有这个BIOS功能调用,我们就DIY,依次尝试不同的参数.总之,扇区数/道猜多了没有坏处,猜少了就会影响效率.
126-138:先从36扇区数/道的3.5" 2.88M开始测试:把sectors更新为.word 0x0024,表示我们假设当前软盘的扇区数/道是36.然后执行BIOS功能调用int 13H,ah=02,读磁盘:根据各个入口参数,读取第一个软盘驱动器,0面,0磁道的36号扇区。
139:显然我们的软盘每个磁道只有18个扇区,失败!!!返回到probe_loop,继续测试。接下来就对啦:读取第一个软盘驱动器,0面,0磁道的18号扇区。如果该扇区没坏的话,则读取成功!!
141 got_sectors:
$0x03, %ah
# read cursor pos
$0x07, %bl
# page 0, attribute 7 (normal)
# % int10 doesn't
# modify it
$msg1, %bp
$0x1301, %ax
# write string, move cursor
# tell the user we're loading..
相关数据:
.byte 13, 10
.ascii "Loading"
142-144:使用BIOS功能调用int 10H,ah=3时读取光标的位置。光标在屏幕上指示字符的显示位置,只有知道了位置我们才能在该位置上显示字符。
145-151:接下来,在光标所在的位置上打印字符串,字符串就是一个回车,接一个换行,然后是"Loading"字样。
$0x0001, %ax
# set sread (sector-to-read) to 1 as
$sread, %si
# the boot sector has already been read
%ax, (%si)
相关数据:
sread: .word 0
# sectors read of current track
head: .word 0
# current head
track: .word 0
# current track
157-159:sread表示我们已经读过的扇区数,现在只读了1个扇区(bootsect),所以更新为1。我们也没有读过别的磁道和盘面,所以head和track置0。
# reset FDC
$0x0200, %bx
# address = 512, in INITSEG
161-163:这次int 13H要复位软盘驱动器,为读取setup代码作准备。
164:es:bx指向setup被加载的位置:0x。
165 next_step:
setup_sects, %al
sectors, %cx
(%si), %cx
# (%si) = sread
no_cyl_crossing
sectors, %ax
(%si), %ax
# (%si) = sread
相关数据:
setup_sects: .byte SETUPSECTS
165-172:setup_sects的默认值是4,实际值在建立内核镜像时由build程序写入,我的setup大小是4.8K,所以setup_sects是10.0磁道中已有一个被引导扇区所占,所以另外17个扇区可供setup使用。我们当然就直接跳到no_cyl_crossing了.
对于setup大于17个扇区的情况,不立刻跳到no_cyl_crossing,从171继续执行.ax保存了0磁道上setup的扇区数,是17.
173 no_cyl_crossing:
read_track
# set % it uses %ax,%cx,%dx
%al, setup_sects
# rest - for next step
270 read_track:
$0xe2e, %ax
# loading... message 2e = .
278 # Accessing head, track, sread via %si gives shorter code.
4(%si), %dx
# 4(%si) = track
(%si), %cx
2(%si), %dx
# 2(%si) = head
$0x0100, %dx
# save for error dump
273-275:int 10H,ah=0xe要显示字符,光标跟着移动。显示一个“.”,表示我们要读取setup了。记住,以后每打印一个点标志着成功的读取了一次。
280-293:又是一次读磁盘:从第一个软盘驱动器,0面,0磁道,2号扇区开始读4个扇区。读到哪里哪??由es:bx寻址,在0x0,这个位置正好在引导扇区副本的上面.如果读操作失败,跳到bad_rt.
我们看一下失败代码:
322 bad_rt:
# save error code
# %ah = error, %al = read
jmp read_track
332 # print_all is for debugging purposes.
334 # it will print out all of the registers.
The assumption is that this is
335 # called from a routine, with a stack frame like
ret &- %sp
344 print_all:
# error code + 4 registers
347 print_loop:
# save count remaining
# &-- for readability
# see if register name is needed
$0xe05 + 'A' - 1, %ax
360 no_reg:
# next register
# print it
print_loop
367 print_nl:
$0xe0d, %ax
374 # print_hex is for debugging purposes, and prints the word
375 # pointed to by %ss:%bp in hexadecimal.
377 print_hex:
# 4 hex digits
(%bp), %dx
# load word into %dx
380 print_digit:
# rotate to use low 4 bits
$0xe0f, %ax
# %ah = request
# %al = mask for nybble
$0x90, %al
# convert %al to ascii hex
# in only four instructions!
$0x40, %al
print_digit
324::调用print_all显示调试信息.在288-291中我们在堆栈中保存了4个寄存器的值;323保存了出错代码;324调用了print_all,所以堆栈的样子如337-342所示,337是高地址,342是低地址.
344-351:堆栈中的error_code和4个寄存器共5个值,这个计数保存在cx中???????????然后在堆栈中保存住这个计数.这样我们就可以通过bp来寻址这个计数了?????????????;然后调用print_nl,这段代码只是使输出更加美观.接下来跳转到no_reg.
361-362:bp指向堆栈中的error_code,然后调用print_hex.
377-:把error_code复制到dx
错误代码就到这里了,295-297恢复堆栈,然后回到175:
# set % it uses %ax,%cx,%dx
%al, setup_sects
# rest - for next step
175-176:ah中保存了出错代码,而al中保存了已读的扇区数(实际情况是10,大s情况是17).因为这个操作过程中没有错误,所以ah为0.因为setup的大小可以超过4扇区,所以为了确认还有多少setup代码没有读取,要保存这个ax值。然后调用set_next:
299 set_next:
(%si), %ax
# (%si) = sread
sectors, %ax
$0x0001, %ax
%ax, 2(%si)
# change head
# next track
308 ok4_set:
300-303:把这次读取的扇区和已经读过的一个扇区加起来存在ax中,实际情况是11,大s情况是18.实际情况跳转到ok3_set,表示我们没有读完一个磁道.
304-309:大s情况就要更新磁盘参数,为了把剩下的setup读取完整.ax是在当前磁道上读取过的扇区数,所以换过磁道后ax应该是0.
310 ok3_set:
%ax, (%si)
# set sread
set_next_fin
$0x10, %ah
319 set_next_fin:
311:实际情况sread被更新为11,大s情况是0.
312-313:计算已经读取的setup长度:实际情况10个扇区×512字节=5K,大s情况17个扇区×512字节=8.5K.
314:bx表示已经读取的字节数,还记得之前bx的值吗?看164,bx是配合es寻址setup存放处时被赋予0x0200的,恰好是512字节.当bx大于64K时就溢出了,我们还早得很呢.....然后我们又返回到了no_cyl_crossing,接下来:
%al, setup_sects
# rest - for next step
177-179:还记得ax是多少吗??忘了就看上面,实际情况是10,大s情况是17。然后,实际情况setup_sects被设为0,表示我们已经完成了setup的读取;大s情况setup_sects减去17,跳到next_step,重复上面的操作,直到将setup全部读取完毕。注意:82提过ox4000,这样我们的setup就不能无限增大,否则会覆盖掉堆栈的.
# %es = SYSSEG
181-183:SYSSEG被定义为0x1000,这个位置就是最关键的内核代码存放的地方。es = 0x1000,这摆明了接下来就是准备搬运内核镜象了嘛。
231 read_it:
# %es = SYSSEG when called
$0x0fff, %ax
# %es must be at 64kB boundary
# %bx is starting address within segment
236 rp_read:
237 #ifdef __BIG_KERNEL__
# look in setup.S for bootsect_kludge
bootsect_kludge = 0x220
# 0x200 + 0x20 which is the size of the
bootsect_kludge
# bootsector + bootsect_kludge offset
$SYSSEG, %ax
# check offset
246 #endif
232-234:这么做是保证内核必须加载到一个与64K对齐的位置,这点由SYSSEG的定义来保证,所以不会跳转的。如果不是的话就进入死循环了,只能手动重启了。
随着Linux的不断发展,Linux的内核越来越大;旧版本的内核一般不到508K,而且还经过压缩;但是现在即使经过压缩也远远大于508K了,所以人们把容量大于508K的内核叫bzImage,而这个bzImage就不放在0x上了,而是放在0xM)上了。这已经高于实模式下的寻址空间了。所以,由实模式过渡到保护模式就是setup代码的工作了。如果我们要使用大内核,就在编译内核时定义了__BIG_KERNEL__;否则的话就没定义__BIG_KERNEL__。
我们先来看大内核的情况:
238-239:远程调用由bootsect_kludge定义的代码,这段代码属于setup:
引自arch/i386/boot/setup.S:
136 bootsect_kludge:
bootsect_helper, SETUPSEG
136-137:SETUPSEG定义为0x9020。我们调用setup内的bootsect_helper,cs临时被设为0x9020,这是我们第一次进到setup里边执行代码,之所以要进入setup执行是因为bootsect实在是没地方了:)
840 bootsect_helper:
$0, %cs:bootsect_es
bootsect_second
$0x20, %cs:type_of_loader
%ah, %cs:bootsect_src_base+2
%ax, %cs:bootsect_es
$SYSSEG, %ax
# nothing else to do for now
相关数据:
902 bootsect_es:
95 type_of_loader: .byte
# = 0, old one (LILO, Loadlin,
Bootlin, SYSLX, bootsect...)
# See Documentation/i386/boot.txt for
# assigned ids
887 bootsect_src_base:
0x00, 0x00, 0x01
# base = 0x010000
# limit16,base24 =0
841-842:我们不会跳转,因为我们还什么也没做呢。
844:type_of_loader表示为0xAB;A表示引导程序的类型,我们用的是bootsect,所以A是2;B表示版本号,填0。
845-:es=ax=0x1000,ax右移4位。ah(0x01)保存在bootsect_src_base+2的位置上,把bootsect_es设为0x1000。bootsect_src_base是个搬运内核的缓冲区的其实地址,后面会讲到.
943-944:ax=0。然后返回到bootsect中。cs也被恢复为0x9000。
我们看看小内核:
$SYSSEG, %ax
# check offset
241-245:计算已读取的小内核的长度.
然后大小内核都到了这里:
syssize, %ax
# have we loaded everything yet?
247:对于小内核,syssize最大为0x7f00,但我们要这样理解:0x7f00 &&*4=508K,同理,大内核最大2.5M.这里测试是否已经搬运完毕.
248:无论大小内核,以现在的情况都要跳转,因为还从未读过内核呢。
252 ok1_read:
sectors, %ax
(%si), %ax
# (%si) = sread
253-257:计算一下当前磁道上还剩余的扇区大小。我们的情况是还剩下7个扇区3.5K。剩下的这部分扇区和接下来要读的数据都是内核。bx表示已经读的内核镜像大小。我们还未读过,所以bx=0,cx=3.5K,即:接下来我们要开始读这3.5K的内核了。
可能有人要问干吗不一起把所有的内核镜像都一次读完啊。因为我们使用的是BIOS功能调用,这是最底层的读取方法。底层的表现就在于它使用的参数都是硬件的物理参数,这些物理参数是:柱面(磁道),磁头(面),扇区,这些我们已经见识过了。而且每次最多读扇区数/道大小的数据,更要命的是还要自己更新参数,切换到下一磁道或者面,所以没那么简单想怎样就怎样呢,这就考验了编程者的能力了。尤其像引导程序这样的代码,要在512字节里干尽可能多的工作,还要尽可能多的提供扩展功能(比如lilo和grub的命令行和多系统引导等等)。
258:这是一个关键的跳转,我们后面还要见到一个。此时的cx表示我们原本已经读过的内核镜像大小bx+即将读取的内核镜像大小cs,显然需要测试是否溢出64K。我们肯定跳到ok2_read。
265 ok2_read:
read_track
268-295:这段代码我们已经分析过了,此时打印第二个点,表示我们准备从软盘上加载内核镜像到内存中。然后又是一次读磁盘,从第一个软盘驱动器,0面,0磁道,12号扇区开始读7个扇区,就是一气把0磁道剩下的扇区全读到读到es:bx=0x0的位置。到此,我们完成了0磁道的读取。
299-307:这段代码也见过了。就像上面分析的那样,我们该换个磁道来读了,所以把head置1。如果我们读过1面的话,就会换下一个磁道。由此可见:读软盘的时候先从柱面0的0面1号扇区开始读,然后换面,再读柱面1的0面1号扇区......如此下去。期间每次读完一个磁道就要调用set_next更新参数。
310-318:还是分析过的,这回读者就自己来分析吧。结果:bx=3.5K,这是我们已经读取的内核镜像的大小。跳到set_next_fin,然后返回
268:正如文字所说,repeat read内核镜像数据,一次9K,因为往后就一次一个磁道的读了。每读完一次就进入set_next,更新sread,track, head参数。
第二次进入了setup:
934-935:这次同上次不一样了,bootsect_es已被设置为0x1000,所以我们进入bootsect_second:
853 bootsect_second:
# 64K full?
bootsect_ex
857:bx表示已读的内核镜像大小,早晚要超过64K,所以要测试一下,我们当然跳走:
870 bootsect_ex:
%cs:bootsect_dst_base+2, %ah
# we now have the number of
# moved frames in %ax
相关数据:
895 bootsect_dst_base:
0x00, 0x00, 0x10
# base = 0x100000
# limit16,base24 =0
0, 0, 0, 0
0, 0, 0, 0
870-878:由于低端内存没有足够的空间存放大内核,所以我们需要把它搬运到扩展内存中.bootsect_dst_base~bootsect_dst_base+2是大内核所在的起始地址--&0xM).872左移4位是为了把bootsect_dst_base+2的低4位移到ah的高4位,这么做的原因是我们每次从低端内存搬运64K的内核镜像到高端内存时,bootsect_dst_base+2就增1.这样ax就保存了以0x100000为基地址的段内偏移值.此时ax本身既表示了已搬运的内核大小,0x100000+ax又表示了下一块64K内核数据的起始地址.因此,我们需要把这些信息反馈给ax,以便于接下来测试内核是否已经读完。
247-248:我们还是返回回到ok1_read,再调用read_track。
270-297:打印第三个点,表示我们接下来继续读取内核镜像。然后读取9k。然后把从1面,0磁道,1扇区开始的18个扇区读到0x0的位置。
299-309:换磁道1。以后就从1柱面1号扇区开始读取。
310-320:ok3_set的任务就是检查已经读取的数据是否溢出64K。这里的测试就是另一个重要的跳转。因为每次读取前都要为下一次读取作测试,没有溢出64K的话再进行实际的读取;溢出的话再跟据实际情况调整要读取的扇区数,从而保证读取64K,决不会多读或少读。
从此往后就是不断的在rp_read到set_next_fin间循环重复。每一个点表示从软盘拷过一次数据那么重复到什么时候啊??直到这里:
257-258:此时bx=57.5K,就是6个9K+3.5K。cx=9K,表示要读取的数据大小。cx再加上已读取的57.5K,等于66.5K,溢出后cx=2.5K。
260:由于我们溢出后是66.5K而不是64K,所以不跳转。此时sread=0,head=0,track=3。
262-264:ax=-57.5K=6.5K。再右移9位,得13个扇区,即:再读13个扇区才读满64K。
然后又是ok2_read,代码请参考上面的。直到:
313-320:bx=64K,溢出,不跳转。既然读完一个64K,就必须更改断寄存器的值,因为实模式下一个段最大只有64K。
由于我们已经读完一个64K,所以根据上面所述,大内核此时要搬运至高端内存,而小内核则继续搬运。我们看大内核的情况:
853 bootsect_second:
# 64K full?
bootsect_ex
$0x8000, %cx
# full 64K, INT15 moves words
$bootsect_gdt, %si
$0x8700, %ax
bootsect_panic
# this, if INT15 fails
%cs:bootsect_es, %es
# we reset %es to always point
%cs:bootsect_dst_base+2
# to 0x10000
相关数据:
880 bootsect_gdt:
0, 0, 0, 0
0, 0, 0, 0
884 bootsect_src:
887 bootsect_src_base:
0x00, 0x00, 0x01
# base = 0x010000
# limit16,base24 =0
892 bootsect_dst:
895 bootsect_dst_base:
0x00, 0x00, 0x10
# base = 0x100000
# limit16,base24 =0
0, 0, 0, 0
0, 0, 0, 0
853-866:由于bx是64K即0,所以不作跳转。 int 15h,ah=0x87,执行扩展内存的拷贝。扩展内存指的是高于1M的内存,由于大内核的缘故,我们把0x0~0x1000:0xffff这64K的数据拷贝到0x100000,扩展内存至多有16M,对于我们的内核已经足够大了。
866:如果int 15h失败,CF置1,ah表示出错代码,我们假设执行正确无误。????????????????/
868-869:我们把0x0~0x1000:0xffff这64K当作从低端内存搬运内核到扩展内存的缓冲区使用,所以es必须重置为0x1000。下次读取时就把原来的64K内容覆盖掉了。同时把bootsect_dst_base+2增1,这样,下次内核镜像就被搬运到0x110000处了。
963-971:没什么特别的,只是ax=0x1000,表示已经搬运了64K内核镜像。
此时,大内核返回到247。从此继续回到刚才的循环。仔细观察这个循环:236-320,我们发现唯一能够返回到主线程的ret就在250:
syssize, %ax
# have we loaded everything yet?
可以看出,当我们的大内核搬运到完时,就返回到了久违的no_cyl_crossing中,当然小内核也是从这里返回的。因此可以这样形容:no_cyl_crossing相当于c里的main函数,而其他的只是被no_cyl_crossing调用的函数而已。
no_cyl_crossing:
kill_motor
397 kill_motor:
# reset FDC
$0x3f2, %dx
406 #endif
399-407:复位软驱,然后返回。
367 print_nl:
$0xe0d, %ax
到目前为止屏幕上显示了:(一个回车,一个换行,Loading,n个点,一个回车,一个换行)
Loading............................
_&-----光标停在这里
root_dev, %ax
root_defined
sectors, %bx
$0x0208, %ax
# /dev/ps0 - 1.2Mb
root_defined
$0x1c, %al
# /dev/PS0 - 1.44Mb
root_defined
$0x20, %al
# /dev/fd0H2880 - 2.88Mb
root_defined
# /dev/fd0 - autodetect
195:这里的root_dev是由在执行build时填写的。关于build的作用详见1.
不指定rootdev的或者使用软盘引导,rootdev默认值是0。不过,就算我们没有指定引导设备是软驱也没关系,接下去的代码会依次检测/dev/fd0H),/dev/PS0 (2,28)和/dev/at0 (2,8)。我们执行到206时跳转,因为已经正确检测到了我们的引导设备是/dev/PS0 - 1.44Mb软盘。
213 root_defined:
%ax, root_dev
214:把0x1c写到root_dev,然后:
$SETUPSEG, $0
至此bootsect的工作全部完成了,控制权正式交给了setup代码。而我们的这次讲解也宣告结束,下次我会继续分析setup部分的代码,还请大家继续捧场^^
Linux引导实录3-实模式到保护模式的过渡
setup的主要工作是把从BIOS获得的系统信息写到0x9ff。这个地方原本是bootsect,由于其使命已告终结,再加上内存空间狭小,所以只好令bootsect“死无全尸”了。而这些系统信息供今后内核初始化使用。所以,setup本身的工作可以说非常重要,但也是非常乏味无聊的,希望大家能耐下心来把它读完。
50 #include &linux/config.h&
51 #include &asm/segment.h&
52 #include &linux/version.h&
53 #include &linux/compile.h&
54 #include &asm/boot.h&
55 #include &asm/e820.h&
56 #include &asm/page.h&
58 /* Signature words to ensure LILO loaded us right */
59 #define SIG1
60 #define SIG2
62 INITSEG
= DEF_INITSEG
# 0x9000, we move boot here, out of the way
= DEF_SYSSEG
# 0x1000, system loaded at 0x1).
64 SETUPSEG = DEF_SETUPSEG
# 0x9020, this is the current segment
# ... and the former contents of CS
67 DELTA_INITSEG = SETUPSEG - INITSEG
69 .code16
70 .globl begtext, begdata, begbss, endtext, enddata, endbss
73 begtext:
75 begdata:
77 begbss:
trampoline
81:一上来我们就跳到trampoline.
164 trampoline:
start_of_setup
166 # End of setup header #####################################################
168 start_of_setup:
169 # Bootlin depends on this being done early
$0x01500, %ax???????????????????????
$0x81, %dl
170-172:读取第二个磁盘的类型:这里这么着急检测第二块硬盘是因为bootin引导程序要尽快获得这个信息。bootin是一种在dos下引导linux的引导程序,在这里我们就不讨论了。
174 #ifdef SAFE_RESET_DISK_CONTROLLER???????????????????????????
175 # Reset the disk controller.
$0x0000, %ax
$0x80, %dl
179 #endif
176-178:复位所有硬盘驱动器和软盘驱动器,这个时候磁头会定位在0磁道上。
181 # Set %ds = %cs, we know that SETUPSEG = %cs at this point
# aka SETUPSEG
182-183:ds=0x9020,即我们的setup代码所在的位置。
184 # Check signature at end of setup
$SIG1, setup_sig1
$SIG2, setup_sig2
相关数据:
59 #define SIG1
60 #define SIG2
1037 setup_sig1:
1038 setup_sig2:
185-189:检查setup的最末断的特征标记。放在最后就是为了检查setup是否加载完全.如果setup加载完全,其末端标记为55AA5A5A;如果不是的话,跳到bad_sig把剩下的部分加载完全,再检查特征标记。如果依然不正确的话就panic了。大家可能会说bootsect不是已经把setup加载完全了吗?实际上,大家熟悉的LILO执行的最后一条指令就是跳转到setup,而旧版本的lilo由于设计问题,只能加载setup的前4个扇区,所以我们要测试。接下来的代码在bootsect情况几乎不会遇到,只有在老版的lilo中才可能遇到.
我们先来看bad_sig:
228 bad_sig:
# SETUPSEG
$DELTA_INITSEG, %ax
(497), %bl
# get setup sect from bootsect
# LILO loads 4 sectors of setup
# convert to words (1sect=2^8 words)
# convert to segment
$SYSSEG, %bx
%bx, %cs:start_sys_seg
62 INITSEG
= DEF_INITSEG
# 0x9000, we move boot here, out of the way
= DEF_SYSSEG
# 0x1000, system loaded at 0x1).
64 SETUPSEG = DEF_SETUPSEG
# 0x9020, this is the current segment
# ... and the former contents of CS
67 DELTA_INITSEG = SETUPSEG - INITSEG
89 start_sys_seg:
229-233:把0x的值赋给bl.这个位置原本是bootsect,参考上次的讲义,偏移值497是setup_sects.
234-236:减去4个扇区大小,这是setup的默认大小,也是最小长度.lilo只读了前4个扇区,所以我们要计算一下没读的扇区数.并把这个大小转化为字的个数,这个值要在下面的串指令中用到.
237:再左移3位,比较234,实际上左移了5位.这有什么意义吗?实际上bx在这里是作为一个段地址,因为实模式下的段地址要再左移4位和偏移值相加得物理地址,我们再把bx左移4位看看,对吧,相对于234,左移了9位,正好换算成了字节数,正如237的注释所说:bx转化成了段地址.
238-239:setup的剩下部分和内核要放在SYSSEG:0000上.setup在低地址段,核心在高地址段,start_sys_seg就是核心的起始地址.
240 # Move rest of setup code/data to here
$2048, %di
# four sectors loaded by LILO
$SYSSEG, %ax
# aka SETUPSEG
$SIG1, setup_sig1
$SIG2, setup_sig2
241-248:老版本的lilo会把大于4个扇区长度的setup放在0x10000处,所以我们必须手动把它们搬到0x处.
249-257:恢复ds.如果加载完全就像191那样回到good_sig.如果依旧错误,进入no_sig:
259 no_sig:
no_sig_mess, %si
222 no_sig_mess: .string
"No setup signature found ..."
193 # Routine to print asciiz string at ds:si
194 prtstr:
208 # Part of above routine, this one just prints ascii al
209 prtchr: pushw
$0x01, %cx
$0x0e, %ah
195-202,209-217:显示222所示的字符串.显示完所有字符后返回:
263 no_sig_loop:
no_sig_loop
264-265:cpu停止执行指令,出于halt状态,直到收到一个中断或者重启信号才恢复.这时如果你敲动键盘,cpu会从265恢复执行,但是不会有任何变化,除了重启......
也就是说,如果setup的剩余部分没有加载正确,我们只有手动重启一条路可走.我们回到正常情况:
267 good_sig:
# aka SETUPSEG
$DELTA_INITSEG, %ax
# aka INITSEG
相关数据:
67 DELTA_INITSEG = SETUPSEG - INITSEG
268-270:ds被设置为0x9000。
271 # Check if an old loader tries to load a big-kernel
$LOADED_HIGH, %cs:loadflags
# Do we have a big kernel?
# No, no danger for old loaders.
$0, %cs:type_of_loader
# Do we have a loader that
# can deal with us?
# Yes, continue.
# No, we have an old loader,
loader_panic_mess, %si
no_sig_loop
相关数据:
100 # flags, unused bits must be zero (RFU) bit within loadflags
101 loadflags:
102 LOADED_HIGH
# If set, the kernel is loaded high
108 #ifndef __BIG_KERNEL__
LOADED_HIGH
112 #endif
286 loader_panic_mess: .string "Wrong loader, giving up..."
272-273:旧版本的lilo是无法加载大内核的,所以我们需要测试是否属于这种情况。大内核的LOADED_HIGH是1,小内核的LOADED_HIGH是0。
275-277:lilo的type_of_loader是0,无法加载大内核,所以进入prtstr.同上面分析的一样,显示完出错信息后就得手动重启了。而我们的bootsect的type_of_loader是0x20,没问题,跳走。
接下来内核要做的就是获得可用的内存数量.这里使用了3种方法:e820h图法获得内存图(memory map);e801h法获得内存数量以及最后的88h法.它们都是通过int 0x15来实现的.由于实现起来很容易,所以每种方法无论成败,都被顺次执行一次.
288 loader_ok:
289 # Get memory size (extended mem, kB)
%eax, %eax
%eax, (0x1e0)
291-292:以KB为单位获得扩展内存(1M以上)的数量。0xe0保存这个数值,当然现在应当把它清0。后面要说的e801h法就把结果放在这个位置.
293 #ifndef STANDARD_MEMORY_BIOS_CALL????????????????
%al, (E820NR)
307 #define SMAP
0x534d4150
309 meme820:
%ebx, %ebx
# continuation counter
$E820MAP, %di
# point into the whitelist
# so we can have the bios
# directly write into it.
相关数据:
#define E820NR 0x1e8
/* # entries in E820MAP */
#define E820MAP 0x2d0
/* our map */
第一种方法:e820图法。之所以称为e820是因为使用int 0x15查询内存分配时入口参数ax等于e820。
edx 534D4150h ('SMAP')
ecx 保存地址范围描述符的内存大小,应该大于等于20字节
es:di 指向保存地址范围描述符的缓冲区
CF 执行成功不置位;失败置位
eax 534D4150h ('SMAP')
es:di 指向保存地址范围描述符的缓冲区,此时缓冲区内的数据已由BIOS填写完毕
ebx 下一个地址范围描述符的计数,如果为0表示内存区域扫描完毕
ecx 当CF置位(表示出错)时返回实际的地址范围描述符大小
ah 失败时保存出错代码
不知大家仔细观察过没有,linux启动时会出现类似这样的ascii图形(这是我的系统上的结果,可以使用dmeg命令查看到):
BIOS-e820: 0000 - fc00 (usable)
BIOS-e820: fc00 - a0000 (reserved)
BIOS-e820: f0000 - 0000 (reserved)
BIOS-e820: 0000 - 0000 (usable)
BIOS-e820: ffff0000 - 0000 (reserved)
接下来的代码就是如何构建这张图了:
310-313:ebx是连接计数器;di指向一个缓冲区。每次int 0x15,ah=e820调用后返回一个计数,这个计数保存在ebx中.返回的计数表示下一个地址范围,当检测完毕即扫描了所有的内存后,ebx返回0(第一个地址范围的计数是0).di指向的缓冲区就是一个地址范围描述符,其内容是在调用过程中由BIOS填写的。
315 jmpe820:
# e820, upper word zeroed
$SMAP, %edx
# ascii 'SMAP'
# size of the e820rec
# data record.
# make the call
# fall to e801 if it fails
$SMAP, %eax
# check the return is `SMAP'
# fall to e801 if it fails
317-320:edx是0x534d4150,就是SMAP的ascii码;ecx是地址范围描述符的长度;es=0x9000,地址描述符用es:di来寻址,地址是0xd0。
321-325:如果cf位为0,表示支持e820图法执行成功。此时,eax返回值是0x534d4150('SMAP'标志),如果不是的话表明调用失败,使用e801h法;es:di作用不变;ebx返回下一个地址范围;ecx是实际的大小。我们来看一下地址范围描述符的结构:
Bytes 0-3 基地址的低32位
Bytes 4-7 基地址的高32位
Bytes 8-15 长度(以字节为单位)
Bytes 16-19 地址范围的类型:
1 = AddressRangeMemory,可以供OS使用
2 = AddressRangeReserved,不可用
3 = AddressRangeACPI,可用
4 = AddressRangeNVS,不可用(NVS是不挥发存储器的意思,包括ROM,EPROM,EEPROM,Flash等等)
Other = Not defined,不可用
可以看出,每一个地址范围描述符描述了e820图中的一项。每个地址范围描述符占20字节。
333 good820:
(E820NR), %al
# up to 32 entries
$E820MAX, %al
相关数据:
#define E820MAX 32
/* number of entries in E820MAP */
334-336:我们刚刚完成一个地址范围描述符,远远没有达到32个(这是允许的最多的地址范围描述符数量),所以不跳转。
338-341:E820NR加1,表示我们完成了1个地址范围描述符;修改di使它指向下一个要填充的地址范围描述符。
342 again820:
# check to see if
# %ebx is set to EOF
343-344:这个检测过程还要重复多回,上面说过最后一次的时候ebx=0,表示检测完毕。注意:这个图要在后面的sanitize_e820_map()中得到修正,因为有些BIOS做的不是很好.
接下来的第2个方法:e801h法。
AX = E801h
CF 执行成功不置位;失败置位
ax 1M-16M范围中的内存数量,以KB为单位(我想大多数人的机器都应该配置了更多的内存,算一下,应该是3c00,表示15M)
bx 16M以上的内存数量,以64K为单位
cx 作用同ax,有些BIOS将返回值保存在这里
dx 作用同bx,有些BIOS将返回值保存在这里
345 bail820:
356 meme801:
# fix to work around buggy
# BIOSes which dont clear/set
# carry on pass/error of
# e801h memory size call
# or merely pass cx,dx though
# without changing them.
$0xe801, %ax
357:由于某些BIOS的设计缺陷,从int 0x15,ax=e801返回时,既不设置(表示出错)也不清除(表示成功)CF位,所以我们手动把CF被置1。如果BIOS不支持e801或者执行失败,则使用最后一种方法。
358-362:同时也把cx,dx清0,为了下面的测试。
# Kludge to handle BIOSes
e801usecxdx
# which report their extended
# memory in AX/BX rather than
e801usecxdx
The spec I have read
# seems to indicate AX/BX
# are more reasonable anyway...
367-368:由于诸多原因,int 0x15,ax=e801返回的值可能在ax/bx中,也可能在cx/dx中。所以我们需要测试实际属于哪种情况。如果cx或dx改变了,即返回值保存在cx/dx,我们就跳到e801usecxdx。如果返回值保存在ax/bx,我们先把它们转移到cx/dx,再做进一步处理.
374 e801usecxdx:
$0xffff, %edx
# clear sign extend
# and go from 64k to 1k chunks
%edx, (0x1e0)
# store extended memory size
$0xffff, %ecx
# clear sign extend
%ecx, (0x1e0)
# and add lower memory into
# total size.
375-376:edx中保存了16M以上的内存数量,单位是64K。左移6位表示edx以1K为单位。然后把这个值保存在0xe0的位置.
378-379:ecx中保存了1M-16M的内存数量,单位是1K。然后把这个值加到0xe0,这就是我们可以使用的所有的扩展内存数量(以1K为单位)。
最后的方法:88h法。
CF 执行成功不置位;失败置位
ax 从1M开始的内存数量,单位是1KB
ah 出错状态:
80h 非法指令 (PC,PCjr)
86h 不支持该功能 (XT,PS30)
384 mem88:
386 #endif
$0x88, %ah
387-389:检测扩展内存大小。可见,最多能检测到64M扩展内存。然后把这个值写到0x2的位置上。
由于BIOS设计上的差异,setup必须针对每一种情况作出适当的处理。显然,e820图法是最好的,但是BIOS不一定支持;最后一种方法虽然被普遍支持,但是只能检测到至多64M内存,而现在装载256M内存的pc机是很平常的了。因此,如果BIOS设计存在缺陷,那么你的内存数就有可能无法被正确的检测到。但是大家不用灰心,像lilo,grub这样的高级引导程序是支持命令行的,其中有一条命令就是“MEM=XXX”,这样它会覆盖BIOS检测到的值,把你指定的XXX传递给内核。这在后面的代码中会做介绍.
391 # Set the keyboard repeat rate to the max
$0x0305, %ax
392-394:设置键盘的击键频率和延迟。bh=0表示延迟是250ms;bl=0表示击键频率,为每秒30个字符(汗......地球人大概作不到吧)。
396 # Check for video adapter and its parameters and allow the
397 # user to browse video modes.
# NOTE: we need %ds pointing
# to bootsector
398 实际上setup文件包含arch/i386/boot/video.S这个文件,这是在setup中明确说明的:
1032 # Include video setup & detection code
1034 #include "video.S"
因此video实际上在video.S中,我们来看一看:
108 # This is the main entry point called by setup.S
109 # %ds *must* be pointing to the bootsector
110 video:
# We use different segments
# FS contains original DS
# DS is equal to CS
# ES is equal to CS
# GS is zero
basic_detect
# Basic adapter type testing (EGA/VGA/MDA/CGA)
110-120:检测显卡类型和相关参数。初始化相关的段寄存器:fs=0x9000,ds=es=0x9020,gs=0。
141 # Detect if we have CGA, MDA, EGA or VGA and pass it to the kernel.
142 basic_detect:
$0, %fs:(PARAM_HAVE_VGA)
$0x12, %ah
# Check EGA/VGA
$0x10, %bl
%bx, %fs:(PARAM_VIDEO_EGA_BX)
# Identifies EGA to the kernel
$0x10, %bl
# No, it's a CGA/MDA/HGA card.
相关数据:
86 #define PARAM_HAVE_VGA
84 #define PARAM_VIDEO_EGA_BX
142:开始检测显卡类型:CGA,MDA,EGA或者VGA。现在的显卡基本上都是SVGA,EVGA或者XVGA之类的了,但是开机时它们总是工作在EGA或VGA模式。我们把检测到的型号以参数的形式传递给内核。
143:先检测是否是EGA,如果连EGA都不是,那就别提VGA了。
144-146:返回当前适配器的设置信息:bh=0单色模式,bh=1彩色模式;bl=VRAM容量,0=64K,1=128K,2=192K,3=256K。
147:然后把检测到的信息写入0x9000:0xa
148:噢!我们的显卡一般不会老到CGA,MDA或者HGA什么的所以不跳转。
$0x1a00, %ax
# Check EGA or VGA?
$0x1a, %al
# 1a means VGA...
# anything else is EGA.
相关数据:
1932 adapter:
# Video adapter: 0=CGA/MDA/HGA,1=EGA,2=VGA
151:至少目前是EGA。
152-155:注释写的很明白了。我们的显卡一般在开机时处在VGA模式下。
%fs:(PARAM_HAVE_VGA)
# We've detected a VGA
157-159:很自然的把相关参数更新为适用于VGA的。这些参数一定要保存好,供日后内核初始化时使用。
121 #ifdef CONFIG_VIDEO_SELECT
%fs:(0x01fa), %ax
# User selected video mode
$ASK_VGA, %ax
# Bring up the menu
# Set the mode
badmdt, %si
# Invalid mode ID
133 #ifdef CONFIG_VIDEO_RETAIN
restore_screen
# Restore screen contents
135 #endif /* CONFIG_VIDEO_RETAIN */
136 #endif /* CONFIG_VIDEO_SELECT */
121-136:这段代码允许你自己指定显示模式。由于真正使用的显示模式是以参数的形式传递到内核,所以你可以在源代码的Makefile文件里的SVGA_MODE=....这里来指定,也可以在lilo的命令行里使用vga=XXX来指定,还可以使用vidmode命令来指定:
videmode /boot/vmlinuz-2.4.20-8 -3
然后重启后在内核解压缩之前时会提示你键入你想使用的显示模式。
具体细节可以参考Documentation/svga.txt和man vidmode,这里就不讲了。而且一般情况下,我们的显示模式是NORMAL_VGA:80×25。
mode_params
# Store mode parameters
# Restore original DS
165 mode_params:
166 #ifdef CONFIG_VIDEO_SELECT
$0, graphic_mode
169 #endif
$0x03, %ah
# Read cursor position
%dx, %fs:(PARAM_CURSOR_POS)
$0x0f, %ah
# Read page/mode/width
%bx, %fs:(PARAM_VIDEO_PAGE)
%ax, %fs:(PARAM_VIDEO_MODE)
# Video mode and screen width
# MDA/HGA =& segment differs
相关数据:
80 #define PARAM_CURSOR_POS
81 #define PARAM_VIDEO_PAGE
82 #define PARAM_VIDEO_MODE
170-172:读取光标位置。这个调用我们在bootsect里见过,这里就不多说了。dh是光标位置的行号,dl是光标位置的列号。
174-179:返回视频参数:al是当前视频模式;ah是字符列数;bh是页号。al为7时,表示80x25,单色文本模式(MDA,HERC,EGA,VGA),我们就是这种情况。
$0xb000, video_segment
182 mopar0: movw
%gs:(0x485), %ax
# Font size
%ax, %fs:(PARAM_FONT_POINTS)
# (valid only on EGA/VGA)
force_size, %ax
# Forced size?
相关数据:
1933 video_segment:
# Video memory segment
1934 force_size:
# Use this size instead of the one in BIOS vars
181:在低端内存,0xa00是视频RAM的图形区,0xb00是视频RAM的文本区。
182-185:%gs:(0x485)寻址到BIOS通信区中的视频控制数据区,找到字体大小;我们也可以使用自定义的字号,这里我们使用BIOS提供的字体大小。
192 mopar1: movb
$0, adapter
# If we are on CGA/MDA/HGA, the
# screen must have 25 lines.
%gs:(0x484), %al
# On EGA/VGA, use the EGA+ BIOS
# location of max lines.
198 mopar2: movb
%al, %fs:(PARAM_VIDEO_LINES)
192-199:已经确定我们使用的是VGA了,并且设置好了所有的相关参数,我们返回到138。
138-139:恢复ds=0x9000,返回到setup中。
401 # Get hd0 data...
(4 * 0x41), %si
# aka SETUPSEG
$DELTA_INITSEG, %ax
# aka INITSEG
$0x0080, %di
$0x10, %cx
415 # Get hd1 data...
(4 * 0x46), %si
$0x0090, %di
424 # Check that there IS a hd1 :-)
$0x01500, %ax
$0x81, %dl
402-404:这里0x41是中段向量号0x41,在实模式下,中断号×4就是该中断处理程序的地址。而这里的0x41×4=0x104显然只是像bootsect里的软盘参数表一样,仅仅是第一块硬盘的参数表指针。我们让ds:si指向硬盘参数表。
405-414:典型的串操作指令:把由ds:si指向的第一块硬盘的参数表的内容拷贝到由es:di指向的0x,大小是16字节。
416-423:如法炮制,把第二块硬盘的参数表的内容拷贝到0x,大小是16字节。
424-428:检查我们究竟有没有第二块硬盘。
433 no_disk1:
# aka SETUPSEG
$DELTA_INITSEG, %ax
# aka INITSEG
$0x0090, %di
$0x10, %cx
433-442:很简单,没有第二块硬盘的话就把第二块硬盘的参数表清0。
443 is_disk1:
444 # check for Micro Channel (MCA) bus
# aka SETUPSEG
$DELTA_INITSEG, %ax
# aka INITSEG
%ax, (0xa0)
# set table length to 0
$0xc0, %ah
# moves feature table to es:bx
445-453:返回系动配置参数,es:bx指向一个系统描述符表,449预设这个表的长度为0,以防找不到这个表。这个表的结构如下(括号中是一般情况下的默认数值):
Bytes 1-2 描述符长度(8)
Byte 3 model位 (FCh = AT)
Byte 4 Sub model位(01h = AT)
Byte 5 BIOS revision level (0)
Byte 6 特征信息(见下表):
Byte 7 保留
Byte 8 保留
|7|6|5|4|3|2|1|0|
| | | | | | | `---- Reserved(0)
| | | | | | `----- 0=PC bus, 1=Micro Channel(0,ISA-type I/O channel)
| | | | | `------ Extended BIOS Data Area(0,EDBA not allocated)
| | | | `------- wait for external event supported(0)
| | | `-------- INT 15,4F used(Keyboard intercept)(1,called by INT 09h)
| | `--------- Real time clock present present(1)
| `---------- Second 8259PIC present(1)
`----------- DMA channel 3 used by fixed disk BIOS(0,Fixed disk BIOS does not use DMA channel 3)
451:当然,如果BIOS不支持这个调用,一般会置位CF;但有些BIOS并不置位,所以我们在451中设置CF。
# aka SETUPSEG
$DELTA_INITSEG, %ax
# aka INITSEG
$0xa0, %di
(%si), %cx
# table length is a short
$0x10, %cx
sysdesc_ok
$0x10, %cx
# we keep only first 16 bytes
469 sysdesc_ok:
455-472:ds:si指向这个系统描述符表,然后拷贝16字节到0xa0。
473 no_mca:
474 # Check for PS/2 pointing device
# aka SETUPSEG
$DELTA_INITSEG, %ax
# aka INITSEG
$0, (0x1ff)
# default is no pointing device
# int 0x11: equipment list
$0x04, %al
# check if mouse installed
no_psmouse
$0xAA, (0x1ff)
# device present
478:默认情况下我们假设没有点击设备。
479:获取设备信息,返回的设备信息保存在ax中:
|F|E|D|C|B|A|9|8|7|6|5|4|3|2|1|0|
| | | | | | | | | | | | | | | `---- IPL diskette installed
| | | | | | | | | | | | | | `----- math coprocessor
| | | | | | | | | | | | `-------- old PC system board RAM & 256K
| | | | | | | | | | | | | `----- pointing device installed (PS/2)
| | | | | | | | | | | | `------ not used on PS/2
| | | | | | | | | | `--------- initial video mode
| | | | | | | | `------------ # of diskette drives, less 1
| | | | | | | `------------- 0 if DMA installed
| | | | `------------------ number of serial ports
| | | `------------------- game adapter installed
| | `-------------------- unused, internal modem (PS/2)
`----------------------- number of printer ports
480:测试bit2,看看是否安装了安装了PS/2鼠标。
483:安装了PS/2鼠标就写入0xAA;否则就是0。这里注意的是0xfe,0xff 本该是bootsect的末端标记,0xfe是0x55,0xff是0xAA。由于bootsect的工作已经完成,所以他的数据区可以随便使用(在上面已经多次看到了)。这里用0xAA表示安装了PS/2鼠标只不过是对0x55aa这个标记的怀念而已(lol)。
484 no_psmouse:
486 #if defined(CONFIG_APM) || defined(CONFIG_APM_MODULE)
487 # Then check for an APM BIOS...
# %ds points to the bootsector
# version = 0 means no APM BIOS
$0x05300, %ax
# APM BIOS installation check
done_apm_bios
# Nope, no APM BIOS
$0x0504d, %bx
# Check for "PM" signature
done_apm_bios
# No signature, no APM BIOS
$0x02, %cx
# Is 32 bit supported?
done_apm_bios
# No 32-bit, no (good) APM BIOS
$0x05304, %ax
# Disconnect first just in case
# ignore return code
$0x05303, %ax
# 32 bit connect
%ebx, %ebx
# paranoia :-)
%esi, %esi
no_32_apm_bios
# Ack, error.
# BIOS code segment
%ebx, (68)
# BIOS entry point offset
# BIOS 16 bit code segment
# BIOS data segment
%esi, (78)
# BIOS code segment lengths
# BIOS data segment length
541 no_32_apm_bios:
$0xfffd, (76)
# remove 32 bit support bit
543 done_apm_bios:
544 #endif
486:如果内核配置为支持APM高级电源管理,那么检查APM BIOS。
489:清除在0x的APM BIOS标志,这是预设值。
490-493:检查APM装置,bx是电源设备ID。不支持的话就跳走。
495-496:检查PM签名:bh=P,bl=M,不是的话跳走。
498-499:检查APM信息,保存在cx中:
Bit 0 1 = 16 bit Prot Mode supported
Bit 1 1 = 32 Bit Prot Mode supported
Bit 2 1 = CPU IDLE slows down CPU speed.
Requires APM CPU Busy service
Bit 3 1 = BIOS Power Management is disabled
Bit 4 1 = APM disengaged
因为我们的linux最终在32位保护模式下运行,所以检查bit1,如果APM不支持32位保护模式的话就跳走。
501-503:关闭与32位保护模式APM接口的连接。
504-518:开启与32位保护模式APM接口的连接。ax是APM 32位模式代码段地址(以实模式段基地址表示),ebx是APM 32位模式代码段内的偏移值,cx是APM 16位模式代码段地址(实模式段地址),dx是APM数据段地址(实模式段地址),esi是APM BIOS代码段长度,di是APM BIOS数据段长度。如果不支持32位保护模式APM,就清除APM信息中的bit1。
$0x05300, %ax
# APM BIOS installation check
# paranoia
apm_disconnect
# error -& shouldn't happen
$0x0504d, %bx
# check for "PM" signature
apm_disconnect
# no sig -& shouldn't happen
# record the APM BIOS version
# and flags
done_apm_bios
521-524:由于成功完成了刚才的操作,证明了我们的APM BIOS正常,所以我们需要把APM BIOS版本号和APM信息也保存起来,所以只能重新检查APM BIOS。
525,528:按理说不会失败,因为刚才的连接操作已经成功了。唯一可能的失败原因就是我们失去了与32位保护模式APM接口的连接,我们跳到apm_disconnect。
534 apm_disconnect:
$0x05304, %ax
# Disconnect
# ignore return code
541 no_32_apm_bios:
$0xfffd, (76)
# remove 32 bit support bit
543 done_apm_bios:
544 #endif
535-539:关闭与32位保护模式APM接口的连接。
530-532:记录APM BIOS版本号和APM信息,这样我们就完成了所有的APM操作。
$0, %cs:realmode_swtch
rmodeswtch_normal
%cs:realmode_swtch
rmodeswtch_end
554 rmodeswtch_normal:
default_switch
相关数据:
88 realmode_swtch: .word
# default_switch, SETUPSEG
547-548:realmode_swtch是用于loadlin这种运行在dos下的“钩子”函数,我们不用太在意。
829 default_switch:
# no interrupts allowed !
$0x80, %al
# disable NMI for bootup
# sequence
%al, $0x70
830:现在是从实模式过渡到保护模式的关键时期,所以要关闭中断。
831-833:关闭不可屏蔽中断NMI。0x70是I/O空间的段口地址。
834:注意555,我们执行完lret,先从堆栈中弹出ip,然后弹出cs。
558 rmodeswtch_end:
559 # we get the code32 start address and modify the below 'jmpi'
560 # (loader may have changed it)
%cs:code32_start, %eax
%eax, %cs:code32
相关数据:
121 code32_start:
# here loaders can put a different
# start address for 32-bit code.
123 #ifndef __BIG_KERNEL__
0x1000 = default for zImage
# 0x100000 = default for big kernel
127 #endif
813 code32: .long
# will be set to 0x100000
# for big kernels
561-562:也许你会感到奇怪,为什么在这里使用32位指令.事实上在实模式下的每条指令前使用超越前缀0x66,0x67就可以使用32位指令了(还依赖其他的条件,具体参见intel的官方文档).当然我们无需手动添加超越前缀,汇编器会自动帮我们添加的.使用大内核的情况时把0x100000写到code32;小内核的话就写0x1000,表示内核代码的起始地址.setup最终会跳到这个位置执行内核代码.
$LOADED_HIGH, %cs:loadflags
# .. then we have a normal low
# loaded zImage
# .. or else we have a high
# loaded bzImage
# ... and we skip moving
566-567:小内核执行do_move0,大内核忽略本次搬运,跳转。
我们先跟踪小内核:
573 do_move0:
$0x100, %ax
# start of destination segment
# aka SETUPSEG
$DELTA_INITSEG, %bp
# aka INITSEG
%cs:start_sys_seg, %bx
# start of source segment
579 do_move:
# destination segment
# instead of add ax,#0x100
# source segment
$0x100, %bx
$0x800, %cx
# assume start_sys_seg & 0x200,
# so we will perhaps read one
# page more than needed, but
# never overwrite INITSEG
# because destination is a
# minimum one page below source
573-588:把从start_sys_seg开始的4K内核数据复制到0x1000处.ax=0x200,bp=0x9000,bx=start_sys_seg+0x100.start_sys_seg是个段地址,加上刚刚搬运过的4K内核(0x100&&4),bx指向了下一次要搬运的内核数据的起始段地址.注意:由于lilo也使用setup,所以bootsect和lilo对start_sys_seg的解释不一样:前者的值是0x1000,后者是0x1000+setup余部.
589-595:直到bx大于等于0x9000为止,否则不停的持续这个4K内核复制的循环.注意:对于lilo,由于start_sys_seg不是从0x1000开始,所以
?????????????
597 end_move:
598 # then we load the segment descriptors
# aka SETUPSEG
602 # Check whether we need to be downward compatible with version &=201
$0, cmd_line_ptr
end_move_self
# loader uses version &=202 features
$0x20, type_of_loader
end_move_self
# bootsect loader, we know of it
145 cmd_line_ptr:
599-600:恢复ds=0x9020.
602-606:linux的引导是相当复杂的,从最初的引导协议到如今的,共经历了5代:初代,2.00,2.01,2.02,2.03.其中2.03是从2.4.18-pre1开始的.每一个新版本都要在一定程度上兼容旧版本.这里的cmd_line_ptr是从2.02协议开始有的,顾名思义它就是命令行指针.我们的bootsect不支持命令行,因此这个指针为空.填0;对于lilo,grub,这个指针指向了setup末尾到0xa0000之间的区域.我们的type_of_loader是0x20,所以我们跳到end_move_self.而对于命令行指针为空且不是bootsect的情况,说明使用的引导协议低于2.02.我们就不讨论这种情况了.
652 end_move_self:
# now we are at the right place
655 # Enable A20.
This is at the very best an annoying procedure.
656 # A20 code ported from SYSLINUX 1.52-1.63 by H. Peter Anvin.
657 # AMD Elan bug fix by Robert Schwebel.
660 #if defined(CONFIG_MELAN)
movb $0x02, %al
# alternate A20 gate
outb %al, $0x92
# this works on SC410/SC520
663 a20_elan_wait:
call a20_test
jz a20_elan_wait
jmp a20_done
667 #endif
660-667:这是针对AMD Elan CPUs的,我们不理它.
670 A20_TEST_LOOPS
# Iterations per wait
671 A20_ENABLE_LOOPS
# Total loops to try
674 a20_try_loop:
# First, see if we are on a system with no A20 gate.
677 a20_none:
这里涉及了一个A20地址线的问题.由于在实模式下的地址计算方法是:16位段基地址*16+16位段偏移值,写作:base:offset.因此,用这种方法可以表示的最大内存单元是:0xffff0+0xffff=0x10ffef=1MB+64KB-16Bytes.由于只有20条地址线,如果访问0xx10ffef之间的内存,系统不会出错,因为它会忽略掉第21位,上面的内存范围会变成0xffef.但是到了80286,地址线扩充到24根,可以访问2^24=16MB的内存.但是80286的设计目标之一就是在实模式下和兼容,而且这之后的cpu也一直遵从这个目标.但是,这给80286带来了一个严重的bug:如果我们访问0xx10ffef之间的内存,系统将实际访问这块内存,而不是象过去一样重新从0x00000开始.
为了解决这个问题,IBM使用键盘控制器上剩余的一些输出线来管理第21根地址线,称为A20 gate.如果A20 gate被打开,则当访问0xx10ffef之间的地址时,系统将真正访问这块内存区域;如果A20 gate被禁止,则访问0xx10ffef之间的地址时,系统仍然使用的处理方式.绝大多数IBM PC兼容机默认情况下A20 gate是被禁止的.由于在当时没有更好的方法来解决这个问题,所以IBM使用了键盘控制器来操作A20 gate,但这只是一种黑客行为,毕竟A20 gate和键盘操作没有任何关系.在许多新型PC上存在着一种通过芯片来直接控制A20 gate的BIOS功能.从性能上,这种方法比通过键盘控制器来控制A20 gate要稍微高一点.而在80286及更高的cpu中,即使A20 gate被打开,在实模式下所能够访问的内存最大也只能为0x10ffef,尽管它们的地址总线所能够访问的能力都大大超过这个限制.为了能够访问0x10ffef以上的内存,则必须进入保护模式.
有了上述的基础知识,我们再来看代码:
925 A20_TEST_ADDR = 4*0x80
927 a20_test:
# Low memory
# High memory area
$A20_TEST_LOOPS, %cx
%fs:(A20_TEST_ADDR), %ax
937 a20_test_wait:
%ax, %fs:(A20_TEST_ADDR)
# Serialize and make delay constant
928-936:这段代码测试A20是否开启.我们在之前已经提到,如果A20 gate被打开了,则可以直接访问0xx10ffef之间的内存;如果A20 gate被禁止,则访问FFEFH之间的内存会被硬件自动转换为0xffef之间的内存.所以我们可以利用这个差异来检测A20 gate是否被打开.把0x0的内容保存到堆栈里.0x0是中断向量0x80的中断服务的地址,我们这么做没有什么特别的意思,只是为了在0xffef之间找一个内存单元作测试,何况之后我们还会恢复过来.fs=0,gs=0xffff.
938-940:测试值加1,再调用delay:
1003 delay:
????????????????
:BIOS的POST代码一般使用0x80 io端口显示调试信息.所以在POST之后就不会再用到这个端口了,所以这样很安全.因为cpu的预取功能和cache等因素可能会造成影响.所以我们故意不直接执行out指令,而是执行call-ret故意延长时间.
%gs:(A20_TEST_ADDR+0x10), %ax
a20_test_wait
%fs:(A20_TEST_ADDR)
941:左面的操作数的有效地址是:0xffff:0x200+0x10.如果开启了A20 gate,就是0x100200;没有开启的话,就是0x200.ax的值是(0x200).由于(0x200)在938-939中改变了,所以没开启A20 gate的话就执行942,反复测试,最多测试A20_TEST_LOOPS次(32).如果最后依旧没有成功,执行944;开启的话就直接执行944.注意:有可能A20 gate已经开启,但恰好(0x100200)==(0x200).这种情况只会在第一次测试才会出现.因为938-939在每次测试前都会改变一次(0x200).
944-947:恢复相关寄存器和中断向量0x80的中断服务程序地址.
返回到679:
679:如果开启了A20 gate,941会使ZF置0,跳到a20_反之ZF置1,不做跳转.
我们先来看没有开启A20 gate的情况:
682 a20_bios:
$0x2401, %ax
# Be paranoid about flags
683-685:使用int 15h,ah=2401h,开启A20 gate.由于这个BIOS调用不是每个BIOS都支持,所以根据返回值(ah,CF)来判断是否开启了A20 gate并不可靠.毕竟能否开启A20 gate对我们能否进入保护模式是十分关键的.所以我们不作任何测试,恢复flags,直接调用a20_test来测试.如果开启了则在689跳到a20_否则就持续这个过程.注意:整个测试过程就是不断递增(0x200),即便真的不支持A20 gate,由于(0x100200)是不变的,总有一天(0x100200)==(0x200),就是说,即使无法开启A20 gate,最终也会返回到a20_done.?????????????
751 a20_done:
753 # set up gdt and idt
# load idt with 0,0
%eax, %eax
# Compute gdt_base
# (Convert %ds:gdt to a linear ptr)
$gdt, %eax
%eax, (gdt_48+2)
# load gdt with whatever is
# appropriate
相关数据:
1023 idt_48:
# idt limit = 0
# idt base = 0L
1026 gdt_48:
# gdt limit=2048,
256 GDT entries
# gdt base (filled in later)
0, 0, 0, 0
0, 0, 0, 0
# 4Gb - (0xx1000 = 4Gb)
# base address = 0
# code read/exec
# granularity =
(+5th nibble of limit)
# 4Gb - (0xx1000 = 4Gb)
# base address = 0
# data read/write
# granularity =
(+5th nibble of limit)
:idt_48和gdt_48最终要加载到GDTR和LDTR寄存器,48表示这两个寄存器的长度:32位基地址和16位限长.
754:加载中断描述符表寄存器。基地址为0,限界为0。因为实模式的中断机制太过薄弱,所以为了顺利进入保护模式我们必须建立新的中断描述符表。以后大家还会见到内核进行低级初始化时还要建立一个新的中断描述符表,现在建立的只不过是临时的。而且实际上这个临时idt根本就没有产生任何作用.
755-757:这时eax=0x。
758-760:eax=0x+$gdt,这时eax保存了这个临时gdt的物理地址.然后把它保存在gdt_48+2,这个位置表示段描述符的基地址.这样,GDTR寄存器中的基地址指向这里的gdt,限长为32KB.由于一个段描述符占8字节,再加上?????????????????
:可以看出.这个临时的gdt有两项:代码段描述符和数据段描述符.基地址都是0,限界是fffffh,G=1,覆盖了整个4G地址空间.有关描述符的更多说明请查阅相关书籍.
%al, $0xf0
%al, $0xf1
764-769:复位协处理器。
765:向I/O端口0xf0写0,清除数学协处理器的busy信号。
768:向I/O端口0xf1写0,复位数学协处理器。
$0xFF, %al
# mask all interrupts for now
%al, $0xA1
$0xFB, %al
# mask all irq's but irq2 which
%al, $0x21
# is cascaded
773-775:屏蔽掉所有的从8259A中断。
777-778:屏蔽掉所有的主8259A中断,除了IRQ2,因为主8259A的IR2引脚连接了从8259A。
到了这里,setup所有的检测工作都完成了.从现在开始就不在使用BIOS了,毕竟它太烦了^^现在才该说我们真的要进入保护模式了.
# protected mode (PE) bit
# This is it!
flush_instr
793-794:CR0控制寄存器的最低位是PE位,置1时,cpu切换到保护模式。我们在前面也看到了,并不是简单的一上来置1就完了,为了执行这个指令我们已经付出相当艰辛的努力了.
795:执行这条jmp的意义远不只其指令意义,更重要的是该指令冲掉了cpu内部的指令队列(因为cpu为了加快取指速度而采用了预取方式,当然这不是唯一的优化手段)。后面开启分页机制时也是采用类似的方法冲掉cpu内部的指令队列的。执行795之后,cpu看见的都是逻辑地址,由mmu透明的转换为线性地址,而所有的物理地址都是直接映射到线性地址空间的,这个线性地址空间的大小是4G。还有一点,由于切换到了保护模式,要把段选择子加载到段寄存器,但是作者希望切换到保护模式尽可能简单,而把剩下的工作交给32位的汇编代码来作(后面将看到),所以我们的flush_instr离jmp很近,这样仅仅在ip上加一个很小的偏移值就可以继续执行了,但是我们无法执行任何寻址指令,只能执行一些简单的寄存器指令。
797 flush_instr:
# Flag to indicate a boot
%esi, %esi}

我要回帖

更多关于 启动战网时遇到问题 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信