您的位置:首页 > 移动开发 > IOS开发

关于BIOS加载BOOT.S的经典解答

2014-08-24 10:27 183 查看
摘录论坛问答(一)

问:我需要一个什么样的环境才能编译 0.11的内核 ,比如应安装什么版本的linux ,什么样的硬件比较好。需要在安装LINUX的机器上再安装什么软件包和编译工具。

答:

一般带有编译环境的Linux机器都可以编译0.11内核。但必须对0.11内核代码进行一些调整。因为编译器已经有了变化。变化大的部分主要是汇编程序部分。

  要调整的地方主要有:

a. 内核汇编代码的字节对齐操作语句;

b. 嵌入式汇编语句的格式;

c. 汇编程序中的注释格式也要调整;

-----------------

问:linux系统是如何装到硬盘上去的?,

答:

Linux最初刚能“动”时是使用MINIX系统的引导程序使系统从硬盘引导的。不久就有了Linux自己的硬盘引导程序LILO(Linux Loader).

  使用的步骤是首先在硬盘上建立Linux的分区,并在其上建立minix文件系统(<64M)。在使用bootimage软盘能正常引导并进入硬盘的系统根文件系统后,即可进行一些常用的操作。此时即可安装硬盘引导启动程序Lilo。

  有关硬盘引导启动方面的原始资料可以从下面得到,其中几乎包括了所有着方面的早期资料。

http://oldlinux.org/Linux.old/boot/

  新的资料可以从www.tldp.org 或下面得到。

http://plinux.org/LDP

  目前Linux上常用的硬盘引导程序是grub或Lilo。

-----------

问:

关于80386保护模式的问题想请教一下。

1、在80386中使用颗粒度来确定段限长度域的含义,这个颗粒度是在什么位置定义的(我是指哪个寄存器等?)

2、段描述符中有一个特权级标志(DPL),共分4级,在Windows中对用户只开放了0和3级权限,在Linux中是否也是如此,就操作系统本身来说,是否仍然用到了这4级,对用户开放了几级呢。

答:

1. 是在段描述符表中设置的。每个描述符为8个字节。而描述符表的基地址在gdt或ldt描述符表寄存器中,是用lgdt指令加载 的。 加载的段寄存器值实际上是描述符表中对应描述符的偏移值。而实际的描述符信息在该段寄存器的不可见部分。因此颗粒度DPL也包括在该不可见部分中。在 boot/setup.s的208和213行以及boot/head.s中的234行开始都是一些描述符的实际值。还可参见boot/setup.s第 128行的说明,或见附录。

2. Linux操作系统也只用到了两级:0和3级。这在boot/setup.s的193行有说明。

-----------------

问:bootsec.s中:

54 rep ! 重复执行,直到cx = 0

55 movw ! 移动1 个字;

请问movw是把什么移动一个字,以前没有见到过这种用法。

答:movw源是ds:si,目标是es:di

又问:是将ds:si的一个字节移到ds:di中去,用rep重复256次

对吗?

又答:是每次移动一个字。

--------------

问:判断64k边界

在p.23, 第153行代码: "test ax, #0x0fff"

该行代码是判断ax的值是否位于内存地址64KB边界处,但我觉得应该写成:

"text ax, #0xffff"。

答:

如果物理地址是0x12345的话,那么用Intel的方式表示即是:0x1234:0x0005。也即:

段地址:  0x1234

段内偏移:+ 0x0005

----------------

物理地址: 0x12345

ax中的值是由上一句中从es段寄存器中获得。因此所表示的实际地址范围是0x0fff0加上0x0000f的偏移范围值,就是64K。

--------------

问:

请教,列表3.1中的(110行)高速缓存是拿来放啥东东?

答:

当应用程序需要外设(例如硬盘、软盘等)中的数据时,内核首先将数据传输到空闲的缓冲中,然后通知应用程序进程来取。只要缓冲区中有空闲空间,并且应用程 序没有明确要求清除这些数据,或数据不脏(即没有改动过),则这些数据块就一直存在于缓冲区中。主要可以取到数据共享的作用。

--------

问:关于64kb边界

bootsect.s中用read_it子程序测试是否位于64的边界处,会执行如下程序:

mov ax,es

test ax,0x0fff

die: jne die

xor bx,bx

在这里进入一个死循环有什么用处?如果进入了这个死循环,系统就无法引导,必须重启?是不是这样的意思?

答:如果不在64k boundary处就死机。。。

-----------

问:.align x(内存x字节对齐)是傻意思?

内存地址是按照字节为单位计数和编址的。如果程序要求处于双字节(word)的地址处,则就是要求“内存2字节对齐”,同理,若要求处于8字节开始的位置,则是“内存8字节对齐”。

答:以前的as86中.align x中的x的取值是对齐字节数的幂次,比如4字节对齐,则x=2。现在则直接取对齐的字节数值。

问:这样对齐有何意义,还有,它是使得程序中的那部分对齐?象列表4.4中就有好几处这样的用法!!

答:对齐的意义在于提高访问内存的效率。

现在的32位机器,一次可以处理32位,也就是4个字节,实际访问内存,它会4的整数倍的内存地址,一个可以读出4个字节,也就是0、1、2、3,就可以一次处理了,而4、5、6、7则要到下一次。

如果你不对齐的话,可能出现这样的情况,一个字或是双字恰号跨越了两次访问的内容,比如说它在1、2、3、4,那么要取它的话,必然要访问两次内存,你说浪费不浪费。:)

问:它是使得程序的那部分对齐,是.align 下面的部分到下一个.aling之间的代码?

答:对。.align x开始处的代码到下一段。

------------------

问:第二章bootsect.s初始位置的问题

第二章提到:

它将可启动设备的第一个扇区(磁盘引导扇区,512 字节)读入内存地址0x7C00 处,并跳转到这个地方。

这个说法,我觉得不妥,通过bootsect.s的代码,我们可以看出0x7C00是用来表示一个段地址,也就是说,应该是读入内存的0x7C00:0x0000处。

而且在下面还有:

Linux 的最最前面部分是用8086 汇编语言 编写的(boot/bootsect.s),它将由BIOS读入到内存0x7C00处,当它被执行时就会把自己移到绝对地址0x90000 处,……

这里移动后的地址已经写成了绝对地址,为什么前面的不用绝对地址(0x7C000)呢?或者干脆二者都用seg

ff表示方式。

答:移动到段地址0x7c0处,也即0x07c0:0000处,也即绝对地址0x7c00处。

看bootsect.s第35行。

问:想再问一下,为什么会选中0x90000作为搬移代码的目标?

最好能解释一下,为什么初始代码会加载0x7C00。我知道这是BIOS完成的工作,所以我想知道,为什么会有这种设计。

答:

PC刚出现时,内存很小,不大于1M。同时,PC有很多内容需在内存中有确定的位置,如中断向量表、显存、ROM等。

人们最自然的想法不是放到内存最高处,就是放到最低处,IBM选择了最低处放中断向量表0~0x400,而高于0XA0000放显存,(好象0x400往 上还有一部内存放系统信息、键盘缓存区什么的,说错了别见怪),这就注定了系统的初始引导程序必定被加载到内存一个中间地址开始的地方,至于为什么是 0x7c00而不是0x8c00,我猜测:0x?c00正好是一个整K地址的始位置,0x7c00=31K,位于前32K内存的最后1K,(实在再不能靠 后了,要不然应用程序快没内存了,要知道哪个时代512K内存就是一种奢侈),至于30K前面空出的内存,除用过的之外,就是IBM为将扩展功能予留的。

(您不会再问为什么不是30K处或29K处了吧?)。

至于0x90000这个位置,你可以算一下,PC的基本内存是640K,boot及中断向量等内存已加起来已用了前32K,Linus预设Linux的核 心最大不超过512K,所以需要在内存中有连续8个64K的内存,所以Linus没有使用32K至64K之间的内存,而是使用了64K至576K,从 576K即0x90000开始到0x90200放boot,0x90200~0x90600放setup.s,0x90600到0xA0000作堆栈用。

假如让你来设计操作系统,你仍可以使Linux的核心被允许作得再大32K,变成:从0x8000~0x98000,但你基本上用完了所有的内存。

通过上面的计算,只要从设计者的角度出发想一想,本着即要尽可能节约内存,又要考虑到系统的扩展,就能理解这样设计的“合理性”。

当然,其后硬件技术的飞迅发展,不是IBM当时所预料得到的,以现在我们的观点来回头看,真是有点怪模怪样,直到今天仍没有被改变的原因,主要是出于向下兼容,后来的事实证明,保持良好的兼容,是INTER得发展、壮大最关键的法宝。

在Linux内核情景分析中,看看i386段描述子的设计,更是怪得离奇,但同样为了兼容,INTER真是用尽的心智,无可奈何才做成那个样子。

这也就能理解INTER下一代的64位处理器为什么不再采用全兼容的办法了,而AMD仍采用全兼容的路子了。

INTER已经有了全世界范围内的广泛信徒,它要利用这种优势甩掉保持兼容带来的性能及硬件设计等包袱。

AMD则相反,它无法与INTER在同一条船上硬拼,只好象INTER当年一样,用“兼容”这相法宝与INTER对抗。

至于0x90000这个位置,你可以算一下,PC的基本内存是640K,boot及中断向量等内存已加起来已用了前32K,Linus预设Linux的核 心最大不超过512K,所以需要在内存中有连续8个64K的内存,所以Linus没有使用32K至64K之间的内存,而是使用了64K至576K,从 576K即0x10000~0x90000放核心,0x90000开始到0x90200放boot,0x90200~0x90600放 setup.s,0x90600到0xA0000作堆栈用。

又问:看看我理解得对不对。

系统在实模式下启动后,我们能够使用的只有640K内存,也就是0xA000之下的内存,是我们的应用这个时候可以使用的,比较一下,我们启动部分的代码最大就只能有640-576=64K,不过,现在一般用不到这么大。

实际上,我们还可以继续把这部分代码向上移,以充分利用内存,同时可以给内核更大的空间。

又答:你说得没错,实际上从Linux内核代码boot.s看(实际上boot.s算不得内核,只不过是一个引导过程),你要真的把32K ~64K用起赤,也不会出什么问题(当然,一定要修改setup.s中的相关地址)。

至“浪费”这个词倒是用不着,因为对Linux来说,即然现在没有使用这段内存,那么必然是因为现在内核加载时(保护模式建立前),内存并没有你想象中紧张,不需要这样安排得“滴水不漏”。

