Linux-0.11操作系统实验1-操作系统引导

实验环境:实验楼之操作系统引导
实验理论:
Linux-0.11操作系统实验1-操作系统引导_第1张图片
Linux-0.11操作系统实验1-操作系统引导_第2张图片

bootsect.s

  1. bootsect.s 被BIOS启动子程序加载至0x7c00 (31k)处,并将自己 移到了地址0x90000 (576k)处,并跳转至那里。
  2. 然后使用BIOS 中断INT 0x13将’setup’直接加载到自己的后面(0x90200)(576.5k),同时读取磁盘参数表中当前启动引导盘的参数,共读4 个扇区,即将setup模块从磁盘加载到内存。
  3. 屏幕上显示“Loading system … ”
  4. 将system操作系统模块从磁盘上加载到内存0x10000开始的地方

实验任务中需要修改的代码片段:

修改以下代码的字符个数

! Print some inane message ! 在显示一些信息('Loading system ...'回车换行,共24 个字符)。

mov ah,#0x03 ! read cursor pos
xor bh,bh ! 读光标位置。
int 0x10

mov cx,#29 !24 个字符。
mov bx,#0x0007 ! page 0, attribute 7 (normal)
mov bp,#msg1 ! 指向要显示的字符串。
mov ax,#0x1301 ! write string, move cursor
int 0x10 ! 写字符串并移动光标。

修改以下代码中.ascii对应的字符串

msg1:
.byte 13,10 ! 回车、换行的ASCII 码。
.ascii "Loading dios system ..."
.byte 13,10,13,10 !24 个ASCII 码字符。

setup.s

  1. setup.s 负责从BIOS 中获取硬件参数以及系统数据,并将这些数据放到0x90000开始的位置,覆盖掉bootsect程序所在的地方。
  2. 将system模块整体向下移动到内存绝对地址0x00000处,加载全局描述符表GDT和中断描述符表IDT,开启A20 地址线,重新设置两个中断控制芯片8295A。
  3. 从16位模式 转到 32位模式,即从实时模式 转到 保护模式,并执行到jmp 0,8跳转到system模块前面的head.s程序处继续执行。在进入保护模式之前,必须首先设置好将要用到的段描述符表,如GDT等。

bootsect 的代码为什么不把系统模块直接加载到物理地址 0x0000 开始处而要在 setup 程序中再进行移动呢?

这是因为随后执行的 setup 开始部分的代码还需要利用 ROM BIOS 提供的中断调用功能来获取有关机器配置的一些参数(例如显示卡模式、硬盘参数表等)。而当 BIOS 初始化时会在物理内存开始处放置一个大小为 0x400 字节(1KB)的中断向量表,直接把系统模块放在物理内存开始处将导致该中断向量表被覆盖掉。因此引导程序需要在使用完 BIOS 的中断调用后才能将这个区域覆盖掉。

在 setup.s 程序执行结束后,系统模块 system 被移动到物理内存地址 0x00000 开始处,而从位置0x90000 开始处则存放了内核将会使用的一些系统基本参数。

实验任务:
1.bootsect.s能完成setup.s的载入,并跳转到setup.s开始地址执行。而setup.s向屏幕输出一行"Now we are in SETUP"。
新建setup.s文件:

entry _start
_start:

!设置cs=ds=es
	mov	ax,cs
	mov	ds,ax
	mov	es,ax

	mov	ah,#0x03		! read cursor pos
	xor	bh,bh
	int	0x10
	
	mov	cx,#28
	mov	bx,#0x000c		! page 0, attribute c 
	mov	bp,#msg1
	mov	ax,#0x1301		! write string, move cursor
	int	0x10

! ok, the read went well so we get current cursor position and save it for
! posterity.

msg1:
	.byte 13,10
	.ascii "Now we are in SETUP..."
	.byte 13,10,13,10
	
.text
endtext:
.data
enddata:
.bss
endbss:

2.setup.s能获取至少一个基本的硬件参数(如内存参数、显卡参数、硬盘参数等),将其存放在内存的特定地址,并输出到屏幕上。
3.setup.s不再加载Linux内核,保持上述信息显示在屏幕上即可。

.globl begtext, begdata, begbss, endtext, enddata, endbss
.text
begtext:
.data
begdata:
.bss
begbss:
.text

BOOTSEG  = 0x07c0			! original address of boot-sector
INITSEG  = 0x9000			! we move boot here - out of the way
SETUPSEG = 0x9020			! setup starts here

entry _start
_start:

!设置cs=ds=es
	mov	ax,cs
	mov	ds,ax
	mov	es,ax

	mov	ah,#0x03		! read cursor pos
	xor	bh,bh
	int	0x10
	
	mov	cx,#28
	mov	bx,#0x000c		! page 0, attribute c 
	mov	bp,#msg1
	mov	ax,#0x1301		! write string, move cursor
	int	0x10

! ok, the read went well so we get current cursor position and save it for
! posterity.

! 获取光标位置 =>  0x9000:0
	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.

! Get memory size (extended mem, kB)
! 获取拓展内存大小 => 0x9000:2
	mov	ah,#0x88
	int	0x15
	mov	[2],ax

! Get hd0 data
! 获取硬盘参数 => 0x9000:80  大小:16B
	mov	ax,#0x0000
	mov	ds,ax
	lds	si,[4*0x41]
	mov	ax,#INITSEG
	mov	es,ax
	mov	di,#0x0080
	mov	cx,#0x10
	rep
	movsb

! 前面修改了ds寄存器,这里将其设置为0x9000
	mov ax,#INITSEG
	mov ds,ax
	mov ax,#SETUPSEG
	mov	es,ax  

!显示 Cursor POS: 字符串
	mov	ah,#0x03		! read cursor pos
	xor	bh,bh
	int	0x10
	mov	cx,#11
	mov	bx,#0x0007		! page 0, attribute c 
	mov	bp,#cur
	mov	ax,#0x1301		! write string, move cursor
	int	0x10

!调用 print_hex 显示具体信息
	mov ax,[0]
	call print_hex
	call print_nl

!显示 Memory SIZE: 字符串
	mov	ah,#0x03		! read cursor pos
	xor	bh,bh
	int	0x10
	mov	cx,#12
	mov	bx,#0x0007		! page 0, attribute c 
	mov	bp,#mem
	mov	ax,#0x1301		! write string, move cursor
	int	0x10

!显示 具体信息
	mov ax,[2]
	call print_hex

!显示相应 提示信息
	mov	ah,#0x03		! read cursor pos
	xor	bh,bh
	int	0x10
	mov	cx,#25
	mov	bx,#0x0007		! page 0, attribute c 
	mov	bp,#cyl
	mov	ax,#0x1301		! write string, move cursor
	int	0x10

!显示具体信息
	mov ax,[0x80]
	call print_hex
	call print_nl

!显示 提示信息
	mov	ah,#0x03		! read cursor pos
	xor	bh,bh
	int	0x10
	mov	cx,#8
	mov	bx,#0x0007		! page 0, attribute c 
	mov	bp,#head
	mov	ax,#0x1301		! write string, move cursor
	int	0x10

!显示 具体信息
	mov ax,[0x80+0x02]
	call print_hex
	call print_nl

!显示 提示信息
	mov	ah,#0x03		! read cursor pos
	xor	bh,bh
	int	0x10
	mov	cx,#8
	mov	bx,#0x0007		! page 0, attribute c 
	mov	bp,#sect
	mov	ax,#0x1301		! write string, move cursor
	int	0x10

!显示 具体信息
	mov ax,[0x80+0x0e]
	call print_hex
	call print_nl

!死循环
l:  jmp l

!16进制方式打印ax寄存器里的16位数
print_hex:
	mov cx,#4   ! 4个十六进制数字
	mov dx,ax   ! 将ax所指的值放入dx中,ax作为参数传递寄存器
print_digit:
	rol dx,#4  ! 循环以使低4比特用上 !! 取dx的高4比特移到低4比特处。
	mov ax,#0xe0f  ! ah = 请求的功能值,al = 半字节(4个比特)掩码。
	and al,dl ! 取dl的低4比特值。
	add al,#0x30  ! 给al数字加上十六进制0x30
	cmp al,#0x3a
	jl outp  !是一个不大于十的数字
	add al,#0x07  !是a~f,要多加7
outp:
	int 0x10
	loop print_digit
	ret

!打印回车换行
print_nl:
	mov ax,#0xe0d
	int 0x10
	mov al,#0xa
	int 0x10
	ret

msg1:
	.byte 13,10
	.ascii "Now we are in SETUP..."
	.byte 13,10,13,10
cur:
	.ascii "Cursor POS:"
mem:
	.ascii "Memory SIZE:"
cyl:
	.ascii "KB"
	.byte 13,10,13,10
	.ascii "HD Info"
	.byte 13,10
	.ascii "Cylinders:"
head:
	.ascii "Headers:"
sect:
	.ascii "Secotrs:"

.text
endtext:
.data
enddata:
.bss
endbss:

完整新建的setup.s如上所示,最终结果如下所示:
Linux-0.11操作系统实验1-操作系统引导_第3张图片

head.s

head.s 程序在被编译生成目标文件后会与内核其他程序的目标文件一起被链接成 system 模块,并位于 system 模块的最前面开始部分。从此开始,内核完全都是在保护模式下运行了。

  1. 加载各个数据段寄存器,重新设置中断描述符表IDT和全局描述符表GDT,内核对于每个任务(进程)使用一个 LDT,而 LDT 也是由 GDT 中的描述符来指定。
  2. 通过设置控制寄存器cr0 的标志(PG 位31)来启动对内存的分页处理功能,将页目录表放在绝对物理地址0开始处。
  3. 利用返回指令将预先放置在堆栈中的/init/main.c程序的入口地址弹出,去运行main程序。

总结:

  • 引导加载程序 bootsect.s 主要将 setup.s 代码和 system 模块加载到内存中,其中 system 模块的首部包含有 head.s 代码。在把自己移动到物理地址 0x90000 处并将 setup.s 代码放到 0x90200 处后,就将执行权交给了 setup 程序。
  • setup 程序的主要作用是利用 ROM BIOS 的中断程序获取机器的一些基本参数,并保存在 0x90000 开始的内存块中,供后面程序使用。同时把 system 模块往下移动到物理地址 0x00000 开始处,这样, system中的 head.s 代码就处在 0x00000 开始处了。然后加载描述符表基地址到描述符表寄存器中,为进行 32 位保护模式下的运行作好准备。接下来对中断控制硬件进行重新设置,最后通过设置机器控制寄存器 CR0并跳转到 system 模块的 head.s 代码开始处,使 CPU 进入 32 位保护模式下运行。
  • Head.s 代码的主要作用是初步初始化中断描述符表中的 256 项门描述符,检查 A20 地址线是否已经打开,测试系统是否含有数学协处理器。然后初始化内存页目录表,为内存的分页管理作好准备工作。最后跳转到 system 模块中的初始化程序 init/main.c 继续执行。

内核初始化过程main.c

高速缓冲是用于磁盘等块设备临时存放数据的地方,以 1K(1024)字节为一个数据块单位。主内存区域的内存由内存管理模块 mm 通过分页机制进行管理分配,以 4K(4096)字节为一个内存页单位。内核程序可以自由访问高速缓冲中的数据,但需要通过 mm 才能使用分配到的内存页面。
Linux-0.11操作系统实验1-操作系统引导_第4张图片

  1. main.c 程序首先利用 setup.s 程序取得的系统参数设置系统的根文件设备号以及一些内存全局变量,这些内存变量指明了主内存的开始地址、系统所拥有的内存容量和作为高速缓冲区内存的末端地址。
  2. 内核进行所有方面的硬件初始化工作。包括陷阱门、块设备、字符设备和 tty ,包括人工创建第一个任务( task 0 )。待所有初始化工作完成就设置中断允许标志,开启中断。
  3. 在整个内核完成初始化后,内核将执行权切换到了用户模式,也即 CPU 从 0 特权级切换到了第 3 特权级。然后系统第一次调用创建进程函数 fork() ,创建出一个用于运行 init() 的子进程。在该进程(任务)中系统将运行控制台程序。如果控制台环境建立成功,则再生成一个子进程,用于运行 shell 程序 /bin/sh 。若该子进程退出,父进程返回,则父进程进入一个死循环内,继续生成子进程,并在此子进程中再次执行 shell 程序 /bin/sh ,而父进程则继续等待。

