一.学习内容
1、操作系统接口
完成setup之后,操作系统的代码都被读入到从0地址开始的地方,还创建了一些初始的结构,如mem_map(管理内存的数据结构)、GDT、IDT等。而应用程序都放在了内存的上端。
最终,内存的下方放置的为系统代码和数据、上方放置的为应用程序,这样子一个结构情况。
什么是操作系统接口?操作系统直接面对用户吗?用户是怎么用操作系统的?
-
命令行
首先应用程序编写的程序将编译成一个可执行文件。而与此同时,系统在刚开始的初始化完成后,会循环停留在shell里(可以理解为桌面,不断等你施加命令),当用户输入命令行指令后,系统将运行上面的那个可执行文件。
-
图形按钮
-
由getmessage函数把消息从内核的队列中抽出来,然后根据消息调用消息处理函数,做相应的反应。
-
操作系统接口(系统调用)
操作系统接口连接谁?连接操作系统和应用软件,并不是直接与硬件交互了。 如何连接?C语言 。所以,操作系统提供这样的重要函数,表现为:函数调用,所以又称为系统调用system_call。
2.系统调用的实现
1.假设用户程序内使用printf()函数。 2.根据lib下的_syscalln()和include/unistd.h下的模板,对printf()函数进行宏定义展开。 3.调用展开后的函数,触发int 0x80中断,将kernel下的system_call对应的IDT表中的DPL设为3,从而让用户程序可获取system_call地址作为IP。然后,再设置CS=8,使其对应的CPL=0,从而让用户可以进入内核态。 4.在system_call函数中,会使用从include/unistd.h中获得的存入eax的值,来查询include/linux/sys.h中sys_call_table表里对应的系统调用函数。 5.使用对应的系统调用函数处理数据后,将结果存入eax并返回给用户程序。
不该随意访问内核
-
应用程序是不可以随意地调用内核的数据,不可以随意jmp。这会导致安全和隐私问题,如:可以看到root密码,可以修改root密码。
如何去访问内核?
-
操作系统调用提供了能够合理进入内核的一种手段-------“硬件设计”,把非内核的和内核的东西划分成了用户态和内核态,对应的内存中的区域叫用户段和内核段。内核态可以访问任何数据,但用户态不能访问内核数据,只有当前的指令大于或等于目标的特权级,这条指令才被允许执行。
-
不论是内核段还是用户段都需要通过段寄存器进行访问,主要使用了两个段寄存器CPL(CS低两位)和DPL来实现不同权限的控制。其中CPL存放在CS中,DPL存放在GDT中。当想访问其他段时,会从GDT中查询目标段的DPL来和当前所执行段CS中的CPL进行对比。即当:
DPL>=CPL
时,才允许执行。 -
中断是进入内核的唯一方法,该方法通过硬件来实现。因此,如果用户程序想要进入内核,就需要包含一段int中断指令的代码,这段代码由库函数实现,由宏来展开成一段汇编代码。进入内核之后,操作系统就会写中断处理过程,来获取想调程序的编号。然后,操作系统会根据编号执行相应的代码。
-
3.以printf()为例,详细分析其在Linux0.11中该系统调用的过程。
-
在lib/write.c中的代码:
#define __LIBRARY__
#include <unistd.h>
_syscall3(int,write,int,fd,const char *,buf,off_t,count)
-
进入include/unistd.h
-
这是一些宏定义,表示系统调用的编号,在这里我们可以看到write的编号是4.
write.c中调用#define _syscall3()宏,这个宏的作用是封装了通过系统调用号码(_NR##name)来调用系统调用的过程。
_syscall3(int,write,int,fd,const char *,buf,off_t,count)
这段代码执行完毕后,把系统调用的编号(4)放到了eax寄存器中(因为后续要通过编号查表),把参数fd放到了ebx寄存器中,*buf放到了ecx寄存器中,把count放到了edx寄存器中。
-
当发生了中断以后,CPU就会去Linux内核中找到内核实现的idt表项。通过0x80中断向量找到具体的中断描述符,再通过中断描述符找到具体的回调方法。
-
-
sched_init()用来初始化,set_system_gate(0x80,&system_call)设置了系统调用门
-
set_system_gate是个宏,上图是关于它的定义,其中n表示中断号,addr表示地址,它又调用了_set_gate宏。
这段代码主要是初始化IDT表,然后再根据中断指令去查表,跳转到对应地址进行执行.
将addr
=&system_call
组装到了处理函数入口点偏移,把dpl
=3组装到了DPL,将0x0008
组装到了段选择符。所以现在,CS=8,IP=&system_call。当CS=8时,CS的最后两位CPL就等于00,这时DPL>=CPL,进入内核态。
-
查看中断处理程序:linux/kernel/system_call.s
此实验只需关心划线代码,意思是call sys_call_table + 4 * %eax,eax中现在存的是系统调用号4,__NR_write的值,sys_call_table就是基址(是一个函数表)。4表示每个系统调用对应的函数占四个字节(32位)。当要查找sys_write(位于第5个函数)时,会设置 eax=4(数组下标从0开始)。
成功找到sys_write。
总结:
1、在main方法启动内核时会通过sched_init方法对gdt、ldt、idt表项以及其他操作初始化。所以这里对0x80中断向量进行了初始化,当触发此中断,会调用到system_call函数来进行处理。 2、当发生int 0x80中断操作后,CPU会去找内核实现的idt表项,通过0x80中断向量找,最后找到0x80对应的处理函数system_call。 3、所以找到system_call,这里是汇编代码,只看最重要的一步。这里通过call指令找sys_call_table表,通过eax寄存器(eax是之前内联汇编传来的索引)*4(因为int数组的一个单元是4个字节)定位到具体的系统调用。最终找到了sys_write系统调用。
过程:
二、实验过程。
内核层面修改:
1、修改kernel/system_call.s 文件
nr_system_calls = 74 # 新增2个系统调用`
2、修改 include/linux/sys.h 文件,进行extern声明,以及添加新的系统调用函数指针
extern int sys_whoami();
extern int sys_iam();
3.添加 kernel/who.c 文件 编写实现 sys_iam函数和sys_whoami函数
#include <string.h>
#include <errno.h>
#include <asm/segment.h>
char msg[24]; //23个字符 +'\0' = 24
int sys_iam(const char * name)
/***
function:将name的内容拷贝到msg,name的长度不超过23个字符
return:拷贝的字符数。如果name的字符个数超过了23,则返回“•-1”,并置errno为EINVAL。
****/
{
int i;
//临时存储 输入字符串 操作失败时不影响msg
char tmp[30];
for(i=0; i<30; i++)
{
//从用户态内存取得数据
tmp[i] = get_fs_byte(name+i);
if(tmp[i] == '\0') break; //字符串结束
}
//printk(tmp);
i=0;
while(i<30&&tmp[i]!='\0') i++;
int len = i;
// int len = strlen(tmp);
//字符长度大于23个
if(len > 23)
{
// printk("String too long!\n");
return -(EINVAL); //置errno为EINVAL 返回“•-1” 具体见_syscalln宏展开
}
strcpy(msg,tmp);
return i;
}
int sys_whoami(char* name, unsigned int size)
/***
function:将msg拷贝到name指向的用户地址空间中,确保不会对name越界访存(name的大小 由size说明)
return: 拷贝的字符数。如果size小于需要的空间,则返回“•-1”,并置errno为EINVAL。
****/
{
//msg的长度大于 size
int len = 0;
for(;msg[len]!='\0';len++);
if(len > size)
{
return -(EINVAL);
}
int i = 0;
//把msg 输出至 name
for(i=0; i<size; i++)
{
put_fs_byte(msg[i],name+i);
if(msg[i] == '\0') break; //字符串结束
}
return i;
}
4.修改kernel/Makefile ,将 who.c 编译进内核
5.回到上级目录下make,内核重新编译,出现sync就成功了。
应用层面修改
1、在oslab目录下挂载文件。
sudo ./mount-hdc
2. 在~/oslab/hdc/usr/include/目录下修改unistd.h文件
/* 添加系统调用号 */
#define __NR_whoami 72
#define __NR_iam 73
3. 在~/oslab/hdc/usr/root目录下写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 */
#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;
}
4.卸载
sudo umount hdc
测试
在~/oslab目录下./run,依次输入以下命令。
gcc -o iam iam.c
gcc -o whoami whoami.c
./iam zxy.
./whoami
结果: