实验任务中需要修改的代码片段:
修改以下代码的字符个数
! 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 码字符。
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:
head.s 程序在被编译生成目标文件后会与内核其他程序的目标文件一起被链接成 system 模块,并位于 system 模块的最前面开始部分。从此开始,内核完全都是在保护模式下运行了。
高速缓冲是用于磁盘等块设备临时存放数据的地方,以 1K(1024)字节为一个数据块单位。主内存区域的内存由内存管理模块 mm 通过分页机制进行管理分配,以 4K(4096)字节为一个内存页单位。内核程序可以自由访问高速缓冲中的数据,但需要通过 mm 才能使用分配到的内存页面。
由图可见,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() */
}