1.设置寄存器gdtr寄存器
cld
lgdt boot_gdt_descr - __PAGE_OFFSET
//ds=0x18 boot_gdt_descr=0xc033103a
// boot_gdt_descr-__PAGE_OFFSET=0X33103a
//
//0x1060001f 0x00000033
// gdtr=0x331060 0x33
movl $(__BOOT_DS),%eax
movl %eax,%ds
movl %eax,%es
movl %eax,%fs
movl %eax,%gs
boot_gdt_descr是一个标签,定义在本文件中。从System.map中符号表:
c033103a D boot_gdt_descr。boot_gdt_descr - __PAGE_OFFSET就是物理地址,从这个地址可以找到boot_gdt_descr的定义。
我的疑问是:
boot_gdt_descr - __PAGE_OFFSET,就是boot_gdt_descr的入口地址,怎么对应boot_gdt_descr的入口地址?
boot_gdt_descr:
.word __BOOT_DS+7 //0x18+7=0x001f
.long boot_gdt_table - __PAGE_OFFSET //00331060
.word 0 # 32-bit align idt_desc.address//0x00 00
从System.map中符号表:c0331060 D boot_gdt_table。这样boot_gdt_descr的值如注释所示。
2. 清空BBS
/*
* Clear BSS first so that there are no surprises...
* No need to cld as DF is already clear from cld above...
*/
xorl %eax,%eax
movl $__bss_start - __PAGE_OFFSET,%edi //0x003e3000
movl $__bss_stop - __PAGE_OFFSET,%ecx //c03fe0fc A __bss_stop 0x004160fc
subl %edi,%ecx //ecx=ecx-edi 0x330fc
shrl $2,%ecx //右移两个字节,相当于除以4 0xcc3f
rep ; stosl //清空BBS
__bss_start和__bss_stop定义在harch/i386/kernel/vmlinux.lds.S中
从System.map中符号表:0xc03e3000 A __bss_start,知道__bss_start是c03fe0fc中。
3内核临时页表
3.1产生背景
当内核被解压到线性地址0x100000后,为了继续启动内核,即启动内核的第一进程即swapper进程,内核需要建立一张临时页表供其使用。
3.2临时页表的功能
假设内核使用的段、临时页表和128K的内存范围能容纳于RAM前8MB寻址。
分页第一阶段的目标是允许在实模式和保护模式下都能很容易对8MB寻址;也
就是内,内核必须创建一个映射,把0x0000 0000到0x0007 ffff的线性地址和0xC000 0000到 0xC07f ffff的线性地址映射到0x0000 0000到0x007f ffff的物理地址。临时页表的主要功能就是完成这种映射。
举个例子:
0x0005 088C和0xC005 08CC怎么完成相同的对物理映射。
线性地址的格式
页目录表偏移 | 页表偏移 | 页内偏移 |
---|---|---|
10 bit (pgdt_offset) | 10 bit(paget_offset) | 12 bit(page_offset) |
线性地址 : 0x0050 088c (0000 0000 0101 0000 0000 1000 1000 1100)
所以:
pgdt_offset=0000 0000 01=0x1 paget_offset=01 0000 0000=0x100
page_offset=1000 1000 1100=0x88c
根据下面示意图,
因为表项 pgdt_offset=0x1,指向0x418 000那个页表,然后paget_offset=0x100,指向页表的第0x100项目,
value=0x40 0007+n0x1000=0x40 0007+0x1000x1000=0x500007
指向0x500 000的页,页内偏移量page_offset=0x88c,所以指向物理地址:
0x500 000+0x88c=0x50088c
线性地址 : 0xc050 088c (1100 0000 0101 0000 0000 1000 1000 1100)
pgdt_offset=1100 0000 01= 11 0000 0001=0x301=769
paget_offset=01 0000 0000=0x1 page_offset=1000 1000 1100=0x88c
因为表项 pgdt_offset=769,指向0x418 000那个页表,然后paget_offset=0x100,指向页表的第0x100项目,
value=0x40 0007+n0x1000=0x40 0007+0x1000x1000=0x500007
指向0x500 000的页,页内偏移量page_offset=0x88c,所以指向物理地址:
0x500 000+0x88c=0x50088c
这样就完成用户空间地址0x50088c和内核地址0xc050088c都指向了同一个地址0x50088c的映射。
说明:这就是768和769的偏移是内核的页目录项,也就是0xc00,因为只取10位,也就是0xc00向右偏移两位,得到0x300,这样得到768和769。
3.3 代码实现
page_pde_offset = (__PAGE_OFFSET >> 20); //
movl $(pg0 - __PAGE_OFFSET), %edi //0x417 000
movl $(swapper_pg_dir - __PAGE_OFFSET), %edx //0x3e3 000
movl $0x007, %eax /* 0x007 = PRESENT+RW+USER */
10:
leal 0x007(%edi),%ecx /* Create PDE entry */
movl %ecx,(%edx) /* Store identity PDE entry */
movl %ecx,page_pde_offset(%edx) /* Store kernel PDE entry */
addl $4,%edx
movl $1024, %ecx
11:
stosl
addl $0x1000,%eax
loop 11b
/* End condition: we must map up to and including INIT_MAP_BEYOND_END */
/* bytes beyond the end of our own page tables; the +0x007 is the attribute bits */
leal (INIT_MAP_BEYOND_END+0x007)(%edi),%ebp
cmpl %ebp,%eax
jb 10b
movl %edi,(init_pg_tables_end - __PAGE_OFFSET)
标签10,是用来创建页目录项。
标签11,是用来创建页码项。
会进入标签10第1次,标签10会分别创建第0项和第768项,都是pg0,然后标签11里会创建pg0指向的1024个页项。然后再进入10次,然后再创建第1项和第769项,都是pg1,然后在标签11里创建pg1指向的1024个页项。
11里的循环控制是通过 loop 11b,每次ecx=ecx-1,ecx不为0则跳转到标号11处。
10的循环控制是通过cmpl %ebp,%eax是否相等.
在标签11里,
假设地址为addr,填充的值为value,填充项个数为N,第一个地址为addr1,addr1里的值为val1则会有下面公式:
addr=addr1+(N-1)*4
value=val1+(N-1)*0x1000
第一次进入标签11,addr1=ecx=0x417 000 val1=0x0000 0007,则进行了1024次后;
addr= 0x417 000+(1024-1)*4=0x417ffc
value=0x7+(1024-1)*0x1000=0x3FF007
第二次进入标签11,edi随着stosl在增加,则edi=1024*4+0x417 000=0x418 000
addr1=ecx=0x418 000 val1=0x400 007,则进行了1024次后:
addr= 0x418 000+(1024-1)*4=0x418ffc
value=0x400 007+(1024-1)*0x1000=0x7FF007
画出如下的图:
说明:
说明:
1)PDE就是页目录项,页目录通过swapper_pg_dir: .fill 1024,4,0生成了1024项,每个项4个字节,每项填充0,第0项和第1项,第0x300项和第0x301项;分别填充了值.
2)swapper_pg_dir的第一项是0x4f2007,前20位是页表的物理地址, 12位是属性,就是0x4f200,从箭头可以看出;第二项是0x4f3007, 前20位是页表的物理地址,就是0x4f300,从箭头指向可以看出.页表pg0的第一项是0x7,指向的页框0x0,后12位是属性,7是属性.
3)edi在进行第二次进入页表后,则edi=10244+0x417 000+10244=0x419 000
下面语句:movl %edi,(init_pg_tables_end - __PAGE_OFFSET) ,就是跳过swapper_pg_dir开始页目录和两个页表(就是8M的页表)。swapper_pg_dir之前是__init_end,__init_end之前包括内核映像的text和rwdata段等等。
在init_pg_table_end的值跳过了内核的text和rwdata和临时页目录和页表。
4. 打开分页
/*
* Enable paging
*/
movl $swapper_pg_dir-__PAGE_OFFSET,%eax //eax=3e3 000
movl %eax,%cr3 /* set the page table pointer.. */ //cr3=3e3 000
movl %cr0,%eax //eax= 6 00000 11
orl $0x80000000,%eax //eax=e 00000 11
movl %eax,%cr0 /* ..and set paging (PG) bit */ //cr0=e 00000 11
ljmp $__BOOT_CS,$1f /* Clear prefetch and normalize %eip */
在内存中存放了,临时页表,需要把目录的基地址放到CR3中,然后开启分页,硬件MMU单元,会把每个输入的线性地址转化为物理地址进行访问。
可以看到bochs的线性地址从10002等地址变成0xC001 0002。
CR0的第31位是控制分页。
5. 设置堆栈
1:
/* Set up the stack pointer */
lss stack_start,%esp //*(331000)=0xc03b 4000 esp=0xc03b 4000
6. 设置中断描述表IDT
中断描述表IDT
当通过INT指令进入一个中断服务程序时,在指令中中给出一个中断向量。CPU先根据该向量的中断向量中找到一扇门(描述项),在这种情况下一般是中断门。然后,将这个门的DPL与CPU的CPL相比,CPL必须小于或等于DPL,才能穿过这扇门,执行这个门中的中断处理函数。
中断描述表储存在内存中,中断描述表项的结构如上图。
这个结构64 bit(8个字节),最高16 bit(63:48)和最低16 bit(15-0) ,存储的中断相应的入口地址。
47:32= 1000 1110 P=1 DPL=0 0 D=1 110-中断门
31:16=段选择符
setup_idt:
lea ignore_int,%edx //把中断处理程序的地址放到edx中
movl $(__KERNEL_CS << 16),%eax //把__KERNEL_CS放到eax的高16位
movw %dx,%ax /* selector = 0x0010 = cs *///把edx的低16位放到eax的低16位
movw $0x8E00,%dx /* interrupt gate - dpl=0, present *///edx的低16位写入0x8E00
//这样 edx存放的中断描述项的高32位,eax存放中断描述项的低32位
lea idt_table,%edi //把idt_table标签指向的地址放到edi中
mov $256,%ecx //256次循环代表 初始化256个描述项
rp_sidt:
movl %eax,(%edi) //写入中断描述项的低32位
movl %edx,4(%edi) //写入中断描述项的高32位
addl $8,%edi
dec %ecx
jne rp_sidt
ret
idt_descr:
.word IDT_ENTRIES*8-1 # idt contains 256 entries 256*8-1指定IDT的大小
.long idt_table //中断描述项的地址
什么时候会用到idt_descr这个变量?
就是当lidt汇编指令初始化idtr寄存器采用到这个变量。
中断响应函数:
/* This is the default interrupt "handler" :-) */
ALIGN
ignore_int:
cld
pushl %eax
pushl %ecx
pushl %edx
pushl %es
pushl %ds
movl $(__KERNEL_DS),%eax
movl %eax,%ds
movl %eax,%es
pushl 16(%esp)
pushl 24(%esp)
pushl 32(%esp)
pushl 40(%esp)
pushl $int_msg
call printk
addl $(5*4),%esp
popl %ds
popl %es
popl %edx
popl %ecx
popl %eax
iret
int_msg:
.asciz "Unknown interrupt or fault at EIP %p %p %p\n"
这个中断处理程序,是一个空的处理程序,主要做了如下工作:
1)在栈中保存一些寄存器的内容
2)调用printk()函数打印"Unknown interrupt or fault at EIP"系统消息
3)从栈中恢复寄存器的内容
4)执行iret指令以恢复被中断的程序
7. 启动参数的拷贝
启动参数是从0x90000长度为0x800。0x90000的数据如下:
0x90000-0x9001FF:是boosect的二进制,以前bootsect放在0x90000,然后覆盖应该覆盖的。
0x900200以后会存放从setup.S中存放的数据比如,e820信息,硬盘,BIOS信息等。以便后面调用使用。
0x9001FF-0x9023f:也是bzImae里面的,可能是boot参数struct linux_kernel_header结构体,该结构体里头说明了这个镜像使用的boot协议版本、实模式大小、加载标记位和需要GRUB填写的一些参数(比如:内核启动参数地址)。这些数值来自于setup.o中的头。
0x90240-0x907ff:为0
0x90800起放setup.o文件内容。
/*
* Copy bootup parameters out of the way.
* Note: %esi still has the pointer to the real-mode data.
*/
movl $boot_params,%edi //edi=0xc03d8ea0 0x90000
movl $(PARAM_SIZE/4),%ecx //ecx=0x200
cld
rep
movsl
movl boot_params+NEW_CL_POINTER,%esi
andl %esi,%esi
jnz 2f # New command line protocol
cmpw $(OLD_CL_MAGIC),OLD_CL_MAGIC_ADDR
jne 1f
movzwl OLD_CL_OFFSET,%esi
addl $(OLD_CL_BASE_ADDR),%esi
2:
movl $saved_command_line,%edi
movl $(COMMAND_LINE_SIZE/4),%ecx
rep
movsl
8. 跳转start_kernel
call start_kernel
杂的
1:
checkCPUtype:
movl $-1,X86_CPUID # -1 for no CPUID initially
/* check if it is 486 or 386. */
/*
* XXX - this does a lot of unnecessary setup. Alignment checks don't
* apply at our cpl of 0 and the stack ought to be aligned already, and
* we don't need to preserve eflags.
*/
movb $3,X86 # at least 386
pushfl # push EFLAGS
popl %eax # get EFLAGS
movl %eax,%ecx # save original EFLAGS
xorl $0x240000,%eax # flip AC and ID bits in EFLAGS
pushl %eax # copy to EFLAGS
popfl # set EFLAGS
pushfl # get new EFLAGS
popl %eax # put it in eax
xorl %ecx,%eax # change in flags
pushl %ecx # restore original EFLAGS
popfl
testl $0x40000,%eax # check if AC bit changed
je is386
movb $4,X86 # at least 486
testl $0x200000,%eax # check if ID bit changed
je is486
/* get vendor info */
xorl %eax,%eax # call CPUID with 0 -> return vendor ID
cpuid
movl %eax,X86_CPUID # save CPUID level
movl %ebx,X86_VENDOR_ID # lo 4 chars
movl %edx,X86_VENDOR_ID+4 # next 4 chars
movl %ecx,X86_VENDOR_ID+8 # last 4 chars
orl %eax,%eax # do we have processor info as well?
je is486
movl $1,%eax # Use the CPUID instruction to get CPU type
cpuid
movb %al,%cl # save reg for future use
andb $0x0f,%ah # mask processor family
movb %ah,X86
andb $0xf0,%al # mask model
shrb $4,%al
movb %al,X86_MODEL
andb $0x0f,%cl # mask mask revision
movb %cl,X86_MASK
movl %edx,X86_CAPABILITY
is486: movl $0x50022,%ecx # set AM, WP, NE and MP
jmp 2f
is386: movl $2,%ecx # set MP
2: movl %cr0,%eax
andl $0x80000011,%eax # Save PG,PE,ET
orl %ecx,%eax
movl %eax,%cr0
call check_x87
incb ready
lgdt cpu_gdt_descr
lidt idt_descr
ljmp $(__KERNEL_CS),$1f
1: movl $(__KERNEL_DS),%eax # reload all the segment registers
movl %eax,%ss # after changing gdt.
movl $(__USER_DS),%eax # DS/ES contains default USER segment
movl %eax,%ds
movl %eax,%es
xorl %eax,%eax # Clear FS/GS and LDT
movl %eax,%fs
movl %eax,%gs
lldt %ax
cld # gcc2 wants the direction flag cleared at all times
把中断描述符和GDT加载到相关寄存器。
ps:之前的启动过程调试都是基于bochs调试的,下面可以基于qemu调试了。