引言:偶尔在爱课程网上找到一个关于操作系统的高质量课程,课程内容深邃且细致,在此准备做一系列关于此课程的学习历程,有兴趣的朋友可以登录下述网址学习:点击打开链接 本文主要参考了《Linux内核完全注释》,同时鉴于本人没有汇编基础,很多地方可能存在理解性错误,还望能予以斧正,谢谢!
一.什么是操作系统?
1.是计算机硬件和应用之间的一层软件
1).方便我们使用硬件,比如使用显存...
2).高效地使用硬件,比如打开多个终端
2.管理硬件
cpu管理,内存管理,终端管理,磁盘管理,文件管理,网络管理,电源管理,多核管理等
二.操作系统启动
1.计算机概要
1).1946年提出的冯·诺依曼存储程序思想
2).存储程序主要思想:将程序和数据存放到计算机内部的存储器中,计算机在程序的控制下一步一步进行处理
3).计算机由五大部件组成:输入设备、输出设备、存储器、运算器、控制器
2.计算机启动的第一段代码
1).x86 PC刚开机时CPU处于实模式(real-mode)
2).开机时,CS=0xFFFF;IP=0x0000
3).寻址0xFFFF0(ROM BIOS映射区)
4).检查RAM,键盘,显示器,软盘硬盘
5).将磁盘0磁道0扇区读入0x7c00
6).设置cs=0x07c0,ip=0x0000
该过程的目的即:跳至引导扇区执行
实模式:Intel公司80286及以后的x86兼容处理器的一种操作模式,特殊定义为20位地址内存可访问空间(物理内存和BIOS-ROM)。实模式下处理器没有硬件级的内存保护概念和多道任务的工作模式。由于8086/8088使用寄存器为16位,其实际表示的内存只有64K,因此Intel采用分段寻址模式即:物理地址=(cs)左移4位段地址+(ip)偏移地址。
3.0x7c00处存放的代码
1).引导扇区就是启动设备的第一个扇区
2).启动设备信息被设置在CMOS中
3).磁盘第一个扇区上存放着开机后执行的第一段我们可以控制的程序
CMOS:互补金属氧化物半导体(64B-128B)。是主板上的一块可读写的并行或串行FLASH芯片,是用来保存BIOS的硬件配置和用户对某些参数的设定
4.bootsect.s
到此bootsect执行完成,总结其流程大致是:
1).在PC机加电,ROM BIOS自检后,将引导扇区代码bootsect加载到内存地址0x7c00开始执行,之后bootsect将自己移动到内存的绝对地址0x90000处继续执行。
2).把磁盘第二个扇区开始的四个扇区的setup模块加载到内存紧接着bootsect后面的0x90200,利用int 0x13中断读取磁盘参数表中当前启动引导盘的参数,在屏幕上显示logo.
3).再将磁盘上setup模块后面的system加载到内存0x10000开始处
4).最后长跳转到setup程序开始处执行setup模块
.globl begtext, begdata, begbss, endtext, enddata, endbss // 定义了6个全局标识符 .text // 文本段 begtext: .data // 数据段 begdata: .bss // 未初始化数据段 begbss: .text // 文本段 SETUPLEN = 4 // 程序的扇区数(setup-sectors)值 BOOTSEG = 0x07c0 // bootsect 的原始地址(段地址) INITSEG = 0x9000 // 将bootsect移到这里; SETUPSEG = 0x9020 // 程序从这里开始 ENDSEG = SYSSEG + SYSSIZE // 停止加载的段地址 entry start // 关键字entry告诉链接器"程序入口" start: mov ax, #BOOTSEG mov ds, ax // 将ds段寄存器置为0x7c0 mov ax, #INITSEG mov es, ax // 将es段寄存器置为0x900 mov cx, #256 // 设置移动计数值256字 sub si, si sub di, di // 源地址 ds:si = 0x07c0:0x0000 // 目的地址 es:di = 0x9000:0x0000 rep movw // 从源地址处移动512字节到目的地址 jmpi go, INITESG // 跳转到段地址INITSEG,段内地址go的位置 go: // cs = 0x9000 mov ax,cs mov ds,ax mov es,ax mov ss,ax // 设置堆栈 mov sp,#0xFF00 // 利用BIOS中断 int 0x13 将setup模块从磁盘第2个扇区读到0x90300开始处,共读入4个扇区(因此此时第一个扇区已经被bootsect占据) // 如果读出错,复位驱动器,并重试 // ah = 0x02 -读磁盘扇区到内存 // al = 需要读出的扇区数量(SETUPLEN = 4) // ch = 柱面号 // cl = 开始扇区 // dh = 磁头号 // dl = 驱动器号 // es:bx = 内存地址 load_setup: // 读入setup模块 mov dx,#0x0000 mov cx,#0x0002 mov bx,#0x0200 mov ax,#0x0200+SETUPLEN int 0x13 // BIOS中断 jnc ok_load_setup mov dx,#0x0000 mov ax,#0x0000 // 复位 int 0x13 j load_setup // 重读 ok_load_setup: // 载入setup模块 mov dl,#0x00 mov ax,#0x0800 // ah = 8 获得磁盘参数 int 0x13 mov ch,#0x00 mov sectors,cx mov ah,#0x03 xor bh,bh int 0x10 // 读光标 mov cx,#24 mov bx,#0x0007 // 共显示24个字符 mov bp,#msg1 mov as,#1301 int 0x10 // 显示字符 // 将 system 模块加载到0x10000(64KB)开始处 mov ax,#SYSSEG // SYSSEG = 0x1000 mov es,ax call read_it // 读入system 模块 jmpi 0,SETUPSE // 转交控制权给setup read_it: mov ax,es cmp ax,#ENDSEG jb okl_read ret okl_read: mov ax,sectors sub ax,sread // sread为当前磁道已读扇区数,ax未读扇区数 call read_track // 读磁道
其中各个模块在磁盘上的分布情况入下图所示
5.setup.s
start: mov ax,#INITSEG mov ds,ax // 将ds 设置成 0x9000 mov ah,#0x03 xor bh,bh int 0x10 //取出光标位置 mov [0],dx mov ah,#0x88 int 0x15 // 获取物理内存大小 mov [2],ax // 9000左移4位再加2,即:0x90002(扩展内存数) ...... cli // 不允许中断 mov ax,#0x0000 cld // 将system模块全部移动到0地址 do_move: mov es,ax add ax,#0x1000 cmp ax,#0x9000 jz end_move mov ds,ax sub di,di mov cx,#0x8000 rep movsw jmp do_move mov ax,#0x0001 mov cr0,ax // 切换到保护模式 jmpi 0,8 // 实际是查询gdt表,跳至指定地址(实际跳转到0地址处)
cr0寄存器:将0置给PE,启动保护模式
保护模式:由于实模式下访问内存的大小限制,因此引入保护模式,又称为虚拟地址保护模式。保护模式下,段被一系列的"描述符表"定义,段寄存器存储的是指向这些表的指针。而定义内存段的表又两种:全绝描述符表(GDT)和局部描述符表(LDT),对于每一个操作系统都必须定义一个GDT,而每一个正在运行的任务都会有一个相应的LDT。每一个描述符的长度是8个字节。总的来说就是通过程序内部的地址(虚拟地址)由操作系统转化成物理地址去访问,进而起到了保护的作业
IDT:中断描述符表,类似于GDT,记录了0~255的中断信号和调用函数间的关系
保护模式下的地址翻译:
实模式下:cs左移4位+ip
保护模式下:根据cs查表+ip
同理在保护模式下中断处理函数:
// 因此setup需要临时初始化gdt,idt表 end_move: mov ax,#SETUPSEG move ds,ax lidt idt_48 lgdt qdt 48 // 设置保护模式下的中断和寻址 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 // gdt表设置完成
据此总结一下setup的作用:
1).读入系统的硬件参数
2).将system模块移到0地址处
3).初始化gdt,idt表并开启保护模式
4).跳转至0地址处,开始执行system模块
6.system模块
Makefile:相当于程序编译过程中的批处理文件
system由许多的文件编译而成,这里用到了Makefile,具体的教程请参考链接点击打开链接
// linux/Makefile disk: Image dd bs=8192 if=Image of=/dev/PS0 Image: boot/bootsect boot/setup tools/system tools/build tools/build boot/bootsect boot/setup tools/system> Image tools/system: boot/head.o init/main.o $(DRIVERS) … // 编译 $(LD) boot/head.oinit/main.o $(DRIVERS) … -o tools/system // 链接
system第一个模块 -- head.s
// 32位汇编代码 startup_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 l:incl %eax movl %eax,0x000000 cmpl %eax,0x100000 je lb // 开启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:
1).该程序处于内存开始的0地址段
2).加载各个数据段寄存器,重新设置中断描述符表idt
成功设置页表之后,head.s要跳出执行main.c文件
// 从汇编跳入至C函数 after_page_tables: pushl $0 pushl $0 pushl $0 // main函数的三个参数 pushl $L6 // main的返回地址(该处不会被执行) pushl $_main jmp set_paging // 设置页表 ret L6: jmp L6 setup_paging: 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(); // 新建的子进程中执行 } }
展开其中的mem_init函数
# 内存的初始化 void mem_init(long start_mem, long end_mem) // 这两个参数从0x90002(setup初始化系统硬件的参数) { 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; // 左移12位,每4K一页 while(end_mem-- > 0) mem_map[i++] = 0; // 标记未使用 }
最后总结,整个内核在内存中的位置和移动后的位置情况入下图所示: