目录
一、实验内容
二、实验准备
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);
(2)第二个系统调用是 whoami() ,其 API 原型为:
int whoami(char* name, unsigned int size);
3、测试程序。
编写两个测试程序:iam.c 和 whoami.c 。先运行添加过新编写的系统调用的 Linux 0.11,然后在 Linux 0.11 操作系统环境下编译运行这两个测试程序,测试新增的系统调用是否成功。
通常情况下,应用程序想要调用系统调用和调用一个普通的自定义函数在代码上并没什么区别,但调用后发生的事情有很大不同。
(1)调用自定义函数,通过 call 指令直接跳转到该函数的地址,继续运行,结束后返回。
(2)调用系统调用,首先通过调用系统库中为该系统调用编写的一个接口函数,即 API(Application Programming Interface)- 应用程序编程接口,触发 0x80 号中断(即调用system_call 函数);然后根据系统调用编号跳转到对应的系统调用函数 system_xxx ,继续执行,结束后返回。
API 是一些预先定义好的函数,为的是提供给应用程序和开发人员基于某软件或硬件的以访问一组例程的能力,同时又无需访问源码,也无需理解内部工作机制的细节。API 并不能完成系统调用的真正功能,它要做的是通过 0x80 号中断去调用真正的系统调用,API 实现的内容:
Tip linux-0.11/lib 目录下有一些已经实现的 API 。Linus 编写它们的原因是在内核加载完毕后,会切换到用户模式下,做一些初始化工作,然后启动 shell。而用户模式下的很多工作需要依赖一些系统调用才能完成,因此在内核中实现了这些系统调用的 API。
例 以 lib/close.c 为例,分析一下 close() 的API:
/* linux-0.11/lib/close.c */
#define __LIBRARY__
#include
_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; \
}
(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中)。
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 的地址。
(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
(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)
system_call 中的 call sys_call_table(,%eax,4) 指令就是跳转去执行对应的系统调用函数 sys_xxx ,这个系统调用函数就是要实现系统调用的功能。
例 fs/open.c 中的 sys_close 函数,其代码就是实现 close() 应有的功能:
应用程序实现系统调用的第一步是调用库函数 API ,所以首先要添加 iam() 和 whoami() 的 API 。这里可以直接使用预先写好的系统调用 API 宏模板 _syscalln() ,非常方便,所以我们暂时不添加,后面编写测试程序 iam.c 和 whoami.c 时再添加。
/* 在应用程序中,要有: */
/* 有它,_syscall1 等才有效。详见unistd.h */
#define __LIBRARY__
/* 有它,编译器才能获知自定义的系统调用的编号 */
#include "unistd.h"
/* iam()在用户空间的接口函数 */
_syscall1(int, iam, const char*, name);
/* whoami()在用户空间的接口函数 */
_syscall2(int, whoami,char*,name,unsigned int,size);
之后 API 将系统调用号存入 EAX,然后调用中断进入内核态。这里新增了两个系统调用,所以要添加新的系统调用号,还要修改系统调用总数。
(1)进入 linux-0.11/include 目录,打开 unistd.h ,增添新的系统调用编号。
(2)进入 linux-0.11/kernel 目录,打开 system_call.s ,修改系统调用总数。
中断处理函数根据系统调用号,调用对应的内核函数,所以要为新增的系统调用添加系统调用函数名并维护系统调用表。
(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
#include
#include
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;
}
要想让我们添加的 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 修改后,和往常一样在 linux-0.11 目录下执行 make all 命令就能自动把 who.c 加入到内核中了。
到此为止,系统调用的内核函数已经完成。应用程序想要使用我们新增的系统调用 iam 和 whoami ,还需要添加对应的系统调用 API 。
(1)在 oslab 目录下创建一个 iam.c 和 whoami.c 文件
/* iam.c */
#define __LIBRARY__
#include
#include
#include
#include
_syscall1(int, iam, const char*, name);
int main(int argc, char *argv[])
{
/*调用系统调用iam()*/
iam(argv[1]);
return 0;
}
/* whoami.c */
#define __LIBRARY__
#include
#include
#include
#include
#include
_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;
}
现在还不能直接编译运行,因为我们编写的 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
拷贝成功!目标目录下存在对应的两个文件就可以启动虚拟机进行测试了。
(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
运行结果: