1.打开电源

(1)x86 PC开机时CPU处于实模式,实模式的寻址方式是CS:IP (CS左移4位+IP)

(2)开机时段寄存器CS=0xFFFF,偏移量IP=0x0000,段寄存器左移4位加上偏移量是实际地址,也就是寻址地址为0xFFFF0 (ROM BIOS映射区)

(3)检查RAM,键盘,显示器,磁盘

(4)将0磁道0扇区512个字节读入0x7c00处(操作系统的引导扇区)

(5)设置cs=0x7c0,ip=0x0000

2.引导扇区代码bootsect.s

实验bootsect.s代码

(1)将bootsect从0x7c00处移动到0x90000处

(2)将setup读入到0x90200处

(3)将system读入0x10000处

(4)将PC移动到0x90200处,bootsect结束

  1. 移动bootsect.s。读取0x7c00处的内存,首先设置源地址和目标地址,并且将0x7c00内存处的256个字(512字节)移动到了0x90000
1
2
3
4
5
6
7
8
9
start:
mov ax, #BOOTSEG mov ds, ax ! 将 ds 段寄存器设置为0x07C0(16位汇编,后面的放入前面)
mov ax, #INITSEG mov es, ax ! 将 es 段寄存器设置为0x9000
sub si, si           ! 源地址
ds:si 0x7c00
sub di, di           ! 目标地址 es:di 0x90000
mov cx, #256          ! 设置移动计数值256字(512字节)
rep movw             ! 重复移动直到cx为0,将bootsect移动到了0x90000处
jmpi go, INITSEG ! 跳转到0x9000:go处,将设置es、ss、sp都为0x9000(相当于是初始化)
  1. 0x13读磁盘中断,读取setup代码。在磁盘第2个扇区开始,读取4个扇区的内容,并将这4个扇区的内容写入到0x90200内存处。因为bootsect占用512字节,所以需要写在0x90000的512个字节之后,16位表示就是在0x90200处写入。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
go: mov ax, cs                ! 0x9000
! 将ds、es、ss都置成移动后代码所在段处0x9000、栈顶地址设置为0x9fff00,来远离代码位置
mov ds, ax
mov es, ax
mov ss, ax
mov sp, #0xFF00
load_setup: // 载入setup模块, 从磁盘第2个扇区开始读4个扇区, 写入0x90200处
mov dx, #0x0000 ! 数据寄存器
mov cx, #0x0002 ! 计数寄存器 ch:cx高8位, 0x00(柱面号0); cl:低8位, 0x02(开始扇区为2)
mov bx, #0x0200 ! 基址寄存器 写到es:bx 0x90200处
mov ax, #0x0200+SETUPLEN ! 累加器 ah: ax高8位, 0x20(读磁盘); al:ax低8位, 0x04(读取4个扇区)
int 0x13 ! BIOS读磁盘扇区中断
jnc ok_load_setup ! 跳转指令, CF=0则跳转。如果没有读错,则跳转到加载setup执行;如果读错,则复位驱动器重新读。
mov dx, #0x0000 ! 对驱动器0进行操作
mov ax, # 0x0000 ! 复位
int 0x13 ! 执行0x13中断(BIOS读磁盘扇区的中断)
j load_setup ! 重读
  1. 0x10显示字符中断,显示字符在显示器上。ok_load_setup,显示欢迎页面,调用read_it读入system,最后将控制权交接给setup模块,也就是将PC移动到0x90200处,开始执行setup。bootsect正式结束。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ok_load_setup:    ! 载入setup,显示开机文字,读取system
! 读取setup,显示开机文字
mov dl, #0x00
mov ax, #0x0800 ! ah=8,获得磁盘驱动器的参数
int 0x13
mov ch, #0x00
mov sectors, cx
mov ah, #0x03
xor bh, bh
int 0x00 ! 读光标
mov cx, #24
mov bx, #0x0007 ! 7是显示属性
mov bp, #msg1
mov ax, #1301
int 0x10 !显示字符
! 读取system模块到0x10000处
mov ax, #SYSSEG ! SYSSEG=0X1000
mov es, ax
call read_it ! 读入system
jmpi 0, SETUPSEG ! 转入0x9020:0X0000执行setup.s

3.引导扇区代码setup.s

实验setup.s代码

