从 01 开始 从 01 开始
首页
  • 📚 计算机基础

    • 计算机简史
    • 数字电路
    • 计算机组成原理
    • 操作系统
    • Linux
    • 计算机网络
    • 数据库
    • 编程工具
    • 装机
  • 🎨 前端

    • Node
  • JavaSE
  • Java 高级
  • JavaEE

    • 构建、依赖管理
    • Ant
    • Maven
    • 日志框架
    • Junit
    • JDBC
    • XML-JSON
  • JavaWeb

    • 服务器软件
    • 环境管理和配置管理-科普篇
    • Servlet
  • Spring

    • Spring基础
  • 主流框架

    • Redis
    • Mybatis
    • Lucene
    • Elasticsearch
    • RabbitMQ
    • MyCat
    • Lombok
  • SpringMVC

    • SpringMVC 基础
  • SpringBoot

    • SpringBoot 基础
  • Windows 使用技巧
  • 手机相关技巧
  • 最全面的输入法教程
  • 最全面的浏览器教程
  • Office
  • 图片类工具
  • 效率类工具
  • 最全面的 RSS 教程
  • 码字工具
  • 各大平台
  • 校招
  • 五险一金
  • 职场规划
  • 关于离职
  • 杂谈
  • 自媒体
  • 📖 读书

    • 读书工具
    • 走进科学
  • 🌍 英语

    • 从零开始学英语
    • 英语兔的相关视频
    • Larry 想做技术大佬的相关视频
  • 🏛️ 政治

    • 反腐
    • GFW
    • 404 内容
    • 审查与自我审查
    • 互联网
    • 战争
    • 读书笔记
  • 💰 经济

    • 关于税
    • 理财
  • 💪 健身

    • 睡眠
    • 皮肤
    • 口腔健康
    • 学会呼吸
    • 健身日志
  • 🏠 其他

    • 驾驶技能
    • 租房与买房
    • 厨艺
  • 电影

    • 电影推荐
  • 电视剧
  • 漫画

    • 漫画软件
    • 漫画推荐
  • 游戏

    • Steam
    • 三国杀
    • 求生之路
  • 小说
  • 关于本站
  • 关于博主
  • 打赏
  • 网站动态
  • 友人帐
  • 从零开始搭建博客
  • 搭建邮件服务器
  • 本站分享
  • 🌈 生活

    • 2022
    • 2023
    • 2024
    • 2025
  • 📇 文章索引

    • 文章分类
    • 文章归档

晓林