另外,head.s及内核仅仅是加载时使用0x10000 ~0x90000,加载完成后就会移到0x00000~ 0x80000处(实际上真正的内核绝对不会用完这块内存,因为内核要是大于512K的话,就有可能把setup.s自己都覆盖掉,系统也起不来了。

又答:在0.11版,内核不大于512K也是Linus的一个假设,对较新版本在解决基本内存不够的问题采用了压缩内核的办法。具体细节我也不清楚,问版主吧。

你的尽可能用尽当前基本的内存的想法没错,但对新版内核如果超过512K,那么就不仅是超几十K的问题了,杯水车薪,无济于事,只能从根本上寻求解决的办法。

--------

问: 假设内核将来大于压缩后也不能放在基本内存中时又该如何呢?办法还是有的,我没有做过保护模式下的编程,思来想去,也简单,先读入一部分,待进入保护模式 后,再读入更大的一部分,这样就是几十M的内核也能读进来。(我真的不知道现在新版本是如何实现的,只是在猜,版主在此更有权威)。

答1:“一小块读一大块程序,一大块程序再读更大的程序”,这本来就是系统最基本的引导思路,因此,只要你有足够大的内存,足够高的硬件配置,就是内核再大,也一定有办法让它运行起来。

答2:新内核将自己放到1M以上内存中去了。

  你说的不错,先被读入到内存低端的是head的解压程序代码 块部分,然后其对内核的压缩部分进行解压,放在了从1M开始的地方。

  若使用了专门的引导加载程序(比如LILO),则情况稍微有些变化,此时会由Lilo来寻找盘上内核文件的位置,并加载它。内核SYSTEM则分为首部的未压缩部分和压缩部分。解压则是靠SYSTEM的未压缩部分来进行。。。。

----------------

补充参考资料:

尽管我们总是以一个数字来说明一台PC机的物理内存数量(比如,我的PC机的内存是256M),但事实上,PC机的物理内存被分为多个不同的区域,尽管几乎每一个PC的用户都不希望是这样。这样的设计是IBM PC机早期版本的系统资源限制所造成的后遗症。

与其去责备这种内存组织方法,不如去看一看造成这种结果的历史环境。在1981年,当IBM PC机被初次发布的时候,1MB是个很大数量的内存。在那个时候最流行的家庭计算机是Apple,但它的内存最大只能访问到64KB,那个时候,一台微型 计算机的内存通常只有8KB,并且内存非常昂贵,所以在当时,IBM PC机的设计者认为1M内存已经是海量内存了。

后来,由于计算机工业的迅速发展,1M内存已经显得捉襟见肘,而后续系统又必须和早期系统兼容,所以IBM PC机的内存被分为四个基本区域(有一些区域被进一步细分):

常规内存(Conventional Memory):系统内存的第一个640 KB就是著名的常规内存。每一个PC机用户随着时间的推移都更加清楚的知道它,(也更加恨它

),它是标准DOS程序、DOS驱动程序、常驻内存程序等可用的区域,它们统统都被放置在00000h~9FFFFh之间。

上位内存区(Upper Memory Area):系统内存的第一个1M内存顶端的384 KB(1024 KB - 640 KB)就是UMA,它紧随在常规内存之后。也就是说,第一个1M内存被分成640KB常规内存和384KB的UMA。这个区域是系统保留区域,用户程序不 能使用它。它一部分被系统设备(CGA、VGA等)使用,另外一部分被用做ROM shadowing和Drivers。UMA使用内存区域A0000h~FFFFFh。

高端内存区(High Memory Area):系统内存第2个1M内存的第一个64 KB区域,被称做HMA。从技术上讲,它属于扩展内存的第一个64 KB,但它和其他扩展内存区域所不同的是,它可以在real mode下被直接访问,其它的则不然。所以在DOS时代,后期的DOS版本允许用户通过配置将DOS本身放置在HMA,从而让用户可以有更多的常规内存可 以使用。HMA占据地址100000h~10FFEFh。

扩展内存(Extended Memory):从HMA结束的位置到系统物理内存的最大值之间的区域都属于扩展内存。当一个OS运行在Protected Mode时,它可以被访问,而在Real Mode下,则无法被访问(除非通过某些Hacker方法)。它的地址范围是10FFF0h~Last address of system momory(maximum of 4G-1M)。从技术上说,HMA也属于扩展内存,这依赖于你如何看待这个问题。

1. Conventional Memory

系统的第一个640 KB内存被称做Conventional Memory(常规内存)。之所以被这么叫,是由于在8086时代,这是仅有的可以被OS以及Application使用的RAM。而到了80286以及 后续Intel 80x86时代,其它的RAM都只能在Protected Mode下被访问,而这块RAM却能够在Real Mode下被访问 (HMA是个例外,但那不是Intel设计的本意,事实上那是一个BUG)。所以,对于今天的PC来说,尽管有更多的RAM可供使用,但这640 KB常规内存在许多情况下仍然是最重要的,因为使用它们不需要任何的软件支持,也不需要进入Protected
Mode受其复杂的机制的影响。常规内存占据物理地址00000h~9FFFFh。

为什么常规内存有640 KB的限制?原因是IBM所做的病态的决定——IBM把那些为系统功能保留的内存空间(UMA)放在常规内存的顶端,而不是将它们放在底部。在过去的20 多年里,后续的PC为了保持向后兼容,一直在沿用这种模式,后来新加入的RAM(扩展内存)都从1 MB的位置开始,而与这640 KB RAM被UMA所分割。如果最初UMA被放置在常规内存的底部,那么后来扩展的内存就可以和常规内存连为一体,那么使用和管理这些RAM就会被简化很多。

但这640 KB也不是完全归用户程序或OS使用的,比如,起始位置的1 KB被用做BIOS中断向量表,随后的1 KB被用做BIOS数据区,在顶端的位置还有BIOS扩展数据区。这些空间在Real Mode下必须被保留。而当PC启动阶段,CPU默认处于Real Mode下,所以OS在Boot阶段如何合理的使用常规内存需要进行良好的规划。

一旦OS进入Protected Mode,常规内存就变的远没有在Real Mode下那么重要了,这个时候更大容量的扩展内存可以被使用(如果有的话),并且扩展内存还都是连续的。

2. Upper Memory Area

PC机组织内存的方式有些让人感到困惑——欢迎来到最让人困惑的部分

。在640 KB常规内存之上的384 KB部分被称为Upper Memory Area或者UMA,这是一个非常繁忙的地带。它非常重要,因为它是许多系统配置问题的根源。

UMA让人感到如此困惑的一个原因是它实际上是2个互相交叠的内存。这里存在一段占据物理地址A0000h~FFFFFh的RAM;然而,也有一段不同类 型的ROM被影射到这段地址的大部分区间。其中,和ROM有相同地址的部分的RAM被系统隐藏了,它们被BIOS用做ROM shadowing。没有被隐藏的部分仍然可以被OS和Application使用。

为什么会造成这种情况?部分原因是为了实用:如果一台PC机有640 KB的常规内存,以及384 KB的UMA,那么你插在主板上的物理内存是连续的——没有一种实用的方法构造一个SIMM(Single In-line Memory Module,见本节末尾注解)为UMA留出一个384 KB的物理块(当然,从技术上可以做到,但成本比浪费这384 KB RAM的代价要高的多)。其它的主要原因是:RAM给你提供一种提高性能的选项,比如当你使用UMA存放drivers,或者UMA被用做ROM shadowing的时候。

RAM覆盖了整个384 KB区域,而ROM只覆盖了一部分。所以你可以这样考虑UMA中的RAM和ROM:RAM是放在桌子上的一条彩色的纸带,而ROM是一条同样尺寸的白色纸 带,白色纸带被剪了一些洞,然后覆盖在彩带上面。那些有洞的部分露出的彩色部分就是可以供OS和应用程序使用的UMA RAM,剩余的部分就是ROM部分,OS和应用程序不能使用它们。

Address

First 16K

(x0000h-x3FFFh)

Second 16K

(x4000h-x7FFFh)

Third 16K

(x8000h-xBFFFh)

Fourth 16K

(xC000h-xFFFFh)

A0000- AFFFFh

VGA Graphics Mode Video RAM

B0000- BFFFFh

VGA Monochrome Text Mode Video RAM

VGA Color Text Mode Video RAM

C0000- CFFFFh

VGA Video BIOS ROM

IDE Hard Disk BIOS ROM

Optional Adapter ROM BIOS or RAM UMBs

D0000- DFFFFh

Optional Adapter ROM BIOS or RAM UMBs

E0000h- EFFFFh

System BIOS Plug and Play Extended Information

F0000- FFFFFh

System BIOS ROM

UMA的布局

下面是对上面表格中内容的概要介绍:

VGA Graphics Mode Video RAM: 内存A0000h~AFFFFh的64 KB块被系统保留作为VGA图形显示内存。最初的VGA是320x200,256色模式,这种模式需要64,000 bytes。当然现代显卡使用更多的内存。这个RAM区域被系统保留,应用程序被禁止使用它。

VGA Monochrome Text Mode Video RAM: 当显卡处于单色文本模式时,会使用这32 KB RAM。然而,在现代系统中,显卡已经几乎不使用黑白文本模式;即使当需要显示黑白文本的时候,也只会使用彩色文本模式黑白色彩来显示黑白文本而已。在某 些环境下,这块内存可以被用做驱动程序,来节省常规内存的空间。

VGA Color Text Mode Video RAM: 当显卡处于彩色文本模式时,会使用这32 KB RAM。这个RAM区域被系统保留,应用程序被禁止使用它。

VGA Video BIOS ROM: C0000~C7FFFh是Video BIOS ROM的内存位置。Video BIOS ROM中包含着被Video Card使用的代码。注意这块ROM里存放的是BIOS INT 10H所使用的服务代码,而不是被Video Card显示的数据,如果是显示数据,就应该被放置在RAM中。

IDE Hard Disk BIOS ROM: C8000h 是存放控制IDE Hard Disk的BIOS的默认位置。在系统被启动的阶段,这块BIOS代码将会被调用。

Optional Adapter ROM BIOS or RAM UMBs: CC000h ~ DFFFFh 之间的内存块可以被PC厂商随意使用。这块内存可以被Hardware Adapters用来存放ROM代码(例如,一些网卡,或者SCSI host adapters)。 也可以被用做进行Loading Drivers的上位内存块RAM。这块内存通常是空的,除非你使用了那些刚才被提及的特殊硬件。

System BIOS Plug and Play Extended Information: E0000h ~ EFFFFh 之间的内存被Plug and Play BIOSes用来存放它的Extended System Configuration Data (ESCD)。

System BIOS ROM: UMA的最后64K区域被用来存放system BIOS代码。

以下表格中列出了除Video RAM之外的UMA内存布局:

Address Width Content Description

C000:0 - EGA/VGA BIOS ROM (thru C7FF)

C400:0 - Video adapter ROM space

C600:0 256bytes PGA communication area

C800:0 16K Hard disk adapter BIOS ROM

C800:5 - XT Hard disk ROM format, AH=Drive, AL=Interleave

D000:0 32K Cluster adapter BIOS ROM

D800:0 - PCjr conventional software cartridge address

E000:0 64K Expansion ROM space (hardwired on AT+)

- 128K PS/2 System ROM (thru F000)

F000:0 - System monitor ROM

- - PCjr: software cartridge override address

F400:0 - System expansion ROMs

F600:0 - IBM ROM BASIC (AT)

F800:0 - PCjr software cartridge override address

FC00:0 - BIOS ROM

FF00:0 - System ROM

FFA6:E - ROM graphics character table

FFFF:0 - ROM bootstrap code

FFFF:5 8 bytes ROM date (not applicable for all clones)

FFFF:E byte ROM machine id

在绝大多数PC中,UMA中包含一个384 KB的RAM内存。如果UMA区域中的任何地址被ROM使用,那么这个地址后面的RAM就被系统隐藏了。然而,这并不意味着这些被隐藏的RAM被浪费了。

对于这些被用做System ROM和Video ROM的ROM来说,它的一个问题就是它比RAM的访问速度要慢。ROM的访问时间一般为120到200纳秒,而RAM的访问时间一般为50到70纳秒; 更何况,系统RAM一次访问32-bit,而ROM是16-bit。结果就是,相对于访问系统内存,访问BIOS代码是非常慢的。

所以,我肯定你已经知道我要说什么了:既然在ROM的后面隐藏着RAM,我们可以将ROM中的内容影射到RAM中以提高性能。事实上,绝大多数系统也是这么做的,这被叫做ROM Shadowing。你可以通过一组BIOS参数来控制哪些ROM区域被影射到RAM中。

当一个内存区域的shadowing被通过配置BIOS参数打开的话,如果PC机被加电,则在机器启动的过程中,BIOS会将ROM中的内容拷贝到相应的 RAM中,然后将这段RAM设为写保护,然后禁止使用ROM。这个时候,这些RAM就看起来象ROM一样了。只不过性能要高于真正的ROM。

3. High Memory Area

高端内存区High Memory Area (缩写为HMA),是扩展内存(Extended Memory)起始位置的65,520 bytes(64 KB - 16 bytes)。从技术上说,它是地址为100000h~10FFEFh的内存。它很特别,因为它是扩展内存中唯一可以在Real Mode下可以被直接访问的一部分。在正常的情况下,CPU根本无法在Real Mode下直接访问扩展内存,如果要访问,则必须进入保护模式,或者使用特殊的驱动程序。

请不要把它和上位内存区(Upper Memory Area)混肴,它们是完全不同的区域。

由于早年奇怪的设计决策,或者因为当时的不寻常的环境背景,造成了今天PC机上存在许多怪异的硬件和软件设计。HMA或许是所有这些怪异的设计之中最奇怪 的设计之一。尽管前面我们一直在说HMA可以在Real Mode下可以被直接访问,但事实上,它能够被访问与否取决于一个被称做A20 Gate的家伙是否被打开。如果A20 Gate是被禁止的,HMA则无法被访问;只有A20 Gate是被打开的,HMA才可以被访问。(关于A20 Gate请参考1.4 A20 Gate)

HMA的一个用途是,由于DOS是运行在Real Mode下的,所以当后来640 KB常规内存开始使用紧张的时候,DOS就通过一个特殊的驱动程序HIMEM.SYS将自己放在HMA中,以腾出更多的常规内存供应用程序使用。 HIMEM.SYS所做的就是将A20 Gate打开。

由于A20 Gate的打开与否,决定的HMA是否可以在Real Mode下被访问,所以HMA的另外一个用途就是用来测试A20 Gate是否已经被打开。

其实,从我们前面的介绍中就已经明白,其实HMA仅仅在Real Mode下才有其特殊性,这也是其被单独拿出来被叫做HMA的意义,在Protected Mode下,HMA和其它的扩展内存没有什么性质上的不同,所以在Protected Mode下谈论HMA是没有意义的。

4. Extended Memory

1 MB以上的所有物理内存都被称做Extended Memory (扩展内存)。它们之所以被叫做扩展内存是因为这些内存是早期8086 1 MB内存限制的扩展。

除了最初的65,520 bytes,在Real Mode下,扩展内存是不能被直接访问的。如果要访问全部的扩展内存,必须进入Protected Mode。

80286提供了24-bit的地址线,在PM下可以访问15 MB的扩展内存,80386极其后续系列提供了32-bit的地址线,在PM下可以访问高达4 GB-1 MB的扩展内存。这为以后的OS设计以及应用程序提供了足够的空间。

-------------

问:系统原始的中断向量表到哪里去了?

我现在刚看完setup.s,有点纳梦。到这里,head.s及system被移到了0x00000~0x800000处,那么PC开机时0x0000~0x0400处及之后的一些参数不是也被覆盖了吗?

象除法错误、单步调试等等原来的中断,以后还怎么用?是不是Linux在这之后就完全不用PC自己的中断程序,而纯粹自己作中断程序呢?

答:没错。在head.s中的78行(setup_idt)开始,首先在232行的_idt处设置了256个亚中断向量,指向一个只显示"Unknown interrupt"的中断处理程序。然后会在init的main()中各个硬件的初始化函数中一个一个地分别设置所用到的实际中断向量。

--------------

问:System.map的一个问题

书中解释System.map用作存放内核符号,但实际上内核并使用它。

我的问题是

1 既然System.map是与内核相关的,也就是每次重新生成内核就会生成一个System.map,那么它一旦生成就是固定的了?

2 使用它的软件(比如书中讲到的klogd)如何使用它?(我不知道klogd到底是干什么的)

3 在makefile里面,是用gld生成的makefile,现在还可以用吗?

答:

1. 一旦生成,这些标号在内核中的位置(地址)就固定了;

2. 软件根据内核中的地址与System.map中的进行匹配就可以知道调试的代码在内核中的大概位置了;

3. ? 是说生成的System.map吧。你自己可以作个实验。


又问:

第三个问题大概使我说的不明白,我想问的是现在是否还可以使用gld来生成这个System.map吗?还是要用其它的工具。

对于第二个问题的解答,我还想问一下,知道调试的代码在内核中的大概位置,可以干些什么?

又答:

现在内核编译一般使用目标文件符号列表nm程序。

$(NM) vmlinux | grep -v '/(compiled/)/|/(/.o$$/)/|/( [aUw] /)/|/(/./.ng$$/)/|/(LASH[RL]DI/)' | sort > System.map

  当内核出错时,会显示出错时所有寄存器当前的内容,包括当前IP的位置,并会将一些信息保留到盘上。这样,使用system.map文件就可以找出内核具体出错的位置,对内核代码进行调试。

--------------

问:请把makefile解释的再详细一些!

1 gcc的几个参数的含义

-fomit-frame-pointer

-fcombine-regs

linus自己加的-mstring-insns代表了什么含义?(我知道这个可能没什么用,想知道而已^_^)

2 $*.s $<的含义

我看了你的解释,没看懂!:(

什么是自动目标变量?什么是第一个先决条件

3 boot/head.o: boot/head.s

你写道“利用上面给出的.s.o规则生成head.o目标文件”,究竟是怎么利用规则生成?

4 dep

这段的作用就是重新生成依赖关系,对吧?

但是生成这个依赖关系为了什么呢?

答:

1. 这是Linus优化字符串的编译参数。我也不太清楚它的具体含义。只是从以前的maillist中找到的这点信息;

2. 这些可以从任何一本有关Linux编程的书里找到有关信息。书中可参阅1.4.1节。

3. 这些是Make的使用方法问题,同样在1.4.1中给出了简单描述。

4. 创建的这些依赖关系就是给make用来确定是否要重建一个目标对象的。比如当某个头文件被改动过后,make就通过生成的依赖关系,重新编译与该头文件有关的所有*.c文件。

----------------

参考资料1:

Makefile简介

  makefile文件是make工具程序的配置文件。Make工具程序的主要用途是能自动地决定一个含有很多源程序文件的大型程序中哪个文件需要被重 新编译。makefile的使用比较复杂,这里只是根据上面的makefile文件作些简单的介绍。详细说明请参考GNU make使用手册。

  为了使用make程序,你就需要makefile文件来告诉make要做些什么工作。通常,makefile文件会告诉make如何编译和连接一个文件。当明确指出时,makefile还可以告诉make运行各种命令(例如,作为清理操作而删除某些文件)。

  make的执行过程分为两个不同的阶段。在第一个阶段,它读取所有的makefile文件以及包含的makefile文件等,记录所有的变量及其值、 隐式的或显式的规则,并构造出所有目标对象及其先决条件的一幅全景图。在第二阶段期间,make就使用这些内部结构来确定哪个目标对象需要被重建,并且使 用相应的规则来操作。

  当make重新编译程序时,每个修改过的C代码文件必须被重新编译。如果一个头文件被修改过了,那么为了确保正确,每一个包含该头文件的C代码程序都 将被重新编译。每次编译操作都产生一个与源程序对应的目标文件(object file)。最终,如果任何源代码文件被编译过了,那么所有的目标文件不管是刚编译完的还是以前就编译好的必须连接在一起以生成新的可执行文件。

  简单的makefile文件含有一些规则,这些规则具有如下的形式:

目标(target)... : 先决条件(prerequisites)...

命令(command)

...

...

  其中'目标'对象通常是程序生成的一个文件的名称;例如是一个可执行文件或目标文件。目标也可以是所要采取活动的名字,比如'清除 '('clean')。'先决条件'是一个或多个文件名,是用作产生目标的输入条件。通常一个目标依赖几个文件。而'命令'是make需要执行的操作。一 个规则可以有多个命令,每一个命令自成一行。请注意,你需要在每个命令行之前键入一个制表符!这是粗心者常常忽略的地方。

  如果一个先决条件通过目录搜寻而在另外一个目录中被找到,这并不会改变规则的命令;它们将被如期执行。因此,你必须小心地设置命令,使得命令能够在 make发现先决条件的目录中找到需要的先决条件。这就需要通过使用自动变量来做到。自动变量是一种在命令行上根据具体情况能被自动替换的变量。自动变量 的值是基于目标对象及其先决条件而在命令执行前设置的。例如,’$^’的值表示规则的所有先决条件,包括它们所处目录的名称;’$<’的值表示规则 中的第一个先决条件;’$@’表示目标对象;另外还有一些自动变量这里就不提了。

  有时,先决条件还常包含头文件,而这些头文件并不愿在命令中说明。此时自动变量’$<’正是第一个先决条件。例如:

foo.o : foo.c defs.h hack.h

cc -c $(CFLAGS) $< -o $@

其中的’$<’就会被自动地替换成foo.c,而$@则会被替换为foo.o

  为了让make能使用习惯用法来更新一个目标对象,你可以不指定命令,写一个不带命令的规则或者不写规则。此时make程序将会根据源程序文件的类型(程序的后缀)来判断要使用哪个隐式规则。

  后缀规则是为make程序定义隐式规则的老式方法。(现在这种规则已经不用了,取而代之的是使用更通用更清晰的模式匹配规则)。下面例子就是一种双后 缀规则。双后缀规则是用一对后缀定义的:源后缀和目标后缀。相应的隐式先决条件是通过使用文件名中的源后缀替换目标后缀后得到。因此,此时下面 的’$<’值是*.c文件名。而整条make规则的含义是将*.c程序编译成*.s代码。

.c.s:

$(CC) $(CFLAGS) /

-nostdinc -Iinclude -S -o $*.s $<

  通常命令是属于一个具有先决条件的规则,并在任何先决条件改变时用于生成一个目标(target)文件。然而,为目标而指定命令的规则也并不一定要有 先决条件。例如,与目标'clean'相关的含有删除(delete)命令的规则并不需要有先决条件。此时,一个规则说明了如何以及何时来重新制作某些文 件,而这些文件是特定规则的目标。make根据先决条件来执行命令以创建或更新目标。一个规则也可以说明如何及何时执行一个操作。

  一个makefile文件也可以含有除规则以外的其它文字,但一个简单的makefile文件只需要含有适当的规则。规则可能看上去要比上面示出的模板复杂得多,但基本上都是符合的。

  makefile文件最后生成的依赖关系是用于让make来确定是否需要重建一个目标对象。比如当某个头文件被改动过后,make就通过这些依赖关系,重新编译与该头文件有关的所有*.c文件。

---------------

又问:

根据如下说法:

为了让make能使用习惯用法来更新一个目标对象,你可以不指定命令,写一个不带命令的规则或者不写规则。此时make程序将会根据源程序文件的类型(程序的后缀)来判断要使用哪个隐式规则。

得出结论:

boot/head.o: boot/head.s

make会根据文件类型来判断使用哪些隐式规则。

我理解得对吗?

自答又问。

.c.s:

$(CC) $(CFLAGS) /

-nostdinc -Iinclude -S -o $*.s $<

问题是,既然你已经说了这个规则现在已经不再使用,那就再给个现在如何使用的例子吧!

下面是我找到的一个模式规则的写法,看看对不对

%.c:%.o

$(CC) $(CFLAGS) $(CPPFLAGS) -c -o $@ $<

答:这个就是上面例子对应的使用模式规则的写法。

----------------

补充参考资料2

GNU make 和 makefile

?GNU make

?makefile 基本结构

?makefile 变量

?GNU make 的主要预定义变量

?隐含规则

?makefile 范例

?运行 make

1.8.1 GNU make

在大型的开发项目中,通常有几十到上百个的源文件,如果每次均手工键入 gcc 命令进行编译的话,则会非常不方便。因此,人们通常利用 make 工具来自动完成编译工作。这些工作包括:如果仅修改了某几个源文件,则只重新编译这几个源文件;如果某个头文件被修改了,则重新编译所有包含该头文件的源 文件。

利用这种自动编译可大大简化开发工作,避免不必要的重新编译。

实际上,make 工具通过一个称为 makefile 的文件来完成并自动维护编译工作。makefile 需要按照某种语法进行编写,其中说明了如何编译各个源文件并连接生成可执行文件,并定义了源文件之间的依赖关系。

当修改了其中某个源文件时,如果其他源文件依赖于该文件,则也要重新编译所有依赖该文件的源文件。

makefile 文件是许多编译器,包括 Windows NT 下的编译器维护编译信息的常用方法,只是在集成开发环境中,用户通过友好的界面修改 makefile 文件而已。

默认情况下,GNU make 工具在当前工作目录中按如下顺序搜索 makefile:

* GNUmakefile

* makefile

* Makefile

在 UNIX 系统中,习惯使用 Makefile 作为 makfile 文件。如果要使用其他文件作为 makefile,则可利用类似下面的 make 命令选项指定 makefile 文件:

$ make -f Makefile.debug

1.8.2 makefile 基本结构

makefile 中一般包含如下内容:

* 需要由 make 工具创建的项目,通常是目标文件和可执行文件。通常使用“目标(target)”一词来表示要创建的项目。

* 要创建的项目依赖于哪些文件。

* 创建每个项目时需要运行的命令。

例如,假设你现在有一个 C++ 源文件 test.C,该源文件包含有自定义的头文件 test.h,则目标文件 test.o 明确依赖于两个源文件:test.C 和 test.h。另外,你可能只希望利用 g++ 命令来生成 test.o 目标文件。

这时,就可以利用如下的 makefile 来定义 test.o 的创建规则:

# This makefile just is a example.

# The following lines indicate how test.o depends

# test.C and test.h, and how to create test.o

test.o: test.C test.h

g++ -c -g test.C

从上面的例子注意到,第一个字符为 # 的行为注释行。第一个非注释行指定 test.o 为目标,并且依赖于test.C 和 test.h 文件。随后的行指定了如何从目标所依赖的文件建立目标。

当 test.C 或 test.h 文件在编译之后又被修改,则 make 工具可自动重新编译 test.o,如果在前后两次编译之间,test.C 和 test.h 均没有被修改,而且 test.o 还存在的话,就没有必要重新编译。这种依赖关系在多源文件的程序编译中尤其重要。通过这种依赖关系的定义,make 工具可避免许多不必要的编译工作。当然,利用 Shell 脚本也可以达到自动编译的效果,但是,Shell 脚本将全部编译任何源文件,包括哪些不必要重新编译的源文件,而 make 工具则可根据目标上一次编译的时间和目标所依赖的源文件的更新时间而自动判断应当编译哪个源文件。

一个 makefile 文件中可定义多个目标,利用 make target 命令可指定要编译的目标,如果不指定目标,则使用第一个目标。通常,makefile 中定义有 clean 目标,可用来清除编译过程中的中间文件,例如:

clean:

rm -f *.o

运行 make clean 时,将执行 rm -f *.o 命令,最终删除所有编译过程中产生的所有中间文件。

1.8.3 makefile 变量

GNU 的 make 工具除提供有建立目标的基本功能之外,还有许多便于表达依赖性关系以及建立目标的命令的特色。其中之一就是变量或宏的定义能力。如果你要以相同的编译选项 同时编译十几个 C 源文件,而为每个目标的编译指定冗长的编译选项的话,将是非常乏味的。但利用简单的变量定义,可避免这种乏味的工作:

# Define macros for name of compiler

CC = gcc

# Define a macr o for the CC flags

CCFLAGS = -D_DEBUG -g -m486

# A rule for building a object file

test.o: test.c test.h

$(CC) -c $(CCFLAGS) test.c

在上面的例子中,CC 和 CCFLAGS 就是 make 的变量。GNU make 通常称之为变量,而其他 UNIX 的 make工具称之为宏,实际是同一个东西。在 makefile 中引用变量的值时,只需变量名之前添加 $ 符号,如上面的 $(CC) 和 $(CCFLAGS)。

1.8.4 GNU make 的主要预定义变量

GNU make 有许多预定义的变量,这些变量具有特殊的含义,可在规则中使用。表 1-5 给出了一些主要的预定义变量,除这些变量外,GNU make 还将所有的环境变量作为自己的预定义变量。

表 1-5 GNU make 的主要预定义变量

预定义变量 含义

$* 不包含扩展名的目标文件名称。

$+ 所有的依赖文件,以空格分开,并以出现的先后为序,可能包含重复的依赖文件。

$< 第一个依赖文件的名称。

$? 所有的依赖文件,以空格分开,这些依赖文件的修改日期比目标的创建日期晚。

$@ 目标的完整名称。

$^ 所有的依赖文件,以空格分开,不包含重复的依赖文件。

$% 如果目标是归档成员,则该变量表示目标的归档成员名称。例如,如果目标名称为 mytarget.so(image.o),则 $@ 为 mytarget.so,而 $% 为 image.o。

AR 归档维护程序的名称,默认值为 ar。

ARFLAGS 归档维护程序的选项。

AS 汇编程序的名称,默认值为 as。

ASFLAGS 汇编程序的选项。

CC C 编译器的名称,默认值为 cc。

CFLAGS C 编译器的选项。

CPP C 预编译器的名称,默认值为 $(CC) -E。

CPPFLAGS C 预编译的选项。

CXX C++ 编译器的名称,默认值为 g++。

CXXFLAGS C++ 编译器的选项。

FC FORTRAN 编译器的名称,默认值为 f77。

FFLAGS FORTRAN 编译器的选项。

1.8.5 隐含规则

GNU make 包含有一些内置的或隐含的规则,这些规则定义了如何从不同的依赖文件建立特定类型的目标。

GNU make 支持两种类型的隐含规则:

* 后缀规则(Suffix Rule)。后缀规则是定义隐含规则的老风格方法。后缀规则定义了将一个具有某个后缀的文件(例如,.c 文件)转换为具有另外一种后缀的文件(例如,.o 文件)的方法。每个后缀规则以两个成对出现的后缀名定义,例如,将 .c 文件转换为 .o 文件的后缀规则可定义为:

.c.o:

$(CC) $(CFLAGS) $(CPPFLAGS) -c -o $@ $<

* 模式规则(pattern rules)。这种规则更加通用,因为可以利用模式规则定义更加复杂的依赖性规则。模式规则看起来非常类似于正则规则,但在目标名称的前面多了一个 % 号,同时可用来定义目标和依赖文件之间的关系,例如下面的模式规则定义了如何将任意一个 X.c 文件转换为 X.o 文件:

%.c:%.o

$(CC) $(CFLAGS) $(CPPFLAGS) -c -o $@ $<

1.8.6 makefile 范例

#SAMPLE#

CCE 的 Makefile

1.8.7 运行 make

我们知道,直接在 make 命令的后面键入目标名可建立指定的目标,如果直接运行 make,则建立第一个目标。我们还知道可以用 make -f mymakefile 这样的命令指定 make 使用特定的 makefile,而不是默认的 GNUmakefile、makefile 或 Makefile。但 GNU make 命令还有一些其他选项,表 1-6 给出了这些选项。

表 1-6 GNU make 命令的常用命令行选项

命令行选项 含义

-C DIR 在读取 makefile 之前改变到指定的目录 DIR。

-f FILE 以指定的 FILE 文件作为 makefile。

-h 显示所有的 make 选项。

-i 忽略所有的命令执行错误。

-I DIR 当包含其他 makefile 文件时,可利用该选项指定搜索目录。

-n 只打印要执行的命令,但不执行这些命令。

-p 显示 make 变量数据库和隐含规则。

-s 在执行命令时不显示命令。

-w 在处理 makefile 之前和之后,显示工作目录。

-W FILE 假定文件 FILE 已经被修改。

-----------

问:Makefile 文件中命令部分为何有的有括号,有的没,如列表1.2中的98-103行,

括号是不是表示里面的命令要一块执行完,才往下走?

答:make规则要求一条命令一行,因此这里将两条命令括起来,使用一个shell来执行。

-------

问:在bootsect.s中的seg cs是什么意思?

答:表示下一条语句的操作数在cs段寄存器所指的段中。

--------

问:不知道这么写用意何在,其时直接访问sectors难道访问不到吗?这时的cs和ds指向的都是0x9000吧?

答:知道中断int 0x13后ds值还是原来的值吗?你可能知道,但当时Linus不知道啊。

------------

问:

请问列表4.4中48行的任务结构中变量的偏移值是啥?

能把48行到59行解释的更详实?

答:

这里所说的偏移值是指从一个数据结构的头指针位置算起,其中的某个指定变量离该头指针的字节数。例如,对于结构:

struct file {

unsigned short f_mode;

unsigned short f_flags;

unsigned short f_count;

struct m_inode *f_inode;

off_t f_pos;

};

其中short类型的变量实例将占用2个字节的内存。

如果我们定义了上述结构的一个指针变量F,那么相对结构的头指针,其中的f_mode的偏移量就是0,f_flags的偏移量就是2,f_count的偏移量就是4。

# 以下这些是任务结构(task_struct)中变量的偏移值,参见include/linux/sched.h,77行开始。

48 state = 0 # these are offsets into the task-struct. # 进程状态码

49 counter = 4 # 任务运行时间计数(递减)(滴答数),运行时间片。

50 priority = 8 // 运行优先数。任务开始运行时counter=priority,越大则运行时间越长。

51 signal = 12 // 是信号位图,每个比特位代表一种信号,信号值=位偏移值+1。

52 sigaction = 16 # MUST be 16 (=len of sigaction) // sigaction结构长度必须是16字节。

// 信号执行属性结构数组的偏移值,对应信号将要执行的操作和标志信息。

53 blocked = (33*16) // 受阻塞信号位图的偏移量。

54

# 以下定义在sigaction结构中的偏移量,参见include/signal.h,第48行开始。

55 # offsets within sigaction

56 sa_handler = 0 // 信号处理过程的句柄(描述符)。

57 sa_mask = 4 // 信号量屏蔽码

58 sa_flags = 8 // 信号集。

59 sa_restorer = 12 // 恢复执行的地址位置。

--------------

问:请问列表4。8中27行复制sigaction数据到fs数据断to处有何目的?fs数据段在哪里?

答:在系统调用sys_sigaction(参见第63行开始)中,当参数oldaction不为空时,会调用save_old()函数。用于将当前进程中指定信号量的sigaction数据复制给用户程序。

由于内核和用户分属不同的数据段,而在Linux中,fs一般默认是用户数据段的选择符,因此save_old()函数是将内核中数据复制到用户段中。

---------------

问:刚看了IA-32关于存储管理部分,有些地方不明白。

“采用虚拟存储方式,如果我需要把内核运行空间和用户运行空间分离,比如我要内核程序在linear address 0~1G地址空间运行,用户程序在1G~4G地址空间运行,我需要在GDT中定义4个描述 符:_KERNEL_CS,_KERNEL_DS,_USR_CS,_USR_DS.然后通过分页机制把_KERNEL_CS,_KERNEL_DS影射 到主存内核物理地址空间。

接着,在创建每个进程的时候,需要知道该进程的运行空间,因此需要LDT来指定该进程的代码段,数据段,堆栈段等。访问具体的段根据selector来指定(根据其中的TI决定选择LDT还是GDT中的相应段)。

我不知道上面是不是对的,”

我现在的问题是,当访问PROCESS的某个地址的时候,采用seletor

ffset 时通过段机制不是生成一个linear address?那么生成的这个linear address通过分页机制映射后不是与内核空间的地址重叠了?比如内核从0~1G影射到主存0~16M,而进程生成的linear
address 落在0~1G访问的主存不是在内核区了?难道每个进程都有自己的页表?那么页表如何保存摆放?如何更换系统中原有的页表结构呢?

正在看linux0.11存储管理部分,如果能够知道她的实现原理,过程,那么看代码就容易多了。

答:

在32为保护模式下时,每个进程都有自己的代码段和数据段描述符。(参见boot/head.s,233)。每创建一个进程就会在gdt中占用两个描述符槽(TSSn,LDTn),TSSn用于存放第n个进程的TSS段描述符,LDTn是进程的局部描述符表的描述符。

你说的selector就是对应进程中LDT表中的描述符的选择符,也既是对应描述符的偏移值,用于选择对应的描述符。而不是用于生成线性地址。比如selector是程序的数据段描述符,则会使用该描述符中设置的线性地址等参数。

又问:今天看了<IA-32 手册3>关于任务管理部分,在“任务地址空间”部分提到:为了使得各个任务彼此独立,每个任务都有自己的页表,当任务切换的时候,处理器根据TSS 中的CR3重新装载页表,从而每个任务可以通过该页表把线性地址转化为相应的物理地址进行访问。那么为了使任务的虚拟地址空间达到3G大小,是不是需要为 该任务建立一个LDT,其中描述符的base_address=0,limit=3G,同时建立页表?

又答:每个任务均建立一个ldt.其中的base_address,limit则是虚拟的内存空间,使用时会请求分配实际的物理页面。

-------------

问:

列表4.7中53行的stack字符数组程序成员是啥结构?

在那有它的说明?

答:

这是一个union结构,因此,stack[PAGE_SIZE]与task结构成员占用同一块内存区。

PAGE_SIZE = 1024 字节。而task结构<

AGE_SIZE,因此这个联合字节长度为PAGE_SIZE。

stack[]就是一个字符数组而已,就在这里定义的。

stack[PAGE_SIZE]末端即是该task的堆栈顶端。

--------------

问:

初始化程序main.c运行完后是不是就不在运行了?程序中138-139行的fork()调用是创键那个进程?0进程是在那部分创建的?

我看到现在怎么还是不太明白这轮子是怎样转起来的?各个程序照你的注释看感觉能明白,可是,进程的模型是怎样运作起来不是很明白!我看Bach的书讲到实 现上下文切换时,他用的是伪代码,所以切换的机理看似明白,但真正的代码是如何实现的?在本书中是那部分实现这功能,它的机理和Bach讲的是一样的吗?

答:

138-139创建的应该说是第2个进程(task1)。第1个进程(task0)是在程序中人工设置创建的,其初始值在sched.h中(113行)给出的。

task0是在sched_init()中创建的(sched.c,385行)。

你说的问题我刚开始看时也遇到过,后来反复看了保护模式运行原理,再加上多看了几遍源代码,才慢慢弄清楚的。说明本书有些地方还是有待于重新编写。目前我正在这方面努力着。

---------------

问:在Bootsect中用Int13读磁盘时,是直接读Drive 0的,Drive 0好像是软驱,如果Linux装在硬盘怎么办?此时读出的就不对了,难道是一定装在软驱0的软盘上?

答:若linux是由硬盘引导启动的,则一般会使用Lilo或grub引导程序,此时用的就不是这个bootsect程序了。

---------------

问:系统启动后RAM中内存地址的分配

系统启动后在刚进入BootSect.s执行之前,内存布局是什么样子,内存怎么编址,例如BIOS中的代码是不是和内存统一编址,0xffff:0是不是和内存编址在一起,那能不能读0xffff:0处的地址,BIOS中的代码初始化中断向量吗?

答:你问的这些问题涉及到一般PC兼容机启动时的情况。

Intel CPU兼容机在启动时是在实模式运行方式下执行的,因此此时内存的寻址使用段寄存器和偏移地址。BIOS ROM也是和RAM统一编址的,CPU可以访问有效地址范围的任何内容。只是最初机器的内存容量比较小(设计时认为不会超过1M),因此ROM BIOS就放在1M的顶端地址处,容量大约是8K(具体是多少记不清了)。

当机器加电时,CPU中的IP积存器指针的初始值被设定为0xffff:0,该内存地址正好在ROM BIOS中,而且是一条跳转指令转移到ROM BIOS程序开始处执行。BIOS会执行一些机器检测、诊断程序,并初始化各个标准的PC中断向量。最后检查并确定引导的块设备,并将该块设备的引导块 bootsect(处于块设备的第1块)读到内存0x7c0:0处,然后就将执行控制权交给了bootsect程序。。。。

----------------

问:页目录及页表从0000:0000处加载大小为5X4k,如何知道它不会覆盖system的有用部分?



inux 0.11内核页目录占1页内存放在从物理地址0000开始的地方,0x1000开始存放页表(0x1000 - pg0, 0x2000-pg1, 0x3000-pg2, 0x4000-pg3)。共占物理内存位置0x0000 - 0x4fff(20K)。在head.s程序中已经使用了.org命令为这些内存页面预留了空间,所以不会被覆盖掉。见head.s程序114行开始。

又问:head.s是system的一部分装.org是在编译时预留的空间还是在连接时预留了4K空间?

又答:编译时就已经确定了预留的空间。system模块的连接生成是使用build.c程序进行的,你可以参考它。

---------------



inux BootSect.s为什么要将自己从0x7c00从移动到0x90000处

请问BootSect.s为什么要将自己从0x7c00从移动到0x90000处,按照我的理解可以直接将Setup.s读到 Bootsect.s(0x7c00+200)处,或者直接将其读到0x90200处,然后将System读到0x10000处,另外为什么要将 System读到0x10000处,而不直接读到0x00000处。

答:

第1个为什么原则上应该是可以按照你的想法做到的。但若是将setup.s程序移动到0x7c00+200处,则setup.s就不能把system往下 移动了。若是自己不动而将setup.s加载到0x90200处,那么当进行加载时代码和被加载的数据不在同一个段中,则程序有些烦琐,就象加载和移动 system一样。

第2个说法不成立。因为setup程序还需要使用位于内存低段ROM BIOS设置的中断表等信息。

又问:但若是将setup.s程序移动到0x7c00+200处,则setup.s就不能把system往下移动了?为什么不能向下移动?我觉得可以办到

又答:setup要把system移动到绝对物理地址0x0000开始的地方,system会覆盖掉0x7cxxx的位置,不就把自己也覆盖掉了。

--------------------

问:有关地址的概念问题

物理地址、虚拟地址、线性地址之间的联系是什么? 他们之间是如何转换的?通常来说在什么情况下需要转换?

答:逻辑地址 -- 在保护模式下,由偏移地址和选择符组合构成的地址。类似于在实模式下由段寄存器与偏移地址组合成的内存地址;

线性地址 -- 由逻辑地址,其段选择符通过段描述符变换,再与该段内的偏移量组成的地址;

物理地址 -- 是实际系统中内存的编址。比如你的机器里有一根16MB内存条,则其物理地址范围是从0x000000到0x1000000。线性地址通过页变换会映射到物理地址。

又问:

在linux中针对i386架构的cpu所作的地址映射实际上是使线性地址和逻辑地址相等,

也就是说逻辑地址就是线性地址,因此cpu可以通过ecs寄存器中的段地址(也就是线性地址)再到pgd/pte中找到相应的物理地址。

我的疑惑是:在编译器编译一段程序后,会给程序分配一个入口地址,这个地址其实就是

虚拟地址吧?(也就是线性地址?)可是编译器根据什么给程序分配入口地址呢?也就是

说在编译连接时都有什么因素会影响到程序的入口地址?为什么不将程序的入口地址分配

为虚存空间的起始地址?因为反正虚拟地址在程序被调入系统空间时还需要根据情况来分

配物理空间与之相对应。既然分配了线性地址,就说明在系统空间中分配给进程的相应物

理空间在编译时就已经确定了,这样的话如果内核将程序调进系统空间时该页面已经被使

用了那岂不很麻烦?

由以上的疑惑才会想知道虚拟地址、线性地址、物理地址之间的关系和他们之间是如何转

换的。也许是我上面的理解就根本不对。我觉得理清了他们之间在内核中实际的转换关系

后也许会明白这些东西。

另外,linux走了一条捷径,在i386中将虚拟地址和线性地址映射成了同一种东西,那么

如果虚拟地址和线性地址不是相等的话,它们之间又应该如何互相映射呢?

又答:

在编译汇编程序时,你可以在程序中指定入口标号或在编译时指定。在C语言中则是有main()默认的,当然在调用main()之前,便宜程序会加上一段stub程序(crt0),以进行某些初始化等活动。

编译出的执行程序一般按照一定的执行程序格式(a.out格式或elf格式)存放的。其中将执行程序分成了几个部分,主要的由代码部分、数据部分,另外还 有符号表等。内核在使用execve()执行该程序时,会根据程序执行头中的设置来将各部分定位在一个线性地址范围内,然后往分配给这个程序的进程的虚拟 线性空间中放入这个执行代码和数据等。至于实际被放到了物理内存的什么地方,这是由内存分配管理程序mm动态确定的,mm给它什么页面的物理地址它就被加 载到什么地方。

接着问:

你帮忙看一下问题出在哪里:

假设:程序编译连接后的文件经过反汇编后可以看出程序的入口地址

08003728 <main>

或程序中的某一子程序)

当内核调度此进程进入系统空间时,首先找到程序的入口地址(就是08003728),将它放入ecs段寄存器中作为寻址的起点,然后将此线性地址(我认为 此地址就是虚拟地址也即线性地址,不知对不对)映射为物理地址(内存地址),也就是沿着gdt-gmt-gte-page的顺序找下去,这时可能有几种情 况,比较简单的是发现内存中有此页并且没有被别的进程使用,那就直接拿来用了,稍复杂一些的是,发现内存中没有此页面的映射,此时就发出缺页异常,然后给 此虚拟空间分配一个相应的物理空间(是从空闲页表中分配还是把虚地址08003728所指的页面分配给他?),另外一种情况是这个页面存在并且被别的进程
占用了(此时需要将这个页面交换出去还是去找另外一个空闲的页面分配给他?),我不知道这种情况内核会怎么处理。

以上的想法是基于这样一种理解:在给定一个虚拟地址(线性地址)后,在内存中就确定了一个物理页面。如果这种理解不对的话,那么线性地址-〉gdt-〉gte-〉page这种过程应该怎样理解?

接着答:

当执行内核sys_exec()系统调用执行一个文件时,是将保存在硬盘上的执行文件看做是即将放入分配的内存页上的一个映像往内存中加载的。根据执行文 件执行头中的e_entry,内核知道了执行入口点。在堆栈中将原来程序的返回eip换成该值,并且将原来的堆栈指针指向新的堆栈当前位置。

加载执行程序时,执行头部也作为该执行程序的第一个页面被放入内存中。

由于各个进程的线性地址空间是互相独立的,各自有自己的ldt来指明,因此不存在一个页面存在而被其它进程站用的情况。执行程序的虚拟地址确定后,与哪个 物理页面没有任何关系。当该程序执行了一个指令引起页陷阱发生时,内核就会申请一块内存页,若申请成功就将该执行程序还未被加载到内存中的所需代码加载进 来。

问:pgd表也是每个进程有一个吗?

答:不是,pgd和pgx是属于整个系统的,或者说属于mm管理的。其它的进程只能向mm申请内存页,包括内核中的其它函数也不能去动的。

问:虚拟地址和物理地址没有必然的联系是符合逻辑的

但是pgd是全局的,那么通过虚拟地址的pgd-pte-page的映射以后不就变成有联系了嘛?

我觉得这里面肯定有什么地方有问题

答:没错,是这样进行联系的,pgd-pte是CPU自动管理,物理page也是跟着确定了的。但是一个执行文件会使用哪个pte项也即使用哪一页内存,则是由mm来定的,这是mm根据内存使用情况动态分配的。

"好象明白了,自答:"

我明白了

虽然经过映射可以知道使用的是pte中的那一项,也可以知道那一项所指的页面的偏移量是多少,但是却无法知道那一项所指的页面时哪一页,具体给pte中的项分配页面指针的工作是由mm根据当前物理页面的分配情况来分配的,这样理解对吧?

也就是说虽然通过线性地址可以找到pte中那一项的下标,但是不知道这一项所指的究竟是哪一页。

纠正答:

有些误解。

执行程序的线性地址与pte不存在对应关系,执行程序的线性地址只是说明在自己的虚拟空间(max 4G)中的位置,而其中的一个指定的页面对应哪个pte项是由mm来确定的。也即使用哪个实际物理页面是不定的。

提问自答:连pgd相中的内容都是mm分配的!明白了

----------------------

问:

读过了setup.s最后和head.s开头的注释让我感觉很是费解。

下面是setup.s中的gdt

gdt:

206 .word 0,0,0,0 ! dummy

207

208 .word 0x07FF ! 8Mb - limit=2047 (2048*4096=8Mb)

209 .word 0x0000 ! base address=0

210 .word 0x9A00 ! code read/exec

211 .word 0x00C0 ! granularity=4096, 386

212

213 .word 0x07FF ! 8Mb - limit=2047 (2048*4096=8Mb)

214 .word 0x0000 ! base address=0

215 .word 0x9200 ! data read/write

216 .word 0x00C0 ! granularity=4096, 386

根据setup.s中对193行的注释,8代表对全局描述符表的第“1”项的取用

同样,在head.s中,对0x10的解释是对选择全局描述符表(位2=0)、选择表中第“1”项(位3-15=1)

上面的解释显然有些矛盾,因为0x10写成2进制是0000 0000 0001 0000,(位3-15=2?),书中的解释显然指得是后面一项,因为写得是212,213行。

因为我不清楚gdt中究竟哪个算第“1”项,特此跑来问问。

答:

当写“位”时,是从位0开始计的,比如1字节bit 7 - bit 0 ,则写成位7-位0。

选择符共16个bit,其位2是TI -- 表指示比特位:

0x0000 0000 0000 1000 (8)

|--- Index ----||___TI (Table Indicater)

Index 是指表中的描述符项,也是从0开始编号。

setup.s中的206是第0项,208开始是第1项。

head.s中应该是指第2项描述符,也即数据段描述符, 位15-3=2。

这里是笔误,因为后面的文字叙述是正确的。

-------------------

问:

在head.s中32-36行的注释中有这样一句话:

表示地址A20 线没有选通,结果不能对1M 以上内存寻址。

我从darwin的书中看到,A20禁用的结果是访问内存只能访问奇数1M段,即1M、3M、5M等等,如果A20打开,则可访问内存是连续的。

答:

darwin说的更确切些。

但对于内核来说道理是一样的。若不能访问第2个1MB内存,则所有1M以上就不能用了。将这局注释该成“结果内核就不能使用1M以上的内存”可能就更妥帖一些。

--------------------

问:请教: main.c中init函数, L183,184

183 execve( "/bin/sh",argv_rc,envp_rc); // 装入/bin/sh 程序并执行。

>>>execve是如何处理当前进程映象的?

184 _exit(2);

>>>以2作为返回值出于什么考虑?

答:

简单地讲,execve()首先为被执行程序建立环境和堆栈,然后用被执行的程序将自己的控制替换掉(利用堆栈返回地址的替换方式)。所以如果执行成功,就不应该返回到184行,所以返回2(文件或目录不存在)。

---------------------

问:请教:traps.c/trap_init

outb_p(inb_p(0x21)&0xfb,0x21);// 允许主8259A 芯片的IRQ2 中断请求。

//>>>IRQ2用于级连

outb(inb_p(0xA1)&0xdf,0xA1); // 允许从8259A 芯片的IRQ13 中断请求。

//>>>IRQ13用于协处理器

>>>为什么在此处使能这两个中断?

答:因为IRQ2需要用来响应从片的中断。

这里是内核开启irq13中断信号的地方,所以irq2也要开放。

又问:可是IRQ13的开启和Traps有何关系呢?

又答:

由协处理器(FPU)产生的8259IRQ13信号(对应中断int45)和陷阱门int16都是为协处理器服务的,只是两者的作用有区别。一个是CPU产生的,而另一个是由协处理器发出的。

  当CPU遇到FPU指令时,若FPU不存在或FPU有问题,那么CPU就会产生int16陷阱中断。此时若是需要通过软件来仿真FPU,则可以在该陷阱门处理程序中进行FPU仿真处理。

  而中断信号IRQ13的功能主要是为了让FPU的操作与CPU的操作协调进行。当FPU在执行自己的运算时,CPU需要等待FPU操作的完成。 IRQ13中断信号的作用就是起这种同步作用。在FPU当前的运算结束时,FPU会通过8259A发出IRQ13信号,在该中断信号的处理程序中,会执行 清除CPU忙等待的状态(通过向端口F0发送0),让等待FPU执行完成的进程能够继续运行(或处于就绪状态)。

  另外,这两个中断之间也有联系。若FPU存在,则CPU在遇到FPU的指令而交由FPU处理时,就需要等待响应IRQ13信号,并对其进行处理,因此 需要把对IRQ13中断信号的屏蔽去掉。若FPU不存在,则CPU在遇到FPU的指令时必须发出int16。通知系统对这种情况进行处理:或者调用软件仿 真函数或者处理这种不存在FPU的出错情况。

  程序中使用set_trap_gate(45,&irq13),是用于设置int45中断门值的(类似实模式下的中断向量)。

**************************************************************************************************************************************

bootsect.s程序

PC加电后X86CPU自动进入实模式,并从地址0XFFFF0处开始自动执行程序代码,这个地址通常是ROM-BIOS中的地址。PC机的 BIOS将执行某些系统的检测,并从物理地址0处开始初始化中断向量。此后,它将可启动设备的第一个扇区(磁盘引导扇区,512字节)读入内存绝对地址 0X7C00处,并跳转到这个地址。启动设备通常是软驱伙食硬盘。

Linux0.11最前面的部分是用8086汇编语言编写的,它有?BIOS入内存绝对地址0X7c00处(31KB)。当她被执行时,他就会 把自己移动到内存绝对地址0x90000(576KB)处,并把启动代码setup.s度入到内存0X90200处(2KB大小)。
bootsect.s代码是磁盘引导块程序,驻留在磁盘的第一个扇区中0磁道,0磁头,第一个扇区。PC加电ROM BIOS自检之后,ROM BIOS会把引导扇区代码bootsect.s加载到内存地址)0X7c00开始并执行之。在bootsect代码执行期间, 它会把自己移动到内存绝对地址0X9000开始出并继续执行。该程序的主要作用是首先把从磁盘第2个扇区开始的4个扇区的setup模块(由
setup.s编译生成)加载到内存紧接着bootsect后面位置处(0x90200),然后利用BIOS中断0x13取磁盘参数表中当前启动引导盘的 参数,接着在屏幕上显示“loading system”字符串。再者把磁盘上setup模块后面的system模块加载到内存0x10000开始的地方。随后确定根文件系统的设备号,若没有指 定,则根据所保存的引导盘的每个磁道扇区数判别出盘的类型和种类(是1.44M A盘吗?)并保存其设备号与root_dev (引导块的508地址处),最后长跳转到setup程序的开始出(0x90200)执行setup程序。在磁盘上,引导块、setup模块和system
模块的扇区位置和大小示意图如下所示。



