ch2 系统调用实验

文章详细阐述了Linux系统调用的过程,包括应用程序通过API调用系统调用,系统调用号的存储,中断0x80的触发,以及内核如何处理系统调用。在内核中,通过设置中断描述符表(IDT)来处理中断,最终调用相应的系统服务例程。系统调用参数通过特定的寄存器传递,如EAX、EBX、ECX和EDX。在用户态和核心态之间,利用段寄存器FS来访问用户空间的数据。
摘要由CSDN通过智能技术生成

lab2 系统调用

系统调用过程

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

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

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

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

系统调用展开

首先系统调用syscall是一个宏

#define __LIBRARY__
#include <unistd.h>

_syscall1(int, close, int, fd)
#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; \
}

将其展开可后,结合内联汇编模板来看

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;
}
asm ( assembler template
        : output operands                /* optional */
        : input operands                   /* optional */
        : list of clobbered registers   /* optional */
);
r	Register(s)
a	%eax, %ax, %al
b	%ebx, %bx, %bl
c	%ecx, %cx, %cl
d	%edx, %dx, %adl
S	%esi, %si
D	%edi, %di

结合内联扩展汇编可知,要求汇编代码必须在被放置的位置执行(例如不能被循环优化而移出循环),我们就要在asm之后的“()”前,放一个volatile关键字。

这个指令的输出形式是将eax的值给变量res。看输入形式,"0”就是指定使用和第一个输出相同的寄存器,所以将NR_close的值给到eax,fd给ebx。最后通过eax给到res的值返回。

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

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

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

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

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

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

内核中断处理

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

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

#define set_system_gate(n,addr) \
    _set_gate(&idt[n],15,3,addr)
    
#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))
“m”: 使用一个内存操作数,内存地址可以是机器支持的范围内。
“o”: 使用一个内存操作数,但是要求内存地址范围在在同一段内。例如,加上一个小的偏移量来形成一个可用的地址。
“V”: 内存操作数,但是不在同一个段内。换句话说,就是使用除了”o” 以外的”m”的所有的情况。
“i”: 使用一个立即整数操作数(值固定);也包含仅在编译时才能确定其值的符号常量。
“n”: 一个确定值的立即数。很多系统不支持汇编常数操作数小于一个字(word)的长度的情况。这时候使用n就比使用i好。
“g”: 除了通用寄存器以外的任何寄存器,内存和立即整数。    

在内核初始化时,main函数调用了sched_init() 初始化函数。看一下该函数调用了一个建立系统门的宏set_system_gate,建立系统中断宏又是基于建立表宏set_gate。

从set_get可以看出,双冒号没有输出,addr即system_call的地址给到edx,执行第一条指令后给到ax。

第二条指令将i开头的数给到dx。第三条指令将ax的值写到o开头代表的&idt[0x80],而下一条写到&idt[0x80+4]

system_call写至int 0x80的中断描述符,调用int 0x80会自动调用system_call

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

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/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();

用户态与核心态之间传递

以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)));
//    ……
}

看出,系统调用是用 eax、ebx、ecx、edx 寄存器来传递参数的。其中 eax 传递了系统调用号(0表示和输出传递到同一个寄存器),而 ebx、ecx、edx 是用来传递函数的参数的
ebx 对应第一个参数,ecx 对应第二个参数,依此类推
如 open 所传递的文件名指针是由 ebx 传递的,也即进入内核后,通过 ebx 取出文件名字符串。open 的 ebx 指向的数据在用户空间,而当前执行的是内核空间的代码,如何在用户态和核心态之间传递数据?

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执行

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() 即可。
而从核心态拷贝数据到用户态:

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() 都是用户空间和内核空间之间的桥梁,在后面的实验中还要经常用到。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值