操作系统-系统调用

接触教学操作系统的第一个实验可能就是在用户态实现系统调用了。

关于选择哪个教学操作系统学习,操作系统课上选择的是NachOS,罗佬推荐去学PintOS,李学长推荐去学xv6。
但是如果愿意当“烈士”,选择学长们开发的LoOS吧~
NachOS 官方文档
PintOS 官方文档
xv6 官方文档
操作系统课程资源
LoOS 项目主页

Intro

  • NachOS
    出现时间:1992年。
    开发者:威斯康星大学麦迪逊分校的Tom Anderson教授。
    特点:NachOS是一个模块化设计的教学操作系统,包含线程管理、同步机制、文件系统和内存管理等基本功能。它的代码简洁,便于学生逐步学习和扩展。
    影响:NachOS在大学计算机专业的操作系统课程中被广泛使用,帮助学生理解操作系统的各种概念。
  • pintos
    出现时间:2004年。
    开发者:麻省理工学院(MIT)。
    特点:PintOS是一个基于x86架构的教学操作系统,主要用于本科操作系统的实验教学。它提供了线程管理、内存管理和文件系统等基本功能。
    影响:PintOS帮助学生理解操作系统的实现细节,是MIT操作系统课程的重要组成部分
  • xv6
    出现时间:2006年。
    开发者:麻省理工学院(MIT)。
    特点:xv6是一个类似于Unix的简单操作系统,主要用于教学。它基于C语言编写,代码简洁,易于理解和扩展。
    影响:xv6被广泛用于MIT的操作系统课程(如6.S081),帮助学生理解操作系统的底层实现

Background

在解释后续的系统调用实验之前,有一些操作系统的概念需要首先澄清
“Seeing how the sausage was made is nearly as important as understanding what the sausage is good for.”

系统调用的一般流程:

用户程序触发系统调用=>系统捕获系统调用的异常=>调用异常处理=>调用该系统调用的具体实现函数=>将结果返回给用户程序。
在这个 NachOS 实验中,用户程序由start.s和test.c共同组成
以下是 NachOS 中系统调用的完整流程:

  1. 用户程序触发系统调用:

    • 用户程序调用 Add 函数(通过汇编封装),将系统调用号放入 $2 寄存器,并执行 syscall 指令。
  2. NachOS 捕获 SyscallException:

    • mipssim.cc 中,syscall 指令触发 SyscallException

      1
      2
      3
      case OP_SYSCALL:
      RaiseException(SyscallException, 0);
      return;
  3. 调用 ExceptionHandler:

    • machine.cc 中,RaiseException 调用 ExceptionHandler

      1
      2
      3
      4
      5
      void Machine::RaiseException(ExceptionType which, int badVAddr) {
      kernel->interrupt->setStatus(SystemMode);
      ExceptionHandler(which);
      kernel->interrupt->setStatus(UserMode);
      }
  4. ExceptionHandler 调用 SysAdd:

    • 在 exception.cc 中,ExceptionHandler 根据系统调用号调用 SysAdd

      1
      2
      3
      4
      5
      6
      case SC_Add:
      int op1 = kernel->machine->ReadRegister(4);
      int op2 = kernel->machine->ReadRegister(5);
      int result = SysAdd(op1, op2);
      kernel->machine->WriteRegister(2, result);
      break;
  5. SysAdd 执行加法操作:

    • SysAdd 在内核模式下执行加法操作,并返回结果:

      1
      2
      3
      int SysAdd(int op1, int op2) {
      return op1 + op2;
      }
  6. 返回结果给用户程序:

    • ExceptionHandler 将结果写回 $2 寄存器,用户程序从 $2 寄存器中读取结果。

双模态:

我们希望让程序更快运行,很容易想到就是让程序直接运行在CPU上,但是,程序还可能进行IO操作和其它有访问限制的操作,这又要求我们不能给以程序对系统的完全控制。
如果程序任意执行它的在I/O和相关操作,那么对于一些系统的保护就会失效,(程序可以对全盘进行读取,这是不期望的设计)。
因此,就引入了双模态——用户态和内核态:
必须区分操作系统代码和用户代码的执行。大多数计算机系统采用硬件支持,以便区分各种执行模式。
大多数现代操作系统,Windows 7,UNIX,Linux,都利用了双模态的特点,为操作系统提供了更强的保护。

双模态提供保护手段,以便防止操作系统和用户程序收到错误的用户程序的影响。
这种保护的实现:将可能引起损害的机器指令作为特权指令,并且硬件只有在内核模式下才允许执行特权指令。

链接

本实验有一个易错点就是,要理解链接是对.c文件进行的,它们的函数名都要保持一致(理解链接的作用)
程序的链接是将多个目标文件和库文件组合成一个可执行文件的过程。链接的主要目的是将程序的各个部分(如函数和变量)正确地连接起来,以便程序可以运行。

链接器的作用

链接器是完成链接过程的工具,常见的链接器包括 GNU 的 ld 和 Microsoft 的 link。它们的主要任务是:

  • 合并多个目标文件。
  • 解析符号并处理依赖关系。
  • 生成最终的可执行文件或库文件。

通过链接,程序的各个部分得以整合,形成一个完整的、可以运行的应用程序。

汇编

可以注意到我们NachOS的实验中使用了汇编代码,那么这里为什么非要用汇编,汇编代码的作用是什么?

现代操作系统(如 Ubuntu 的 Linux 内核)中,系统调用仍然需要底层汇编的支持,但用户程序通常不直接使用汇编来触发系统调用。这是因为现代编程环境提供了更高层次的抽象(如 C 标准库),隐藏了底层的汇编细节。以下是原因和机制:

1. 系统调用仍然依赖汇编

  • 系统调用的本质:

系统调用是用户程序进入内核模式的唯一途径,通常通过触发特定的 CPU 指令(如 syscall 或 int 0x80)实现。
这些指令是硬件支持的,必须用汇编语言触发。

  • 汇编的必要性:

即使使用 C 语言调用系统调用,底层仍然需要汇编代码来完成寄存器设置和触发系统调用指令。

2. 为什么用户程序不直接使用汇编

  • C 标准库的封装:
    在 Linux 系统中,C 标准库(如 glibc)为系统调用提供了封装。例如,调用 write() 函数时,glibc 会在内部使用汇编触发系统调用。
    用户程序只需调用高层次的 C 函数,而不需要关心底层实现。
  • 可移植性:
    如果用户程序直接使用汇编代码,程序将与特定的硬件架构绑定(如 x86 或 ARM)。
    使用 C 标准库封装后,程序可以在不同架构上运行,而无需修改代码。
  • 简化开发:
    汇编代码难以维护和调试。通过使用 C 语言和标准库,开发者可以专注于业务逻辑,而不必处理底层细节。

为什么 NachOS 需要显式汇编
NachOS 是一个教学操作系统,目的是帮助学生理解操作系统的底层机制。
在 NachOS 中,start.s 明确展示了系统调用的触发过程(如将系统调用编号放入寄存器并执行 syscall 指令),以便学生学习和实验。
NachOS 没有像 Linux 那样的标准库封装,因此需要显式使用汇编代码。

start.s 文件在 NachOS 中的主要作用是为用户程序提供启动代码和系统调用的汇编封装。虽然理论上可以用 C 语言代替部分功能,但由于 NachOS 的架构和底层实现,start.s 的存在是必要的,原因如下:


1. 系统调用的实现依赖于汇编

  • 系统调用需要触发 syscall 指令:
    • 在 NachOS 中,用户程序通过触发 syscall 指令进入内核模式,而 syscall 是一个汇编指令,无法直接用 C 语言实现。
    • start.s 中的封装函数(如 AddHalt 等)将系统调用编号和参数放入寄存器,然后触发 syscall 指令。这种底层操作必须用汇编实现。

2. 用户程序的启动代码

  • 初始化用户程序的入口点:
    • NachOS 的用户程序需要一个固定的入口点(__start),这是内核加载用户程序后跳转的第一条指令。
    • start.s 定义了这个入口点,并调用用户程序的 main 函数。
    • 如果用 C 语言实现,仍然需要依赖汇编代码来设置入口点和初始化寄存器。

3. C 语言的局限性

  • C 语言无法直接操作寄存器:

    • NachOS 的系统调用机制依赖于将系统调用编号和参数放入特定的寄存器(如 $2$4$5 等),然后触发 syscall 指令。
    • C 语言无法直接操作这些寄存器,因此必须使用汇编代码。
  • C 语言无法定义全局入口点:

    • 在 NachOS 的架构中,用户程序的入口点是由 start.s 定义的。如果完全用 C 语言实现,无法直接定义全局的 _start__start 符号。

