阅读赵先生的Linux0.11内核分析有一段时间了,最近决定要分析2.6内核中的内存分配,但发觉基础不足,所以回头把Linux0.11中内存管理的部分又看了一下,写下学习笔记以加强自己的理解.
写的不对的地方请大家一定要拍砖指正~ = 3=)/
学习的框架如下:
1.80386的分段和分页管理
2.80386的保护模式
3.Linux0.11的初始化,主要分析内存管理和使用部分
下面将按Linux的启动过程进行分析
80386上电之后进行BIOS的自检,自检完成后将软驱或者硬盘中的引导程序拷贝到0x7C00中,并跳转到这个程序之中,这个时候80386处于实模式中.
Linux0.11中这个引导程序为Bootsect.s
刚进入Bootsect.s中时的寄存器值如下:
EAX : 0xAA55 ECX : 0xF0001 EDX : 0x0 EBX : 0x0 ESP : 0xFFFE EBP : 0x0 ESI : 0x733F EDI : 0xFFDE EIP : 0x7C00 EFLAGS : 0x282 CS : 0x0 SS : 0x0 DS : 0x0 ES : 0x0 FS : 0x0 GS : 0x0
|
Bootsect.s的代码如下:
SYSSIZE = 0x3000
.globl begtext, begdata, begbss, endtext, enddata, endbss .text begtext: .data begdata: .bss begbss: .text
SETUPLEN = 4 ! nr of setup-sectors BOOTSEG = 0x07c0 ! original address of boot-sector INITSEG = 0x9000 ! we move boot here - out of the way SETUPSEG = 0x9020 ! setup starts here SYSSEG = 0x1000 ! system loaded at 0x10000 (65536). ENDSEG = SYSSEG + SYSSIZE ! where to stop loading ROOT_DEV = 0x306
entry start start: //取得自检完成后CPU执行引导程序的首地址 mov ax,#BOOTSEG //将该地址设为数据段的段基址 mov ds,ax //取得bootsect.s将复制到的地址 mov ax,#INITSEG //将该地址设为附加段的段基址 mov es,ax //设置计数器为256 mov cx,#256 //清零si寄存器 -> ds:si = 0x07C0:0x0000 sub si,si //清零di寄存器 -> es:di = 0x9000:0x0000 sub di,di //直到cx为0之前重复执行movw rep //拷贝ds:si所指的数据到es:di //每拷贝1次,si di自增 , 每次拷贝一个字 movw //跳跃到INITSEG的偏移go的位置上 //执行完之后cs为INITSEG,ip为go //也就是跳转到复制的bootsect.s中继续执行 jmpi go,INITSEG go: //取得代码段寄存器cs的值 //也就是INITSEG,0x9000 mov ax,cs //将cs的值赋给数据段寄存器ds mov ds,ax //将cs的值赋给附加段寄存器es mov es,ax //将cs的值赋给堆栈指针寄存器ss mov ss,ax //设置堆栈指针偏移寄存器sp的值为0xFF00 //则栈空间为0x90000 - 0x9FF00 mov sp,#0xFF00 ! arbitrary value >>512 //加载setup.s程序到地址0x90200中 load_setup: mov dx,#0x0000 ! drive 0, head 0 mov cx,#0x0002 ! sector 2, track 0 mov bx,#0x0200 ! address = 512, in INITSEG mov ax,#0x0200+SETUPLEN ! service 2, nr of sectors int 0x13 ! read it jnc ok_load_setup ! ok - continue mov dx,#0x0000 mov ax,#0x0000 ! reset the diskette int 0x13 j load_setup ok_load_setup: mov dl,#0x00 mov ax,#0x0800 ! AH=8 is get drive parameters int 0x13 mov ch,#0x00 seg cs mov sectors,cx mov ax,#INITSEG mov es,ax mov ah,#0x03 ! read cursor pos xor bh,bh int 0x10 mov cx,#24 mov bx,#0x0007 ! page 0, attribute 7 (normal) mov bp,#msg1 mov ax,#0x1301 ! write string, move cursor int 0x10 mov ax,#SYSSEG mov es,ax ! segment of 0x010000 call read_it call kill_motor seg cs mov ax,root_dev cmp ax,#0 jne root_defined seg cs mov bx,sectors mov ax,#0x0208 ! /dev/ps0 - 1.2Mb cmp bx,#15 je root_defined mov ax,#0x021c ! /dev/PS0 - 1.44Mb cmp bx,#18 je root_defined undef_root: jmp undef_root root_defined: seg cs mov root_dev,ax //加载完成,跳转到setup.s中 //0x90200也就是0x9020:0 jmpi 0,SETUPSEG sread: .word 1+SETUPLEN ! sectors read of current track head: .word 0 ! current head track: .word 0 ! current track read_it: mov ax,es test ax,#0x0fff die: jne die ! es must be at 64kB boundary xor bx,bx ! bx is starting address within segment rp_read: mov ax,es cmp ax,#ENDSEG ! have we loaded all yet? jb ok1_read ret ok1_read: seg cs mov ax,sectors sub ax,sread mov cx,ax shl cx,#9 add cx,bx jnc ok2_read je ok2_read xor ax,ax sub ax,bx shr ax,#9 ok2_read: call read_track mov cx,ax add ax,sread seg cs cmp ax,sectors jne ok3_read mov ax,#1 sub ax,head jne ok4_read inc track ok4_read: mov head,ax xor ax,ax ok3_read: mov sread,ax shl cx,#9 add bx,cx jnc rp_read mov ax,es add ax,#0x1000 mov es,ax xor bx,bx jmp rp_read read_track: push ax push bx push cx push dx mov dx,track mov cx,sread inc cx mov ch,dl mov dx,head mov dh,dl mov dl,#0 and dx,#0x0100 mov ah,#2 int 0x13 jc bad_rt pop dx pop cx pop bx pop ax ret bad_rt: mov ax,#0 mov dx,#0 int 0x13 pop dx pop cx pop bx pop ax jmp read_track kill_motor: push dx mov dx,#0x3f2 mov al,#0 outb pop dx ret sectors: .word 0 msg1: .byte 13,10 .ascii "Loading system ..." .byte 13,10,13,10 .org 508 root_dev: .word ROOT_DEV boot_flag: .word 0xAA55 .text endtext: .data enddata: .bss endbss:
|
Bootsect.s首先将自身复制到地址0x90200中,并跳转到复制后的地址中执行,如下图所示:
执行jmpi go,INITSEG后就由开始的Bootsect.s跳转到复制后的Bootsect.s中的标号go处继续执行.
然后Bootsect.s把Setup.s从磁盘中读取到内存位置0x90200处,如下图所示:
加载完Setup.s后在屏幕上打印"Loading system ...".
接着把SYSTEM,也就是LINUX0.11的内核读取到内存位置0x10000处,如下图所示:
然后使用指令jmpi 0,SETUPSEG跳转到0x90200地址处的第一条指令继续执行,也就是进入到了Setup.s中
刚进入Setup.s中时的寄存器值如下:
EAX : 0x301 ECX : 0x111600 EDX : 0xE00 EBX : 0x0 ESP : 0xFF00 EBP : 0x13F ESI : 0x200 EDI : 0xEFDF EIP : 0x0 EFLAGS : 0x202 CS : 0x9020 SS : 0x9000 DS : 0x9000 ES : 0x4000 FS : 0x0 GS : 0x0
|
Setup.s的代码如下:
INITSEG = 0x9000 ! we move boot here - out of the way SYSSEG = 0x1000 ! system loaded at 0x10000 (65536). SETUPSEG = 0x9020 ! this is the current segment
.globl begtext, begdata, begbss, endtext, enddata, endbss .text begtext: .data begdata: .bss begbss: .text
entry start start: //设置ax为0x9000,也就是bootsect.s的起始地址 mov ax,#INITSEG ! this is done in bootsect already, but... //将该地址赋给数据段寄存器ds mov ds,ax //设置ah为0x03,为读取光标位置做准备 mov ah,#0x03 ! read cursor pos //清零bh xor bh,bh //启用10号BOIS中断中的0x03号功能来读取数据 int 0x10 ! save it in known place, con_init fetches //将读取到得数据保存在 ds:0 中 , 也就是 9000:0 -> 0x90000 mov [0],dx ! it from 0x90000. //设置ah为0x88,为读取内存大小做准备 mov ah,#0x88 //启动15号BIOS中断中的0x88号功能来读取数据 int 0x15 //将读取到的数据保存在 ds:2 中,也就是9000:2 -> 0x90002 mov [2],ax
mov ah,#0x0f int 0x10 mov [4],bx ! bh = display page mov [6],ax ! al = video mode, ah = window width mov ah,#0x12 mov bl,#0x10 int 0x10 mov [8],ax mov [10],bx mov [12],cx mov ax,#0x0000 mov ds,ax lds si,[4*0x41] mov ax,#INITSEG mov es,ax mov di,#0x0080 mov cx,#0x10 rep movsb mov ax,#0x0000 mov ds,ax lds si,[4*0x46] mov ax,#INITSEG mov es,ax mov di,#0x0090 mov cx,#0x10 rep movsb mov ax,#0x01500 mov dl,#0x81 int 0x13 jc no_disk1 cmp ah,#3 je is_disk1 no_disk1: mov ax,#INITSEG mov es,ax mov di,#0x0090 mov cx,#0x10 mov ax,#0x00 rep stosb is_disk1: //禁止中断 cli ! no interrupts allowed ! //设置ax为0x0000,这也是system模块将要复制到的位置 mov ax,#0x0000 //设置si和di的递增方向为向前 cld ! 'direction'=0, movs moves forward do_move: //设置附加段寄存器的值为ax mov es,ax ! destination segment //ax的值自增0x1000 add ax,#0x1000 //检测ax的值是否达到0x9000 cmp ax,#0x9000 //达到则跳到end_move jz end_move //将数据段寄存器的值设为ax mov ds,ax ! source segment //清零di sub di,di //清零si sub si,si //设置计数寄存器的值为0x8000 , 拷贝0x8000个字 , 在8086中也就是64k字节,每字2个字节 mov cx,#0x8000 //直到cx为0之前重复执行movsw rep //拷贝ds:si的数据到es:di , si di自增 , 每次拷贝一个字 (movsw和movw一样?) movsw //跳回到do_move jmp do_move //拷贝system模块完成 end_move: //设置ax的值为SETUPSEG , 也就是0x9020 mov ax,#SETUPSEG ! right, forgot this at first. didn''t work :-) //设置数据段寄存器为SETUPSEG,也就是0x9020 mov ds,ax //加载中断描述符表地址为idt_48 lidt idt_48 ! load idt with 0,0 //加载全局描述表地址为gdt_48 lgdt gdt_48 ! load gdt with whatever appropriate call empty_8042 mov al,#0xD1 ! command write out #0x64,al call empty_8042 mov al,#0xDF ! A20 on out #0x60,al call empty_8042
mov al,#0x11 ! initialization sequence out #0x20,al ! send it to 8259A-1 .word 0x00eb,0x00eb ! jmp $+2, jmp $+2 out #0xA0,al ! and to 8259A-2 .word 0x00eb,0x00eb mov al,#0x20 ! start of hardware int''s (0x20) out #0x21,al .word 0x00eb,0x00eb mov al,#0x28 ! start of hardware int''s 2 (0x28) out #0xA1,al .word 0x00eb,0x00eb mov al,#0x04 ! 8259-1 is master out #0x21,al .word 0x00eb,0x00eb mov al,#0x02 ! 8259-2 is slave out #0xA1,al .word 0x00eb,0x00eb mov al,#0x01 ! 8086 mode for both out #0x21,al .word 0x00eb,0x00eb out #0xA1,al .word 0x00eb,0x00eb mov al,#0xFF ! mask off all interrupts for now out #0x21,al .word 0x00eb,0x00eb out #0xA1,al //设置保护模式比特位 mov ax,#0x0001 ! protected mode (PE) bit //加载机器状态字 lmsw ax ! This is //跳跃到临时全局表中的第2项中 //8转换为段选择符格式为1000,低3位为属性 //Index部分为1,也就是0x1,第2个描述符 //0x0为第1个描述符 jmpi 0,8 ! jmp offset 0 of segment 8 (cs) empty_8042: .word 0x00eb,0x00eb in al,#0x64 ! 8042 status port test al,#2 ! is input buffer full? jnz empty_8042 ! yes - loop ret gdt: //全局表的第1项为空 .word 0,0,0,0 ! dummy //全局表的第2项,这里为代码段描述符 //因为0代表4KB,所以2048-1=2047 .word 0x07FF ! 8Mb - limit=2047 (2048*4096=8Mb) //基地址为0 .word 0x0000 ! base address=0 // P=1,S=1,TYPE=1010 .word 0x9A00 ! code read/exec // G=1,D/B=1 .word 0x00C0 ! granularity=4096, 386 //全局表的第3项,这里为数据段描述符 //因为0代表4KB,所以2048-1=2047 .word 0x07FF ! 8Mb - limit=2047 (2048*4096=8Mb) //基地址为0 .word 0x0000 ! base address=0 // P=1,S=1,TYPE=0010 .word 0x9200 ! data read/write // G=1,D/B=1 .word 0x00C0 ! granularity=4096, 386 idt_48: //限长为0 .word 0 ! idt limit=0 //基地址为0 .word 0,0 ! idt base=0L gdt_48: //256个描述符,每个8字节,256*8 = 2048字节 .word 0x800 ! gdt limit=2048, 256 GDT entries //基地址为0x90200 + gdt (0x200 = 512) -> (SETUPSEG) + gdt .word 512+gdt,0x9 ! gdt base = 0X9xxxx .text endtext: .data enddata: .bss endbss:
|
Setup.s首先读取BIOS自检时设置好的内存,显示卡,硬盘等信息,保存到内核中的对应地址中,然后将System模块从0x10000处移动到0x00000处,如下图所示:
然后准备进入保护模式之前的处理,首先加载一个临时的GDT表和设置IDT表基址寄存器,因为在进入保护模式之前关闭了中断,所以再开启中断之前不会读取IDT表的项目,所以把IDTR的基地址设置成0x0也不用担心会产生错误,如下图所示:
加载完成后便开启保护模式,然后跳到全局描述符表中的第2个描述符的偏移0x0处继续执行,第2个描述符为代码段描述符,其基地址为0x0,呢么就是执行物理地址0x0处的指令,setup.s程序之前将System模块移动到了0x0地址处,而System模块中的head.s代码处于模块头,也就是在0x0地址上,所以这里会执行head.s的代码.
这里介绍一下实模式和保护模式寻址的不同.
在实模式中寻址分为段地址和偏移地址,段提供一个0x0-0xFFFF的范围,偏移地址在这个范围内进行定位,段地址由段寄存器中的值向左移动4位得出.
例如要表示0x90200这个地址,可以写成0x9000:0x200,0x9000向左移动4位得0x90000,再加上偏移地址0x200,就是0x90000+0x200=0x90200,也可以写成0x9020:0x0,0x9020向左移动4位得0x90200,再加上偏移地址0x0,就是0x90200+0=0x90200.
而在保护模式中,寻址依然分为段地址和偏移地址,不过段地址不再由段寄存器直接给出,段寄存器给出的是一个索引值,要在一个表中根据这个索引值得出段地址.
例如0x8:0x0,0x8换成2进制为1000,其中低3位为索引的属性,呢么Index就是1,也就是说0x8表示取表中的第1个段描述符,假设该段描述符提供的段地址为0x1000,呢么0x8:0x0就是寻址0x1000+0x0=0x1000.
刚进入head.s中时的寄存器值如下:
EAX : 0x1 ECX : 0x110000 EDX : 0x1181 EBX : 0x3 ESP : 0xFF00 EBP : 0x13F ESI : 0x0 EDI : 0x0 EIP : 0x0 EFLAGS : 0x46 CS : 0x8 SS : 0x9000 DS : 0x9020 ES : 0x8000 FS : 0x0 GS : 0x0
|
head.s的代码如下:
/* * linux/boot/head.s * * (C) 1991 Linus Torvalds */ .text .globl _idt,_gdt,_pg_dir,_tmp_floppy_area _pg_dir: startup_32: //将eax寄存器的值设置为0x10 //0x10,换算成段描述符也就是10000,低3位为属性 //也就是index段为10,也就是0x2,也就是第3个描述符 movl $0x10,%eax //设置数据段寄存器的值为0x10,也就是数据描述符 mov %ax,%ds //设置附加段寄存器的值为0x10,也就是数据描述符 mov %ax,%es //设置附加数据段寄存器fs的值为0x10,也就是数据描述符 mov %ax,%fs //设置附加数据段寄存器gs的值为0x10,也就是数据描述符 mov %ax,%gs //设置堆栈指针指向_stack_start lss _stack_start,%esp //设置中断描述符表 call setup_idt //设置全局描述符表 call setup_gdt //因为更改了全局描述表基地址寄存器 //需要重新加载一次段寄存器 //将eax寄存器的值设置为0x10 movl $0x10,%eax # reload all the segment registers //设置数据段寄存器的值为0x10,也就是数据描述符 mov %ax,%ds # after changing gdt. CS was already //设置附加段寄存器的值为0x10,也就是数据描述符 mov %ax,%es # reloaded in 'setup_gdt' //设置附加数据段寄存器fs的值为0x10,也就是数据描述符 mov %ax,%fs //设置附加数据段寄存器gs的值为0x10,也就是数据描述符 mov %ax,%gs //设置堆栈指针指向_stack_start lss _stack_start,%esp 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 movl %cr0,%eax # check math chip andl $0x80000011,%eax # Save PG,PE,ET orl $2,%eax # set MP movl %eax,%cr0 call check_x87 jmp after_page_tables check_x87: fninit fstsw %ax cmpb $0,%al je 1f /* no coprocessor: have to set bits */ movl %cr0,%eax xorl $6,%eax /* reset MP, set EM */ movl %eax,%cr0 ret .align 2 1: .byte 0xDB,0xE4 /* fsetpm for 287, ignored by 387 */ ret setup_idt: //设置edx寄存器的值为ignore_int函数的地址 lea ignore_int,%edx //设置eax寄存器的值为0x00080000 , 也就是段选择符为0x0008 , 偏移地址的0-15位为0x0 movl $0x00080000,%eax //设置偏移地址的0-15位为edx中的低16位也就是dx中的值 movw %dx,%ax /* selector = 0x0008 = cs */ //设置P=1,DPL=0,D=1,TYPE=110,为中断门 movw $0x8E00,%dx /* interrupt gate - dpl=0, present */ //设置edi寄存器的值为_idt的地址,也就是中段描述符表的地址 lea _idt,%edi //设置计数寄存器的值为256 mov $256,%ecx rp_sidt: //设置edi所指地址的值为eax movl %eax,(%edi) //设置edi所指地址+4的地址的值为edx movl %edx,4(%edi) //使edi指向下一个中断描述符 addl $8,%edi //减少计数寄存器 dec %ecx //检测计数寄存器是否为0,不为0则跳回到rp_sidt jne rp_sidt //装载中断描述符寄存器 lidt idt_descr //返回到调用setup_idt的地方 ret setup_gdt: //装载全局描述符寄存器 lgdt gdt_descr //返回到调用setup_gdt的地方 ret .org 0x1000 pg0: .org 0x2000 pg1: .org 0x3000 pg2: .org 0x4000 pg3: .org 0x5000 _tmp_floppy_area: .fill 1024,1,0 after_page_tables: //压入main的参数envp pushl $0 # These are the parameters to main :-) //压入main的参数argv pushl $0 //压入main的参数argc pushl $0 //压入main的返回地址,地址为L6 pushl $L6 # return address for main, if it decides to. //压入main的地址,当执行ret的时候就会转入到main函数中 pushl $_main jmp setup_paging L6: jmp L6 # main should never return here, but # just in case, we know what happens. int_msg: .asciz "Unknown interrupt\n\r" .align 2 ignore_int: pushl %eax pushl %ecx pushl %edx push %ds push %es push %fs movl $0x10,%eax mov %ax,%ds mov %ax,%es mov %ax,%fs pushl $int_msg call _printk popl %eax pop %fs pop %es pop %ds popl %edx popl %ecx popl %eax iret .align 2 setup_paging: //5个页表,一共1024*5个页面,设置计数寄存器 movl $1024*5,%ecx /* 5 pages - pg_dir+4 page tables */ //清零eax xorl %eax,%eax //清零edi xorl %edi,%edi /* pg_dir is at 0x000 */ //拷贝eax的值到edi的地址上,直到ecx为0,也就是清零所有页帧 cld;rep;stosl // P=1,R/W=1,U/S=1,pg0地址为0x1000,其中低12位用于存储页属性,实际为0x1007 movl $pg0+7,_pg_dir /* set present bit/user r/w */ // P=1,R/W=1,U/S=1,pg1地址为0x2000,其中低12位用于存储页属性,实际为0x2007 movl $pg1+7,_pg_dir+4 /* --------- " " --------- */ // P=1,R/W=1,U/S=1,pg2地址为0x3000,其中低12位用于存储页属性,实际为0x3007 movl $pg2+7,_pg_dir+8 /* --------- " " --------- */ // P=1,R/W=1,U/S=1,pg3地址为0x4000,其中低12位用于存储页属性,实际为0x4007 movl $pg3+7,_pg_dir+12 /* --------- " " --------- */ //设置edi指向pg3页表的最后一页 movl $pg3+4092,%edi //设置页的地址为16MB中的最后一页,属性为P=1,R/W=1,U/S=1 movl $0xfff007,%eax /* 16Mb - 4096 + 7 (r/w user,p) */ //方向位向前,edi向低地址移动 std //拷贝eax中的内容到es:edi所指向的地址中,数据长度为l->long 1: stosl /* fill pages backwards - more efficient :-) */ //减少一页,每页为4K字节 subl $0x1000,%eax //当eax大于或者等于0则向前跳转到符号1处 jge 1b //清零eax xorl %eax,%eax /* pg_dir is at 0x0000 */ //清零cr3控制寄存器,也就是设置CR3中的页目录表基地址为0x0,指向_pg_dir movl %eax,%cr3 /* cr3 - page directory start */ //读取cr0中的数据到eax中 movl %cr0,%eax //置PG标志为1 orl $0x80000000,%eax //将置位后的eax回写到cr0中,这时候开始就启动分页了 movl %eax,%cr0 /* set paging (PG) bit */ //跳到之前压入的main函数中 ret /* this also flushes prefetch-queue */ .align 2 .word 0 idt_descr: //设置限长,每个中段描述符为8个字节,中段描述符256个,呢么大小就是256*8 .word 256*8-1 # idt contains 256 entries //设置基地址为_idt .long _idt .align 2 .word 0 gdt_descr: //设置限长,每个描述符为8个字节,描述符256个,呢么大小就是256*8 .word 256*8-1 # so does gdt (not that that''s any //设置基地址为_gdt .long _gdt # magic number, but it works for me :^) .align 3 //中段描述符表 //256项,每项8字节,每项填充为0 _idt: .fill 256,8,0 # idt is uninitialized _gdt: //第1项为空 .quad 0x0000000000000000 /* NULL descriptor */ //第2项为系统代码描述符 // G=1,D/B=1 // P=1,S=1,TYPE=1010 // 基地址为0 // 因为0代表4KB,(4096 - 1)*4KB = 16MB .quad 0x00c09a0000000fff /* 16Mb */ //第3项为系统数据描述符 // G=1,D/B=1 // P=1,S=1,TYPE=0010 // 基地址为0 // 因为0代表4KB,(4096 - 1)*4KB = 16MB .quad 0x00c0920000000fff /* 16Mb */ //第4项为空 .quad 0x0000000000000000 /* TEMPORARY - don't use */ //252项,每项8字节,每项填充为0 .fill 252,8,0 /* space for LDT's and TSS's etc */
|
head.s首先初始化中断描述符表中的项,然后设置IDTR,完成后设置新的GDT表中的项,然后重新设置GDTR,使其指向新的GDT表,如下图:
然后head.s将main函数的参数和返回地址压入栈中,跳转到分页初始化中,Linux0.11在head.s中预留了5张页,每张页1024项,第1张页用来填写页目录项,其余4张页填写页表项,每张页可寻址4MB地址空间,4张页表寻址16MB,也就是Linux0.11默认支持的最大内存大小,如下图:
完成之后设置CR3寄存器为0x0,也就是页目录表的基地址.
分页设置完成后打开分页属性,之后保护模式下的地址经过分段处理后还要进行分页处理.
最后将执行中断返回,跳转到之前压入的main函数中.
介绍一下分页的寻址方法,分页的寻址方法和保护模式下的寻址方法差不多,也是进行查表寻址,在分页管理中,把32位的地址分成了3个部分:
1. 偏移地址:0-11位.
2. 页表索引号:12-21位.
3. 页目录索引号:22-31位.
举个例子, 0x00405008,将这个地址拆成2进制,就是0000 0000 0100 0000 0101 0000 0000 1000,从右往左计算,0到11位为偏移地址,呢么偏移地址就是0x8,12到21位为页表号,呢么页表号就是0x5,22位到31位为页目录号,呢么页目录号就是0x4.
寻址过程如下:首先取得页目录表的基地址,该地址存在CR3中,假设CR3的值为0x0,然后根据页目录表的基地址(0x0)和页目录号(0x4)计算对应的页目录项,在页目录项中取得页表的基地址, 假设0x4号页目录中的页表基地址为0x4000,然后根据页表的基地址(0x1000)和页表号(0x5)计算对应的页表项, 在页表项中取得页面的基地址, 假设0x4号页表中的页面基地址为0x9000,呢么最后0x00405008所指的物理地址为0x9000+0x8 = 0x9008,过程如下图所示:
main函数的代码如下:
void main(void) { //指向地址0x901FC,这个地址保存了根文件系统所在设备号 ROOT_DEV = ORIG_ROOT_DEV; //指向地址0x90080,这个地址保存了硬盘参数表基址 drive_info = DRIVE_INFO; //保存在0x90002地址处的数据为扩展内存的大小,单位为1KB //这里计算内存的大小 //计算的方法为1MB+扩展内存的大小*1KB memory_end = (1<<20) + (EXT_MEM_K<<10); //最小单位为1KB,舍弃不足1KB的部分 memory_end &= 0xfffff000; //检测内存大小是否大于16MB if (memory_end > 16*1024*1024) //大于16MB则只要16MB memory_end = 16*1024*1024; //检测内存大小是否大于12MB if (memory_end > 12*1024*1024) //大于12MB则设置缓冲区的结束位置为 4MB处 buffer_memory_end = 4*1024*1024; //小于12MB则检测是否大于6MB else if (memory_end > 6*1024*1024) //大于6MB则设置缓冲区的结束位置为2MB处 buffer_memory_end = 2*1024*1024; //小于6MB else //设置缓冲区的结束位置为1MB处 buffer_memory_end = 1*1024*1024; //设置主内存的起始位置为缓冲区的结束位置 main_memory_start = buffer_memory_end; #ifdef RAMDISK main_memory_start += rd_init(main_memory_start, RAMDISK*1024); #endif //初始化内存管理 mem_init(main_memory_start,memory_end); trap_init(); blk_dev_init(); chr_dev_init(); tty_init(); time_init(); //初始化调度程序 sched_init(); buffer_init(buffer_memory_end); hd_init(); floppy_init(); //打开中断 sti(); //切换到task0中继续执行接下来的代码 move_to_user_mode(); //创建一个新进程task1完成init函数 if (!fork()) { /* we count on this going ok */ init(); } //task0负责进程调度 for(;;) pause(); }
|
main函数首先根据内存的不同大小设置主内存区域的开始和结束地址对于不同的内存大小,LINUX0.11对于主内存区实现了3种不同的分配方案:
1. 内存大小在12MB到16MB范围之内,则主内存区从4MB开始到最大.
2. 内存大小在6MB到12MB范围之内,则主内存区从2MB开始到最大.
3. 内存大小在6MB之内,则主内存区从1MB开始到最大.
在以后的分析中我们假设内存的大小为16MB,不使用RAMDISK,之后的初始化函数中主要关注mem_init ,sched_init, sti, move_to_user_mode和fork.
首先进入到mem_init中,mem_init的代码如下:
void mem_init(long start_mem, long end_mem) { int i;
//设置内存地址的结束位置 HIGH_MEMORY = end_mem; //历遍内存管理数组,进行初始化 for (i=0 ; i<PAGING_PAGES ; i++) //设置为已使用 mem_map[i] = USED; //计算主内存区域的起始位置在第几个页帧 i = MAP_NR(start_mem); //计算主内存区域的大小 end_mem -= start_mem; //计算主内存区域占用多少个页 end_mem >>= 12; //历遍主内存区域的页 while (end_mem-->0) //设置内存管理数组对应的页为未使用 mem_map[i++]=0; }
|
在LINUX0.11中使用一个mem_map的unsigned char数组来管理内存的分配状态,这个数组用于管理物理内存地址1M以上的页面,其中的每一项都对应内存中的一个页面, mem_map中有3840项,最大可管理3840*4KB=15MB的内存,对于物理内存不足16MB的情况,LINUX0.11将mem_map中对应的项设置为已使用,不进行分配,从而在逻辑上消除了不对称的影响.
上图展示了一个拥有15MB内存时候mem_map的映像图,低于4MB,也就是内核区域设置为已使用,不进行分配,高于15MB,也就是高于物理内存的部分也设置为已使用,主内存区域设置为0,也就是未使用.
首先将mem_map中的项全部设置为已使用,如下图
然后根据主内存区域的起始位置和结束位置将mem_map数组中的对应项设置为未使用,如下图
mem_init完成后来到sched_init中, sched_init的代码如下:
void sched_init(void) { int i; struct desc_struct * p;
if (sizeof(struct sigaction) != 16) panic("Struct sigaction MUST be 16 bytes"); //将全局描述符表中的第5项设为init_task.task.tss set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss)); //将全局描述符表中的第6项设为init_task.task.ldt set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt)); //指向全局描述符表中的第7项 p = gdt+2+FIRST_TSS_ENTRY; //初始化进程管理数组 for(i=1;i<NR_TASKS;i++) { task[i] = NULL; //初始化tss描述符,清零 p->a=p->b=0; p++; //初始化ldt描述符,清零 p->a=p->b=0; p++; } //清除NT标志,这样在之后执行中断返回的时候不会导致嵌套执行 //将flag寄存器的值压栈 //pushfl; //修改栈中刚压进的flag的值,置NT标志为0 //andl $0xffffbfff,(%esp) ; //弹出修改的值给flag寄存器 //popfl __asm__("pushfl ; andl $0xffffbfff,(%esp) ; popfl"); //将任务0的tss描述符装载到任务寄存器tr中 ltr(0); //将任务0的ldt描述符装载到局部描述符表寄存器中 lldt(0); //初始化8253定时器 outb_p(0x36,0x43); /* binary, mode 3, LSB/MSB, ch 0 */ outb_p(LATCH & 0xff , 0x40); /* LSB */ outb(LATCH >> 8 , 0x40); /* MSB */ //设置时钟中断处理函数 set_intr_gate(0x20,&timer_interrupt); //设置中断控制器,允许时钟中断 outb(inb_p(0x21)&~0x01,0x21); //设置系统调用处理函数 set_system_gate(0x80,&system_call); }
|
在分析sched_init前先分析一下TSS(任务状态段描述符)和LDT(局部段描述符表).
TSS(任务状态段描述符)用于保存任务状态,任务状态的结构如下:
struct tss_struct { //前一进程任务的TSS的描述符的地址 long back_link; //存放进程任务在特权级0运行时的堆栈指针 long esp0; long ss0; //存放进程任务在特权级1运行时的堆栈指针 long esp1; long ss1; //存放进程任务在特权级2运行时的堆栈指针 long esp2; long ss2; //页目录基地址寄存器 long cr3; //指令指针 long eip; //标志寄存器 long eflags; //通用寄存器 long eax,ecx,edx,ebx; //变址寄存器 long esp; long ebp; long esi; long edi; //段寄存器 long es; long cs; long ss; long ds; long fs; long gs; //任务的LDT选择符 long ldt; //I/O比特位图的基地址 long trace_bitmap; //协处理器信息 struct i387_struct i387; };
|
任务状态保存了任务运行时的寄存器信息,这样在任务切换中就能迅速得到原先任务的状态,并恢复,继续执行原本的指令流.
LDT(局部段描述符表)是全局段描述符表的补充,用于存放任务自己的段描述符信息,如何判断一个索引值是LDT中的项还是GDT中的项取决于索引值中的TI属性.
索引,也就是段选择符的格式如下:
1. RPI : 0-1位 : 请求特权级.
2. TI : 2位 : 当TI为0时,说明使用的是GDT,当TI为1时,说明使用的是LDT.
3. Index : 3-15位 : 段描述符的索引号.
举个例子,0x8,转换成2进制就是1000,呢么该索引使用GDT表中的第0x1项;0xC,转换成2进制就是1100,呢么该索引使用LDT表中的第0x1项.
init_task是Linux0.11中静态分配好的任务,他处于任务结构数组task中的第0项,所以俗称task0.
sched_init首先设置GDT表中的第5项指向task0的TSS,第6项指向task0的LDT.
set_tss_desc是一个宏,代码如下:
#define set_tss_desc(n,addr) _set_tssldt_desc(((char *) (n)),addr,"0x89")
|
set_ldt_desc也是一个宏,代码如下:
#define set_ldt_desc(n,addr) _set_tssldt_desc(((char *) (n)),addr,"0x82")
|
他们都调用了_set_tssldt_desc, _set_tssldt_desc的代码如下:
#define _set_tssldt_desc(n,addr,type) \ __asm__ ( //设置段限长的0-15位为0x68 "movw $104,%1\n\t" \ //设置基地址的0-15位为eax的低16位 "movw %%ax,%2\n\t" \ //将eax高16位的内容移动到低16位中 "rorl $16,%%eax\n\t" \ //设置基地址的16-23位为eax低16位中的低8位 "movb %%al,%3\n\t" \ //设置TYPE为type,P,DPL,S为0 "movb $" type ",%4\n\t" \ //设置G,D/B,保留,AVL和段限长的16-19位为0 "movb $0x00,%5\n\t" \ //设置基地址的16-23位为eax低16位中的高8位 "movb %%ah,%6\n\t" \ //清零eax "rorl $16,%%eax" \ //eax中存储addr //%1表示地址n,也就是段限长的0-15位 //%2表示地址n偏移2个字节,也就是基地址的0-15位 //%3表示地址n偏移4个字节,也就是基地址的16-23位 //%4表示地址n偏移5个字节,也就是P,DPL,S,TYPE //%5表示地址n偏移6个字节,也就是G,D/B,保留,AVL和段限长的16-19位 //%6表示地址n偏移7个字节,也就是基地址的24-31位 ::"a" (addr), "m" (*(n)), "m" (*(n+2)), "m" (*(n+4)), \ "m" (*(n+5)), "m" (*(n+6)), "m" (*(n+7)) \ )
|
设置完成后的GDT表如下: