硬盘里放着啥
- 把bootsect.s编译成 bootsect 放在硬盘的 1 扇区。bootsect.s负责各种从硬盘到内存的加载,以及内存到内存的复制。
- 把setup.s编译成setup放在硬盘的 2~5 扇区。
- 把剩下的全部代码(head.s 作为开头)编译成system放在硬盘的随后240个扇区。
bootsect.s文件:加载操作系统文件
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
mov ax,#BOOTSEG
mov ds,ax
mov ax,#INITSEG
mov es,ax
mov cx,#256
sub si,si
sub di,di
rep movw
jmpi go,INITSEG
go:
mov ax,cs
mov ds,ax
mov es,ax
! put stack at 0x9ff00.
mov ss,ax
mov sp,#0xFF00
! arbitrary value >>512
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
! 省略非主逻辑代码
......
mov ax,#SYSSEG
mov es,ax ! segment of 0x010000
call read_it
......
! after that (everyting loaded), we jump to
! the setup-routine loaded directly after
! the bootblock:
jmpi 0,SETUPSEG
BIOS 将操作系统代码加载到内存0x7c00,通过mov指令将默认的数据段寄存器ds寄存器的值改为0x07c0方便以后进行基址寻址。
mov ax,#BOOTSEG
:将0x07c0复制到ax寄存器中。mov ds,ax
:将ax的值复制到ds段寄存器中,ds是一个16位的段寄存器,具体表示数据段寄存器,在内存寻址时充当段基址的作用。当我们之后用汇编语言写一个内存地址时,其实只写了偏移地址,实际地址=ds的值+偏移地址。
将内存地址0x7c00处开始往后的512字节的数据,原封不动复制到0x90000处。
mov ax,#INITSEG
:将0x9000复制到ax寄存器中。mov es,ax
:将ax的值复制到es段寄存器中,es为附加段寄存器。mov cx,#256
:将256(十进制)复制到ax寄存器中。sub si,si
:即si = si - si,si置0。sub di,di
:即di = di - 地,di置0。rep movw
:rep表示重复执行后面的指令,movw 表示复制一个字(16位),重复执行256次(cx寄存器中的值),从ds:si处复制到es:di。
跳转到此处往后偏移go这个标签所代表的偏移地址处
jmpi go,INITSEG
:跳转到0x90000 + go这个内存地址处执行,即跳转到mov ax,cs
命令。
初步内存规划:将数据段寄存器 ds 和代码段寄存器 cs 设置为了 0x9000,方便代码的跳转与数据的访问。并将栈顶地址 ss:sp 设置在了离代码的位置 0x90000 足够遥远的 0x9FF00,保证栈向下发展不会轻易撞见代码的位置。复制值, ds,es和ss寄存器被赋值为0x9000。
mov ax,cs
:将cs的值复制到ax寄存器中。cs寄存器表示代码段寄存器,由于段间跳转指令:jmpi go,INITSEG
,cs寄存器里的值就是0x9000,ip寄存器里的值是go标签的偏移地址。cs:ip这组寄存器配合指向的是CPU当前正在执行的代码在内存中的位置,其中cs是基址,ip是偏移地址。mov ds,ax
:将ax的值复制到ds寄存器中。mov es,ax
:将ax的值复制到es寄存器中。mov ss,ax
:将ax的值复制到ss寄存器中,ss寄存器存放栈的段地址。mov sp,#0xFF00
:将0xFF00复制到sp寄存器中,SP(堆栈寄存器,stack pointer)存放栈的偏移地址。目前的栈顶地址就是ss:sp所指向的地址0x9FF00。
从硬盘的第2个扇区开始,把数据加载到内存0x90200处,共加载4个扇区。
mov dx,#0x0000
:将0x0000复制到ax寄存器中。mov cx,#0x0002
:将0x0002复制到ax寄存器中。mov bx,#0x0200
:将0x0200复制到ax寄存器中。mov ax,#0x0200 + SETUPLEN
:将0x0200 + SETUPLEN复制到ax寄存器中。int 0x13
:int是汇编指令,int 0x13
表示发起0x13号中断,dx,cx,bx,ax的值都将作为中断程序的参数。中断发起后,CPU会通过这个中断号,去寻找对应的中断处理程序的入口地址,并跳转过去执行,0x13 号中断的处理程序是BIOS提前给我们写好的,是读取磁盘的相关功能的函数。进入操作系统内核后,中断处理程序需要我们自己写。jnc ok_load_setup
:如果复制成功,就跳转到ok_load_setup这个标签,如果失败,则会不断重复执行这段代码。
将硬盘第6个扇区开始往后的240个扇区,加载到内存0x10000处。
call read_it
:CALL,汇编代码,指令用于调用其他函数。
跳转执行
jmpi 0,SETUPSEG
:跳转到内存中的0x90200处的代码,也就是硬盘中第二个扇区的代码。即setup.s文件的第一行代码。
setup.s文件:进入保护模式之前做的事
信息获取
读取光标位置
mov ax,#INITSEG ! this is done in bootsect already, but... mov ds,ax mov ah,#0x03 ! read cursor pos xor bh,bh int 0x10 ! save it in known place, con_init fetches mov [0],dx ! it from 0x90000.
int 0x10
:此中断触发BIOS提供的显示服务中断处理程序,而ah寄存器被赋值为0x03表示显示服务里具体的读取光标位置功能。int 0x10中断程序执行完毕并返回时,dx寄存器里的值表示光标的位置,其高八位dh存储了行号,低八位dl存储了列号(计算机在加电自检后会自动初始化到文字模式,在这种模式下,一屏幕可以显示 25 行,每行 80 个字符,也就是 80 列)。mov [0],dx
:把光标位置存储在[0]这个内存地址处。即0x9000处(加ds偏移地址)。
获取内存信息,原理同读取光标位置
mov ah,#0x88 int 0x15 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
存储位置:最终存储在内存中的信息的位置
内存地址 长度(字节) 名称 0x90000 2 光标位置 0x90002 2 扩展内存数 0x90004 2 显示页面 0x90006 1 显示模式 0x90007 1 字符列数 0x90008 2 未知 0x9000A 1 显示内存 0x9000B 1 显示状态 0x9000C 2 显卡特性参数 0x9000E 1 屏幕行数 0x9000F 1 屏幕列数 0x90080 16 硬盘1参数表 0x90090 16 硬盘2参数表 0x901FC 2 根设备号 关闭中断:我们之后要将BIOS写好的中断向量表给覆盖掉,写上我们自己的中断向量表,这个时不允许中断进入。
cli ! no interrupts allowed !
改变内存布局:把内存地址0x10000处开始往后一直到0x90000的内容,复制到内存的最开始的0位置;从0x90000地址开始记录着内存、硬盘、显卡等一些临时存放的数据;栈顶地址仍然是0x9FF00没有改变。
mov ax,#0x0000 cld ! 'direction'=0, movs moves forward do_move: mov es,ax ! destination segment add ax,#0x1000 cmp ax,#0x9000 jz end_move mov ds,ax ! source segment sub di,di sub si,si mov cx,#0x8000 rep movsw jmp do_move
cmp ax,#0x9000
:为第一个操作减去第二个操作数,但不影响第两个操作数的值,它影响flag的CF,ZF,OF,AF,PF,ZF=1 则说明两个数相等。jz end_move
:通过判断ZF标志位决定是否跳转,当执行到JZ或者JE指令时,如果ZF=1则跳转,如果ZF=0,不跳转。- rep movsw:内存地址0x10000处开始往后一直到0x90000的内容,复制到内存的最开始的0位置。这部分内容被system模块给占用,system 模块是除了bootsect和setup之外的全部程序链接在一起的结果,可以理解为操作系统的全部。
模式的转换:从16位的实模式转变为32位的保护模式。
end_move: mov ax,#SETUPSEG ! right, forgot this at first. didn't work :-) mov ds,ax lidt idt_48 ! load idt with 0,0 lgdt gdt_48 ! load gdt with whatever appropriate ...... gdt: .word 0,0,0,0 ! dummy .word 0x07FF ! 8Mb - limit=2047 (2048*4096=8Mb) .word 0x0000 ! base address=0 .word 0x9A00 ! code read/exec .word 0x00C0 ! granularity=4096, 386 .word 0x07FF ! 8Mb - limit=2047 (2048*4096=8Mb) .word 0x0000 ! base address=0 .word 0x9200 ! data read/write .word 0x00C0 ! granularity=4096, 386 idt_48: .word 0 ! idt limit=0 .word 0,0 ! idt base=0L gdt_48: .word 0x800 ! gdt limit=2048, 256 GDT entries .word 512+gdt,0x9 ! gdt base = 0X9xxxx
lgdt gdt_48
:加载全局描述符。在实模式下,ds寄存器里存储的值叫做段基址,在保护模式下叫段选择子,段选择子里存储着段描述符的索引,通过段描述符索引,可以从全局描述符表gdt中找到一个段描述符,段描述符里存储着段基址。计算机通过lgdt gdt_48
指令加载全局描述符,把值(gdt_48)放在gdtr寄存器中。lgdt指令用于加载全局描述符表(gdt)寄存器,其操作数格式与lidt指令的相同。全局描述符表中的每个描述符项(8字节)描述了保护模式下数据和代码段(块)的信息。其中包括段的最大长度限制(16位)、段的线性基址(32位)、段的特权级、段是否在内存、读写许可以及其它一些保护模式运行的标志。gdt_48
:此处表示一个48位的数据,其中高32位存储着的正是全局描述符表gdt的内存地址,即0x90200 + gdt。gdt
:此处是全局描述符表在内存中的真正数据。目前全局描述符表有三个段描述符,第一个为空,第二个是代码段描述符(type=code),第三个是数据段描述符(type=data)lidt idt_48
:加载中断描述符表,原理同全局描述符表gdt。
打开 A20 地址线:突破地址信号线20位的宽度,变成32位可用。
! that was painless, now we enable A20 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 ....... empty_8042: .word 0x00eb,0x00eb in al,#0x64 ! 8042 status port test al,#2 ! is input buffer full? jnz empty_8042 ! yes - loop ret
对可编程中断控制器8259芯片进行编程(看不懂)。
! well, that went ok, I hope. Now we have to reprogram the interrupts :-( ! we put them right after the intel-reserved hardware interrupts, at ! int 0x20-0x2F. There they won't mess up anything. Sadly IBM really ! messed this up with the original PC, and they haven't been able to ! rectify it afterwards. Thus the bios puts interrupts at 0x08-0x0f, ! which is used for the internal hardware interrupts as well. We just ! have to reprogram the 8259's, and it isn't fun. 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 ! well, that certainly wasn't fun :-(. Hopefully it works, and we don't ! need no steenking BIOS anyway (except for the initial loading :-). ! The BIOS-routine wants lots of unnecessary data, and it's less ! "interesting" anyway. This is how REAL programmers do it. !
经过重新编程后的芯片的引脚与中断号的对应关系:
PIC 请求号 中断号 用途 IRQ0 0x20 时钟中断 IRQ1 0x21 键盘中断 IRQ2 0x22 接连从芯片 IRQ3 0x23 串口2 IRQ4 0x24 串口1 IRQ5 0x25 并口2 IRQ6 0x26 软盘驱动器 IRQ7 0x27 并口1 IRQ8 0x28 实时钟中断 IRQ9 0x29 保留 IRQ10 0x2a 保留 IRQ11 0x2b 保留 IRQ12 0x2c 鼠标中断 IRQ13 0x2d 数学协处理器 IRQ14 0x2e 硬盘中断 IRQ15 0x2f 保留 切换模式:从实模式切换到保护模式,将cr0这个寄存器的位0置1,就从实模式切换到保护模式了。
! Well, now's the time to actually move into protected mode. To make ! things as simple as possible, we do no register set-up or anything, ! we let the gnu-compiled 32-bit programs do that. We just jump to ! absolute address 0x00000, in 32-bit protected mode. mov ax,#0x0001 ! protected mode (PE) bit lmsw ax ! This is it!
跳转到0x00执行:bin(8) = 0b00000000000001000,即描述符索引值是1。跳转到段基址是0,偏移也是0的地方执行,即内存地址的0地址处。零地址处以head.s文件开始。
jmpi 0,8 ! jmp offset 0 of segment 8 (cs)
现在内存是什么模样。
内存 地址 数据段描述符 代码段描述符 setup.s代码 0x90200 系统参数 0x90000 ...... 0x80000 库模块 内存管理模块 内核模块 main.c head.s 0x00000
head.s文件:保护模式之后又该干什么
从这里开始,内核完全都是在保护模式下运行了。heads.s汇编程序与前面的语法格式不同,它采用的是AT&T的汇编语言格式,并且需要使用GNU的gas和gld2进行编译连接且代码中赋值的方向是从左到右。这段程序实际上处于内存绝对地址0处开始的地方。这个程序的功能比较单一:
首先是加载各个数据段寄存器.
pg_dir: .globl startup_32 startup_32: movl $0x10,%eax mov %ax,%ds mov %ax,%es mov %ax,%fs mov %ax,%gs lss stack_start,%esp
- _pg_dir:表示页目录,之后在设置分页机制时,页目录会存放在这里,也会覆盖这里的代码。
- 连续五个mov操作:对于GNU汇编来说,每个直接数要以$开始,否则是表示地址。这里已经处于32位运行模式,因此这里的$0x10并不是把地址0x10装入各个段寄存器,它现在其实是全局段描述符表中的偏移值,或者更正确地说是一个描述符表项的选择符。这里分别给 ds,es,fs,gs 这几个段寄存器赋值为0x10,根据段描述符结构解析,表示这几个段寄存器的值为指向全局描述符表中的第二个段描述符,也就是数据段描述符。
- lss指令让ss:esp栈顶指针指向了_stack_start标号的位置。原来的栈顶指针在0x9FF00。
重新设置中断描述符表idt, 共256项,并使各个表项均指向一个只报错误的哑中断程序:中断描述符表 idt里面存储着一个个中断描述符,每一个中断号就对应着一个中断描述符,而中断描述符里面存储着主要是中断程序的地址,这样一个中断号过来后,CPU就会自动寻找相应的中断程序,然后去执行它。
call setup_idt ...... 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 ...... 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 // 该函数在/kernel/printk.c中。 popl %eax pop %fs pop %es pop %ds popl %edx popl %ecx popl %eax iret ...... idt_descr: .word 256*8-1 # idt contains 256 entries .long idt
然后重新设置全局描述符表gdt:因为原来设置的gdt是在setup程序中,之后这个内存区域要被缓冲区覆盖掉,所以重新设置在head程序中,这块内存区域之后不会被其他程序用到并覆盖。
setup_gdt: lgdt gdt_descr ret
- 和之前设置好的gdt一模一样。也是有代码段描述符和数据段描述符,然后第四项系统段描述符并没有用到。最后还留了 252 项的空间,这些空间后面会用来放置任务状态段描述符TSS和局部描述符LDT。
接着使用物理地址0与1M开始处的内容相比较的方法,检测A20地址线是否已真的开启(如果没有开启,则在访问高于1Mb物理内存地址时CPU实际只会访问(IP MOD 1Mb)地址处的内容),如果检测下来发现没有开启,则进入死循环。
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 xorl %eax,%eax incl %eax # check that A20 really IS enabled movl %eax,0x000000 # loop forever if it isn't cmpl %eax,0x100000 je 1b
然后程序测试PC机是否含有数学协处理器芯片(80287、 80387 或其兼容芯片),并在控制寄存器CR0中设置相应的标志位。
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
接着设置管理内存的分页处理机制,将页目录表放在绝对物理地址0开始处(也是本程序所处的物理内存位置,因此这段程序将被覆盖掉),紧随后面放置共可寻址16MB内存的4个页表,并分别设置它们的表项。
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 ...... .org 0x1000 pg0: .org 0x2000 pg1: .org 0x3000 pg2: .org 0x4000 pg3: .org 0x5000 ...... 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 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 */
- 在没有开启分页机制时,由程序员给出的逻辑地址,需要先通过分段机制转换成物理地址。但在开启分页机制后,逻辑地址仍然要先通过分段机制进行转换,只不过转换后不再是最终的物理地址,而是线性地址,然后再通过一次分页机制转换,得到最终的物理地址。
- CPU 在看到我们给出的内存地址后,首先把线性地址被拆分成:高 10 位:中间 10 位:后 12 位,高 10 位负责在页目录表中找到一个页目录项,这个页目录项的值加上中间 10 位拼接后的地址去页表中去寻找一个页表项,这个页表项的值,再加上后 12 位偏移地址,就是最终的物理地址。
- 这一切的操作,都由计算机硬件MMU(内存管理单元)来负责将虚拟地址转换为物理地址。
- 开启分页机制,更改cr0寄存器中的第31位。
- linux-0.11认为,总共可以使用的内存不会超过16M,也即最大地址空间为0xFFFFFF。而按照当前的页目录表和页表这种机制,1个页目录表最多包含1024个页目录项(也就是1024个页表),1 个页表最多包含 1024 个页表项(也就是1024个页),1 页为 4KB(因为有 12 位偏移地址),因此,16M的地址空间可以用1个页目录表 + 4个页表搞定。
xorl %eax,%eax
,movl %eax,%cr3
:告诉 cr3 寄存器,0 地址处就是页目录表,再通过页目录表可以找到所有的页表。
最后利用返回指令将预先放置在堆栈中的init/main.c程序的入口地址弹出,去运行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会很机械地把栈顶的元素值当做返回地址,跳转去那里执行。
- 把esp寄存器(栈顶地址)所指向的内存处的值,赋值给eip寄存器,而cs:eip就是CPU要执行的下一条指令的地址。而此时栈顶刚好是main.c里写的main函数的内存地址,是我们特意压入栈的,所以CPU就理所应当跳过来了。
总结
三个汇编文件的总流程
谁在处理 流程 干了啥 开机 BIOS 加载启动区 bootsect.s 加载setup.s bootsect.s 加载内核 setup.s 分段 全局描述符GDT setup.s 进入保护模式 head.s 中断机制 中断描述符表IDT head.s 分页机制 页目录与页表 head.s 跳转到内核 main.c 内存布局
数据项 地址 栈顶 sched.c文件中 setup.s 0x90200 临时存放的变量 0x90000 ...... 0x80000 操作系统全部代码 页表3 页表2 页表1 页表0 页目录 0x00000
Comments | NOTHING