读取硬件参数,将操作系统移动到0地址处,进入保护模式,初始化一个很简单的gdt表,跳转到system模块

  1. setup主要任务是读取硬件参数,初始化一个数据结构来管理这些硬件设备,并移动操作系统到内存0地址处。其中0x15中断是读取扩展内存大小(Intel原先内存只有1m,我们把1m以外的内存称为扩展内存)放在ax寄存器里,之后放到内存0x90002处。接下来do_move将0x90000处开始的数据移动到内存绝对地址0x00000处。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
! 取光标位置,包括其他硬件参数,放到0x90000处
start : mov ax, #INITSEG
mov ds, ax
mov ah, #0x03
xor bh, bh
int 0x10
mov [0], dx     ! 光标位置设置为0x90000
mov ah, #0x88
int 0x15
mov [2], ax ... ! 读取扩展内存的大小(1m以外的内存称为扩展内存), 放在0x90002处
cli ! 不允许中断
mov ax, #0x0000, cld ! 设置读取目标位置和递增读取方式
! 将system模块从0x10000动到0x00000处
do_mov: mov es, ax
add ax, #0x1000
cmp ax, #0x9000
jz end_mov              ! 若ax=0x9000,表示移动完成,则跳出
mov ds, ax
sub di, di
sub si, si ! 设置源地址0x90000和目标地址0x00000
mov cx, #0x8000 ! 设置移动数据的长度
rep movsw ! 将system模块移动到0地址
jmp do_move
  1. 进入保护模式。cs左移4位+ip最大有20位地址,也就是最大内存空间只有1M。setup最后一步要从16位机切换到32寻址方式。

  实模式:CS:IP,CS << 4 + IP,共20位寻址空间

  保护模式:CS实际上是查表GDT(Global Descriptor Table),地址为CS查表 + IP

1
2
3
4
5
mov ax,#0x0001  ! 将0x0001放入ax寄存器
mov cr0,ax    ! 将ax放入cr0寄存器,这一步非常重要
! cr0寄存器最后一位是0的时候是CS:IP实模式
! cr0寄存器最后一位是1的时候是保护模式,需要走另外一条解释指令的电路
jmpi 0,8     ! cs:8, ip:0; 这里实际上跳转的地址就是0地址

4.head.s

head.s是system中的第一个模块,负责重新加载各个数据段寄存器,重新设置中断描述符表,重新设置全局描述符表,设置分页处理机制(一个页目录表和4个页表),打开20号地址线访问4G内存,并且将main函数压栈,head执行完后弹出main,转到main函数。

1
2
3
4
5
6
7
8
9
10
startup_32:
movl $0x10, %eax  ! 现在开始是32位汇编,前面的放入后面
mov %ax, %ds
mov %ax, %es
mov %as, %fs
mov %as, %gs        ! 指向gdt的0x10数据项
lss _stact_start, %esp  ! 设置系统栈
call setup_idt       ! 设置中断描述符表子程序
call setup_gdt       ! 设置全局描述符表子程序
mov $0x10, %eax      ! 重载所有的段寄存器 ...

5.main函数

main函数中有大量初始化函数,例如初始化硬盘,初始化内存,将每4K内存作为一个页等等

6.系统调用

普通函数调用是通过call跳转对应函数的地址继续执行

