操作系统是计算机硬件和应用之间的一层软件 。
操作系统管理哪些硬件CPU管理内存管理终端管理磁盘管理文件管理计算机工作的原理是取指执行,而指对应的就是各种指令。
计算模型,我们要关注指针IP及其指向的内容,也就是所谓的取指执行。
计算机刚开电源时,IP=多少呢?由硬件设计者决定以x86架构的pc为例
x86PC刚开机时CPU进入实模式。
开机时,CS(段地址)=0xFFFF;IP(偏移地址)=0x0000。这是设计时就决定好了
寻址物理地址0xFFFF0(ROM BIOS映射区)这是刚开机唯一有代码的地方。由BIOS读取bootsect.s。
也就是运行图解中1号的bootsect.s程序了。
段 地 址 ∗ 16 + 偏 移 地 址 = 物 理 地 址 段地址*16+偏移地址=物理地址 段地址∗16+偏移地址=物理地址
( C S < < 4 ) + I P = 物 理 地 址 (CS << 4) + IP = 物理地址 (CS<<4)+IP=物理地址
( 0 x F F F F < < 4 ) + 0 = ( 0 x F F F F 0 ) + 0 = 0 x F F F F 0 (0xFFFF << 4) + 0 = (0xFFFF0) + 0 = 0xFFFF0 (0xFFFF<<4)+0=(0xFFFF0)+0=0xFFFF0
检查RAM,键盘,显示器,软硬磁盘。
将磁盘0磁道0扇区(引导扇区512字节)读入0x7c00处。
设置cs=0x07c0,ip=0x0000
该扇区在硬盘的0柱面、0磁头、1扇区,该记录占用512个字节,它用于硬盘启动时将系统控制权转给用户指定的、在分区表中登记了某个操作系统分区。
boot扇区是启动设备的第一个扇区。启动设备信息被设置在CMOS中因此,磁盘的第一个扇区上存放着开机后执行的第一段我们可以控制的程序。bootsect.s是操作系统代码中的操作系统引导程序代码,是通过第一个扇区读入地址0x07C00的内存里。
.globl begtext,begdata,begbss,endtext,enddata,endbss .text ;文本段 begtext: .data ;数据段 begdata: .bss ;未初始化数据段 begbss: entry start ;关键字entry告诉链接器“程序入口” #BOOTSEG=0×07C0 #INITSEG=0×9000 #SETUPLEN=4 start: mov ax,#BOOTSEG ; 将ds段寄存器置为BOOTSEG=0×07C0; mov ds,ax mov ax,#INITSEG ;将es段寄存器置为INITSEG=0×9000; mov es,ax mov cx,#100 ; 移动计数值0x100=256; sub si,si ; 源地址 ds:si = 0×07C0:0×0000 sub di,di ; 目的地址 es:di = 0×9000:0×0000 rep movw ; 重复执行移动1个字,直到cx = 0 移动256个word=移动512个byte jmpi go,#INITSEG ; 间接跳转。这里cs=INITSEG,IP=go跳转到INITSEG:go。 go: mov ax,cs ; 将ds、es 和ss 都置成移动后代码所在的段处(cs=0×9000)。为call做准备 mov ds,ax mov es,ax mov ss,ax mov sp,#0xff00 load_setup: //载入setup模块 mov dx,#0x0000 mov cx,#0x0002 mov bx,#0x0200 mov ax,#0x0200+SETUPLEN ; ah=0x02功能号,al=读取几个扇区(SETUPLEN=4) int 0x13 ; BIOS中断 jnc ok_load_setup mov dx,#0x0000 mov ax,#0x0000 ;复位 int 0x13 j load_setup ;重读一一进行讲解
这个模块通过最开始的图可以清晰的看出是把bootsect.s移动到0x90000
rep movw表示移动字,移动的个数是cx=256,也就是512个字节,也就是说这段代码的作用就是将从0x07c00地址处开始的512个字节移动到0x90000处。
在下面这条指令执行之前,对应图中的1号。而当下面的指令开始到load_setup模块结束就对应运行图解中的3号了。
jmpi go,#INITSEG这条指令是间接跳转,go->ip,INITSEG->CS,前面已经说了INITSEG就是0x9000,go是一个逻辑上的概念,go表示的是距离start模块的地址偏移,现在因为已经将0x07c00处的512个字节移动到了0x90000处,所以逻辑上的start模块已经到了0x90000,go相较于start的偏移其实也就是相较于INITSEG的偏移,所以也就是顺序执行。
跳转之后就是各种对寄存器,段寄存器初始化,这一切都是为了下面的int 0x13中断功能做准备,或者说为了从硬盘上将setup.s读入内存
0x13是BIOS读磁盘扇区的中断:
下图摘自王爽《汇编语言》
axahal0x0200+SETUPLEN0x020x04int 0x13中断功能结束后,setup模块就完整地从硬盘中读入到内存了。对应运行图解中的3号
在读入setup模块后,来到ok_load_setup模块。
这里又出现了一个int 0x13来这里查查对应的功能号吧BIOS int 13H中断介绍,
mov dl,#0x00 mov ax,#0x0800 //ah=8获得磁盘/驱动器参数 int 0x13下面又有2个int 0x10中断功能,这个又是什么呢?INT 10H功能INT 10H功能
mov ch,#0x00 mov sectors,cx mov ah,#0x03 xor bh,bh int 0x10 ; 读光标 mov cx,#24 mov bx,#0x0007 mov bp,#msg1 mov ax,#1301 int 0x10 ;显示字符这里是bootsect.s中的数据
;bootsect.s中的数据 sectors: .word 0 ; 磁道扇区数 msg1:.byte 13,10 .ascii “Loading system...” .byte 13,10,13,10第一个int 10就是在读光标的位置,第二个int 10是将msg1=“Loading system…”输出到光标所在处。下面的call是读入system模块。读入system模块后
jmpi 0,SETUPSEG ; SETUPSEG=0×9020指针跳回0x90200也就是setup.s的起始位置,这里开始bootsect.s的任务就结束了,下面的事情就交给setup.s了。
setup.s是完成系统启动前设置的,它将硬件的参数存放在0x90000处,然后将system部分移动到从地址0开始的位置,对应的就是运行图解的5号了;临时建立gdt、idt表,并且从实模式进入到了保护模式(16位到32位)。
start: mov ax,#INITSEG mov ds,ax mov ah,#0x03 xor bh,bh int 0x10 ; 取光标位置dx mov [0],dx mov ah,#0x88 int 0x15 mov [2],ax ... cli ///不允许中断 mov ax,#0x0000 cld do_move: mov es,ax add ax,#0x1000 cmp ax,#0x9000 jz end_move mov ds,ax sub di,di sub si,si mov cx,#0x8000 rep ; 将system模块移到0地址 movsw jmp do_move操作系统是管理各种硬件的,要管理这些硬件必要首先要知道这些硬件到底是什么东西,是什么型号的,应该用怎样的数据结构来管理。对应运行图解的4号。
上面这段代码的作用就是获取硬件参数,然后将这些信息放在0x90000开始的地方;最后将操作系统从0x90000开始处移到地址0开始处,这就是为什么一开始要移动bootsect.s的原因,因为操作系统可能会覆盖0x07c00这个地址。
地址长度名称0x900002光标位置0x900022扩展内存数0x9000C2显卡参数0x901FC2根设备号将system模块移动到0x00000后,就是运行图解的5号了。需要特别注意的地方是最后的几行汇编
end_move: mov ax,#SETUPSEG mov ds,ax lidt idt_48 lgdt gdt_48 ;设置保护模式下的中断和寻址 进入保护模式的命令...进入保护模式代码
;8042是键盘控制器,其输出端口P2用来控制A20地址线 call empty_8042 mov al,#0xD1 out #0x64,al call empty_8042 mov al,#0xDF out #0x60,al ;选通A20地址线 call empty_8042 ;初始化8259(中断控制) 一段非常机械化的程序 mov ax,#0x0001 mov cr0,ax jmpi 0,8不得不说一下cr0寄存器。
mov ax,#0x0001 mov cr0,ax可以看到cr0是一个32位寄存器,ax为1,将ax赋值给cr0,也就是第0位/PE位为1,启动保护模式/32位模式。寻址模式发生改变。
jmpi 0,8前面提到过jmpi这条指令,意思是将前操作数赋给ip,后操作数赋给cs,然后跳转到cs、ip所表示的位置执行。
但是这里能不能这样解释呢?如果是那么就是跳到0x00080处执行,对吗?
刚刚执行完setup.s后面应该是继续执行system模块才是,system模块都被移动到0地址处了,也就是说现在system模块最开始位置是0地址处,即应该执行0x00000处代码而不是0x00080处,如果直接执行0x00080,结果肯定是死机。
所以说现在jmpi这条指令肯定不能这么解释了。这条指令应该被解释成查GDT表。
上面也提到了进入保护模式后,寻址模式发生了改变,所以下面讲一讲保护模式中该如何寻址。保护模式的另一种说法就是32位模式,换句话说保护模式的寻址方式是为了能够对32位的地址进行寻址。最多寻址4G,这里我总会想到之前逆向学习的时候听到的一句话,32位就是4GB。
lidt idt_48 ! load idt with 0,0 lgdt gdt_48 ! load gdt with whatever appropriate idt_48: .word 0 .word 0,0 ; 保护模式中断函数表 gdt_48: .word 0x800 .word 512+gdt,0x9 gdt: .word 0,0,0,0 .word 0x07FF, 0x0000, 0x9A00, 0x00C0 .word 0x07FF, 0x0000, 0x9200, 0x00C0以前CS里面放的是地址,现在CS里面放的是表的下标(称为”选择子”),这个表就是gdt(全局描述表)表,那么这个表是哪里来的呢?在setup.s的时候建立的。
把目光放到GDT表,每一行都是一个表项,每一个表项由4个word组成。但是在保护模式下jmpi 0,8中的8指的是GDT表第8个byte,或者说是第4个word。也就是第二行的表项。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QWK4u03x-1602170907757)(https://i.loli.net/2020/10/08/ywUDFgG2zNfEYlZ.jpg)]
63-48(16-31)32-47(0-15)0x00C00x9A000x00000x07FFBASE指的是包含段的首字节的线性地址。
————《深入理解Linux内核》P43
我们将BASE对应的几个部分拿出来看看
BASE(0-15)BASE(16-23)BASE(24-31)00000000刚好是一个32位的地址,众所周知32位的地址就是32位的,到这里应该就豁然开朗了。因为在16位中无法直接寻址32位地址,所以通过GDT表这样的方式来拼接出一个32位地址来寻址。
简单来说,jmpi指令应该被解释成查GDT表,而保护模式的寻址方式就是通过GDT表来寻址。
将地址指向0x00000意味着setup.s的任务也完成了。接下来的事情就交给位于0x00000的代码了。
head.s就是位于0x00000的代码了,同时也是system模块的开始。
stratup_32: movl $0x10,%eax mov %ax,%ds mov %ax,%es mov %as,%fs mov %as,%gs ; 指向gdt的0x10项(数据段) lss _stack_start,%esp ; 设置栈(系统栈) call setup_idt ; 再次初始化idt call setup_gdt ; 再次初始化gdt xorl %eax,%eax 1:incl %eax movl %eax,0x000000 cmpl %eax,0x100000 je 1b ;0地址处和1M地址处相同(A20没开启),就死循环 jmp after_page_tables ;页表,什么东东? setup_idt: lea ignore_int,%edx movl $0x00080000,%eax movw %dx,%ax lea _idt,%edi movl %eax,(%edi)在head.s里面会重新设置idt表、gdt表(call setup_idt、call_setup_gdt),前面setup里面设置的gdt和idt都是临时的;这里会重新设置。
还会开启A20地址线(je 1b),开启A20地址线之后寻址范围就是4G而不再是1M。 IDT表是中断函数表,从此int n 不再是DOS中断了,而是在IDT表中找到中断函数的地址执行, 注意是:硬件查表,不是软件,idt、gdt表的查表方法都是硬件规定好的,目的就是为了加快速度。 注意,在head.s使用的汇编又和前面bootsect、setup里面使用的汇编不一样,在head.s里面使用的是产生32位代码汇编,而bootsect、setup里面使用的是产生16位代码的汇编。另外在操作系统的.c文件里面还使用了一种汇编,叫做“内嵌汇编”。
after_page_tables: pushl $0 pushl $0 pushl $0 pushl $L6 pushl $_main jmp setup_paging L6: jmp L6 setup_paging: ; 设置页表 ret前面开启20号地址线之后就jmp到after_page_tables这个标号,在setup_paging执行完后,ret到哪里呢? 到main()函数。在after_page_tables里面将main函数三个参数、L6、main函数的入口地址都压入栈中,在setup_paging的ret直接跳**_main**,如果main函数再返回的话就跳到L6处,从上面可以看到
L6: jmp L6这是一个死循环,也就是说如果操作系统执行了这条指令,那么就会死机。main函数是不会返回的。 其实从head.s到main.c的过程和c语言里面的函数调用是一样的,首先将函数执行完之后的下一个地址和函数参数压入栈中,然后通过jmp命令跳到子函数的执行处,执行完了之后再利用ret跳到程序原来执行的地方。
main函数完成了各种硬件数据结构的初始化。永远不会退出,如果退出就死机了。
void main(void) { mem_init(); trap_init(); blk_dev_init(); chr_dev_init(); tty_init(); time_init(); sched_init(); buffer_init(); hd_init(); floppy_init(); sti(); move_to_user_mode(); if(!fork()){init();} // 这行永远不会退出 }前面说了main函数由三个参数,为什么这里没写出来呢?
main函数的三个参数为envp、argc、argv,但是此处并没有使用,所以此处的main只保留传统main形式。
从main函数内容可以看到,main函数的工作就是init:内存、中断、设备、时钟、CPU等内容的初始化。
这里介绍内存的初始化函数
void mem_init(long start_mem,long end_mem) { int i; 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; }其实这个函数就是初始化mem_map这个数组,start_men、end_men这些参数都是在setup的时候就获取到的。可以回头翻看setup.s的start模块存储了哪些硬件参数。
其实bootsect.s 、setup.s 、heads.s 、main.c这些文件就做了两件事:
读入操作系统并移动到合适的位置.初始化(为每一个硬件建立数据结构、并初始化)