4. 替代方案的复杂性

  • 混合使用 C 和内联汇编:
    • 理论上,可以用 C 语言结合内联汇编来实现 start.s 的功能,但这会增加代码的复杂性,并且可能导致平台相关的问题。
    • 例如,以下是一个用 C 和内联汇编实现的系统调用封装:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      int Add(int op1, int op2) {
      int result;
      asm volatile (
      "li $2, 42\n" // SC_Add
      "move $4, %1\n"
      "move $5, %2\n"
      "syscall\n"
      "move %0, $2\n"
      : "=r" (result)
      : "r" (op1), "r" (op2)
      : "$2", "$4", "$5"
      );
      return result;
      }
      这种实现虽然可行,但代码可读性差,且依赖于特定的汇编语法。

5. NachOS 的设计哲学

  • NachOS 的设计目的是教学,start.s 的存在可以帮助学生理解用户程序与内核之间的交互机制。
  • 使用汇编代码明确展示了系统调用的底层实现,有助于学习操作系统的基本原理。

总结

start.s 是 NachOS 中不可或缺的一部分,因为它:

  1. 提供了用户程序的启动代码。
  2. 实现了系统调用的汇编封装。
  3. 直接操作寄存器和触发 syscall 指令,这是 C 语言无法完成的。

虽然可以用 C 语言结合内联汇编部分替代 start.s 的功能,但这会增加代码复杂性,且违背 NachOS 的教学目的。因此,start.s 是必要的,且是实现 NachOS 系统调用机制的最佳方式。

NachOS实验-实现系统调用

实验:模仿add()的实现,通过系统调用实现Multiply(),Divide(),Power(),Sub()这四种操作。
目的:

  • 在Linux系统中,用户进程如何运行?
  • 用户进程与系统如何交互?
  • 系统调用是如何实现的?

查看用户test目录下Makefile

1
2
3
4
5
6
7
8
9
10
11
# compile rules
#.SUFFICES: .coff .noff
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@

%.coff: %.o
$(LD) $(LDFLAGS) start.o ${LIB_OBJS} $< -o $@

%.noff: %.coff
$(STRIP) $<
$(COFF2NOFF) $< $@

.c文件被先编译成.o文件,再链接(“LD”)成.coff文件,再经处理形成.noff文件——NetBSD Object File Format

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
all: start.o $(LIB_OBJS) $(COFF2NOFF) $(NOFF)

$(COFF2NOFF):
Build COFF2NOFF first!

copy: all newdisk $(EXEC)
./nachos -l

newdisk:
./nachos -f

clean:
$(RM) *.o *.ii
$(RM) *.coff *.noff

distclean: clean
$(RM) $(EXEC) *~ Makefile.bak
$(RM) DISK_*

# special targets

start.o: start.s ../userprog/syscall.h
$(CPP) $(CPPFLAGS) start.s > strt.s
$(AS) $(ASFLAGS) -o start.o strt.s
$(RM) strt.s

# next are automatically generated dependencies
# DO NOT DELETE

所有的用户程序都要跟start.o文件进行链接
而start.o文件依赖于start.s和syscall.h

这个实验共需关注五个文件:
test.c,start.s,syscall.h,ksyscal.h,exception.cc

查看start.s

可以看出start.s定义了用户程序的入口,这些实现的指令就是操作系统提供给用户的接口函数API。

eg:

1
2
3
4
5
6
7
8
9
10
11
12
13
#define IN_ASM
#include "syscall.h"

.text
.align 2

.globl __start
.ent __start
__start:
jal main
move $4,$0
jal Exit /* if we return from main, exit(0) */
.end __start
1
2
3
4
5
6
7
	.globl Add
.ent Add
Add:
addiu $2,$0,SC_Add
syscall
j $31
.end Add

在MIPS体系结构中,$0中永远存着0,addiu是无符号数相加
eg:将系统调用号SC_Add放到$2寄存器中
执行系统调用syscall
j指令进行无条件跳转,跳转到$31中执行

这些指令的更具体实现在/nachos-4.1/code/machine/mipssim.cc中:
从中可以看出系统调用被处理的全过程:
这里的SyscalException由刚刚start.s的syscall触发:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
break;

case OP_SYSCALL:
RaiseException(SyscallException, 0);
return;

case OP_XOR:
registers[instr->rd] = registers[instr->rs] ^ registers[instr->rt];
break;

case OP_XORI:
registers[instr->rt] = registers[instr->rs] ^ (instr->extra & 0xffff);
break;

case OP_RES:
case OP_UNIMP:
RaiseException(IllegalInstrException, 0);
return;

default:
ASSERT(FALSE);

// Now we have successfully executed the instruction.

可以看到像OP_SYSCALL这样的指令就属于特权指令,抛出一个异常: RaiseException(SyscallException, 0);
CRTL单击”RaiseException()”来到machine.cc:

定义在machine.h中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
enum ExceptionType { NoException,           // Everything ok!
SyscallException, // A program executed a system call.
PageFaultException, // No valid translation found
ReadOnlyException, // Write attempted to page marked
// "read-only"
BusErrorException, // Translation resulted in an
// invalid physical address
AddressErrorException, // Unaligned reference or one that
// was beyond the end of the
// address space
OverflowException, // Integer overflow in add or sub.
IllegalInstrException, // Unimplemented or reserved instr.

NumExceptionTypes
};

1
2
3
4
5
6
7
8
9
10
11
void
Machine::RaiseException(ExceptionType which, int badVAddr)
{
DEBUG(dbgMach, "Exception: " << exceptionNames[which]);

registers[BadVAddrReg] = badVAddr;
DelayedLoad(0, 0); // finish anything in progress
kernel->interrupt->setStatus(SystemMode); //
ExceptionHandler(which); // interrupts are enabled at this point
kernel->interrupt->setStatus(UserMode);
}

NachOS 的双模态(用户模式和内核模式)切换是通过 InterruptStatus 来管理。

  • 模式切换发生在 RaiseException 内部:
    1
    2
    3
    kernel->interrupt->setStatus(SystemMode);
    ExceptionHandler(which);//!!!实际系统调用的实现
    kernel->interrupt->setStatus(UserMode);
    • SystemMode 表示进入内核模式。
    • UserMode 表示返回用户模式。

F12转到定义进入ExceptionHandler()的具体实现——exception.cc

查看并修改exception.cc

这里的第一句”int type = kernel->machine->ReadRegister(2);”
就是读取二号寄存器,回想start.s里边,系统调用号被存到了$2中,这里的type就会是系统调用号:
之后通过switch语句,对要求的系统调用SyscallException的操作分别处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ExceptionHandler(ExceptionType which)
{
int type = kernel->machine->ReadRegister(2);

DEBUG(dbgSys, "Received Exception " << which << " type: " << type << "\n");

switch (which) {
case SyscallException:
switch(type) {

//case:Add,Power,Divide,

}

}
}

这里就需要模仿SC_Add,添加SC_Multiply,SC_Divide……的系统调用实现。

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
case SC_Add:
DEBUG(dbgSys, "Add " << kernel->machine->ReadRegister(4) << " + " << kernel->machine->ReadRegister(5) << "\n");

/* Process SysAdd Systemcall*/
int result;
result = SysAdd(/* int op1 */(int)kernel->machine->ReadRegister(4),
/* int op2 */(int)kernel->machine->ReadRegister(5));

DEBUG(dbgSys, "Add returning with " << result << "\n");
/* Prepare Result */
kernel->machine->WriteRegister(2, (int)result);

/* Modify return point */
{
/* set previous programm counter (debugging only)*/
kernel->machine->WriteRegister(PrevPCReg, kernel->machine->ReadRegister(PCReg));

/* set programm counter to next instruction (all Instructions are 4 byte wide)*/
kernel->machine->WriteRegister(PCReg, kernel->machine->ReadRegister(PCReg) + 4);

/* set next programm counter for brach execution */
kernel->machine->WriteRegister(NextPCReg, kernel->machine->ReadRegister(PCReg)+4);
}

return;

ASSERTNOTREACHED();

break;

这里的DEBUG增添了调试信息,方便之后运行时测试。
注意这里就是在内核态运行的程序,是NachOS对系统调用的具体实现:
以SysAdd()为例,4号和5号寄存器中的值被传入SysAdd()函数进行相加
结果返回后被存入2号寄存器($2)

这里是操作系统对程序计数器的管理:
在 MIPS 架构中,程序计数器(PC)用于指向当前正在执行的指令的地址。为了正确执行程序,NachOS 需要维护以下三个寄存器:

  • PrevPCReg: 保存上一条指令的地址(用于调试)。
  • PCReg: 保存当前正在执行的指令的地址。
  • NextPCReg: 保存下一条指令的地址(用于分支预测和流水线执行)。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    {
    /* set previous programm counter (debugging only)*/
    kernel->machine->WriteRegister(PrevPCReg, kernel->machine->ReadRegister(PCReg));

    /* set programm counter to next instruction (all Instructions are 4 byte wide)*/
    kernel->machine->WriteRegister(PCReg, kernel->machine->ReadRegister(PCReg) + 4);

    /* set next programm counter for brach execution */
    kernel->machine->WriteRegister(NextPCReg, kernel->machine->ReadRegister(PCReg)+4);
    }