系统调用是调用系统库中为该系统调用编写的一个接口函数,叫 API(Application Programming Interface)。API 并不能完成系统调用的真正功能,它要做的是去调用真正的系统调用,过程是:

  • 把系统调用的编号存入 %eax ,把函数参数存入其它通用寄存器
  • 触发 int 0x80 号中断(0x80 中断描述符在系统初始化时设置了自动调用system_call
  • 最后 system_call 中根据 %eax 的编号调用对应的函数

这里研究 close() 中的API,以宏 _syscall1(int, close, int, fd) 为例子,它在 include/unistd.h 中定义了展开的形式

__NR_closeinclude/unistd.h 中定义为 6

下面是 API 的定义。它先将宏 __NR_close 存入 EAX,将参数 fd 存入 EBX,然后进行 0x80 中断调用。调用返回后,从 EAX 取出返回值,存入 __res,再通过对 __res 的判断决定传给 API 的调用者什么样的返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*
* 定义了一个返回值 long 型变量 __res
* __asm__ 表示内嵌汇编代码
* volatile 表示编译器不优化该段代码
* =a中 = 表示只写,a表示先将结果输出到 %eax 中,再由 %eax 输出到__res
* 0表示和输出寄存器是同一个寄存器,也就是将 __NR_close 放入 %eax
* b表示将 fd 放入 %ebx
*/
int close(int fd)
{
long __res;
__asm__ volatile ("int $0x80"
: "=a" (__res)
: "0" (__NR_close),"b" ((long)(fd)));
if (__res >= 0)
return (int) __res;
errno = -__res;
return -1;
}

在内核初始化时,主函数调用了 sched_init() 初始化函数:

1
2
3
4
5
6
7
8
void main(void)
{
// ……
time_init();
sched_init();
buffer_init(buffer_memory_end);
// ……
}

sched_init()kernel/sched.c 中定义为:

1
2
3
4
void sched_init(void)
{
// …… set_system_gate(0x80,&system_call);
}

set_system_gate 是个宏,在 include/asm/system.h有定义。这段宏就是填写 IDT(中断描述符表),将 system_call 函数地址写到 0x80 对应的中断描述符中,也就是在中断 0x80 发生后,自动调用函数 system_call

下面是 system_call。该函数纯汇编编写,是真正的中断处理程序,定义在 kernel/system_call.s 中。

call sys_call_table(,%eax,4) 之前是一些压栈保护,修改段选择子为内核段,之后是看看是否需要重新调度。

根据汇编寻址方法它实际上是:call sys_call_table + 4 * %eax,其中 eax 中放的是系统调用号,即 __NR_xxxxxx

sys_call_table 一定是一个函数指针数组的起始地址,它定义在 include/linux/sys.h 中:

fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,...}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
!…… 
! # system_call部分代码
! # 这是系统调用总数。如果增删了系统调用,必须做相应修改 nr_system_calls = 72
!……

! # system_call 用 .globl 修饰为其他函数可见
.globl system_call
.align 2
system_call:

! # 检查系统调用编号是否在合法范围内
cmpl \$nr_system_calls-1,%eax
ja bad_sys_call
push %ds
push %es
push %fs
pushl %edx
pushl %ecx

! # push %ebx,%ecx,%edx,是传递给系统调用的参数
pushl %ebx

! # 让ds, es指向GDT,内核地址空间
movl $0x10,%edx
mov %dx,%ds
mov %dx,%es
movl $0x17,%edx
! # 让fs指向LDT,用户地址空间
mov %dx,%fs
call sys_call_table(,%eax,4) ! # 关键代码
pushl %eax
movl current,%eax
cmpl $0,state(%eax)
jne reschedule
cmpl $0,counter(%eax)
je reschedule

7.实验

增加系统调用whoami和iam,首先在 include/linux/sys.h 中添加我们的系统调用函数

include/unistd.h 中增加我们的宏定义编号,这里需要去系统里修改,使用 sudo ./mount-hdc 可以挂载Linux的系统盘,然后在这里修改它的系统调用号,不然打包以后这里不会修改到系统里

kernel/system_call.s 中修改调用的总数

修改 kernel/Makefile 中对程序源文件的编译和链接规则,Make指令完全按照Makefile文件工作

编写who.c文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <asm/segment.h>
#include <errno.h>
#include <string.h>

char _myname[24];

int sys_iam(const char *name)
{
char str[25];
int i = 0;
do
{// get char from user input
str[i] = get_fs_byte(name + i);
}
while (i <= 25 && str[i++] != '\0');
if (i > 24)
{
errno = EINVAL;
i = -1;
}
else
{// copy from user mode to kernel mode
strcpy(_myname, str);
}
return i;
}

int sys_whoami(char *name, unsigned int size)
{
int length = strlen(_myname);
printk("%s\n", _myname);
if (size < length)
{
errno = EINVAL; length = -1;
}
else
{
int i = 0;
for (i = 0; i < length; i++)
{ // copy from kernel mode to user mode
put_fs_byte(_myname[i], name + i);
}
}
return length;
}

编写我们的系统调用代码iam.c和whoami.c,接下来就完成了一个系统调用

测试我们编写的系统调用是否可以运行

完整的流程是在 iam.c 中通过 _syscall1 进行宏展开并调用,展开后会根据传入的函数名组成对应的 __NR_iam 调用号并存入 %eax,然后触发 int0x80 中断,进入 system_call 汇编函数,该函数中通过 call sys_call_table(,%eax,4) 调用真正的处理函数 sys_iam 进行处理