目录
一、实验内容
二、实验准备
1、系统调用的具体流程
(一)调用接口函数 API
(二)触发 0x80 号中断
(三)跳转到 system_call 函数
(四)执行系统调用函数 sys_xxx
2、总结概括实现系统调用的过程
三、正式实验
1. 添加系统调用 API
2. 添加系统调用号 + 修改系统调用总数
3. 维护系统调用表 + 编写系统调用函数(内核函数)
4. 修改 Makefile
5. make all
6. 编写测试程序
7. 拷贝 iam.c 和 whoami.c 到 Linux 0.11 目录
8. 启动虚拟机进行测试
一、实验内容
1、建立对系统调用接口的深入认识、掌握系统调用的基本过程、能完成系统调用的全面控制。
2、在 Linux 0.11 功能的基础上新添两个系统调用:iam() 和 whoiam() 。
(1)第一个系统调用是 iam() ,其 API 原型为:
int iam(const char * name);
- iam 函数实现将字符串参数 name 的内容拷贝到内核中保存下来
- 要求 name 的长度不超过 23 个字符,返回值是拷贝的字符数。如果 name 的字符个数超过了 23 ,则返回 -1 ,并置 errno 为 EINVAL
- 在 kernal/who.c 中完成此系统调用的实现函数
(2)第二个系统调用是 whoami() ,其 API 原型为:
int whoami(char* name, unsigned int size);
- whoami 函数实现将内核中由 iam() 保存的名字拷贝到 name 指向的用户地址空间中,同时确保不会对 name 越界访存(name 的大小由 size 说明)
- 返回值是拷贝的字符数。如果 size 小于需要的空间,则返回 -1 ,并置 errno 为 EINVAL
- 在 kernal/who.c 中完成此系统调用的实现函数
3、测试程序。
编写两个测试程序:iam.c 和 whoami.c 。先运行添加过新编写的系统调用的 Linux 0.11,然后在 Linux 0.11 操作系统环境下编译运行这两个测试程序,测试新增的系统调用是否成功。
二、实验准备
- 实验环境:Ubuntu 16.04 LTS
1、系统调用的具体流程
通常情况下,应用程序想要调用系统调用和调用一个普通的自定义函数在代码上并没什么区别,但调用后发生的事情有很大不同。
(1)调用自定义函数,通过 call 指令直接跳转到该函数的地址,继续运行,结束后返回。
(2)调用系统调用,首先通过调用系统库中为该系统调用编写的一个接口函数,即 API(Application Programming Interface)- 应用程序编程接口,触发 0x80 号中断(即调用system_call 函数);然后根据系统调用编号跳转到对应的系统调用函数 system_xxx ,继续执行,结束后返回。
(一)调用接口函数 API
API 是一些预先定义好的函数,为的是提供给应用程序和开发人员基于某软件或硬件的以访问一组例程的能力,同时又无需访问源码,也无需理解内部工作机制的细节。API 并不能完成系统调用的真正功能,它要做的是通过 0x80 号中断去调用真正的系统调用,API 实现的内容:
- 把系统调用的编号存入 EAX
- 把接口函数的参数存入其它通用寄存器(EBX、ECX……)
- 触发 0x80 号中断(int 0x80)
Tip linux-0.11/lib 目录下有一些已经实现的 API 。Linus 编写它们的原因是在内核加载完毕后,会切换到用户模式下,做一些初始化工作,然后启动 shell。而用户模式下的很多工作需要依赖一些系统调用才能完成,因此在内核中实现了这些系统调用的 API。
例 以 lib/close.c 为例,分析一下 close() 的API:
/* linux-0.11/lib/close.c */
#define __LIBRARY__
#include <unistd.h>
_syscall1(int,close,int,fd)
(1)这里的 _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; \
}
- include/unistd.h 中有 linus 预先写好的系统调用 API 宏模板 _syscalln() ,其中 n 表示系统调用的参数个数
- 如果对带参宏定义不太熟悉可以先看一下这篇帖子:宏定义(无参宏定义和带参宏定义),C语言宏定义详解
(2)进行宏展开后,得到 close() 较为完整的 API 代码(C语言内嵌汇编):
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;
}
① API 整体代码分析:
② 内嵌汇编代码分析:
(3)其中 __NR_close 是系统调用的编号。所有的系统调用都是通过 0x80 号中断进入系统内核,但是之后具体执行哪个系统调用函数就由调用编号决定(存在EAX中)。
- 所以添加系统调用时还需要添加新增系统调用的编号,才能找到对应的系统调用函数
- 系统调用编号在 include/unistd.h 中定义
(二)触发 0x80 号中断
API 触发 0x80 号中断后,就要进行内核的中断处理,也就是调用 system_call 函数。但 0x80 中断为什么就能跳转去执行 system_call 函数呢?——这其实就是在内核初始化时完成的工作。
所以我们可以先了解一下 Linux 0.11 处理 0x80 号中断的过程。
(1)内核初始化时,主函数 init/main.c 调用了 sched_init 初始化函数:
(2)sched_init 在 kernel/sched.c 中定义,重点看最后一条语句:
(3)set_system_gate 是个宏,在 include/asm/system.h 中定义:
#define set_system_gate(n,addr) \
_set_gate(&idt[n],15,3,addr)
(4)_set_gate 又是一个宏,也在 include/asm/system.h 中定义(C语言内嵌汇编):
#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))
(5)上述使用了一系列的宏定义套娃,完全展开得到:
__asm__ ("movw %%dx,%%ax\n\t" \
"movw %0,%%dx\n\t" \
"movl %%eax,%1\n\t" \
"movl %%edx,%2" \
: \
: "i" ((short) (0x8000+(3<<13)+(15<<8))), \
"o" (*((char *) (idt[0x80]))), \
"o" (*(4+(char *) (idt[0x80]))), \
"d" ((char *) (&system_call)),"a" (0x00080000))
看起来很复杂,但其实这段代码的功能就是填写 IDT - 中断描述符表,将 system_call 函数的地址写到了 0x80 中断对应的中断描述符中。所以之后我们调用 0x80 号中断,就会自动跳转到函数 system_call 的地址。
(三)跳转到 system_call 函数
(1)system_call 函数定义在 kernel/system_call.s 中(纯汇编):
!……
! # 这是系统调用总数。如果增删了系统调用,必须做相应修改
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 行的 call sys_call_table(,%eax,4) 语句,前面都是一些压栈保护操作。
根据汇编的寻址方法翻译得到: call sys_call_table + 4 * %eax
- 其中 eax 存放的是系统调用号 __NR_xxx ,也就是说 sys_call_table + 4 * __NR_xxx 就是相应的系统调用函数的入口
(2)sys_call_table 在 include/linux/sys.h 中定义:
(3)fn_ptr 在 include/linux/sched.h 中定义:
typedef int (*fn_ptr)();
现在可以看出 sys_call_table 数组就是一个函数指针数组。数组名 sys_call_table 就是这个数组的起始地址,sys_call_table + 4 * __NR_xxx 就是系统调用函数的入口,所以系统调用的函数在 sys_call_table 数组中的位置必须和 __NR_xxx 的值对应,才能跳转到正确的函数入口。
另外 sys_call_table 数组中的所有系统调用的函数名统一规范为 sys_xxx ,这是我们学习和模仿的好对象。(所以我们添加的系统调用函数应该命名为 sys_iam 和 sys.whoami)
- 如果对函数指针不熟悉可以先看看这篇文章:进阶C语言 - 指针(3):函数指针数组_渡上舟的博客-CSDN博客
(四)执行系统调用函数 sys_xxx
system_call 中的 call sys_call_table(,%eax,4) 指令就是跳转去执行对应的系统调用函数 sys_xxx ,这个系统调用函数就是要实现系统调用的功能。
例 fs/open.c 中的 sys_close 函数,其代码就是实现 close() 应有的功能:
2、总结概括实现系统调用的过程
- 应用程序调用库函数(API)
- API 将系统调用号存入 EAX,然后通过中断调用使系统进入内核态
- 内核中的中断处理函数根据系统调用号,调用对应的内核函数(系统调用)
- 系统调用完成相应功能,将返回值存入 EAX,返回到中断处理函数
- 中断处理函数返回到 API 中
- API 将 EAX 返回给应用程序
三、正式实验
1. 添加系统调用 API
应用程序实现系统调用的第一步是调用库函数 API ,所以首先要添加 iam() 和 whoami() 的 API 。这里可以直接使用预先写好的系统调用 API 宏模板 _syscalln() ,非常方便,所以我们暂时不添加,后面编写测试程序 iam.c 和 whoami.c 时再添加。
- 添加 API 时注意事项:
/* 在应用程序中,要有: */
/* 有它,_syscall1 等才有效。详见unistd.h */
#define __LIBRARY__
/* 有它,编译器才能获知自定义的系统调用的编号 */
#include "unistd.h"
/* iam()在用户空间的接口函数 */
_syscall1(int, iam, const char*, name);
/* whoami()在用户空间的接口函数 */
_syscall2(int, whoami,char*,name,unsigned int,size);
2. 添加系统调用号 + 修改系统调用总数
之后 API 将系统调用号存入 EAX,然后调用中断进入内核态。这里新增了两个系统调用,所以要添加新的系统调用号,还要修改系统调用总数。
(1)进入 linux-0.11/include 目录,打开 unistd.h ,增添新的系统调用编号。
- 系统调用编号统一格式:__NR_xxx
(2)进入 linux-0.11/kernel 目录,打开 system_call.s ,修改系统调用总数。
3. 维护系统调用表 + 编写系统调用函数(内核函数)
中断处理函数根据系统调用号,调用对应的内核函数,所以要为新增的系统调用添加系统调用函数名并维护系统调用表。
(1)进入 linux-0.11/include/linux 目录,打开 sys.h ,维护系统调用表:
注 系统调用函数名在 sys_call_table 数组中的位置必须和 unistd.h 中 __NR_name 的值相同
(2)进入 linux-0.11/kernel 目录,创建一个 who.c 文件,为新增的系统调用函数编写代码,即实现 iam() 和 whoami() 要求的功能。
#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;
}
4. 修改 Makefile
要想让我们添加的 kernel/who.c 和其它 Linux 代码编译链接到一起,必须要修改 Makefile 文件。
Makefile 里记录的是所有源程序文件的编译、链接规则,《注释》3.6 节有简略介绍。我们之所以简单地运行 make 命令就可以实现编译整个代码树,是因为 make 完全按照 Makefile 里的指示进行工作。
Makefile 在代码树中有很多,分别负责不同模块的编译工作。我们要修改的是 kernel/Makefile ,需要修改两处。
(1)第一处,在【OBJS】后添加 who.o
(2)第二处,在【Dependencies】后添加 who.s who.o: who.c ../include/linux/kernel.h ../include/unistd.h
- 双击 Makefile 默认以 vim 打开,如果对 vim 编辑器不熟悉可以使用 gedit 进行编辑
5. make all
Makefile 修改后,和往常一样在 linux-0.11 目录下执行 make all 命令就能自动把 who.c 加入到内核中了。
6. 编写测试程序
到此为止,系统调用的内核函数已经完成。应用程序想要使用我们新增的系统调用 iam 和 whoami ,还需要添加对应的系统调用 API 。
- 在 include/unistd.h 中,有预先写好的系统调用 API 宏模板 _syscalln() ,其中 n 表示系统调用的参数个数。根据 iam 和 whoami 的参数个数,可以看出应该分别使用 _syscall1 和 _syscall2
(1)在 oslab 目录下创建一个 iam.c 和 whoami.c 文件
- iam.c 完整代码:
/* iam.c */
#define __LIBRARY__
#include <unistd.h>
#include <errno.h>
#include <asm/segment.h>
#include <linux/kernel.h>
_syscall1(int, iam, const char*, name);
int main(int argc, char *argv[])
{
/*调用系统调用iam()*/
iam(argv[1]);
return 0;
}
- whoami.c 完整代码:
/* whoami.c */
#define __LIBRARY__
#include <unistd.h>
#include <errno.h>
#include <asm/segment.h>
#include <linux/kernel.h>
#include <stdio.h>
_syscall2(int, whoami,char *,name,unsigned int,size);
int main(int argc, char *argv[])
{
char username[64] = {0};
/*调用系统调用whoami()*/
whoami(username, 24);
printf("%s\n", username);
return 0;
}
7. 拷贝 iam.c 和 whoami.c 到 Linux 0.11 目录
现在还不能直接编译运行,因为我们编写的 iam.c 和 whoami.c 还位于宿主机的 oslab 目录下,Linux 0.11 虚拟机目录下没有这两个文件,所以无法直接编译和运行。
(1)挂载
以上两个文件需要放到启动后的 linux-0.11 操作系统上运行。这里可以采用 挂载 的方式实现宿主机与虚拟机操作系统的文件共享。
在 oslab 目录下执行以下命令挂载 hdc 目录到虚拟机操作系统上:
sudo ./mount-hdc
如果对挂载不熟悉可以先看看这篇文章:Linux 学习笔记(三):挂载 是什么_linux挂载的概念_Amentos的博客-CSDN博客
(2)拷贝
在 oslab 目录下执行以下命令将上述两个文件拷贝到虚拟机 Linux 0.11 操作系统 /usr/root/ 目录下:
cp iam.c whoami.c hdc/usr/root
拷贝成功!目标目录下存在对应的两个文件就可以启动虚拟机进行测试了。
8. 启动虚拟机进行测试
(1)编译
[/usr/root]# gcc -o iam iam.c
[/usr/root]# gcc -o whoami whoami.c
编译后显示:
说明之前修改的 unistd.h 没有加载到 linux 0.11 中,需要手动添加或直接拷贝。
(2)进入 hdc/usr/include/unistd.h ,为新增的系统调用添加系统调用号(需要先挂载):
(3)再次编译
没有返回信息,说明编译成功:
(4)运行
[/usr/root]# ./iam lqn
[/usr/root]# ./whoami
运行结果: