程序6-3 linux/boot/head.s
1 /*
2 * linux/boot/head.s
3 *
4 * (C) 1991 Linus Torvalds
5 */
6
7 /*
8 * head.s contains the 32-bit startup code.
9 *
10 * NOTE!!! Startup happens at absolute address 0x00000000, which is also where
11 * the page directory will exist. The startup code will be overwritten by
12 * the page directory.
13 */
/*
* head.s含有32位启动代码�
* 注意!!! 32位启动代码是从绝对地址0x00000000开始的,这里也同样是页目录将存在的地方�
* 因此这里的启动代码将被页目录覆盖掉�
*/
14 .text
15 .globl _idt,_gdt,_pg_dir,_tmp_floppy_area
16 _pg_dir: # 页目录将会存放在这里�
# 再次注意!!! 这里已经处于32位运行模式,因此这里�$0x10并不是把地址0x10装入各个
# 段寄存器,它现在其实是全局段描述符表中的偏移值,或者更准确地说是一个描述符表项
# 的选择符。有关选择符的说明请参�setup.s�193行下的说明。这�$0x10的含义是请求
# 特权�0(�0-1=0)、选择全局描述符表(�2=0)、选择表中�2�(�3-15=2)。它正好�
# 向表中的数据段描述符项。(描述符的具体数值参见前�setup.s�212�213行)
# 下面代码的含义是:设�ds,es,fs,gs�setup.s 中构造的数据段(全局段描述符表第2项)
# 的选择�=0x10,并将堆栈放置在stack_start指向�user_stack数组区,然后使用本程�
# 后面定义的新中断描述符表和全局段描述表。新全局段描述表中初始内容与setup.s中的基本
# 一样,仅段限长�8MB修改成了16MB�stack_start定义�kernel/sched.c�69行。它是指�
# user_stack数组末端的一个长指针。第23行设置这里使用的栈,姑且称为系统栈。但在移动到
# 任务0执行�init/main.c�137行)以后该栈就被用作任务0和任�1共同使用的用户栈了�
17 startup_32: # 18-22行设置各个数据段寄存器�
18 movl $0x10,%eax # 对于GNU汇编,每个直接操作数要以'$'开始,否则表示地址�
# 每个寄存器名都要�'%'开头,eax表示�32位的ax寄存器�
19 mov %ax,%ds
20 mov %ax,%es
21 mov %ax,%fs
22 mov %ax,%gs
23 lss _stack_start,%esp # 表示_stack_startèss:esp,设置系统堆栈�
# stack_start定义�kernel/sched.c�69行�
24 call setup_idt # 调用设置中断描述符表子程序�
25 call setup_gdt # 调用设置全局描述符表子程序�
26 movl $0x10,%eax # reload all the segment registers
27 mov %ax,%ds # after changing gdt. CS was already
28 mov %ax,%es # reloaded in 'setup_gdt'
29 mov %ax,%fs # 因为修改�gdt,所以需要重新装载所有的段寄存器�
30 mov %ax,%gs # CS代码段寄存器已经�setup_gdt中重新加载过了�
# 由于段描述符中的段限长从setup.s中的8MB改成了本程序设置�16MB(见setup.s�208-216
# 和本程序后面�235-236行),因此这里再次对所有段寄存器执行加载操作是必须的。另外,通过
# 使用bochs跟踪观察,如果不�CS再次执行加载,那么在执行�26行时CS代码段不可见部分�
# 的限长还�8MB。这样看来应该重新加�CS。但是由�setup.s中的内核代码段描述符与本程序�
# 重新设置的代码段描述符除了段限长以外其余部分完全一样,8MB的限长在内核初始化阶段不会有
# 问题,而且在以后内核执行过程中段间跳转时会重新加载CS。因此这里没有加载它并没有让程序
# 出错�
# 针对该问题,目前内核中就在第25行之后添加了一条长跳转指令�'ljmp $(__KERNEL_CS),$1f'�
# 跳转到第26行来确保CS确实又被重新加载�
31 lss _stack_start,%esp
# 32-36行用于测�A20地址线是否已经开启。采用的方法是向内存地址0x000000处写入任�
# 一个数值,然后看内存地址0x100000(1M)处是否也是这个数值。如果一直相同的话,就一�
# 比较下去,也即死循环、死机。表示地址A20线没有选通,结果内核就不能使�1MB以上内存�
#
# 33行上�'1:'是一个局部符号构成的标号。标号由符号后跟一个冒号组成。此时该符号表示活动
# 位置计数�Active location counter)的当前值,并可以作为指令的操作数。局部符号用于帮�
# 编译器和编程人员临时使用一些名称。共�10个局部符号名,可在整个程序中重复使用。这些符�
# 名使用名�'0'�'1'�...�'9'来引用。为了定义一个局部符号,需把标号写�'N:'形式(其�N
# 表示一个数字)。为了引用先前最近定义的这个符号,需要写�'Nb',其�N是定义标号时使用�
# 数字。为了引用一个局部标号的下一个定义,需要写�'Nf',这�N�10个前向引用之一。上�
# 'b'表示�向后�backwards���'f'表示�向前�forwards��。在汇编程序的某一处,我们最�
# 可以向后/向前引用10个标号(最远第10个)�
32 xorl %eax,%eax
33 1: incl %eax # check that A20 really IS enabled
34 movl %eax,0x000000 # loop forever if it isn't
35 cmpl %eax,0x100000
36 je 1b # '1b'表示向后(backward)跳转到标�1去(33行)�
# 若是'5f'则表示向�(forward)跳转到标�5去�
37 /*
38 * NOTE! 486 should set bit 16, to check for write-protect in supervisor
39 * mode. Then it would be unnecessary with the "verify_area()"-calls.
40 * 486 users probably want to set the NE (#5) bit also, so as to use
41 * int 16 for math errors.
42 */
/*
* 注意! 在下面这段程序中�486应该将位16置位,以检查在超级用户模式下的写保�,
* 此后 "verify_area()" 调用就不需要了�486的用户通常也会想将NE(#5)置位,以�
* 对数学协处理器的出错使用int 16�
*/
# 上面原注释中提到�486 CPU�CR0控制寄存器的�16是写保护标志WP�Write-Protect),
# 用于禁止超级用户级的程序向一般用户只读页面中进行写操作。该标志主要用于操作系统在创�
# 新进程时实现写时复制�copy-on-write)方法�
# 下面这段程序�43-65)用于检查数学协处理器芯片是否存在。方法是修改控制寄存�CR0,在
# 假设存在协处理器的情况下执行一个协处理器指令,如果出错的话则说明协处理器芯片不存在�
# 需要设�CR0中的协处理器仿真�EM(位2),并复位协处理器存在标�MP(位1)�
43 movl %cr0,%eax # check math chip
44 andl $0x80000011,%eax # Save PG,PE,ET
45 /* "orl $0x10020,%eax" here for 486 might be good */
46 orl $2,%eax # set MP
47 movl %eax,%cr0
48 call check_x87
49 jmp after_page_tables # 跳转�135行�
50
51 /*
52 * We depend on ET to be correct. This checks for 287/387.
53 */
/*
* 我们依赖�ET标志的正确性来检�287/387存在与否�
*/
# 下面fninit�fstsw是数学协处理器(80287/80387)的指令�
# finit 向协处理器发出初始化命令,它会把协处理器置于一个未受以前操作影响的已知状态,设置
# 其控制字为默认值、清除状态字和所有浮点栈式寄存器。非等待形式的这条指令(fninit)还会让
# 协处理器终止执行当前正在执行的任何先前的算术操作�fstsw 指令取协处理器的状态字。如果系
# 统中存在协处理器的话,那么在执行�fninit指令后其状态字低字节肯定为0�
54 check_x87:
55 fninit # 向协处理器发出初始化命令�
56 fstsw %ax # 取协处理器状态字�ax寄存器中�
57 cmpb $0,%al # 初始化后状态字应该�0,否则说明协处理器不存在�
58 je 1f /* no coprocessor: have to set bits */
59 movl %cr0,%eax # 如果存在则向前跳转到标号1处,否则改写cr0�
60 xorl $6,%eax /* reset MP, set EM */
61 movl %eax,%cr0
62 ret
# 下面是一汇编语言指示符。其含义是指存储边界对齐调整�"2"表示把随后的代码或数据的偏移位置
# 调整到地址值最�2比特位为零的位置�2^2),即按4字节方式对齐内存地址。不过现�GNU as
# 直接时写出对齐的值而非2的次方值了。使用该指示符的目的是为了提�32�CPU访问内存中代�
# 或数据的速度和效率。参见程序后的详细说明�
# 下面的两个字节值是80287协处理器指令fsetpm的机器码。其作用是把80287设置为保护模式�
# 80387无需该指令,并且将会把该指令看作是空操作�
63 .align 2
64 1: .byte 0xDB,0xE4 /* fsetpm for 287, ignored by 387 */ # 287协处理器码�
65 ret
66
67 /*
68 * setup_idt
69 *
70 * sets up a idt with 256 entries pointing to
71 * ignore_int, interrupt gates. It then loads
72 * idt. Everything that wants to install itself
73 * in the idt-table may do so themselves. Interrupts
74 * are enabled elsewhere, when we can be relatively
75 * sure everything is ok. This routine will be over-
76 * written by the page tables.
77 */
/*
* 下面这段是设置中断描述符表子程序 setup_idt
*
* 将中断描述符�idt设置成具�256个项,并都指�ignore_int中断门。然后加载中�
* 描述符表寄存�(�lidt指令)。真正实用的中断门以后再安装。当我们在其他地方认为一�
* 都正常时再开启中断。该子程序将会被页表覆盖掉�
*/
# 中断描述符表中的项虽然也�8字节组成,但其格式与全局表中的不同,被称为门描述�
# (Gate Descriptor)。它�0-1,6-7字节是偏移量�2-3字节是选择符,4-5字节是一些标志�
# 这段代码首先�edx�eax中组合设置出8字节默认的中断描述符值,然后�idt表每一项中
# 都放置该描述符,�256项�eax含有描述符低4字节�edx含有�4字节。内核在随后的初�
# 化过程中会替换安装那些真正实用的中断描述符项�
78 setup_idt:
79 lea ignore_int,%edx # �ignore_int的有效地址(偏移值)�èedx寄存�
80 movl $0x00080000,%eax # 将选择�0x0008置入eax的高16位中�
81 movw %dx,%ax /* selector = 0x0008 = cs */
# 偏移值的�16位置�eax的低16位中。此�eax含有
# 门描述符�4字节的值�
82 movw $0x8E00,%dx /* interrupt gate - dpl=0, present */
83 # 此时edx含有门描述符�4字节的值�
84 lea _idt,%edi # _idt是中断描述符表的地址�
85 mov $256,%ecx
86 rp_sidt:
87 movl %eax,(%edi) # 将哑中断门描述符存入表中�
88 movl %edx,4(%edi) # eax内容放到 edi+4 所指内存位置处�
89 addl $8,%edi # edi指向表中下一项�
90 dec %ecx
91 jne rp_sidt
92 lidt idt_descr # 加载中断描述符表寄存器值�
93 ret
94
95 /*
96 * setup_gdt
97 *
98 * This routines sets up a new gdt and loads it.
99 * Only two entries are currently built, the same
100 * ones that were built in init.s. The routine
101 * is VERY complicated at two whole lines, so this
102 * rather long comment is certainly needed :-).
103 * This routine will beoverwritten by the page tables.
104 */
/*
* 设置全局描述符表� setup_gdt
* 这个子程序设置一个新的全局描述符表gdt,并加载。此时仅创建了两个表项,与前
* 面的一样。该子程序只有两行,“非常的”复杂,所以当然需要这么长的注释了J�
* 该子程序将被页表覆盖掉�
*/
105 setup_gdt:
106 lgdt gdt_descr # 加载全局描述符表寄存�(内容已设置好,见234-238�)�
107 ret
108
109 /*
110 * I put the kernel page tables right after the page directory,
111 * using 4 of them to span 16 Mb of physical memory. People with
112 * more than 16MB will have to expand this.
113 */
/* Linus将内核的内存页表直接放在页目录之后,使用�4个表来寻址16 MB的物理内存�
* 如果你有多于16 Mb的内存,就需要在这里进行扩充修改�
*/
# 每个页表长为4 Kb字节�1页内存页面),而每个页表项需�4个字节,因此一个页表共可以存放
# 1024个表项。如果一个页表项寻址4 KB的地址空间,则一个页表就可以寻址4 MB的物理内存�
# 页表项的格式为:项的�0-11位存放一些标志,例如是否在内存中(P�0)、读写许�(R/W�1)�
# 普通用户还是超级用户使�(U/S�2)、是否修改过(是否脏了)(D�6)等;表项的位12-31�
# 页框地址,用于指出一页内存的物理起始地址�
114 .org 0x1000 # 从偏�0x1000处开始是�1个页表(偏移0开始处将存放页表目录)�
115 pg0:
116
117 .org 0x2000
118 pg1:
119
120 .org 0x3000
121 pg2:
122
123 .org 0x4000
124 pg3:
125
126 .org 0x5000 # 定义下面的内存数据块从偏�0x5000处开始�
127 /*
128 * tmp_floppy_area is used by the floppy-driver when DMA cannot
129 * reach to a buffer-block. It needs to be aligned, so that it isn't
130 * on a 64kB border.
131 */
/* �DMA(直接存储器访问)不能访问缓冲块时,下面�tmp_floppy_area内存�
* 就可供软盘驱动程序使用。其地址需要对齐调整,这样就不会跨�64KB边界�
*/
132 _tmp_floppy_area:
133 .fill 1024,1,0 # 共保�1024项,每项1字节,填充数�0�
134
# 下面这几个入栈操作用于为跳转�init/main.c中的main()函数作准备工作。第139行上
# 的指令在栈中压入了返回地址,而第140行则压入�main()函数代码的地址。当head.s
# 最后在�218行执�ret指令时就会弹�main()的地址,并把控制权转移�init/main.c
# 程序中。参见第3章中有关C函数调用机制的说明�
# 前面3个入�0值应该分别表�envp�argv指针�argc的值,�main()没有用到�
# 139行的入栈操作是模拟调�main.c程序时首先将返回地址入栈的操作,所以如�
# main.c程序真的退出时,就会返回到这里的标�L6处继续执行下去,也即死循环�
# 140行将main.c的地址压入堆栈,这样,在设置分页处理(setup_paging)结束后
# 执行'ret'返回指令时就会将main.c程序的地址弹出堆栈,并去执�main.c程序了�
# 有关C函数调用机制请参见程序后的说明�
135 after_page_tables:
136 pushl $0 # These are the parameters to main :-)
137 pushl $0 # 这些是调�main程序的参数(�init/main.c)�
138 pushl $0 # 其中�'$'符号表示这是一个立即操作数�
139 pushl $L6 # return address for main, if it decides to.
140 pushl $_main # '_main'是编译程序对main的内部表示方法�
141 jmp setup_paging # 跳转至第198行�
142 L6:
143 jmp L6 # main should never return here, but
144 # just in case, we know what happens.
# main程序绝对不应该返回到这里。不过为了以防万一�
# 所以添加了该语句。这样我们就知道发生什么问题了�
145
146 /* This is the default interrupt "handler" :-) */
/* 下面是默认的中断“向量句柄�J */
147 int_msg:
148 .asciz "Unknown interrupt\n\r" # 定义字符串“未知中�(回车换行)”�
149 .align 2 # �4字节方式对齐内存地址�
150 ignore_int:
151 pushl %eax
152 pushl %ecx
153 pushl %edx
154 push %ds # 这里请注意!�ds,es,fs,gs等虽然是16位的寄存器,但入栈后
155 push %es # 仍然会以32位的形式入栈,也即需要占�4个字节的堆栈空间�
156 push %fs
157 movl $0x10,%eax # 置段选择符(�ds,es,fs指向gdt表中的数据段)�
158 mov %ax,%ds
159 mov %ax,%es
160 mov %ax,%fs
161 pushl $int_msg # 把调�printk函数的参数指针(地址)入栈。注意!�int_msg
162 call _printk # 前不�'$',则表示�int_msg符号处的长字�'Unkn')入�J�
163 popl %eax # 该函数在/kernel/printk.c中�'_printk'�printk编译后模块中
164 pop %fs # 的内部表示法�
165 pop %es
166 pop %ds
167 popl %edx
168 popl %ecx
169 popl %eax
170 iret # 中断返回(把中断调用时压入栈�CPU标志寄存器(32位)值也弹出)�
171
172
173 /*
174 * Setup_paging
175 *
176 * This routine sets up paging by setting the page bit
177 * in cr0. The page tables are set up, identity-mapping
178 * the first 16MB. The pager assumes that no illegal
179 * addresses are produced (ie >4Mb on a 4Mb machine).
180 *
181 * NOTE! Although all physical memory should be identity
182 * mapped by this routine, only the kernel page functions
183 * use the >1Mb addresses directly. All "normal" functions
184 * use just the lower 1Mb, or the local data space, which
185 * will be mapped to some other place - mm keeps track of
186 * that.
187 *
188 * For those with more memory than 16 Mb - tough luck. I've
189 * not got it, why should you :-) The source is here. Change
190 * it. (Seriously - it shouldn't be too difficult. Mostly
191 * change some constants etc. I left it at 16Mb, as my machine
192 * even cannot be extended past that (ok, but it was cheap :-)
193 * I've tried to show which constants to change by having
194 * some kind of marker at them (search for "16Mb"), but I
195 * won't guarantee that's all :-( )
196 */
/*
* 这个子程序通过设置控制寄存�cr0的标志(PG �31)来启动对内存的分页处理功能�
* 并设置各个页表项的内容,以恒等映射前16 MB的物理内存。分页器假定不会产生非法�
* 地址映射(也即在只有4Mb的机器上设置出大�4Mb的内存地址)�
*
* 注意!尽管所有的物理地址都应该由这个子程序进行恒等映射,但只有内核页面管理函数能
* 直接使用>1Mb的地址。所有“普通”函数仅使用低于1Mb的地址空间,或者是使用局部数�
* 空间,该地址空间将被映射到其他一些地方去 -- mm(内存管理程序)会管理这些事的�
*
* 对于那些有多�16Mb内存的家� � 真是太幸运了,我还没有,为什么你会有J。代码就�
* 这里,对它进行修改吧。(实际上,这并不太困难的。通常只需修改一些常数等。我把它设置
* �16Mb,因为我的机器再怎么扩充甚至不能超过这个界限(当然,我的机器是很便宜�J)�
* 我已经通过设置某类标志来给出需要改动的地方(搜索�16Mb�),但我不能保证作这�
* 改动就行�L)�
*/
# 上面英文注释�2段的含义是指在机器物理内存中大于1MB的内存空间主要被用于主内存区�
# 主内存区空间�mm模块管理。它涉及到页面映射操作。内核中所有其他函数就是这里指的一�
#(普通)函数。若要使用主内存区的页面,就需要使�get_free_page()等函数获取。因为主�
# 存区中内存页面是共享资源,必须有程序进行统一管理以避免资源争用和竞争�
#
# 在内存物理地址0x0处开始存�1页页目录表和4页页表。页目录表是系统所有进程公用的,�
# 这里�4页页表则属于内核专用,它们一一映射线性地址起始16MB空间范围到物理内存上。对�
# 新的进程,系统会在主内存区为其申请页面存放页表。另外,1页内存长度是4096字节�
197 .align 2 # �4字节方式对齐内存地址边界�
198 setup_paging: # 首先�5页内存(1页目� + 4页页表)清零�
199 movl $1024*5,%ecx /* 5 pages - pg_dir+4 page tables */
200 xorl %eax,%eax
201 xorl %edi,%edi /* pg_dir is at 0x000 */
# 页目录从0x000地址开始�
202 cld;rep;stosl # eax内容存到es:edi所指内存位置处,且edi�4�
# 下面4句设置页目录表中的项,因为我们(内核)共�4个页表所以只需设置4项�
# 页目录项的结构与页表中项的结构一样,4个字节为1项。参见上�113行下的说明�
# 例如"$pg0+7"表示�0x00001007,是页目录表中的�1项�
# 则第1个页表所在的地址 = 0x00001007 & 0xfffff000 = 0x1000�
# �1个页表的属性标� = 0x00001007 & 0x00000fff = 0x07,表示该页存在、用户可读写�
203 movl $pg0+7,_pg_dir /* set present bit/user r/w */
204 movl $pg1+7,_pg_dir+4 /* --------- " " --------- */
205 movl $pg2+7,_pg_dir+8 /* --------- " " --------- */
206 movl $pg3+7,_pg_dir+12 /* --------- " " --------- */
# 下面6行填�4个页表中所有项的内容,共有�4(页表)*1024(�/页表)=4096�(0 - 0xfff)�
# 也即能映射物理内� 4096*4Kb = 16Mb�
# 每项的内容是:当前项所映射的物理内存地址 + 该页的标志(这里均为7)�
# 使用的方法是从最后一个页表的最后一项开始按倒退顺序填写。一个页表的最后一项在页表中的
# 位置�1023*4 = 4092。因此最后一页的最后一项的位置就是$pg3+4092�
207 movl $pg3+4092,%edi # ediè最后一页的最后一项�
208 movl $0xfff007,%eax /* 16Mb - 4096 + 7 (r/w user,p) */
# 最�1项对应物理内存页面的地址�0xfff000�
# 加上属性标�7,即�0xfff007�
209 std # 方向位置位,edi值递减(4字节)�
210 1: stosl /* fill pages backwards - more efficient :-) */
211 subl $0x1000,%eax # 每填写好一项,物理地址值减0x1000�
212 jge 1b # 如果小于0则说明全添写好了�
# 设置页目录表基址寄存�cr3的值,指向页目录表�cr3中保存的是页目录表的物理地址�
213 xorl %eax,%eax /* pg_dir is at 0x0000 */ # 页目录表�0x0000处�
214 movl %eax,%cr3 /* cr3 - page directory start */
# 设置启动使用分页处理�cr0�PG标志,位31�
215 movl %cr0,%eax
216 orl $0x80000000,%eax # 添上PG标志�
217 movl %eax,%cr0 /* set paging (PG) bit */
218 ret /* this also flushes prefetch-queue */
# 在改变分页处理标志后要求使用转移指令刷新预取指令队列,这里用的是返回指令ret�
# 该返回指令的另一个作用是�140行压入堆栈中�main程序的地址弹出,并跳转�/init/main.c
# 程序去运行。本程序到此就真正结束了�
219
220 .align 2 # �4字节方式对齐内存地址边界�
221 .word 0 # 这里先空�2字节,这�224行上的长字是4字节对齐的�
! 下面是加载中断描述符表寄存器idtr的指�lidt要求�6字节操作数。前2字节�idt表的限长�
! �4字节�idt表在线性地址空间中的32位基地址�
222 idt_descr:
223 .word 256*8-1 # idt contains 256 entries # �256项,限长=长度 - 1�
224 .long _idt
225 .align 2
226 .word 0
! 下面加载全局描述符表寄存�gdtr的指�lgdt要求�6字节操作数。前2字节�gdt表的限长�
! �4字节�gdt表的线性基地址。这里全局表长度设置为2KB字节�0x7ff即可),因为�8字节
! 组成一个描述符项,所以表中共可有256项。符�_gdt是全局表在本程序中的偏移位置,�234行�
227 gdt_descr:
228 .word 256*8-1 # so does gdt (not that that's any # 注:not à note
229 .long _gdt # magic number, but it works for me :^)
230
231 .align 3 # �8�2^3)字节方式对齐内存地址边界�
232 _idt: .fill 256,8,0 # idt is uninitialized # 256项,每项8字节,填0�
233
# 全局表。前4项分别是空项(不用)、代码段描述符、数据段描述符、系统调用段描述符,其中
# 系统调用段描述符并没有派用处�Linus当时可能曾想把系统调用代码专门放在这个独立的段中�
# 后面还预留了252项的空间,用于放置所创建任务的局部描述符(LDT)和对应的任务状态段TSS
# 的描述符�
# (0-nul, 1-cs, 2-ds, 3-syscall, 4-TSS0, 5-LDT0, 6-TSS1, 7-LDT1, 8-TSS2 etc...)
234 _gdt: .quad 0x0000000000000000 /* NULL descriptor */
235 .quad 0x00c09a0000000fff /* 16Mb */ # 0x08,内核代码段最大长�16MB�
236 .quad 0x00c0920000000fff /* 16Mb */ # 0x10,内核数据段最大长�16MB�
237 .quad 0x0000000000000000 /* TEMPORARY - don't use */
238 .fill 252,8,0 /* space for LDT's and TSS's etc */ # 预留空间