Linux系统调用与哈工大实验二

实验要求

此次实验的基本内容是:在Linux 0.11上添加两个系统调用,并编写两个简单的应用程序测试它们。
具体实验细节可以参考蓝桥云:

https://www.lanqiao.cn/courses/115/learning/?id=374&compatibility=false

  • 系统调用
    1. iam()
      第一个系统调用是iam(),其原型为:
    int iam(const char *name);
    
    完成的功能是将字符串参数name的内容拷贝到内核中保存下来。要求name的长度不能超过23个字符。返回值是拷贝的字符数。如果name的字符个数超过了23,则返回“-1”,并置errnoEINVAL
    kernel/who.c中实现此系统调用。
    2. whoami()
    第二个系统调用是whoami(),其原型为:
    int whoami(char *name, unsigned int size);
    
    它将内核中由iam()保存的名字拷贝到name指向的用户地址空间中,同时确保不会对name越界访存(name的大小由size说明)。返回值是拷贝的字符数。如果size小于需要的空间,则返回“-1”,并置errnoEINVAL
    也是在kernel/who.c中实现。
  • 应用程序
    1. iam.c
      应用程序ima.cmain()函数中调用系统调用iam(),其接收命令行参数作为名字传递给int iam(const char *name)中的参数name
    2. whoami.c
      应用程序whoami.cmain()函数调用系统调用whoami(),将iam()保存在内核空间的name变量读出保存到函数参数name中,并打印输出。

前置内容

1.什么是系统调用

系统调用是操作系统实现硬件解耦与封装,为上层软件提供接口调用的一种途径。应用程序通过系统调用请求操作系统的服务。系统中的各种共享资源都由操作系统统一掌管,因此在用户程序中,凡是与资源有关的操作(如存储分配、I/O操作、文件管理等),都必须通过系统调用的方式向操作系统提出服务请求,由操作系统代为完成。简单来说就是操作系统提供统一的封装函数,用户想要实现对底层硬件资源的使用,就只能通过操作系统提供的API来完成。这样就能确保程序的相对安全与稳定。
在这里插入图片描述
所以系统调用便提供了从用户模式能够访问内核模式的途径。在Linux中实现系统调用是利用了软件中断的模式来实现,将系统调用设置成了一种特殊的中断模式。
其中int $0x80便是实现的唯一的汇编指令,其余代码均是指定输入输出以及指令执行过程中的寄存器及变量的使用。
int $0x80指令属于软中断(software interrupt)。软中断又叫做编程异常(programmed exception),是异常的一种。该指令的作用是以0x80作为索引值,用于在中断描述符表IDT中查找存储了中断处理程序信息的描述符。
之前我们也分析过IDT中断描述符表的结构,中断描述符表(Interrupt Descriptor Table,IDT)是用来告诉处理器在遇到异常或 “INT”操作码(汇编中)时所应调用的中断服务例程 (Interrupt Service Routine,ISR)。简单来说就是会有一段内核空间专门用来存储中断程序的地址,索引值以及优先级。其中set_system_gate(0x80,&system_call)便是用来设置系统调用函数位置的函数。这里具体的设置细节内容就不细讲了。
当设置好了系统调用中断,当触发0x80中断,操作系统便会根据idt中记录的入口函数地址从而调用system_call函数

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 as parameters
	  pushl %ebx		# to the system call
	  movl $0x10,%edx		# set up ds,es to kernel space
	  mov %dx,%ds
	  mov %dx,%es
	  movl $0x17,%edx		# fs points to local data space
	  mov %dx,%fs
	  call sys_call_table(,%eax,4)
	  pushl %eax
	  movl current,%eax
	  cmpl $0,state(%eax)		# state
	  jne reschedule
	  cmpl $0,counter(%eax)		# counter
	  je reschedule
  ret_from_sys_call:
	  movl current,%eax		# task[0] cannot have signals
	  cmpl task,%eax
	  je 3f

system_call会模拟函数调用过程。可以看到system_call函数首先将DS,ES,FS这些数据段寄存器压栈,然后将保存在EBX,ECX,EDX的库函数API的参数逆序压栈。接着通过 call sys_call_table(,%eax,4)调用sys_call_table(%eax存储的系统调用号)从而运行sys_call_table中对应的各种系统调用函数。当系统调用函数结束模拟正常的函数返回过程,从而实现内核态到用户态的切换。
在这里插入图片描述
大致过程如下图:
在这里插入图片描述
因此本次的实验两个系统调用函数只需要在sys_call_table中存入相应的系统调用号和函数入口地址,然后编写sys_iam(),sys_whoami()两个函数即可。系统调用和内核态到用户态的切换过程sys_call()函数都已经帮我们封装好了。

2.通过sys_fork()函数举例

当我们运行一个内核态的fork()函数,fork通过0x80中断(eax=2)进行系统调用,通过system_call()运行了syscall_table中系统调用号为2的sys_fork()系统调用函数,从而实现了fork过程。
在这里插入图片描述

fork()函数

void main(void) {
    ...    
    move_to_user_mode();
    if (!fork()) {
        init();
    }
    for(;;) pause();
}

move_to_user_mode已经让我们目前正在运行的程序模式转入用户态,处于受限状态,如果需要进行特殊操作,需要通过中断陷入内核态才可以。因此此时的fork()已经运行在了用户态,属于进程代码,他的代码段属于进程代码段
在这里插入图片描述
在这里插入图片描述
会根据局部LDT和TSS存储在对应的进行虚拟内存中。而内核态的代码与资源存储在全局描述符表的数据段与代码段中。因此此时的fork()函数需要在函数内部调用系统调用函数,进入内核态,从而能够实现进程空间中代码段数据段与全局描述符表中的代码段与数据段空间进行交互访问与运算。
fork ()函数

static _inline _syscall0(int,fork)

#define _syscall0(type,name) \
type name(void) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
    : "=a" (__res) \
    : "0" (__NR_##name)); \
if (__res >= 0) \
    return (type) __res; \
errno = -__res; \
return -1; \
}

所以,把宏定义都展开,其实就相当于定义了一个函数。

int fork(void) {
     volatile long __res;
    _asm {
        _asm mov eax,__NR_fork
        _asm int 80h
        _asm mov __res,eax
    }
    if (__res >= 0)
        return (void) __res;
    errno = -__res;
    return -1;
}

关键指令就是一个 0x80 号软件中断的触发,int 80h。其中还有一个 eax 寄存器里的参数是 __NR_fork,这也是个宏定义,值是 2。根据上文的介绍,当执行了int 80h时,便会触发system_call()函数并在模拟正常的函数调用,并在system_call()中调用system_call_table[eax对应的系统调用号(这里是__NR_fork)]跳转到sys_fork()的函数入口。

sys_call_table()

那我们接着看 sys_call_table 是个啥。

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

这里的fn_ptr类型是int (*)(),表示一个返回类型为int的函数指针,简单说就是fn_ptr存储的就是对应函数的入口地址。

extern int sys_setup();
extern int sys_exit();
extern int sys_fork();
extern int sys_read();
extern int sys_write();
extern int sys_open();
extern int sys_close();
......
extern int sys_setsid();
extern int sys_sigaction();
extern int sys_sgetmask();
extern int sys_ssetmask();
extern int sys_setreuid();
extern int sys_setregid();

在include/linux/sys.h中已经定义了对应的函数。因此加入我们要设计一个对应的系统调用函数,我们只需要在sys.h中添加一个extern (*int)()函数,并将函数入口地址添加到table中,接下来我们就只需要编写sys_call()的函数功能即可。例如sys_fork()函数

sys_fork()

_sys_fork:
    call _find_empty_process
    testl %eax,%eax
    js 1f
    push %gs
    pushl %esi
    pushl %edi
    pushl %ebp
    pushl %eax
    call _copy_process
    addl $20,%esp
1:  ret

这个函数功能就不具体讲了。从这里的探索我们也可以看出,操作系统通过系统调用,提供给用户态可用的功能,都暴露在 sys_call_table 里了。系统调用统一通过 int 0x80 中断来进入,具体调用这个表里的哪个功能函数,就由 eax 寄存器传过来,这里的值是个数组索引的下标,通过这个下标就可以找到在 sys_call_table 这个数组里的具体函数。
同时也可以看出,用户进程调用内核的功能,可以直接通过写一句 int 0x80 汇编指令,并且给 eax 赋值,当然这样就比较麻烦。
所以也可以直接调用 fork 这样的包装好的方法,而这个方法里本质也是 int 0x80 以及 eax 赋值而已。
这里再放一下刚才的图:
在这里插入图片描述

3.GCC内联汇编

可以参考如下文章:

https://blog.csdn.net/myprogram_player/article/details/121372941?ops_request_misc=&request_id=&biz_id=102&utm_term=内联汇编&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduweb~default-1-121372941.142v87insert_down28,239v2insert_chatgpt&spm=1018.2226.3001.4187

大致格式如下:

asm volatile 
(           这里写指令 
           : 输出操作数                   /* 可选 */
           : 输入操作数                   /* 可选 */
           : 可能被破坏的寄存器列表         /* 可选 */
           );
或者
__asm__ __volatile__ 
(            这里写指令 
           : 输出操作数                   /* 可选 */
           : 输入操作数                   /* 可选 */
           : 可能被破坏的寄存器列表         /* 可选 */
           );

输入输出操作数格式:

[ [asmSymbolicName] ] constraint (cexpression)

例如:
[a_val]"r"(a), [b_val]"r"(b)
"r"(a), "r"(b)

插入到C代码中的汇编语句是以:分隔的四个部分,第一部分是汇编代码本身,通常成为指令部。指令部是必须的,而其它部分可以根据实际情况而省略。 GCC采用如下方法来解决汇编代码中操作数如何与C代码中的变量相结合的问题:对寄存器的使用只需给出**“样板”和约束条件**,具体如何将寄存器与变量结合起来完全由GCC和GAS负责。具体而言就是:在指令部,加上前缀%的数字(如%0,%1)就是需要使用寄存器的**“样板”操作数。指令部中使用几个样板操作数,就表明有几个变量需要与寄存器相结合,这样GCC和GAS在编译和汇编时会根据后面给定的约束条件**进行恰当的处理。由于样板操作数也使用%作为前缀,因此寄存器名前面应该加上两个%,以免产生混淆。 紧跟在指令部后面的是输出部,是规定输出变量如何与样板操作数进行结合的条件,每个条件称为一个“约束”,必要时可以包含多个约束,相互之间用逗号分隔开就可以。每个输出约束都以’=‘号开始,然后紧跟一个对操作数类型进行说明的字后,最后是如何与变量相结合的约束。凡是与输出部中说明的操作数相结合的寄存器或操作数本身,在执行完嵌入的汇编代码后均不保留执行之前的内容,这是GCC在调度寄存器时所使用的依据。 输出部后面是输入部,输入约束与输出约束相似,但不带’='号。如果一个输入约束要求使用寄存器,则GCC在预处理时就会为之分配一个寄存器,并插入必要的指令将操作数装入该寄存器。与输入部中说明的操作数结合的寄存器或操作数本身,在执行完嵌入的汇编代码后也不保留执行之前的内容。 在内联汇编中用到的操作数从输出部的第一个约束开始编号,序号从0开始,每个约束计数一次。需要注意的是,内联汇编语句的指令部在引用一个操作数时总是将其作为32位的长字使用,但实际情况可能需要的是字或者字节,因此应该在约束中指明正确的限定符:

约束限定字符	含义
“a”	将输入变量放入eax
“b”	将输入变量放入ebx
“c”	将输入变量放入ecx
“d”	将输入变量放入edx
“S”	将输入变量放入esi
“D”	将输入变量放入edi
“q”	将输入变量放入eax,ebx ,ecx ,edx中的一个
“r”	将输入变量放入通用寄存器,也就是eax ,ebx,ecx,edx,esi,edi中的一个
“A”	放入eax和edx,把eax和edx,合成一个64位的寄存器(uselong longs)
“m”	内存变量
“o”	操作数为内存变量,但是其寻址方式是偏移量类型,也即是基址寻址,或者是基址加变址寻址
“V”	操作数为内存变量,但寻址方式不是偏移量类型
“,”	操作数为内存变量,但寻址方式为自动增量
“p”	操作数是一个合法的内存地址(指针)
“g”	将输入变量放入eax,ebx,ecx ,edx中的一个或者作为内存变量
“X”	操作数可以是任何类型
“I”	0-31 之间的立即数(用于32位移位指令)
“J”	0-63 之间的立即数(用于64 位移位指令)
“N”	0-255 ,之间的立即数(用于out 指令)
“i”	立即数
“n”	立即数,有些系统不支持除字以外的立即数,这些系统应该使用“n”而不是“i”
“=”	操作数在指令中是只写的(输出操作数)
“+”	操作数在指令中是读写类型的(输入输出操作数)
“f”	浮点数
“t”	第一个浮点寄存器
“u”	第二个浮点寄存器
“G”	标准的80387
%	该操作数可以和下一个操作数交换位置

实验具体流程

这里参考如下链接以及蓝桥云手册即可:

https://blog.csdn.net/leoabcd12/article/details/122268321?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522168639094316800225524154%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=168639094316800225524154&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2alltop_positive~default-1-122268321-null-null.142v88control,239v2insert_chatgpt&utm_term=%E5%93%88%E5%B7%A5%E5%A4%A7%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E5%AE%9E%E9%AA%8C&spm=1018.2226.3001.4187

有一些细节问题注意即可:
在这里插入图片描述
当出现这种问题时,需要进入bochs虚拟机(注意不是你的Linux虚拟机)中修改/usr/include/unistd.h中的调用设置调用号

#define __NR_whoami	   	72
#define __NR_iam	   	73     

很多时候出现 xxx.c:EMOENT 一般是你的文件并不存在导致的。
整体实现过程比较简单,这里就不具体讲了。

Logo

欢迎加入我们的广州开发者社区,与优秀的开发者共同成长!

更多推荐