到现在为止,已经完成了系统调用的实现:
但是刚刚还忽略了两个问题:

  • Q1.exception.cc中为什么不可以直接+-*/实现,还需要调用SysAdd()函数去执行?
    SysAdd()被定义和实现在哪里?
  • Q2.exception.cc中type变量取到了系统调用号,但它是怎么对应到SC_Add这样的系统调用类型的?

查看并修改ksyscall.h

定义了系统调用在内核中的具体实现(如 SysAddSysMultiply 等)

A1:/userprog/ksyscall.h,可以看到这个文件已经不在位于内核的目录下,但却规定了在内核态要被执行的代码。

/userprog 目录是 NachOS 中用户程序支持模块的核心部分,负责用户程序的加载、执行、系统调用处理和异常处理。它是用户程序与内核交互的桥梁,体现了操作系统如何管理和支持用户程序的运行。

这就是为用户程序开的那扇门,灵活地定制了特殊的内核态执行逻辑。

  • 这些函数直接操作 NachOS 的内核对象(如 kernel->interrupt->Halt()),而这些对象只能在内核态访问。
  • 如果这些函数在用户态执行,用户程序将无法直接访问内核资源。
    实验中所实现的加减乘除实际上不是会对内核造成威胁的操作,也不涉及操作内核对象,所以说:
    实际上可以在exception.cc中直接对+-*/进行实现,单独写到ksyscall.h中是为了教学所需,展现一个标准的系统调用过程。

查看并修改syscall.h

这是内核为系统调用提供的接口
A2:可以看到这里是在syscall.h中通过宏定义的方式实现的:

1
2
3
4
#define SC_Add		42
#define SC_Multiply 43 // 新增 Multiply 系统调用编号
#define SC_Divide 44 // 新增 Divide 系统调用编号
#define SC_Power 45 // 新增 Power 系统调用编号

宏定义的特点是简单、灵活、无类型、无作用域限制。也就是说,在全局(所有文件)中,SC_Add就对应42……

/test目录创建test.c进行验证

总结:

在成熟的Linux操作系统中,glibc帮我们实现了这一系列复杂的过程,只需要导入相关的库文件就可以开发用户程序。
在教学操作系统NachOS中,我们开始自己动手去实现这样的库文件,将这些实现展开成start.s、test.c、ksyscall.h……这,NachOS给我们提供了关注系统调用的具体实现过程的机会。
整个实验的通过start.s和test.c的执行线索展开,逐步进入操作系统的内部,亲历实现的全过程
实验中一共修改过5份代码,分成三组:

  • 用户程序:
    test.c 用户程序
    start.s 用户程序的启动代码

  • 内核程序:
    exception.cc
    (mipssim.cc) 异常处理=>系统调用
    (machine.cc) 中断与恢复
    (machine.h) 定义不同的异常捕获与处理

  • 桥梁:
    ksyscall.h 系统调用在内核中的具体实现
    syscall.h 内核为系统调用提供的接口

整个过程可以当作是两组程序的交互过程…,类似一组前后端程序的联调,系统中通过参数传递与函数调用的方式完成这个过程。

Ps

除此之外还有一些值得关注的点:
例如,我们在《计算机组成原理》中刚刚学过的 MIPS架构——无互锁流水线阶段的微处理器
NachOS包含一个MIPS指令集的模拟器,用于模拟MIPS架构的处理器。
RISC与MIPS与NachOS

CPU的MIPS系列是目前最成功和最灵活的设计之一

MIPS架构是RISC指令集架构设计思想的一种具体实现

“采用小端存储方式、按字寻址、三地址结构、定长ISA。它是一个取-存架构,即只有装载和存储指令才能够访问存储器。所有的其他指令必须使用寄存器来存储操作数, 这就意味着这一指令集架构需要大量的寄存器集合。”

“MIPS有一个明确的指令集,这包括五种基本的指令类型:简单的算术指令(add,XOR,NAND,shift)、数据传送指令(load、store、move)、控制指令(branch、jump)、多周期指令(multiply、divide)、和其它指令(load、store、move)”

“MIPS程序员可以使用立即寻址,寄存器寻址,直接寻址,寄存器间接寻址,基址寻址和变址寻址等方式,然而ISA只能提供一种寻址方式(基址寻址),其余的寻址方式是由编译器提供的,”

在NachOS中MIPS的这些特点被很好地印证了:

小端存储方式

例如在exception.cc中作者给出的注释:
// The only difference between this code and the BIG ENDIAN code
// is that the ReadMem call is guaranteed an aligned access as it
// should be (Kane’s book hides the fact that all memory access
// are done using aligned loads - what the instruction asks for
// is a arbitrary) This is the whole purpose of LWL and LWR etc.
// Then the switch uses 3 - (tmp & 0x3) instead of (tmp & 0x3)

// 这段代码与大端(BIG ENDIAN)代码的唯一区别在于,
// ReadMem 调用保证了对齐访问,正如它应该做到的那样(Kane 的书掩盖了所有内存访问
// 都是通过对齐加载完成的事实——指令所要求的是任意的访问)。
// 这也是 LWL(Load Word Left) 和 LWR 等指令的全部目的所在。
// 然后,switch 使用的是 3 - (tmp & 0x3),而不是 (tmp & 0x3)。

又例如:

  • 代码位置: translate.cc, machine.cc
  • 代码示例:
1
2
3
4
5
6
7
8
9
10
11
12
unsigned int WordToHost(unsigned int word) {
#ifdef HOST_IS_BIG_ENDIAN
register unsigned long result;
result = (word >> 24) & 0x000000ff;
result |= (word >> 8) & 0x0000ff00;
result |= (word << 8) & 0x00ff0000;
result |= (word << 24) & 0xff000000;
return result;
#else
return word;
#endif
}
  • 解释: WordToHostShortToHost 函数用于在小端和大端存储之间进行转换。NachOS 默认假设主机是小端存储,因此这些函数通常是 NOP 操作。

按字寻址**

  • 代码位置: mipssim.cc
  • 代码示例:
1
2
if (!ReadMem(registers[PCReg], 4, &raw)) // Fetch instruction
return;
  • 解释: NachOS 的 ReadMem 函数以 4 字节(一个字)的单位读取内存。MIPS 指令长度固定为 4 字节,因此按字寻址。

定长 ISA**

  • 代码位置: mipssim.cc
  • 代码示例:
1
2
3
4
5
6
void Instruction::Decode() {
rs = (value >> 21) & 0x1f;
rt = (value >> 16) & 0x1f;
rd = (value >> 11) & 0x1f;
...
}
  • 解释: MIPS 指令长度固定为 32 位(4 字节)。Decode 函数通过位操作解析指令的各个字段。

又如,刚刚实验中的代码片段:

1
2
3
4
5
6
7
8
9
10
{
/* set previous programm counter (debugging only)*/
kernel->machine->WriteRegister(PrevPCReg, kernel->machine->ReadRegister(PCReg));

/* set programm counter to next instruction (all Instructions are 4 byte wide)*/
kernel->machine->WriteRegister(PCReg, kernel->machine->ReadRegister(PCReg) + 4);

/* set next programm counter for brach execution */
kernel->machine->WriteRegister(NextPCReg, kernel->machine->ReadRegister(PCReg)+4);
}

对PC寄存器进行处理时,指向下一条指令地址是+4操作:4*8=32,这也说明了其MIPS 指令长度固定为 32 位。

取-存架构

  • 代码位置: mipssim.cc
  • 代码示例:
1
2
3
4
5
6
7
8
case OP_LW:
if (!ReadMem(tmp, 4, &value)) return;
registers[instr->rt] = value;
break;

case OP_SW:
if (!WriteMem(tmp, 4, registers[instr->rt])) return;
break;
  • 解释: LWSW 指令分别用于从内存加载数据到寄存器和将寄存器数据存储到内存。其他指令只能操作寄存器。

三地址结构

这是我们实验中mipsim.cc(MIPS模拟器)对MIPS架构所规定的简单的算术指令的实现:
case OP_ADDIUstart.saddiu 指令的底层实现。它模拟了 MIPS 架构中 addiu 指令的行为,将源寄存器的值与立即数相加,并将结果存储到目标寄存器中。
这个addiu就是我们在start.s调用的那条汇编指令!它的底层实现通过如下的三地址结构:

  • 代码位置: mipssim.cc
  • 代码示例:
1
2
3
case OP_ADD:
registers[instr->rd] = registers[instr->rs] + registers[instr->rt];
break;
  • 解释: MIPS 使用三地址结构,rd 是目标寄存器,rsrt 是源寄存器。上述代码实现了 ADD 指令。

