【哈工大李治军】操作系统课程笔记2:系统调用 +【实验 2】系统调用实现

1、操作系统接口

在这里插入图片描述
完成setup后,操作系统的代码都被读入到从0地址开始的地方,还创建了一些初始的结构,如mem_map(管理内存的数据结构)、GDTIDT等。而我们的应用程序都放在了内存的上端。

最终,内存的下方放置的为系统代码和数据上方放置的为应用程序,这样子一个结构情况。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
(1)什么是操作系统接口?
系统调用(接口表现为函数调用,又由系统提供)

(2)操作系统接口连接谁?
连接操作系统和应用软件

(3)如何连接?
C语言程序

在这里插入图片描述
其中,printf是包装了write(系统调用函数)之后的函数。

POSIX是统一的接口

2、系统调用的实现

(0)函数处理过程

(1)假设用户程序内使用printf()函数;
(2)根据lib下的_syscalln()include/unistd.h下的模板,对printf()函数进行宏定义展开;
(3)调用展开后的函数,触发80中断,将kernel下的system_call对应的IDT表中的DPL设为3,从而让用户程序可获取system_call地址作为IP。然后,再设置CS=8,使其对应的CPL=0,从而让用户可以进入内核态;
(4)在system_call函数中,会使用从include/unistd.h中获得的存入eax的值,来查询include/linux/sys.hsys_call_table表里对应的系统调用函数;
(5)使用对应的系统调用函数处理数据后,将结果存入eax并返回给用户程序。

操作系统实现系统调用的基本过程
(1)应用程序调用库函数(API);
(2)API 将系统调用号存入 EAX,然后通过中断调用使系统进入内核态
(3)内核中的中断处理函数根据系统调用号,调用对应的内核函数(系统调用);
(4)系统调用完成相应功能,将返回值存入 EAX返回到中断处理函数
(5)中断处理函数返回到 API 中
(6)API 将 EAX 返回给应用程序。
整个过程中主要通过EAX传递数值。

在通常情况下,调用系统调用调用一个普通的自定义函数在代码上并没有什么区别,但调用后发生的事情有很大不同。

  • 调用自定义函数是通过call指令直接跳转到该函数的地址,继续运行。

  • 调用系统调用,是调用系统库中为该系统调用编写的一个接口函数,叫 API(Application Programming Interface)。API 并不能完成系统调用的真正功能,它要做的是去调用真正的系统调用,过程是:
    (1)把系统调用的编号存入 EAX
    (2)把函数参数存入其它通用寄存器
    (3)触发 0x80 号中断(int 0x80)。

linux-0.11 的 lib 目录下有一些已经实现的 API。Linus 编写它们的原因是在内核加载完毕后,会切换到用户模式下,做一些初始化工作,然后启动 shell。而用户模式下的很多工作需要依赖一些系统调用才能完成,因此在内核中实现了这些系统调用的 API。
在这里插入图片描述
通过硬件实现内核的数据不能被随意的调用,不能随意的jmp。否则,操作系统就会不安全。

系统调用实际上提供了一种进入内核的手段

在这里插入图片描述
将内存分割成内核段用户段两个区域。内核态可以访问任何数据,但用户态不能访问内核数据。只有当前的指令大于或等于目标的特权级,这条指令才被允许执行。

不论是内核段还是用户段都需要通过段寄存器进行访问,主要使用了两个段寄存器CPLDPL来实现不同权限的控制。其中CPL存放在CS中,DPL存放在GDT中。当想访问其他段时,会从GDT中查询目标段的DPL来和当前所执行段CS中的CPL进行对比。若合法则允许访问,若不合法则不允许访问。

保护模式中最重要的一个思想就是通过分级把代码隔离了起来,不同的代码在不同的级别 ,使大多数情况下都只和同级代码发生关系。 在保护模式下,对一个段的描述则包括3方面因素:[Base Address, Limit, Access],它们加在一起被放在一个64-bit长的数据结构中,被称为段描述符