Linux-0.11操作系统实验1-操作系统引导_第5张图片
由图可见,main.c 程序首先确定如何分配使用系统物理内存,然后调用内核各部分的初始化函数分别对内存管理、中断处理、块设备和字符设备、进程管理以及硬盘和软盘硬件进行初始化处理。在完成了这些操作之后,系统各部分已经处于可运行状态。
此后程序把自己“手工”移动到任务 0(进程 0)中运行,并使用 fork()调用首次创建出进程 1(init 进程),并在其中调用 init()函数。在该函数中程序将继续进行应用环境的初始化并执行 shell 登录程序。而原进程 0 则会在系统空闲时被调度执行,因此进程 0通常也被称为 idle 进程。此时进程 0 仅执行 pause()系统调用,并又会调用调度函数。

void main (void)		/* This really IS void, no error here. */
{				/* The startup routine assumes (well, ...) this */
  /* 这里确实是void,并没错。在startup 程序(head.s)中就是这样假设的。 */
  // 参见head.s 程序第136 行开始的几行代码。
  /*
   * Interrupts are still disabled. Do necessary setups, then
   * enable them
   */
  /*
   * 此时中断仍被禁止着,做完必要的设置后就将其开启。
   */
  // 下面这段代码用于保存:
  // 根设备号 ROOT_DEV; 高速缓存末端地址  buffer_memory_end;
  // 机器内存数 memory_end;主内存开始地址   main_memory_start;
  ROOT_DEV = ORIG_ROOT_DEV;
  drive_info = DRIVE_INFO;
  memory_end = (1 << 20) + (EXT_MEM_K << 10);	// 内存大小=1Mb 字节+扩展内存(k)*1024 字节。
  memory_end &= 0xfffff000;	// 忽略不到4Kb(1 页)的内存数。
  if (memory_end > 16 * 1024 * 1024)	// 如果内存超过16Mb,则按16Mb 计。
    memory_end = 16 * 1024 * 1024;
  if (memory_end > 12 * 1024 * 1024)	// 如果内存>12Mb,则设置缓冲区末端=4Mb
    buffer_memory_end = 4 * 1024 * 1024;
  else if (memory_end > 6 * 1024 * 1024)	// 否则如果内存>6Mb,则设置缓冲区末端=2Mb
    buffer_memory_end = 2 * 1024 * 1024;
  else
    buffer_memory_end = 1 * 1024 * 1024;	// 否则则设置缓冲区末端=1Mb
  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 ();			// 陷阱门(硬件中断向量)初始化。(kernel/traps.c,181 行)
  blk_dev_init ();		// 块设备初始化。 (kernel/blk_dev/ll_rw_blk.c,157 行)
  chr_dev_init ();		// 字符设备初始化。 (kernel/chr_dev/tty_io.c,347 行)
  tty_init ();			// tty 初始化。 (kernel/chr_dev/tty_io.c,105 行)
  time_init ();			// 设置开机启动时间??startup_time(见76 行)。
  sched_init ();		// 调度程序初始化(加载了任务0 的tr, ldtr) (kernel/sched.c,385)
  buffer_init (buffer_memory_end);	// 缓冲管理初始化,建内存链表等。(fs/buffer.c,348)
  hd_init ();			// 硬盘初始化。 (kernel/blk_dev/hd.c,343 行)
  floppy_init ();		// 软驱初始化。 (kernel/blk_dev/floppy.c,457 行)
  sti ();			// 所有初始化工作都做完了,开启中断。
  // 下面过程通过在堆栈中设置的参数,利用中断返回指令切换到任务0。
  move_to_user_mode ();		// 移到用户模式。 (include/asm/system.h,第1 行)
  if (!fork ())
    {				/* we count on this going ok */
      init ();
    }
  /*
   * NOTE!! For any other task 'pause()' would mean we have to get a
   * signal to awaken, but task0 is the sole exception (see 'schedule()')
   * as task 0 gets activated at every idle moment (when no other tasks
   * can run). For task0 'pause()' just means we go check if some other
   * task can run, and if not we return here.
   */
  /* 注意!! 对于任何其它的任务,'pause()'将意味着我们必须等待收到一个信号才会返
   * 回就绪运行态,但任务0(task0)是唯一的意外情况(参见'schedule()'),因为任务0 在
   * 任何空闲时间里都会被激活(当没有其它任务在运行时),因此对于任务0'pause()'仅意味着
   * 我们返回来查看是否有其它任务可以运行,如果没有的话我们就回到这里,一直循环执行'pause()'。
   */
  for (;;)
    pause ();
}

