Linux0.11源码学习(三)
Linux0.11源码学习(三)
linux0.11源码学习笔记
参考资料:
https://github.com/sunym1993/flash-linux0.11-talk
https://github.com/Akagi201/linux-0.11/blob/master/boot/head.s
源码查看:
https://elixir.bootlin.com/linux/latest/source
/boot/head.s
_pg_dir:
startup_32:
movl $0x10,%eax #0x10传入到32位eax寄存器
#置ds,es,fs,gs 中的选择符为setup.s 中构造的数据段(全局段描述符表的第2项)=0x10,
mov %ax,%ds
mov %ax,%es
mov %ax,%fs
mov %ax,%gs
lss _stack_start,%esp
解释:
对于GNU 汇编来说,每个直接数要以'$'开始,否则是表示地址。
每个寄存器名都要以'%'开头,eax 表示是32 位的ax 寄存器。
lss
指令相当于让 ss:esp 这个栈顶指针指向了 _stack_start 这个标号的位置。
疑问1:前面两个文件不是GNU汇编吗?前面寄存器名似乎没加"%"。
解答:
是的,由makefile文件,bootsect.s文件和setup.s文件通过8086汇编器和连接器进行编译和链接。而
head.s文件使用的是GNU汇编器。
疑问2:段寄存器赋值那边没看懂,mov
不是把源操作数(逗号右侧)赋值给目标操作数(逗号左侧)吗?加了个"%"就颠倒了?
解答:
上面提到这是GNU汇编,GNU
格式的汇编使用AT&T汇编
,语句格式与 intel
格式的汇编不同,所谓的intel
格式也就是一般的 8086汇编(16bit)
,x86汇编(32bit)
等。
疑问2提出的mov
的用法正是intel
格式的汇编,而head.s
文件使用GNU汇编编译器编译,因此应符合GNU格式,GNU格式的源操作数和目标操作数的位置正好和Intel格式相反。详情参考他人博文。
call setup_idt # 设置中断描述符表
call setup_gdt # 设置全局描述符表
movl $0x10,%eax # reload all the segment registers
mov %ax,%ds # after changing gdt. CS was already
mov %ax,%es # reloaded in 'setup_gdt'
mov %ax,%fs
mov %ax,%gs
lss _stack_start,%esp
解释:
先设置了 idt 和 gdt,然后又重新执行了一遍刚刚执行过的代码。
为什么要重新设置这些段寄存器呢?因为上面修改了 gdt,所以要重新设置一遍以刷新才能生效。
/*
* setup_idt
*
* sets up a idt with 256 entries pointing to
* ignore_int, interrupt gates. It then loads
* idt. Everything that wants to install itself
* in the idt-table may do so themselves. Interrupts
* are enabled elsewhere, when we can be relatively
* sure everything is ok. This routine will be over-
* written by the page tables.
*/
setup_idt:
lea ignore_int,%edx
movl $0x00080000,%eax
movw %dx,%ax /* selector = 0x0008 = cs */
movw $0x8E00,%dx /* interrupt gate - dpl=0, present */
lea _idt,%edi
mov $256,%ecx
rp_sidt:
movl %eax,(%edi)
movl %edx,4(%edi)
addl $8,%edi
dec %ecx
jne rp_sidt
lidt idt_descr
ret
/*
* setup_gdt
*
* This routines sets up a new gdt and loads it.
* Only two entries are currently built, the same
* ones that were built in init.s. The routine
* is VERY complicated at two whole lines, so this
* rather long comment is certainly needed :-).
* This routine will be overwritten by the page tables.
*/
setup_gdt:
lgdt gdt_descr
ret
解释:
这里是定义的各个子程序。
中断描述符表 idt 里面存储着一个个中断描述符,每一个中断号就对应着一个中断描述符,而中断描述符里面存储着主要是中断程序的地址,这样一个中断号过来后,CPU 就会自动寻找相应的中断程序,然后去执行它。
看英文注释,setup_idt子程序设置了 256 个中断描述符,并且让每一个中断描述符中的中断程序例程都指向一个 ignore_int 的函数地址,这个是个默认的中断处理程序,之后会逐渐被各个具体的中断程序所覆盖。比如之后键盘模块会将自己的键盘中断处理程序,覆盖过去。
setup_gdt子程序设置了新的全局描述符表,即gdt表。
记得setup.s文件里设置过了idt和gdt了。
为什么原来已经设置过一遍了,这里又要重新设置一遍?
就是因为原来设置的 gdt 是在 setup 程序中,之后这个地方要被缓冲区覆盖掉,所以这里重新设置在 head 程序中,这块内存区域之后就不会被其他程序用到并且覆盖了。
图解:
xorl %eax,%eax
1: incl %eax # check that A20 really IS enabled
movl %eax,0x000000 # loop forever if it isn't
cmpl %eax,0x100000
je 1b
解释:
emm,这里是用于测试A20 地址线是否已经开启。采用的方法是向内存地址0x000000 处写入任意一个数值,然后看内存地址0x100000(1M)处是否也是这个数值。如果一直相同的话,就一直比较下去,也即死循环、死机。表示地址A20 线没有选通,结果内核就不能使用1M 以上内存。
/*
* NOTE! 486 should set bit 16, to check for write-protect in supervisor
* mode. Then it would be unnecessary with the "verify_area()"-calls.
* 486 users probably want to set the NE (#5) bit also, so as to use
* int 16 for math errors.
*/
movl %cr0,%eax # check math chip
andl $0x80000011,%eax # Save PG,PE,ET
/* "orl $0x10020,%eax" here for 486 might be good */
orl $2,%eax # set MP
movl %eax,%cr0
call check_x87
jmp after_page_tables
...
...
after_page_tables:
pushl $0 # These are the parameters to main :-)
pushl $0
pushl $0
pushl $L6 # return address for main, if it decides to.
pushl $_main
jmp setup_paging
L6:
jmp L6 # main should never return here, but
# just in case, we know what happens.
解释:
注释翻译
注意! 在下面这段程序中,486 应该将位 16 置位,以检查在超级用户模式下的写保护,此后"verify_area()"调用中就不需要了。486 的用户通常也会想将NE(#5)置位,以便对数学协处理器的出错使用int 16。
接着这段程序用于检查数学协处理器芯片是否存在。方法是修改控制寄存器CR0,在假设存在协处理器的情况下执行一个协处理器指令,如果出错的话则说明协处理器芯片不存在,需要设置CR0 中的协处理器仿真位EM(位2),并复位协处理器存在标志MP(位1)。(说实话,没看懂)
然后跳到after_page_tables
标签处。
这边,pushl入栈操作,用于为调用/init/main.c 程序和返回作准备。但是前三个入栈操作似乎没有明确的意义,《Linux内核0.11(0.95)完全注释》的作者赵炯推测是为了调试方便做的。
pushl $L6
入栈操作是模拟调用 main.c 程序时首先将返回地址入栈的操作,所以如果 main.c 程序真的退出时,就会返回到这里的标号L6 处继续执行下去,也即死循环。
pushl $_main
将 main.c 的地址压入堆栈,这样,在设置分页处理(setup_paging)结束后执行'ret'返回指令时就会将 main.c 程序的地址弹出堆栈,并去执行 main.c 程序去了。
然后就跳到setup_paging去设置分页了。
/*
* Setup_paging
*
* This routine sets up paging by setting the page bit
* in cr0. The page tables are set up, identity-mapping
* the first 16MB. The pager assumes that no illegal
* addresses are produced (ie >4Mb on a 4Mb machine).
*
* NOTE! Although all physical memory should be identity
* mapped by this routine, only the kernel page functions
* use the >1Mb addresses directly. All "normal" functions
* use just the lower 1Mb, or the local data space, which
* will be mapped to some other place - mm keeps track of
* that.
*
* For those with more memory than 16 Mb - tough luck. I've
* not got it, why should you :-) The source is here. Change
* it. (Seriously - it shouldn't be too difficult. Mostly
* change some constants etc. I left it at 16Mb, as my machine
* even cannot be extended past that (ok, but it was cheap :-)
* I've tried to show which constants to change by having
* some kind of marker at them (search for "16Mb"), but I
* won't guarantee that's all :-( )
*/
setup_paging:
movl $1024*5,%ecx /* 5 pages - pg_dir+4 page tables */
xorl %eax,%eax
xorl %edi,%edi /* pg_dir is at 0x000 */
cld;rep;stosl
movl $pg0+7,_pg_dir /* set present bit/user r/w */
movl $pg1+7,_pg_dir+4 /* --------- " " --------- */
movl $pg2+7,_pg_dir+8 /* --------- " " --------- */
movl $pg3+7,_pg_dir+12 /* --------- " " --------- */
movl $pg3+4092,%edi
movl $0xfff007,%eax /* 16Mb - 4096 + 7 (r/w user,p) */
std
1: stosl /* fill pages backwards - more efficient :-) */
subl $0x1000,%eax
jge 1b
xorl %eax,%eax /* pg_dir is at 0x0000 */
movl %eax,%cr3 /* cr3 - page directory start */
movl %cr0,%eax
orl $0x80000000,%eax
movl %eax,%cr0 /* set paging (PG) bit */
ret /* this also flushes prefetch-queue */
解释:
注释翻译,看看Linus的解释
/*
* 这个子程序通过设置控制寄存器cr0 的标志(PG 位31)来启动对内存的分页处理功能,
* 并设置各个页表项的内容,以恒等映射前16 MB 的物理内存。分页器假定不会产生非法的
* 地址映射(也即在只有4Mb 的机器上设置出大于4Mb 的内存地址)。
* 注意!尽管所有的物理地址都应该由这个子程序进行恒等映射,但只有内核页面管理函数能
* 直接使用>1Mb 的地址。所有“一般”函数仅使用低于1Mb 的地址空间,或者是使用局部数据
* 空间,地址空间将被映射到其它一些地方去 -- mm(内存管理程序)会管理这些事的。
* 对于那些有多于16Mb 内存的家伙 - 太幸运了,我还没有,为什么你会有?。代码就在这里,
* 对它进行修改吧。(实际上,这并不太困难的。通常只需修改一些常数等。我把它设置为
* 16Mb,因为我的机器再怎么扩充甚至不能超过这个界限(当然,我的机器很便宜的?)。
* 我已经通过设置某类标志来给出需要改动的地方(搜索“16Mb”),但我不能保证作这些
* 改动就行了??)。
*/
解释一下分页模式。
在没有开启分页机制时,由程序员给出的逻辑地址,需要先通过分段机制转换成物理地址。但在开启分页机制后,逻辑地址仍然要先通过分段机制进行转换,只不过转换后不再是最终的物理地址,而是线性地址,然后再通过一次分页机制转换,得到最终的物理地址。
关于开启分页模式后的地址转换,看图:
关于分页模式,可以参考一下我这篇博文
再看这段代码,其实Linus已经说明白了,这段代码就是帮我们把页表和页目录表在内存中写好,之后开启 cr0 寄存器的分页开关。但实际操作看起来还是有点麻烦的,就先囫囵吞枣的看一下吧(心虚:-).
这段子程序运行完之后,就会返回主程序了,按道理接下来就到main函数了。但为什么呢?记得我们把_main压栈了,那它是如何指向main的地址呢?
再看一下这段代码
after_page_tables:
pushl $0 # These are the parameters to main :-)
pushl $0
pushl $0
pushl $L6 # return address for main, if it decides to.
pushl $_main
jmp setup_paging
L6:
jmp L6 # main should never return here, but
# just in case, we know what happens.
解释:
setup_paging子程序最后一个指令是 ret,就是返回指令,返回到哪?
CPU 机械地把栈顶的元素值当做返回地址,跳转去那里执行。此时的栈顶元素是啥,要知道栈其实就是一个箱子,上面我们最后执行了pushl $_main
,因此此时栈顶就是main函数的内存地址。
看图:
所以,setup_paging子程序设置好分页模式后返回,就会开始执行main函数啦。
位置在<init/main.c>
上一篇
Linux0.11源码学习(二)
下一篇
Linux0.11源码学习(四)