Intel 的80286以上的CPU可以识別4个特权级(或特权层) ,0级到3级数值越大特权越小。一般用把系统内核放在0级,系统的其他服务程序位于1、2级,3级则是应用软件。一般情况下代码都在自己的级别下做自己 的工作,同一级别之间可以相互访问,而一般是不允许不同级别的代码间随意访问的。但有时候不同级别的程序之间一定要访问,比如系统的接口函数等,必须能够使得应用程序能够随意调用。0表示内核态,3表示用户态。

DPL(Descriptor Privilege Level):描述符特权
用于描述目标内存段(要跳转访问的目标段)的特权级。 存储在描述符中的权限位,用于描述代码的所属的特权等级,也就是代码本身真正的特权级。一个程序可以使用多个段(Data,Code,Stack)也可以只用一个code段等。正常的情况下,当程序的环境建立好后,操作系统已经初始化好了DPL,段描述符都不需要改变——当然DPL也不需要改变,因此每个段的DPL值是固定。DPLGDT中,一个GDT表的表项用于描述一段内存。OS中区域无论是数据段还是代码段,GDT表中对应的DPL均为0。

CPL(Current Privilege Level):当前任务特权
用于描述当前的执行内存段的特权级。 它的特权级是3,表示用户态
在这里插入图片描述
中断是进入内核的唯一方法,该方法通过硬件来实现。因此,如果用户程序想要进入内核,就需要包含一段int指令的代码,这段代码由库函数实现,由宏来展开成一段汇编代码。进入内核之后,操作系统就会写中断处理过程,来获取想调程序的编号。然后,操作系统会根据编号执行相应的代码。

在这里插入图片描述
规定只有int 0x80中断才能进入内核态。

(1)include/unistd.h

include/linux/sys.h

#define __NR_write	4

lib/close.c

#define __LIBRARY__
#include <unistd.h>

_syscall1(int, close, int, fd)

include/unistd.h

#define _syscall1(type,name,atype,a) \
type name(atype a) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
    : "=a" (__res) \
    : "0" (__NR_##name),"b" ((long)(a))); \
if (__res >= 0) \
    return (type) __res; \
errno = -__res; \
return -1; \
}

_syscall1(int,close,int,fd) 进行宏展开,可以得到:

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;
}

格式:

#define _syscall1(定义的函数类型, 定义的函数名, 入口参数类型, 入口参数名)

#define 来定义宏。该命令允许把一个名称指定成任何所需的文本。 在定义了宏之后,无论宏名称出现在源代码的何处,预处理器都会把它用定义时指定的文本替换掉。

#与 ## 是俩个特殊符号,# 表示将一个宏参数变成一个字符串,## 表把俩个字符串粘在一起。惯例将宏名称每个字母采用大写,这有助于区分宏与一般的变量。

0或空表示使用与相应输出一样的寄存器。

a表示使用eax,并编号%0。

在这里插入图片描述

拓展资料:
宏函数