init()函数的功能可分为 4 个部分:
①安装根文件系统;
②显示系统信息;
③运行系统初始资源配置文件 rc 中的命令;
④执行用户登录 shell 程序。

void init (void)
{
  int pid, i;

  // 读取硬盘参数包括分区表信息并建立虚拟盘和安装根文件系统设备。
  // 该函数是在25 行上的宏定义的,对应函数是sys_setup(),在kernel/blk_drv/hd.c,71 行。
    setup ((void *) &drive_info);
    (void) open ("/dev/tty0", O_RDWR, 0);	// 用读写访问方式打开设备“/dev/tty0”,
  // 这里对应终端控制台。
  // 返回的句柄号0 -- stdin 标准输入设备。
    (void) dup (0);		// 复制句柄,产生句柄1 号 -- stdout 标准输出设备。
    (void) dup (0);		// 复制句柄,产生句柄2 号 -- stderr 标准出错输出设备。
    printf ("%d buffers = %d bytes buffer space\n\r", NR_BUFFERS, NR_BUFFERS * BLOCK_SIZE);	// 打印缓冲区块数和总字节数,每块1024 字节。
    printf ("Free mem: %d bytes\n\r", memory_end - main_memory_start);	//空闲内存字节数。
  // 下面fork()用于创建一个子进程(子任务)。对于被创建的子进程,fork()将返回0 值,
  // 对于原(父进程)将返回子进程的进程号。所以180-184 句是子进程执行的内容。该子进程
  // 关闭了句柄0(stdin),以只读方式打开/etc/rc 文件,并执行/bin/sh 程序,所带参数和
  // 环境变量分别由argv_rc 和envp_rc 数组给出。参见后面的描述。
  if (!(pid = fork ()))
    {
      close (0);
      if (open ("/etc/rc", O_RDONLY, 0))
	_exit (1);		// 如果打开文件失败,则退出(/lib/_exit.c,10)。
      execve ("/bin/sh", argv_rc, envp_rc);	// 装入/bin/sh 程序并执行。
      _exit (2);		// 若execve()执行失败则退出(出错码2,“文件或目录不存在”)。
    }
  // 下面是父进程执行的语句。wait()是等待子进程停止或终止,其返回值应是子进程的进程号(pid)。
  // 这三句的作用是父进程等待子进程的结束。&i 是存放返回状态信息的位置。如果wait()返回值不
  // 等于子进程号,则继续等待。
  if (pid > 0)
    while (pid != wait (&i))
      /* nothing */ ;
  // 如果执行到这里,说明刚创建的子进程的执行已停止或终止了。下面循环中首先再创建一个子进程,
  // 如果出错,则显示“初始化程序创建子进程失败”的信息并继续执行。对于所创建的子进程关闭所有
  // 以前还遗留的句柄(stdin, stdout, stderr),新创建一个会话并设置进程组号,然后重新打开
  // /dev/tty0 作为stdin,并复制成stdout 和stderr。再次执行系统解释程序/bin/sh。但这次执行所
  // 选用的参数和环境数组另选了一套(见上面165-167 行)。然后父进程再次运行wait()等待。如果
  // 子进程又停止了执行,则在标准输出上显示出错信息“子进程pid 停止了运行,返回码是i”,然后
  // 继续重试下去…,形成“大”死循环。
  while (1)
    {
      if ((pid = fork ()) < 0)
	{
	  printf ("Fork failed in init\r\n");
	  continue;
	}
      if (!pid)
	{
	  close (0);
	  close (1);
	  close (2);
	  setsid ();
	  (void) open ("/dev/tty0", O_RDWR, 0);
	  (void) dup (0);
	  (void) dup (0);
	  _exit (execve ("/bin/sh", argv, envp));
	}
      while (1)
	if (pid == wait (&i))
	  break;
      printf ("\n\rchild %d died with code %04x\n\r", pid, i);
      sync ();
    }
  _exit (0);			/* NOTE! _exit, not exit() */
}

你可能感兴趣的:(Linux-0.11OS)