从硬盘启动系统 ,通常需要使用其他多操作系统引导系统加载,例如Shoelace、LILO或Grub等操 作系统引导程序。此时bootsect.s所完成的任务会由这些程序来完成,bootsect程序就不会被执行了。因为如果从硬盘启动系统,那么内核映像 文件Image会存放在活动分区的根文件系统中。因此你就需要知道内核映像文件Image处于文件系统中的位置以及时什么文件系统,即你的引导扇区程序需
要能够识别并访问文件系统,并从中读取内核映像文件。

从硬盘启动的基本流程是: 系统上电后,可启动硬盘的第一个扇区(主引导记录MBR-Master Boot Record)会被BIOS加载到内存Ox7c00处并开始执行。该程序首先把自己向下移动到内存0x600处,然后根据MBR中分区表信息所指明活动分 区中的第一个扇区(引导扇区)加载到内存0X7c00处,然后开始执行之。如果直接使用这种方式来引导系统就会碰到这样一个问题,即根文件系统不能与内核 映像文件Image共存

**************************************************************************************************************************************************

请问内存地址的编码及bootsect.s编译链接后文件大小问题

从很多资料都得知CPU加电后,从地址0xFFFF0处开始自动执行程序代码,这个地址通常是ROM-BIOS中的地址。请问为什么0xFFFF0处是 ROM-BIOS中的地址,到底地址的编码是以什么顺序编码的,除了ROM-BIOS有编入地址外,其它硬件地址又是如何编码,我们通常所说的内存地址又 是如何进行编号的呢。我一直以为地址都在内存中,而且都从0x00000开始,并且一直连续下去,但这是与上述有冲突的,困扰了我很久的问题。

又问,bootsect.s文件编译链接后是怎样保证刚好为一个扇区(512B)大小的,即:使引导程序完全安装在主引导扇区,用这个程序作为主引导程序是不是不包含通常所说的4个分区表项。

再问,哪里有Linux下的可执行程序文件的分析资料啊(类似windows下的PE文件格式)。

kevie 发表于 2004-8-15 10:55

请问内存地址的编码及bootsect.s编译链接后文件大小问题

[这个贴子最后由kevie在 2004/08/15 10:58am 第 1 次编辑]

请问为什么0xFFFF0处是ROM-BIOS中的地址:

因为地址线只有16位,真正的ram只有64k,0xFFFF0是20位,已经很接近0xFFFFF了。

它超出了真正的ram地址,就规定它为ROM-BIOS硬件地址。并且在这个地方固定放跳转指令。这样就可以装载引导程序。

从bootsect.s代码中看没有包含4个分区表项的。呵呵。
http://www.oldlinux.org/Linux.old/docs/
下有一个文件:ELF.doc.tar.gz 。你看看

其他的我也不知道,期待回答中。。。

redgrid 发表于 2004-8-15 14:26

请问内存地址的编码及bootsect.s编译链接后文件大小问题

1. 内存空间问题

上世纪八十年代初IBM公司刚推出IBM PC机时,所使用的8088 CPU 外部地址总线是20位的,共可寻址内存范围 0-1MB。