C语言宏的定义和宏的使用方法(#define)

内嵌汇编学习

C语言的内嵌汇编

在这里插入图片描述
嵌入汇编程序规定把输出输入寄存器统一按顺序编号,顺序是从输出寄存器序列从左到右从上到下以 “%0” 开始,分别记为 %0 、 %1 、 …%9 。因此,输出寄存器的编号是 %0(这里只有一个输出寄存器),输入寄存器前一部分 (__NR_##name)的编号是%1,而后部分((long)(a)))的编号1%

用宏定义_syscall3来调用展开。在实现过程中,先将宏__NR_write(一个系统调用号)存入eax,将参数fd存入ebx*buf存入ecxcount存入edx

输入完参数后,就执行int 0x80指令,在内核态根据获取到的参数,去执行相应的系统调用函数。执行完内核态程序后,再把eax中的值置给__res。最后根据__res的值,决定执行return (type)__resreturn -1,返回int write()的返回值。

include/unistd.h
在这里插入图片描述
在这里插入图片描述

(2)include/asm/system.h + kernal/sched.c

int 0x80 触发后,接下来就是内核的中断处理了。上述的int 0x80的执行过程,实际上就是从IDT表里面取出中断处理函数,然后跳到对应位置去执行。中断处理函数执行完后,再回来把eax赋给__res

首先,了解一下 0.11 处理 0x80 号中断的过程。

在内核初始化时,主函数(在 init/main.c 中,Linux 实验环境下是 main(),Windows 下因编译器兼容性问题被换名为 start())调用了 sched_init() 初始化函数:

void main(void)
{
//    ……
    time_init();
    sched_init();
    buffer_init(buffer_memory_end);
//    ……
}

sched.c是内核中有关任务(进程)调度管理的程序,其中包括有关调度的基本函数(sleep_on()、wakeup()、schedule()等)以及一些简单的系统调用函数(比如getpid())。系统时钟中断处理过程中调用的定时函数do_timer()也被放置在本程序中。
在这里插入图片描述

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

void sched_init(void)
{
//    ……
    set_system_gate(0x80,&system_call);
}

0x80传递给了nsystem_call函数的地址传递给了addr
在这里插入图片描述

set_system_gate是个宏,在include/asm/system.h中定义为:

#define set_system_gate(n,addr) \
    _set_gate(&idt[n],15,3,addr)

其中n表示中断号,addr表示地址。

然后,set_system_gate又调用了_set_gate这个宏。&idt[n](idt是一个全局标量,它是IDT表的起始地址,用n来找到80号中断对应的表项)会传递给gate_addr参数。addr就表示上述的地址,153分别传到了typedpl
在这里插入图片描述
在这里插入图片描述
这段代码主要是初始化IDT表,然后再根据中段指令去查表,跳转到对应地址进行执行。

对应IDT结构

| 处理函数入口点偏移=addr=&system_call  | p | 3 | 01110 |                   |

对应CS:IP结构

| 段选择符=0x0008   | 处理函数入口点偏移=addr=&system_call  |

此时DPL=3,而CS=8,IP=&system_call,其中CS的最后两位为0,即CPL=0

_set_gate的定义是:

 设置门描述符宏
//	根据参数的中段或异常处理过程地址addr、门描述符类型type和特权级信息dpl,设置位于地址 gate_addr 处的门描述符。
//	注意:下面“偏移”值是相对于内核代码或数据段来说的。
//	%0 — 由dpl,type组合成的类型标志字;%1 — 描述符低4字节地址
//	%2 — 描述符高4字节地址; %3 — edx(程序偏移地址addr); %4 — eax(高字中含有段选择符)
#define _set_gate(gate_addr,type,dpl,addr) \
__asm__ ("movw %%dx,%%ax\n\t" \			// 将偏移地址低字与选择符组合成描述符低4字节(eax)。
    "movw %0,%%dx\n\t" \				// 将类型标志字与偏移高字组合成描述符高4字节(edx)。
    "movl %%eax,%1\n\t" \				// 分别设置门描述符的低4字节和高4字节。
    "movl %%edx,%2" \
    : \
    : "i" ((short) (0x8000+(dpl<<13)+(type<<8))), \			// 1111<<13 — 1 1110 0000 0000 0000、0011<<8 — 0011 0000 0000
    "o" (*((char *) (gate_addr))), \						// gate_addr — 0x80
    "o" (*(4+(char *) (gate_addr))), \						
    "d" ((char *) (addr)),"a" (0x00080000))					// addr — &system_call

参数:
gate_addr描述符地址:指定了描述符所处的物理内存地址;
type描述符类型域值:指明所需设置的描述符类型,type=14(0x0E)表示中断门描述符,type=15(0x0F)表示陷阱门描述符;
dpl描述符特权级:对应描述符格式中的DPL(Descriptor Privilege Level);
addr偏移地址:是描述符对应的中断处理过程的32位偏移地址

:因为中断处理过程属于内核段代码,所以它们的段选择符值均为0x0008(在eax寄存器高字中指定)。

虽然代码看起来挺麻烦,但实际上很简单,就是填写 IDT(中断描述符表),将 system_call 函数地址写到 0x80 对应的中断描述符中,也就是在中断 0x80 发生后,自动调用函数 system_call

在这里插入图片描述
详细过程
int 0x80需要根据IDT表找到中断处理函数,然后调到那里去执行,处理完之后再回来,再去执行把eax赋值给__res操作。

在初始化的时候,int 0x80需要通过system_call来进行处理。该处理通过中断处理门来实现,核心是初始化好IDT。一旦初始化完毕后,后续再遇到80中断时,就直接从IDT中取出相应的中断处理函数(system_call),然后调到对应地方去执行。

movl %%eax, %1是将eax赋给了%1"o"(*((char*)(gate_addr))))。

最后实现将addr=&system_call组装到了处理函数入口点偏移,把dpl=3组装到了DPL,将0x0008组装到了段选择符。所以现在,CS=8,IP=&system_call。
因为当CS=8时,CS的最后两位CPL就等于00。

总结
在初始化的时候将80号中断的DPL设为3,故意让用户态程序能够进来。进来之后,CPL就会根据CS=8,其中CPL=0,进入到内核态。执行完内核态中代码后,CS的最后两位又会被设置为3,又变成了用户态的东西。

拓展资料:
什么是调用门?

中断描述符(IDT)、任务门、中断门、陷阱门

system.h中定义了设置或修改描述符/中断门等的嵌入式汇编宏。其中,函数move_to_user_mode()是用于内核在初始化结束时人工切换(移动)到初始进程(任务0)去执行,即从特权级0代码转移到特权级3的代码中去运行。
在这里插入图片描述
使用这种方法进行控制权的转移是由CPU保护机制造成的。CPU允许低级别(例如特权级3)的代码通过调用门或中断、陷阱门来调用或转移到高级别的代码中运行,但反之则不允许。因此内核采用了这种模式IRET返回低级别代码的方法。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

(3)system_call.s

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
kernel/system_call.s


!……
! # 这是系统调用总数。如果增删了系统调用,必须做相应修改
nr_system_calls = 72
!……

.globl system_call
.align 2
system_call:

! # 检查系统调用编号是否在合法范围内
    cmpl \$nr_system_calls-1,%eax		# 调用好如果超出范围就在eax中置-1并对出
    ja bad_sys_call
    push %ds							# 保存原段寄存器值
    push %es
    push %fs
  # 一个系统调用最多可带3个参数,也可不带参数
    pushl %edx							# 存放第3个参数
    pushl %ecx							# 存放第2个参数

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

! # 让ds, es指向GDT,内核地址空间
    movl $0x10,%edx
    mov %dx,%ds
    mov %dx,%es							# ds,es指向内核数据段(全局描述符表中数据段描述符)。
    movl $0x17,%edx
! # 让fs指向LDT(局部数据段,局部描述符表中数据段描述符),用户地址空间。指向执行本次系统调用的用户程序的数据段。
    mov %dx,%fs
    
    call sys_call_table(,%eax,4)		# 间接调用指定功能C函数。调用地址=[sys_call_table + %eax * 4],其中sys_call_table[]是一个指针数组,在include/linux/sys.h中
    
    pushl %eax							# 把系统调用返回值入栈
  # 查看当前任务的运行状态。如果不在就绪状态(state≠0),则去执行调度程序。
  # 如果该任务在就绪状态,但时间片已用完(counter=0),则也去执行调度程序。
    movl current,%eax					# 取当前任务(进程)数据结构地址-> eax。
    cmpl $0,state(%eax)					# state
    jne reschedule
    cmpl $0,counter(%eax)				# counter
    je reschedule

system_call.globl 修饰为其他函数可见。Windows 实验环境下会看到它有一个下划线前缀,这是不同版本编译器的特质决定的,没有实质区别。

call sys_call_table(,%eax,4) 之前的代码主要实现一些压栈保护,修改段选择子为内核段。
call sys_call_table(,%eax,4) 之后的代码是看看是否需要重新调度,这些都与本实验没有直接关系,此处只关心 call sys_call_table(,%eax,4) 这一句。

注: 每个函数都是4个字节(32位),所以乘上4。

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

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

call _sys_call_table(,%eax,4)汇编寻址方法它实际上是:call sys_call_table + 4 * %eax,其中 eax 中放的是系统调用号(可看作该函数的数组下标),即 __NR_xxxxxxsys_call_table就是基址(是一个函数表)。4表示每个系统调用对应的函数占四个字节(32位)。当要查找sys_write(位于第5个函数)时,会设置 eax=4(数组下标从0开始)。

edx置为10,然后将dxes都置为10。而都置为10的原因是,10的最后两位也是0。

其中,iret对应的处理函数就放在第四个位置上

在这里插入图片描述
(1)用户调用printf函数(库函数)。
(2)然后printf会展开宏定义_syscall3,查找unistd.h中对应的系统调用号,将其传入eax中,等其余寄存器存储好其余参数后,触发int 0x80中断,调用set_system_gate函数。此时为用户态,调用set_system_gate后,会将system_callIDT中的DPL设为3,从而让用户程序可以使用其对应地址作为IP。之后,再将CS:IP中的CPL设为0,让用户程序可进入内核态。
(3)进入内核态中,调用system_call来根据eax查找表system_call_table中对应的系统调用函数。
(4)根据已在unisted.h获得的宏定义__NR_write=4,再调用call sys_call_table(,%eax,4)指令,即call sys_call_table + 4 * %eax也就是call sys_write,来调用目标函数进行执行。
(5)执行完后,将结果存入eax中,返回到_syscall3中宏定义的函数,作为其返回值。

在这里插入图片描述

在这里插入图片描述
用户态中CPL=3,内核态中DPL=0。用户程序不能随意的进入内核中调用,要想进去用户必须要先设置系统调用号,通过中断int 0x80,才能通过接口调用内核程序。想要“穿过”内核的方式是将DPL也设置为和CPL相同的数,一旦“穿过”去之后,CPL就被置为0。在_system_call里面通过移动查表就会调用sys_whoami,然后就会跑到内核中真正的sys_whoami()函数调用。

[实验 2]:系统调用

(1)添加系统调用编号

linux-0.11/include/unisted.h中添加系统调用编号,格式__NR__xxxxx
在这里插入图片描述

: 在 0.11 环境下编译 C 程序,包含的头文件都在 /usr/include 目录下。如果只在这里修改会报错,后面会说明。

(2)修改系统调用函数数量和系统调用表

system_call.s中增加了两个系统调用,所以将系统调用总数nr_system_calls更改为74。
在这里插入图片描述
这是系统调用总数。如果增删了系统调用,必须做相应修改。

include/linux/sys.h中,找到fn_ptr sys_call_table[],在里面添加sys_whoamisys_iam这两个函数引用。同时,在上面也添加extern int sys_whami()extern int sys_iam()
在这里插入图片描述
: 函数在 sys_call_table 数组中的位置必须和 unisted.h中的__NR_xxxxxx 的值对应上。

(3)实现内核函数

kernel中创建who.c并修改

#include <errno.h>
#include <string.h>
#include <linux/kernel.h>
#include <asm/segment.h>

static char str[24];
static unsigned long len;

int sys_iam(const char* name) {
    int i, j;
    char tmp[24];				
    for(i = 0; i < 24; i++) {
        tmp[i] = get_fs_byte(name + i);
        if(tmp[i] == 0) break;
    }
    len = i + 1;
    // clear str
    for(j = 0; j < 24; j++) {
        str[j] = 0;
    }       
    if(i == 24) {    	
    	printk("Length over 23! Please enter again!\n");
        return -(EINVAL);
    }     
    // copy
    for(i = 0; tmp[i] != 0; i++) {
        str[i] = tmp[i];
    }
    return len;
}

int sys_whoami(char* name, unsigned int size) {
    int i;
    if(size < len) {
        return -(EINVAL);
    }
    for(i = 0; i < size && str[i] != 0; i++) {
        put_fs_byte(str[i], name + i);
    }
    put_fs_byte(0, name + i);
    return len;
}

注: 不能在for循环里定义int i否则make all时会报错。

实验内容要求系统调用API在参数不合理时返回-1并置errnoEINVAL。从下面的宏展开可知,errno是一个存在于用户空间的全局变量,其值是系统调用处理程序返回值的负值,所以系统调用服务例程在参数不合理时应写成return -(EINVAL)。当传进来的字符串过长,需要return -(EINVAL)。

#define _syscall1(type,name,atype,a) \
type name(atype a) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
	: "=a" (__res) \
	: "0" (__NR_##name),"b" ((long)(a))); \
if (__res >= 0) \
	return (type) __res; \
errno = -__res; \
return -1; \
}

#define _syscall2(type,name,atype,a,btype,b) \
type name(atype a,btype b) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
	: "=a" (__res) \
	: "0" (__NR_##name),"b" ((long)(a)),"c" ((long)(b))); \
if (__res >= 0) \
	return (type) __res; \
errno = -__res; \
return -1; \
}

(4)修改Makefile

Makefile 在代码树中有很多,分别负责不同模块的编译工作。我们要修改的是kernel/Makefile中的OBJSDependencies中的内容:

OBJS处加上who.o
在这里插入图片描述
# Despendencies下添加who.s who.o: who.c ../include/linux/kernel.h ../include/unistd.h
在这里插入图片描述
然后在linux-0.11下使用make all就能自动把 who.c 加入到内核中了。
在这里插入图片描述

(5)编写系统测试程序

想使用系统调用iamwhoami ,就需要有相应的系统调用API。在 /usr/include/unistd.h 中,有linus预先写好的系统调用API宏模板 _syscalln() ,其中n表示的是系统调用的参数个数。

#define _syscall1(type,name,atype,a) \
type name(atype a) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
	: "=a" (__res) \
	: "0" (__NR_##name),"b" ((long)(a))); \
if (__res >= 0) \
	return (type) __res; \
errno = -__res; \
return -1; \
}

#define _syscall2(type,name,atype,a,btype,b) \
type name(atype a,btype b) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
	: "=a" (__res) \
	: "0" (__NR_##name),"b" ((long)(a)),"c" ((long)(b))); \
if (__res >= 0) \
	return (type) __res; \
errno = -__res; \
return -1; \
}

在oslab目录下编写iam.cwhoami.c
iam.c

/* 有它,_syscall1 等才有效。详见unistd.h */
#define __LIBRARY__
/* 有它,编译器才能获知自定义的系统调用的编号 */
#include "unistd.h"
#include <stdio.h>

_syscall1(int, iam, char*, name);

int main(int argc, char* argv[]) {
	if(argc <= 1){
		printf("input error\n");
		return -1;
	}
	iam(argv[1]);
	return 0;
}

whoami.c

#define __LIBRARY__
#include "unistd.h"
#include <stdio.h>

_syscall2(int, whoami, char*, name, unsigned int, size);

char name[24] = {};
int main(int argc, char* argv[]) {

    whoami(name, 24);
    printf("%s\n", name);

    return 0;
}

: 不能用//来注释,否则在linux0.11中会报错。

(6)挂载文件至linux0.11

然后,将这两个文件以挂载的方式实现宿主机与虚拟机操作系统的文件共享,在 oslab 目录下执行以下命令挂载hdc目录到虚拟机操作系统上。

sudo ./mount-hdc 

再通过以下命令将上述两个文件拷贝到虚拟机linux-0.11操作系统/usr/root/目录下,命令在oslab/目录下执行:

cp iam.c whoami.c hdc/usr/root

查看是否挂载成功
在这里插入图片描述
读写完毕后再卸载

卸载
sudo umount hdc

(7)编译iam.c和whoami.c

可以直接在 Linux 0.11 环境下用 vi 编写(别忘了经常执行“sync”以确保内存缓冲区的数据写入磁盘),也可以在 Ubuntu 或 Windows 下编完后再传到 Linux 0.11 下。无论如何,最终都必须在 Linux 0.11 下编译。编译命令是:

gcc -o iam iam.c 
gcc -o whoami whoami.c 

gcc 的 “-Wall” 参数是给出所有的编译警告信息,“-o” 参数指定生成的执行文件名是 iam。
在这里插入图片描述
出现报错,原因是:
之前修改的unistd.h没有加载到linux-0.11中,需要打开挂载后,进入hdc/usr/include中去修改unistd.h
在这里插入图片描述
再用gcc编译,若无提示信息, 则编译成功
在这里插入图片描述

(8)测试

在这里插入图片描述
成功!

参考资料:
操作系统实验(二)——系统调用

超详细!操作系统实验三 系统调用(哈工大李治军)

哈工大-操作系统实验-李治军-实验2:系统调用

  • 7
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

辰阳星宇

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值