程序猿,自由职业者,博主,英语爱好者,健身达人
首页
  • 📚 计算机基础

    • 计算机简史
    • 数字电路
    • 计算机组成原理
    • 操作系统
    • Linux
    • 计算机网络
    • 数据库
    • 编程工具
    • 装机
  • 🎨 前端

    • Node
  • JavaSE
  • Java 高级
  • JavaEE

    • 构建、依赖管理
    • Ant
    • Maven
    • 日志框架
    • Junit
    • JDBC
    • XML-JSON
  • JavaWeb

    • 服务器软件
    • 环境管理和配置管理-科普篇
    • Servlet
  • Spring

    • Spring基础
  • 主流框架

    • Redis
    • Mybatis
    • Lucene
    • Elasticsearch
    • RabbitMQ
    • MyCat
    • Lombok
  • SpringMVC

    • SpringMVC 基础
  • SpringBoot

    • SpringBoot 基础
  • Windows 使用技巧
  • 手机相关技巧
  • 最全面的输入法教程
  • 最全面的浏览器教程
  • Office
  • 图片类工具
  • 效率类工具
  • 最全面的 RSS 教程
  • 码字工具
  • 各大平台
  • 校招
  • 五险一金
  • 职场规划
  • 关于离职
  • 杂谈
  • 自媒体
  • 📖 读书

    • 读书工具
    • 走进科学
  • 🌍 英语

    • 从零开始学英语
    • 英语兔的相关视频
    • Larry 想做技术大佬的相关视频
  • 🏛️ 政治

    • 反腐
    • GFW
    • 404 内容
    • 审查与自我审查
    • 互联网
    • 战争
    • 读书笔记
  • 💰 经济

    • 关于税
    • 理财
  • 💪 健身

    • 睡眠
    • 皮肤
    • 口腔健康
    • 学会呼吸
    • 健身日志
  • 🏠 其他

    • 驾驶技能
    • 租房与买房
    • 厨艺
  • 电影

    • 电影推荐
  • 电视剧
  • 漫画

    • 漫画软件
    • 漫画推荐
  • 游戏

    • Steam
    • 三国杀
    • 求生之路
  • 小说
  • 关于本站
  • 关于博主
  • 打赏
  • 网站动态
  • 友人帐
  • 从零开始搭建博客
  • 搭建邮件服务器
  • 本站分享
  • 🌈 生活

    • 2022
    • 2023
    • 2024
    • 2025
  • 📇 文章索引

    • 文章分类
    • 文章归档
  • 计算机简史

  • 数字电路

  • 计算机组成原理

  • 操作系统

    • 我的操作系统学习笔记

    • 我的操作系统实验笔记

      • 搭建实验环境
      • 操作系统的引导
      • 系统调用
        • 实验之前
        • 实验目的
        • 实验内容
        • 实验报告与评分标准
        • 应用程序如何调用“系统调用”
        • int 0x80 的中断函数是什么
        • int 0x80 中断函数做了什么
        • 实现 sysiam() 和 syswhoami()
        • 修改 Makefile
        • 用 printk() 调试内核
        • 在用户态和核心态之间传递数据
        • 运行脚本程序测试
        • errno
        • 遇到的问题
        • 本实验的总结
        • 参考
    • 操作系统网课-王道考研

  • Linux

  • 计算机网络

  • 数据库

  • 编程工具

  • 装机

  • 计算机基础
  • 操作系统
  • 我的操作系统实验笔记
2022-10-31
目录

系统调用

# 2. 系统调用

简单做一个系统调用

‍

# 实验之前

本实验是 操作系统之基础 - 网易云课堂 (opens new window) 的配套实验,推荐大家进行实验之前先学习相关课程:

  • L4 操作系统接口
  • L5 系统调用的实现

如果网易云上的课程无法查看,也可以看 Bilibili 上的 操作系统哈尔滨工业大学李治军老师 (opens new window) 。

我们复习下操作系统实现系统调用的基本过程:

  • 应用程序调用库函数(API);
  • API 将系统调用号存入 EAX,然后通过中断调用使系统进入内核态;
  • 内核中的中断处理函数根据系统调用号,调用对应的内核函数(系统调用);
  • 系统调用完成相应功能,将返回值存入 EAX,返回到中断处理函数;
  • 中断处理函数返回到 API 中;
  • API 将 EAX 返回给应用程序。 ‍

# 实验目的

  • 建立对系统调用接口的深入认识;
  • 掌握系统调用的基本过程;
  • 能完成系统调用的全面控制;
  • 为后续实验做准备。 ‍

# 实验内容

在 Linux 0.11 上添加两个系统调用,并编写两个简单的应用程序测试它们。

第一个系统调用是 iam()​ ,其原型为:

int iam(const char * name);
1

完成的功能是将字符串参数 name​ 的内容拷贝到内核中保存下来。要求 name​ 的长度不能超过 23 个字符。返回值是拷贝的字符数。如果 name​ 的字符个数超过了 23,则返回 “-1”,并置 errno 为 EINVAL。在 kernal/who.c​ 中实现此系统调用。

第二个系统调用是 whoami()​,其原型为:

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

它将内核中由 iam()​ 保存的名字拷贝到 name 指向的用户地址空间中,同时确保不会对 name​ 越界访存(name​ 的大小由 size​ 说明)。返回值是拷贝的字符数。如果 size​ 小于需要的空间,则返回“-1”,并置 errno 为 EINVAL。也是在 kernal/who.c​ 中实现。

运行添加过新系统调用的 Linux 0.11,在其环境下编写两个测试程序 iam.c 和 whoami.c。最终的运行结果是:

$ ./iam lizhijun

$ ./whoami

lizhijun
1
2
3
4
5

# 实验报告与评分标准

‍

  • 将 testlab2.c 在修改过的 Linux 0.11 上编译运行,显示的结果即内核程序的得分。满分 50%
  • 只要至少一个新增的系统调用被成功调用,并且能和用户空间交换参数,可得满分
  • 将脚本 testlab2.sh 在修改过的 Linux 0.11 上运行,显示的结果即应用程序的得分。满分 30%
  • 实验报告,20%

在实验报告中回答如下问题:

  • 从 Linux 0.11 现在的机制看,它的系统调用最多能传递几个参数?你能想出办法来扩大这个限制吗?
  • 用文字简要描述向 Linux 0.11 添加一个系统调用 foo()​ 的步骤。

在讲如何做之前,我们先回顾下一个普通的系统调用的过程,具体是怎么样,我们依样画葫芦。

# 应用程序如何调用“系统调用”

系统调用也是一些函数,所以调用起来,和我们调用一个普通的自定义函数没有什么区别,但调用后发生的事情有很大不同。

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

而调用系统调用,是调用系统库中为该系统调用编写的一个接口函数,叫 API(Application Programming Interface)。API 并不能完成系统调用的真正功能,它要做的是传参(系统调用号存入 EAX,函数参数传到其他通用寄存器),然后通过中断去调用真正的系统调用,

举个例子,看看 close 这个系统调用:文件在 lib/close.c

#define __LIBRARY__
#include <unistd.h>

_syscall1(int, close, int, fd)
1
2
3
4

‍ 其中 _syscall1​ 是一个宏,在 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; \
}
1
2
3
4
5
6
7
8
9
10
11
12

将 _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;
}
1
2
3
4
5
6
7
8
9
10
11

这就是 API 的定义。它先将宏 __NR_close​ 存入 EAX,将参数 fd 存入 EBX,然后进行 0x80 中断调用。调用返回后,从 EAX 取出返回值,存入 __res​,再通过对 __res​ 的判断决定传给 API 的调用者什么样的返回值。 ‍ 其中 __NR_close​ 就是系统调用的编号,在 include/unistd.h​ 中定义:

#define __NR_setup	0
#define __NR_exit	1
#define __NR_fork	2
#define __NR_read	3
#define __NR_write	4
#define __NR_open	5
#define __NR_close	6
............................
1
2
3
4
5
6
7
8

‍ 而在系统调用的具体的 C 代码文件中,需要引入才能生效

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

然后就是定义函数了

_syscall1(int,close,int,fd)
1

‍ 在 0.11 环境下编译 C 程序,包含的头文件都在 /usr/include​ 目录下。因此我们需要加载 hdc 后,修改这个文件。

学到这里,我们可以先做一部分实验内容了

  1. 编写应用程序的 iam.c 和 whomi.c。
  2. 加载 Linux0.11 的系统盘
  3. 添加系统调用号 ‍ iam.c 的代码如下
# define __LIBRARY__
# include <unistd.h>
# include <stdio.h>
# include <errno.h>

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

int main(int argc, char** argv){
    int wlen = 0;
    if (argc < 1 ){
        printf("not enougth arguments!\n");
    }
    wlen = iam(argv[1]);
    return wlen;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

我们逐行来讲解:

  • 首先是第一第二行,这里我们引入了头文件,用宏替换的方式实现 int 0x80 中断
  • 第三行:引入标准输入输出,用于 printf
  • 第 4 行:引入 errno.h 用于输出错误(后面会讲到 errno)
  • 第 6 行:用于宏替换,iam 是系统调用名,我们只需用一个参数,因此用了 syscall1
  • 第 8 ~ 14 行,用于调用 系统调用 iam ‍ whoami.c 的代码
# define __LIBRARY__
# include <unistd.h>
# include <stdio.h>
# include <errno.h>

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

int main() {
	char s[30];
	int rlen = 0;
	rlen = whoami(s,30); //这里调用了_syscall2 写的 whoami 函数
	printf("%s\n",s);
	return rlen;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

基本和 iam.c 代码结构类似,因为我们要用 2 个参数,因此用了 syscall2。如果不知道为什么是两个参数, 参考实验的要求,whoami()​ 原型为:

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

我们需要通过 Ubuntu 和 Linux0.11 文件交换的方式,将这两个 c 文件放到 ~/oslab/hdc/usr/root。 因为我们到时是需要在 Linux0.11 上面去运行这两个 c 文件实现系统调用 ‍ 第二步是添加系统调用号,因为我们 c 代码里传的系统调用名是 iam,操作系统是不认识的,因此要添加。我们需要去到 Linux0.11 里面的头文件里添加:修改 ~/oslab/usr/include/unistd.h,在系统调用号里的下一行添加如下内容(大概在 132 行处):

#define __NR_whoami   72
#define __NR_iam      73
1
2

# int 0x80 的中断函数是什么

中断函数的地址,是从 IDT 表取出的,我们看看 int 0x80 的中断函数是哪个:

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

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

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

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

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

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

​ ‍ ​_set_gate​ 的定义是:

#define _set_gate(gate_addr,type,dpl,addr) \
__asm__ ("movw %%dx,%%ax\n\t" \
    "movw %0,%%dx\n\t" \
    "movl %%eax,%1\n\t" \
    "movl %%edx,%2" \
    : \
    : "i" ((short) (0x8000+(dpl<<13)+(type<<8))), \
    "o" (*((char *) (gate_addr))), \
    "o" (*(4+(char *) (gate_addr))), \
    "d" ((char *) (addr)),"a" (0x00080000))
1
2
3
4
5
6
7
8
9
10

虽然看起来挺麻烦,但实际上很简单,就是填写 IDT(中断描述符表),将 system_call​ 函数地址写到 0x80​ 对应的中断描述符中,也就是在中断 0x80​ 发生后,自动调用函数 system_call​。具体细节请参考《注释》的第 4 章。

# int 0x80 中断函数做了什么

接下来看 system_call​。该函数纯汇编打造,定义在 kernel/system_call.s​ 中:


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

.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
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​ 用 .globl​ 修饰为其他函数可见。

Windows 实验环境下会看到它有一个下划线前缀,这是不同版本编译器的特质决定的,没有实质区别。

关键是第 30 行,是一个 call 语句,也就是这个 system_call 会根据具体的调用号,执行具体的系统调用函数。

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

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

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

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

增加实验要求的系统调用,需要在这个函数表中增加两个函数引用 ——sys_iam​ 和 sys_whoami​。当然该函数在 sys_call_table​ 数组中的位置必须和 __NR_xxxxxx​ 的值对应上。 ‍ 同时还要仿照此文件中前面各个系统调用的写法,加上:

extern int sys_whoami();
extern int sys_iam();
1
2

不然,编译会出错的。 ‍ 学到这里,我们就可以作如下事情了:

  1. 因为我们增加了系统调用函数的数量,需修改系统调用函数的数量
  2. 新增 2 个函数引用
  3. 然后在 sys_call_table 里添加我们的函数引用

首先我们打开 linux-0.11/kernel/system_call.s​,因为我们添加了两个系统调用,所以将 nr_system_calls ​加 2,修改系统调用总数(第 61 行,从 72 改为 74)。

nr_system_calls = 74
1

‍ ​sys_call_table ​是一个函数指针数组的起始地址,它定义在 include/Computer/Linux/sys.h​ 中。增加实验要求的系统调用,需要在这个函数表中增加两个函数引用 ——sys_iam ​和 sys_whoami​。当然该函数在 sys_call_table ​数组中的位置必须和 __NR_xxxxxx ​的值对应上。

同时还要仿照此文件中前面各个系统调用的写法,加上 extern int sys_whoami(); ​以及 extern int sys_iam();​

extern int sys_whoami();
extern int sys_iam();


fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,
sys_write, sys_open, sys_close, sys_waitpid, sys_creat, sys_link,
sys_unlink, sys_execve, sys_chdir, sys_time, sys_mknod, sys_chmod,
sys_chown, sys_break, sys_stat, sys_lseek, sys_getpid, sys_mount,
sys_umount, sys_setuid, sys_getuid, sys_stime, sys_ptrace, sys_alarm,
sys_fstat, sys_pause, sys_utime, sys_stty, sys_gtty, sys_access,
sys_nice, sys_ftime, sys_sync, sys_kill, sys_rename, sys_mkdir,
sys_rmdir, sys_dup, sys_pipe, sys_times, sys_prof, sys_brk, sys_setgid,
sys_getgid, sys_signal, sys_geteuid, sys_getegid, sys_acct, sys_phys,
sys_lock, sys_ioctl, sys_fcntl, sys_mpx, sys_setpgid, sys_ulimit,
sys_uname, sys_umask, sys_chroot, sys_ustat, sys_dup2, sys_getppid,
sys_getpgrp, sys_setsid, sys_sigaction, sys_sgetmask, sys_ssetmask,
sys_setreuid,sys_setregid, sys_whoami,sys_iam};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

‍

# 实现 sys_iam() 和 sys_whoami()

添加系统调用的最后一步,是在内核中实现函数 sys_iam()​ 和 sys_whoami()​。

每个系统调用都有一个 sys_xxxxxx()​ 与之对应,它们都是我们学习和模仿的好对象。

比如在 fs/open.c​ 中的 sys_close(int fd)​:

int sys_close(unsigned int fd)
{
//    ……
    return (0);
}
1
2
3
4
5

它没有什么特别的,都是实实在在地做 close()​ 该做的事情。

所以只要自己创建一个文件:kernel/who.c​,然后实现两个函数就万事大吉了。当然,我们目前肯定是不知道怎么实现的,我们先新建文件,后面我们会讲到这个系统调用里面怎么做。(主要涉及到用户态和核心态之间传递数据) ‍ 学到这里,我们可以新建 who.c 了。文件内容如下:

#define __LIBRARY__
#include <unistd.h>


int sys_iam(const char * name)
{ 
	return 0;
}

int sys_whoami(char* name, unsigned int size)
{  

	return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

‍

# 修改 Makefile

要想让我们添加的 kernel/who.c​ 可以和其它 Linux 代码编译链接到一起,必须要修改 Makefile 文件。

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

第一处,在 29 行末尾添加 who.o​:

OBJS  = sched.o system_call.o traps.o asm.o fork.o \
        panic.o printk.o vsprintf.o sys.o exit.o \
        signal.o mktime.o who.o
1
2
3

‍ 第二处,在 50 和 51 行之间插入 who.s who.o: who.c ../include/Computer/Linux/kernel.h ../include/unistd.h​:

### Dependencies:
who.s who.o: who.c ../include/Computer/Linux/kernel.h ../include/unistd.h
exit.s exit.o: exit.c ../include/errno.h ../include/signal.h \
  ../include/sys/types.h ../include/sys/wait.h ../include/Computer/Linux/sched.h \
  ../include/Computer/Linux/head.h ../include/Computer/Linux/fs.h ../include/Computer/Linux/mm.h \
  ../include/Computer/Linux/kernel.h ../include/Computer/Linux/tty.h ../include/termios.h \
  ../include/asm/segment.h
1
2
3
4
5
6
7

修改了 Makefile 后,在终端中进入 oslab/linux0.11,输入 make all,进行编译。和往常一样 make all​ 就能自动把 who.c​ 加入到内核中了。

正确的编译最后一行内容为 sync。

如果编译时提示 who.c​ 有错误,就说明修改生效了。所以,有意或无意地制造一两个错误也不完全是坏事,至少能证明 Makefile 是对的。请根据错误信息进行修改,实在不行的话建议从头开始重做一遍。 ‍

# 用 printk() 调试内核

oslab 实验环境提供了基于 C 语言和汇编语言的两种调试手段。除此之外,适当地向屏幕输出一些程序运行状态的信息,也是一种很高效、便捷的调试方法,有时甚至是唯一的方法,被称为“printf 法”。

要知道到,printf() 是一个只能在用户模式下执行的函数,而系统调用是在内核模式中运行,所以 printf() 不可用,要用 printk()。

​printk()​ 和 printf()​ 的接口和功能基本相同,只是代码上有一点点不同。printk() 需要特别处理一下 fs​ 寄存器,它是专用于用户模式的段寄存器。

看一看 printk 的代码(在 kernel/printk.c​ 中)就知道了:

int printk(const char *fmt, ...)
{
//    ……
    __asm__("push %%fs\n\t"
            "push %%ds\n\t"
            "pop %%fs\n\t"
            "pushl %0\n\t"
            "pushl $buf\n\t"
            "pushl $0\n\t"
            "call tty_write\n\t"
            "addl $8,%%esp\n\t"
            "popl %0\n\t"
            "pop %%fs"
            ::"r" (i):"ax","cx","dx");
//    ……
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

显然,printk()​ 首先 push %fs​ 保存这个指向用户段的寄存器,在最后 pop %fs​ 将其恢复,printk() 的核心仍然是调用 tty_write()​。查看 printf() 可以看到,它最终也要落实到这个函数上。 ‍ 现在,我们修改 who.c,使其打印一句话:

int sys_iam(const char * name)
{ 
    printk("Hello OS!\n");
	return 0;
}
1
2
3
4
5

然后我们可以编译与运行一下了。让应用程序调用 iam()​,从结果可以看出系统调用是否被真的调用到了。然后我们重新编译。 ‍ 可以直接在 Linux 0.11 环境下用 vi 编写(别忘了经常执行“sync”以确保内存缓冲区的数据写入磁盘),也可以在 Ubuntu 或 Windows 下编完后再传到 Linux 0.11 下。无论如何,最终都必须在 Linux 0.11 下编译。编译命令是:

$ gcc -o iam iam.c -Wall
1

gcc 的 “-Wall” 参数是给出所有的编译警告信息,“-o” 参数指定生成的执行文件名是 iam,用下面命令运行它:

$ ./iam
1

如果如愿输出了你的信息,就说明你添加的系统调用生效了。否则,就还要继续调试,祝你好运! ‍ ​

# 在用户态和核心态之间传递数据

接下来,就是本节实验的重点了,实现系统调用。我们先看看其他系统调用怎么做的。我们的 c 是在应用程序所在的地址空间。我们在 syscall 里虽然通过指针,传递了字符串的地址,但此指针参数传递的是应用程序所在地址空间的逻辑地址,在内核中如果直接访问这个地址,访问到的是内核空间中的数据,不会是用户空间的。

所以这里还需要一点儿特殊工作,才能在内核中从用户空间得到数据。 ‍ 要实现的两个系统调用参数中都有字符串指针,非常像 open(char *filename, ……)​,所以我们看一下 open()​ 系统调用是如何处理的。

int open(const char * filename, int flag, ...)
{
//    ……
    __asm__("int $0x80"
            :"=a" (res)
            :"0" (__NR_open),"b" (filename),"c" (flag),
            "d" (va_arg(arg,int)));
//    ……
}
1
2
3
4
5
6
7
8
9

可以看出,系统调用是用 eax、ebx、ecx、edx​ 寄存器来传递参数的。

  • 其中 eax 传递了系统调用号,而 ebx、ecx、edx 是用来传递函数的参数的
  • ebx 对应第一个参数,ecx 对应第二个参数,依此类推。

如 open 所传递的文件名指针是由 ebx 传递的,也即进入内核后,通过 ebx 取出文件名字符串。open 的 ebx 指向的数据在用户空间,而当前执行的是内核空间的代码,如何在用户态和核心态之间传递数据?

接下来我们继续看看 open 的处理:

system_call: // 所有的系统调用都从 system_call 开始
!  ……
   pushl %edx
   pushl %ecx
   pushl %ebx     # ebx,ecx,edx 是传递给系统调用的参数
   movl $0x10,%edx  # 接下来让 ds,es 指向 GDT,指向核心地址空间
   mov %dx,%ds
   mov %dx,%es
   movl $0x17,%edx  # 接下来让 fs 指向的是 LDT,指向用户地址空间
   mov %dx,%fs
   call sys_call_table(,%eax,4)    # 即 call sys_open
1
2
3
4
5
6
7
8
9
10
11

‍ 由上面的代码可以看出,获取用户地址空间(用户数据段)中的数据依靠的就是段寄存器 fs,下面该转到 sys_open​ 执行了,在 fs/open.c​ 文件中:

int sys_open(const char * filename,int flag,int mode)  // filename 这些参数从哪里来?
/*是否记得上面的 pushl %edx,    pushl %ecx,    pushl %ebx?
  实际上一个 C 语言函数调用另一个 C 语言函数时,编译时就是将要传递的参数压入栈中(第一个参数最后压,…),然后 call …,
  所以汇编程序调用 C 函数时,需要自己编写这些参数压栈的代码…*/
{
    ……
    if ((i=open_namei(filename,flag,mode,&inode))<0) {
        ……
    }
    ……
}
1
2
3
4
5
6
7
8
9
10
11

它将参数传给了 open_namei()​。

再沿着 open_namei()​ 继续查找,文件名先后又被传给 dir_namei()​、get_dir()​。

在 get_dir()​ 中可以看到:

static struct m_inode * get_dir(const char * pathname)
{
    ……
    if ((c=get_fs_byte(pathname))=='/') {
        ……
    }
    ……
}
1
2
3
4
5
6
7
8

处理方法就很显然了:用 get_fs_byte()​ 获得一个字节的用户空间中的数据。

所以,在实现 iam()​ 时,调用 get_fs_byte()​ 即可。

但如何实现 whoami()​ 呢?即如何实现从核心态拷贝数据到用心态内存空间中呢?

猜一猜,是否有 put_fs_byte()​?有!看一看 include/asm/segment.h​ :

extern inline unsigned char get_fs_byte(const char * addr)
{
    unsigned register char _v;
    __asm__ ("movb %%fs:%1,%0":"=r" (_v):"m" (*addr));
    return _v;
}
1
2
3
4
5
6
extern inline void put_fs_byte(char val,char *addr)
{
    __asm__ ("movb %0,%%fs:%1"::"r" (val),"m" (*addr));
}
1
2
3
4

他俩以及所有 put_fs_xxx()​ 和 get_fs_xxx()​ 都是用户空间和内核空间之间的桥梁,在后面的实验中还要经常用到。

因此,我们可以根据这两个函数,来实现系统调用了。who.c 的代码如下:

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

char msg[24]; //23个字符 +'\0' = 24

int sys_iam(const char * name)
/***
function:将 name 的内容拷贝到 msg, name 的长度不超过23个字符
return:拷贝的字符数。如果 name 的字符个数超过了 23,则返回“ -1”,并置 errno 为 EINVAL。
****/
{ 
	int i;
	//临时存储 输入字符串 操作失败时不影响 msg
	char tmp[30];
	for(i=0; i<30; i++)
	{ 
		//从用户态内存取得数据
		tmp[i] = get_fs_byte(name+i);
		if(tmp[i] == '\0') break;  //字符串结束
	}
	//printk(tmp);
	i=0;
	while(i<30&&tmp[i]!='\0') i++;
	int len = i;
	// int len = strlen(tmp);
	//字符长度大于23个
	if(len > 23)
	{ 
		// printk("String too long!\n");
		return -(EINVAL);  //置 errno 为 EINVAL  返回“-1”  具体见 _syscalln 宏展开
	}
	strcpy(msg,tmp);
	//printk(tmp);
	return i;
}

int sys_whoami(char* name, unsigned int size)
/***
function:将 msg 拷贝到 name 指向的用户地址空间中,确保不会对 name 越界访存(name 的大小由 size 说明)
return: 拷贝的字符数。如果 size 小于需要的空间,则返回“­-1”,并置 errno 为 EINVAL。
****/
{  
	//msg 的长度大于 size
	int len = 0;
	for(;msg[len]!='\0';len++);
	if(len > size)
	{ 
		return -(EINVAL);
	}
	int i = 0;
	//把 msg 输出至 name
	for(i=0; i<size; i++)
	{ 
		put_fs_byte(msg[i],name+i);
		if(msg[i] == '\0') break; //字符串结束
	}
	return i;
}
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59

# 运行脚本程序测试

Linux 的一大特色是可以编写功能强大的 shell 脚本,提高工作效率。本实验的部分评分工作由脚本 testlab2.sh 完成。它的功能是测试 iam.c​ 和 whoami.c​。

首先将 iam.c​ 和 whoami.c​ 分别编译成 iam​ 和 whoami​,然后将 testlab2.sh​(在 GitHub 上可以找到) 拷贝到同一目录下。 ‍ 用下面命令为此脚本增加执行权限:

$ chmod +x testlab2.sh
1

‍ 然后运行之:

$ ./testlab2.sh
1

根据输出,可知 iam.c​ 和 whoami.c​ 的得分。 ‍ ​

​ ‍

# errno

errno 是一种传统的错误代码返回机制。

当一个函数调用出错时,通常会返回 -1 给调用者。但 -1 只能说明出错,不能说明错是什么。为解决此问题,全局变量 errno 登场了。错误值被存放到 errno 中,于是调用者就可以通过判断 errno 来决定如何应对错误了。

各种系统对 errno 的值的含义都有标准定义。Linux 下用“man errno”可以看到这些定义。

# 遇到的问题

‍ 注意,testlab.c 和 testlab2.sh 的文件换行符要换成 Linux 的换行符 LF。不然会报错。

parse error before '{'  
1

​​

因为换行符的问题。Windows、Linux 和 Mac 的换行符是不一样的,Linux 是 LF,因此修改文件的换行符为 LF 就不会报错了。

如果编译有警告:implicit declaration of function 'printf' 要引入 stdio.h

#include <stdio.h>
1

​

testlab2.sh 的话,还要注意修改权限。在 Linux 给 sh 加权限的时候,遇到过报错 chmod: testlab2.sh :Not owner。

解决方法:在 Ubuntu 里加载 hdc,然后去到相应目录下修改为 777 省事

chomod 777 testlab2.sh
1

# 本实验的总结

添加一个系统调用的步骤如下

  1. 编写了 iam.c 和 whoami.c,用于调用系统调用
  2. 添加系统调用号,修改了~/oslab/hdc/include/unistd.h
  3. 修改系统调用总数:linux-0.11/kernel/system_call.s​
  4. 在 sys_table 里添加了我们的函数引用: include/Computer/Linux/sys.h​
  5. 实现内核函数 who.c,通过 get_fs_byte 获取参数,put_fs_byte 输出参数
  6. 修改 Makefile,编译和链接我们的 who.c
  7. 重新编译 Linux
  8. 运行应用程序测试系统调用,运行 testlab2.c 和 testlab2.sh 测试

系统调用最多能传递几个参数?如何扩大这种限制?

最多只有 3 个。例如 open 系统调用,EAX 寄存器保存系统调用号,EBX 寄存器保存的是文件名参数,ECX 寄存器保存的是标志位参数,EDX 寄存器保存的是可变参数的基地址

扩大传递参数的数量的方法:利用堆栈传递参数

# 参考

实验楼

哈工大-操作系统-HitOSlab-李治军-实验 2-系统调用 (opens new window)

(浓缩 + 精华)哈工大-操作系统-MOOC-李治军教授-实验 2-系统调用 (opens new window)

操作系统实验报告-系统调用 - Tradoff - 博客园 (opens new window)

上次更新: 2025/5/9 14:55:39
操作系统的引导
0_课程白嫖指南

← 操作系统的引导 0_课程白嫖指南→

最近更新
01
学点统计学:轻松识破一本正经的胡说八道
06-05
02
2025 年 5 月记
05-31
03
《贫穷的本质》很棒,但可能不适合你
05-27
更多文章>
Theme by Vdoing | Copyright © 2022-2025 | 粤 ICP 备 2022067627 号 -1 | 粤公网安备 44011302003646 号 | 点击查看十年之约
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式