0 - 640KB (640BK)是实际所含RAM内存总容量;

640KB - 960KB (320KB)用于显示卡等外设卡上内存的寻址;

960KB - 1024KB ( 64KB)用于存放ROM BIOS程序。

当加电时,CPU会自动将执行指针设置成0xffff0,该地址正好位于ROM BIOS中。此处存放着一个跳转指令,开始执行BIOS程序。

随着PC技术的发展,采用了80X86 CPU,内存容量也不断增大,地址线也从20位扩展到32位(32机)。但为了与最早的PC机在系统软件一级起就兼容,上述保留地址范围仍然没有改动(只 是在实际应用环境中ROM BIOS可以对这些地址范围实现动态映射等方式进行“移动”)。例如为了方便起见,目前的Linux系统中通常仍然跳开物理内存最前段的1MB内存而从第 2MB开始加载内核。。。。。

2. 启动扇区

启动扇区有两种,一种是位于硬盘第1个扇区上的引导扇区,它是整个硬盘的主引导扇区。其中包含有分区表。另一种是各个分区的第1个扇区,软盘系统就象硬盘 上的一个分区。它只引导启动本分区的操作系统。bootsect不是主引导扇区代码而是分区的引导代码。在编程时为了控制其大小正好为512字节,通常采 用汇编语言的“定位”前导码来指定代码或数据位置,例如我们使用.org命令如下定义:

