IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 系统运维 -> Linux0.11操作系统(哈工大李治军老师)实验楼实验2-系统调用 -> 正文阅读

[系统运维]Linux0.11操作系统(哈工大李治军老师)实验楼实验2-系统调用

Linux0.11操作系统(哈工大李治军老师)实验楼实验2-系统调用

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

原理

1. 应用程序如何调用系统调用

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

而调用系统调用,是调用系统库中为该系统调用编写的一个接口函数,叫 API(Application Programming Interface)。API 并不能完成系统调用的真正功能,它要做的是去调用真正的系统调用,过程是:

  • 把系统调用的编号存入 EAX;
  • 把函数参数存入其它通用寄存器;
  • 触发 0x80 号中断(int 0x80)。

linux-0.11 的 lib 目录下有一些已经实现的 API。Linus 编写它们的原因是在内核加载完毕后,会切换到用户模式下,做一些初始化工作,然后启动 shell。而用户模式下的很多工作需要依赖一些系统调用才能完成,因此在内核中实现了这些系统调用的 API。调用自定义函数是通过 call 指令直接跳转到该函数的地址,继续运行。

而调用系统调用,是调用系统库中为该系统调用编写的一个接口函数,叫 API(Application Programming Interface)。API 并不能完成系统调用的真正功能,它要做的是去调用真正的系统调用,过程是:

  • 把系统调用的编号存入 EAX;
  • 把函数参数存入其它通用寄存器;
  • 触发 0x80 号中断(int 0x80)。

linux-0.11 的 lib 目录下有一些已经实现的 API。Linus 编写它们的原因是在内核加载完毕后,会切换到用户模式下,做一些初始化工作,然后启动 shell。而用户模式下的很多工作需要依赖一些系统调用才能完成,因此在内核中实现了这些系统调用的 API。

首先我看一下lib/close.c,研究一下 close() 的 API:

#define __LIBRARY__
#include <unistd.h>

_syscall1(int,close,int,fd)

发现_syscall1其实是一个定义在unistd.h中的一个宏,我们转到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; \
}

这是C语言的内嵌汇编,具体可参考:内嵌汇编

_syscall1(int,close,int,fd) 进行宏展开,其中冒号是分隔符,简单说就是调用0x80中断,过程为:

fd的值放到寄存器EBX里面,此处的0gcc的匹配符,表示将__NR_close赋值给EAX;
__NR_close是什么呢?就是下面宏定义,表示系统调用的编号,也就是将系统调用的编号赋值给EAX
然后执行系统调用int 0x80;系统调用返回的EAX赋值给__res
最后返回__res

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

__NR_close就是系统调用的编号,在include/unistd.h 中定义:

#define __NR_close    6
/*
所以添加系统调用时需要修改include/unistd.h文件,
使其包含__NR_whoami和__NR_iam。
*/

我也可以打开oslab/linux-0.11/include/linux/sys.h中看到:

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_close正是在第6号位置,则表示:__NR_close = 6就是代表这函数`sys_close。

也就是说,我们如果要添加两个系统调用,形式要和__NR_close一样。

/*
而在应用程序中,要有:
*/

/* 有它,_syscall1 等才有效。详见unistd.h */
#define __LIBRARY__

/* 有它,编译器才能获知自定义的系统调用的编号 */
#include "unistd.h"

/* iam()在用户空间的接口函数 */
_syscall1(int, iam, const char*, name);

/* whoami()在用户空间的接口函数 */
_syscall2(int, whoami,char*,name,unsigned int,size);

在 0.11 环境下编译 C 程序,包含的头文件都在 /usr/include 目录下。

该目录下的 unistd.h 是标准头文件(它和 0.11 源码树中的 unistd.h 并不是同一个文件,虽然内容可能相同),没有 __NR_whoami__NR_iam 两个宏,需要手工加上它们,也可以直接从修改过的 0.11 源码树中拷贝新的 unistd.h 过来。

2.从’0x80’进入内核函数

我们需要先了解下 0.11处理 0x80 号中断的过程。

在内核初始化时,主函数在 init/main.c 中,Linux 实验环境下是 main()

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

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

在中断初始化时,调用sched_init函数,设置中断处理门,即初始化**IDT表**,也就是说,中断0x80对应当系统调用就是system_call

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

set_system_gate 是个宏,在 include/asm/system.h 中定义为:(此处建议去看李治军老师的视频课第五节系统调用的实现)

其中,idt是中断向量表的地址,是全局函数;3DPL,即int 0x80被访问段的权限;用户态开始访问的用户权限为CPL为3,所以可以访问int 0x80的数据段,然后int 0x80CPL置为0,从而访问内核态数据。

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

_set_gate 的定义是:

这里其实就是将system_call的地址写入到0x80对应的 IDT(中断描述符表)中,当调用中断0x80时,自动调用函数system_call;

#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))

下面就是主要的函数system_call,详细注释请参考代码:


!……
! # 这是系统调用总数。如果增删了系统调用,必须做相应修改
!也就是说,如果我们要增加系统调用函数,这里的总数也要相应的增加
nr_system_calls = 72
!……

!.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

此处只关心 call sys_call_table(,%eax,4) 这一句。

根据汇编寻址方法它实际上是:call sys_call_table + 4 * %eax,其中 eax 中放的是系统调用号,即 __NR_xxxxxx。其中的4代表函数指针的大小,具体见下面代码。

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

fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,...
在include/linux/sched.h中
typedef int (fn_ptr*)();

例如刚才的__NR_close = 6,那么这里的eax就是6,对应的时sys_clsoe;则call sys_call_table(,%eax,4)就是调用sys_close.

增加实验要求的系统调用,需要在这个函数表中增加两个函数引用 ——sys_iamsys_whoami。当然该函数在 sys_call_table 数组中的位置必须和 __NR_xxxxxx 的值对应上。

同时还要仿照此文件中前面各个系统调用的写法,加上:

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

最后在``oslab/linux-0.11/kernel/`中写一个c文件,将上面两个函数实现。

3.实现系统调用函数

最后,只需要仿照sys_close(int fd)编写 sys_iam()sys_whoami()即可。、

4. 小结

我们总结下从用户态调用close.c函数,是如何获取内核态数据的:

  • 用户调用close.c函数,利用宏定义_syscall1知道了__NR_close的调用号为6,此时CPL = 3;
  • close.c进入int 0x80中断,DPL = 3,此时就是从用户态进入到了内核态;
  • 利用中断函数system_call进行中断处理,调用sys_call_table,此时已经进入到了内核态,CPL = 0;
  • sys_call_table跳转地址到sys_close系统函数处执行。

流程

1. 添加内核函数

指针参数传递的是应用程序所在地址空间的逻辑地址,在内核中如果直接访问这个地址,访问到的是内核空间中的数据,不会是用户空间的。所以这里还需要一点儿特殊工作,才能在内核中从用户空间得到数据

要实现的两个系统调用参数中都有字符串指针,非常像 open(char *filename, ……),所以我们看一下 open() 系统调用是如何处理的。
这里和之前说的close.c是一样的。

int open(const char * filename, int flag, ...)
{
//    ……
    __asm__("int $0x80"
            :"=a" (res)
            :"0" (__NR_open),"b" (filename),"c" (flag),
            "d" (va_arg(arg,int)));
//    ……
}

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

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

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

这就用到我们前面所说的,利用int 0x80从用户态进入到内核态,继而调用中断函数system_call

system_call: //所有的系统调用都从system_call开始
!    ……
    pushl %edx
    pushl %ecx
    pushl %ebx                # push %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

由上面的代码可以看出,获取用户地址空间(用户数据段)中的数据依靠的就是段寄存器 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) {
        ……
    }
    ……
}

它将参数传给了 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))=='/') {
        ……
    }
    ……
}

处理方法就很显然了:用 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;
}
extern inline void put_fs_byte(char val,char *addr)
{
    __asm__ ("movb %0,%%fs:%1"::"r" (val),"m" (*addr));
}

他俩以及所有 put_fs_xxx() 就是获得用户数据, get_fs_xxx()是相反的,即将内核态的数据赋值给用户态。这两个 都是用户空间和内核空间之间的桥梁,在后面的实验中还要经常用到。

在了解原理之后,我们在/linux_0.11/kernel/who.c中添加系统调用函数;

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

char msg[24];

int sys_iam(const char *name)
{
        int i;
        char tmp[30];
        for( i = 0; i < 30; i++)
        {
                tmp[i] = get_fs_byte(name + i);
                if(tmp[i] == '\0')
                        break;
        }
        i = 0;
        while(i < 30 && tmp[i] != '\0') i++;

        if(i > 23)
        {
                return -(EINVAL);
        }
        strcpy(msg, tmp);
        return i;
}

int sys_whoami(char *name, unsigned int size)
{
        int len = 0;
        for(; msg[len] != '\0'; len++);

        if(len > size)
        {
                return -(EINVAL);
        }

        int i = 0;
        for(i = 0; i < size; i++)
        {
                put_fs_byte(msg[i], name + i);
                if(msg[i] == '\0') break;
        }
        return i;
}

2. 添加全局变量

linux-0.11/include/linux/sys.h中,添加全局变量。

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

3. 修改函数中断表

linux-0.11/kernel/system_call.s中,修改函数中断表。

nr_system_calls = 74

4. 添加调用编号

先在oslab/下运行sudo ./mount-hdc将虚拟机的硬盘挂载在oslab/hdc/下;

然后在oslab/hdc/user/include/unistd.h下添加宏定义:

#define __NR_iam        72
#define __NR_whoami     73

6. 修改Makefile

我们要修改的是 kernel/Makefile。需要修改两处。

(1)第一处

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

改为:

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

添加了 who.o

(2)第二处

### Dependencies:
exit.s exit.o: exit.c ../include/errno.h ../include/signal.h \
  ../include/sys/types.h ../include/sys/wait.h ../include/linux/sched.h \
  ../include/linux/head.h ../include/linux/fs.h ../include/linux/mm.h \
  ../include/linux/kernel.h ../include/linux/tty.h ../include/termios.h \
  ../include/asm/segment.h

改为:

### Dependencies:
who.s who.o: who.c ../include/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/linux/sched.h \
  ../include/linux/head.h ../include/linux/fs.h ../include/linux/mm.h \
  ../include/linux/kernel.h ../include/linux/tty.h ../include/termios.h \
  ../include/asm/segment.h

添加了 who.s who.o: who.c ../include/linux/kernel.h ../include/unistd.h

Makefile 修改后,和往常一样 make all 就能自动把 who.c 加入到内核中了。

如果编译时提示 who.c 有错误,就说明修改生效了。所以,有意或无意地制造一两个错误也不完全是坏事,至少能证明 Makefile 是对的。

7. 编写测试程序

hdc/usr/root下编写测试程序iam.cwhoami.c:

iam.c

#define __LIBRARY__
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <stdio.h>
_syscall1(int,iam,const char*,name)
 
int main(int argc,char* argv[])
{
	
    if(argc>1){
	if(iam(argv[1])<0){
		printf("SystemCall Exception!\n");
		return -1;
	}
    }
    else{
	printf("Input Exception!\n");
	return -1;
    }		
    return 0;
}

whoami.c

#define __LIBRARY__
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <stdio.h>
 
_syscall2(int,whoami,char*,name,unsigned int,size)
 
int main()
{
    int counter;
	char buff[128]={0};
	
    counter=whoami(buff,128);
	if(counter < 0)
	{
	   printf("SystemCall Exception!");
	   return -1;
    	}
	else{
		printf("%s\n",buff);
    	}
    return 0;
}

注:虽然此实验完成,但是中间仍有很多问题

  1. 我在上一个实验的基础上进行,一直在setup.s处,我尝试将之前的循环去掉,但是没有用;
  2. 在重新配置实验后,还碰到了问题,比如没有规则可制作目标who.c等;

希望以后能够解决问题。

  系统运维 最新文章
配置小型公司网络WLAN基本业务(AC通过三层
如何在交付运维过程中建立风险底线意识,提
快速传输大文件,怎么通过网络传大文件给对
从游戏服务端角度分析移动同步(状态同步)
MySQL使用MyCat实现分库分表
如何用DWDM射频光纤技术实现200公里外的站点
国内顺畅下载k8s.gcr.io的镜像
自动化测试appium
ctfshow ssrf
Linux操作系统学习之实用指令(Centos7/8均
上一篇文章      下一篇文章      查看所有文章
加:2022-03-21 21:35:42  更:2022-03-21 21:37:42 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2025年1日历 -2025/1/4 9:23:47-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码