算术指令

  • 代码位置: mipssim.cc
  • 代码示例:
1
2
3
4
5
6
7
case OP_SUB:
registers[instr->rd] = registers[instr->rs] - registers[instr->rt];
break;

case OP_XOR:
registers[instr->rd] = registers[instr->rs] ^ registers[instr->rt];
break;
  • 解释: 算术指令如 SUBXOR 操作寄存器中的数据,并将结果存储到目标寄存器。

数据传送指令

  • 代码位置: mipssim.cc
  • 代码示例:
1
2
3
4
5
6
7
case OP_LW:
registers[instr->rt] = value; // Load word
break;

case OP_SW:
WriteMem(tmp, 4, registers[instr->rt]); // Store word
break;
  • 解释: 数据传送指令如 LWSW 用于在内存和寄存器之间传递数据。

控制指令

  • 代码位置: mipssim.cc
  • 代码示例:
1
2
3
4
5
6
7
8
case OP_BEQ:
if (registers[instr->rs] == registers[instr->rt])
pcAfter = registers[NextPCReg] + IndexToAddr(instr->extra);
break;

case OP_J:
pcAfter = (pcAfter & 0xf0000000) | IndexToAddr(instr->extra);
break;
  • 解释: 控制指令如 BEQJ 用于条件分支和跳转。

** 多周期指令**

  • 代码位置: mipssim.cc
  • 代码示例:
1
2
3
4
5
6
7
8
case OP_MULT:
Mult(registers[instr->rs], registers[instr->rt], TRUE, &registers[HiReg], &registers[LoReg]);
break;

case OP_DIV:
registers[LoReg] = registers[instr->rs] / registers[instr->rt];
registers[HiReg] = registers[instr->rs] % registers[instr->rt];
break;
  • 解释: 多周期指令如 MULTDIV 需要多个时钟周期完成操作,结果存储在 HiRegLoReg

在 NachOS 中,MIPS 的寻址方式(如直接寻址、寄存器间接寻址)由编译器生成的指令和 NachOS 的指令模拟器(mipssim.cc)共同实现。以下是不同寻址方式的代码位置和实现细节:


1. 基址寻址(Base Addressing)

  • 代码位置: mipssim.cc
  • 代码示例:
1
2
3
4
5
case OP_LW:  // Load Word
tmp = registers[instr->rs] + instr->extra; // 基址 + 偏移量
if (!ReadMem(tmp, 4, &value)) return;
registers[instr->rt] = value;
break;
  • 解释:
    • 基址寻址是 MIPS 的主要寻址方式。
    • instr->rs 是基址寄存器,instr->extra 是偏移量。
    • 例如,LW $t0, 4($t1)$t1 + 4 的内存内容加载到 $t0

2. 立即寻址(Immediate Addressing)

  • 代码位置: mipssim.cc
  • 代码示例:
1
2
3
case OP_ADDI:  // Add Immediate
registers[instr->rt] = registers[instr->rs] + instr->extra;
break;
  • 解释:
    • 立即寻址使用指令中的立即数作为操作数。
    • 例如,ADDI $t0, $t1, 10$t1 + 10 的结果存储到 $t0

3. 寄存器间接寻址(Register Indirect Addressing)

  • 代码位置: mipssim.cc
  • 代码示例:
1
2
3
case OP_JR:  // Jump Register
pcAfter = registers[instr->rs]; // 跳转到寄存器地址
break;
  • 解释:
    • 寄存器间接寻址使用寄存器中的值作为目标地址。
    • 例如,JR $t1 将程序跳转到 $t1 中存储的地址。

4. 直接寻址(Direct Addressing)

  • 代码位置: mipssim.cc
  • 代码示例:
1
2
3
case OP_J:  // Jump
pcAfter = (pcAfter & 0xf0000000) | IndexToAddr(instr->extra);
break;
  • 解释:
    • 直接寻址使用指令中的目标地址作为跳转目标。
    • 例如,J 0x00400000 将程序跳转到地址 0x00400000

5. 编译器模拟的寻址方式

MIPS 的其他寻址方式(如变址寻址、寄存器间接寻址)通常由编译器通过生成特定的指令序列来模拟。例如:

  • 变址寻址:

    • 编译器会生成一条 ADD 指令计算目标地址,然后使用 LWSW 访问内存。

    • 示例:

      1
      2
      ADD $t0, $t1, $t2  # 计算目标地址
      LW $t3, 0($t0) # 加载内存内容
  • 寄存器间接寻址:

    • 编译器会直接使用寄存器中的值作为地址。

    • 示例:

      1
      JR $t1  # 跳转到寄存器 $t1 中存储的地址

6. NachOS 的实现机制

NachOS 的 Instruction::Decode 函数负责解析指令的字段(如 rsrtextra),并将其传递给 Machine::OneInstruction 执行。以下是指令解码的代码:

  • 代码位置: mipssim.cc
  • 代码示例:
1
2
3
4
5
6
7
8
9
void Instruction::Decode() {
rs = (value >> 21) & 0x1f; // 源寄存器
rt = (value >> 16) & 0x1f; // 目标寄存器
rd = (value >> 11) & 0x1f; // 结果寄存器
extra = value & 0xffff; // 偏移量或立即数
if (extra & 0x8000) {
extra |= 0xffff0000; // 符号扩展
}
}
  • 解释:
    • Decode 函数解析指令的操作码和操作数。
    • 不同的寻址方式通过 extra(立即数或偏移量)和寄存器字段(rsrt)实现。

总结

NachOS 中的寻址方式主要由 mipssim.cc 中的指令模拟器实现,具体包括:

  1. 基址寻址: 使用 LWSW 指令。
  2. 立即寻址: 使用 ADDIORI 指令。
  3. 寄存器间接寻址: 使用 JR 指令。
  4. 直接寻址: 使用 JJAL 指令。

其他复杂的寻址方式(如变址寻址)通常由编译器通过指令序列模拟实现,而 NachOS 的指令模拟器负责执行这些指令。

LoOS实验-实现系统调用

可以类比NachOS的过程,大同小异,因为LoOS使用qemu模拟器模拟硬件环境,所以看起来会简单一些。

1. 理解系统调用机制

  • 阅读目标架构(如 LoongArch)的 ISA 文档,了解系统调用的实现方式。
  • 系统调用通常通过特定的指令(如 scallsyscall)触发,使用寄存器传递参数和返回值。

2. 在内核中实现系统调用

步骤:

  1. 定义系统调用函数

    • 在内核代码中实现系统调用的逻辑。例如,在 syscall.c 中实现 sys_getppid

      1
      2
      3
      uint64_t sys_getppid(void) {
      return current->parent->pid; // 返回父进程的 PID
      }
  2. 注册系统调用

    • 在系统调用表(如 syscall.tblsyscall.c)中注册系统调用号和对应的函数:

      1
      syscall_table[173] = (void *)sys_getppid; // 173 是 sys_getppid 的系统调用号
  3. 编译内核

    • 重新编译内核以包含新的系统调用实现。

3. 编写用户态测试程序

步骤:

  1. 编写汇编代码

    • 在用户态编写汇编代码,通过系统调用号触发内核中的系统调用。例如,在 sys_test.s 中:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      .section .text
      .global _start

      _start:
      li a7, 173 # 系统调用号 SYS_GETPPID
      scall # 触发系统调用
      li a7, 93 # 系统调用号 SYS_EXIT
      li a0, 0 # 退出状态码
      scall
  2. 编译汇编代码

    • 使用工具链将汇编代码编译为目标文件:

      1
      loongarch64-linux-gnu-as -o /tmp/sys_test.o scripts/gradelib/sys_test.s
  3. 链接生成可执行文件

    • 使用链接器生成静态可执行文件:

      1
      loongarch64-linux-gnu-ld -static -o /tmp/sys_test /tmp/sys_test.o
  4. 运行测试程序

    • 将生成的可执行文件复制到目标环境(如 QEMU 模拟器)并运行:

      1
      ./sys_test

4. 验证系统调用

步骤:

  1. 检查返回值
  • 验证系统调用是否返回正确的结果(如父进程的 PID)。
  1. 调试和修复
  • 如果系统调用未按预期工作,检查内核实现、系统调用号、参数传递等。
  1. 运行自动化测试

    • 使用脚本(如 grade-lab1-sys)验证系统调用的功能:

      1
      2
      chmod +x scripts/gradelib/grade-lab1-sys
      ./scripts/gradelib/grade-lab1-sys

5. 总结

  • 内核部分
    • 实现系统调用逻辑。
    • 注册系统调用号。
    • 重新编译内核。
  • 用户态部分
    • 编写汇编代码触发系统调用。
    • 编译和链接生成可执行文件。
    • 运行测试程序验证功能。

操作系统-系统调用
https://43.242.201.154/2025/04/21/Operating-System0/
Author
Dong
Posted on
April 21, 2025
Licensed under