.org 511

db 0x55, 0xaa

这样就能保证这两个标志字节是扇区的最后两个字节。

3. Linux执行文件格式

Linux的执行格式有a.out, ELF等很多格式。但目前通常只使用ELF格式。其资料网上很多,可在Linux.old中寻找。还有网站www.nondot.org。

请问内存地址的编码及bootsect.s编译链接后文件大小问题

谢谢斑主及各位的指点,我好像理解地址编码了,我简单地总结一下理解内容,请指正是否正确:

在早期IBM PC机由于CPU外部地址总线是20位(即20条地址线,其实在实模式下都是以20位寻址),因此决定最大寻址能力为0-1MB,而当时实际的内存 (RAM)最大仅为640KB,而640KB之后的地址则是其它硬件的编码地址,其中包括显卡的显存,显卡ROM,硬盘ROM地址,网卡地址,其它扩展地 址及系统的BIOS-ROM,而其BIOS-ROM为最后的64KB,即为0xFFFF0。

随着技术的不断发展,实际的内存容量已远远超出640KB,为了向下兼容,真正的640KB之后到1024KB之间的384KB的内存地址,要么简单地被系统屏蔽掉,要么为了提高读取ROM效率,将对应的硬件地址影射到内存的相对位置。

至于在实模式下CPU内部以16位寻址而外部以20位寻址的相互转换,确实需要再找相关资料了
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: