linux0.11源码内核——系统调用,int80的实现细节

系统调用:系统库中为系统调用编写了许多接口函数(API),不同的API对应了不同的真正的(OS内核中)系统调用

系统调用的三个基本步骤:

  1.把系统调用编号存到 寄存器eax中

  2.把函数参数传到其他通用寄存器中:第一个参数:ebx,第二个:ecx ...

  3.触发0x80号中断

在内核加载完毕,切换到用户模式下时,会做一些初始化工作(如main.c里一大堆_init()函数),最后一步启动shell,

用户在shell中可以进行进程调度

  

int 0x80的触发过程和实现细节

1.一些宏定义和头文件include/unistd,h的细节

先看一个系统调用close()的API

#define __LIBRARY__
#include <unistd.h>

_syscall1(int, close, int, fd)

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

close()用该宏展开后就是

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 存入 EAX,将参数 fd 存入 EBX,然后进行 0x80 中断调用。调用返回后,从 EAX 取出返回值,存入 __res,再通过对 __res 的判断决定传给 API 的调用者什么样的返回值

__NR_close就是系统调用close的对应的编号,该宏在include/unistd.h中定义

__NR_name其实就是系统调用name的对应的编号

2.从中断进入内核

OS内核初始化时(init/main.c函数进行初始化),调用了一个sched_init()函数

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

sched_init()在kernel/sched.c中定义。作用是将各种编号的中断和中断处理程序的地址进行绑定

而0x80中断则和system_call函数进行绑定

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

set_system_gate 是个宏,在 include/asm/system.h 中定义:

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

_set_gate()函数的第一个参数是中断描述符表,表中的每个指针指向一个中断描述符,n=80时对应的描述符就是int80的描述符

3表示将DPL修改成3,因为用户态的CPL是3,这么修改使CPL>=DPL,从而得以进入内核(只有int80的DPL是3,所以用户态只能通过int80进入内核)

addr是中断处理程序,这里将system_call填到int80的中断描述符中

事实上,_set_gate()函数也是一个被定义的宏,其展开后为

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

这段代码做的就是上述的事

接下去看int80的中断处理函数system_call,定义在 kernel/system_call.s 中,是纯汇编代码

 1 !……
 2 ! # 这是系统调用总数。如果增删了系统调用,必须做相应修改
 3 nr_system_calls = 72
 4 !……
 5
 6 .globl system_call
 7 .align 2
 8 system_call:
 9
10 ! # 检查系统调用编号是否在合法范围内
11     cmpl \$nr_system_calls-1,%eax
12     ja bad_sys_call
13     push %ds
14     push %es
15     push %fs
16     pushl %edx
17     pushl %ecx
18
19 ! # push %ebx,%ecx,%edx,是传递给系统调用的参数
20     pushl %ebx
21
22 ! # 让ds, es指向GDT,内核地址空间
23     movl $0x10,%edx
24     mov %dx,%ds
25     mov %dx,%es
26     movl $0x17,%edx
27 ! # 让fs指向LDT,用户地址空间
28     mov %dx,%fs
29     call sys_call_table(,%eax,4)
30     pushl %eax
31     movl current,%eax
32     cmpl $0,state(%eax)
33     jne reschedule
34     cmpl $0,counter(%eax)
35     je reschedule

system_call 用 .globl 修饰为其他函数可见。

call sys_call_table(,%eax,4):根据寻址方式 展开成 call sys_call_table + 4 * %eax

  其中eax是系统调用号,

    sys_call_table 是一个指针数组,每个指针指向一个系统调用函数,其定义在 include/linux/sys.h 中

fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,...

  所以4*%eax的意思其实是eax对应的函数指针在表中的地址

至此已经找到了内核中系统调用函数的那个指针,所以直接调用该指针对应的函数就可以啦!

3.用户态和内核态之间数据的传递过程

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

例如,调用open(char *filename, ……),

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里存了open的调用号,ebx里存了filename字符串指针,即指向了用户态的一串字符串,ecx存了flag...

现在的问题是,内核态要找ebx里的地址对应的字符串时,是在内核态的地址里找的,而ebx里的地址对应的是用户态的

所以要进行一些转换,在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

可见,在call sys_open前,system对一些寄存器做了处理,

重点来看fs寄存器!

  首先ds,es指向了gdt,即指向了内核的gdt,

  但是fs指向了ldt,ldt对应的是用户态的地址空间

所以通过fs指向用户态地址,ds,es指向内核态地址来进行用户空间和内核空间的信息传递

不了解gdt和ldt的话看下图,来源:https://www.cnblogs.com/chenwb89/p/operating_system_003.html

其实cs的选择子作用在用户态是体现在ldt上的,即可以通过ldt来寻找某个段的地址

总之,依靠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) {
        ……
    }
    ……
}

sys_open将最重要的功能交给open_namei(),然后open_namei()再交给dir_namei(),get_dir()

最后get_dir()中调用了一个get_fs_byte()

static struct m_inode * get_dir(const char * pathname)
{
    ……
    if ((c=get_fs_byte(pathname))==‘/‘) {
        ……
    }
    ……
}

显然,get_fs_byte(pathname)获得了一个用户空间的fs寄存器指向的字节数据

那么同理,也有put_fs_byte函数向fs指向的用户空间输出函数

这两个函数被定义在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() 都是用户空间和内核空间之间的桥梁

原文地址:https://www.cnblogs.com/zsben991126/p/12019332.html

时间: 2024-10-29 16:05:24

linux0.11源码内核——系统调用,int80的实现细节的相关文章

mysql5.7.11 源码编译安装 (Red hat linux 6.5 )

mysql5.7.11 源码编译安装 (Red hat linux 6.5 ) 一.准备工作 1.1 卸载系统自带mysql 查看系统是否自带mysql, 如果有就卸载了, 卸载方式有两种yum, rpm, 这里通过yum卸载 rpm -qa | grep mysql    //查看系统自带mysql yum -y remove mysql-*   //卸载mysql rpm -e –nodeps mysql-5.1.73-3.el6_5.x86_64 //卸载mysql 1.2 卸载系统自带b

dubbo2.4.11源码编译

本文以dubbo2.4.11源码编译和打包例 dubbo官网:http://dubbo.io/ 源码:https://github.com/alibaba/dubbo 用户指南:http://dubbo.io/User+Guide-zh.htm 开发指南:http://dubbo.io/Developer+Guide-zh.htm 按官方说明2.4.X是GA稳定版本,去https://github.com/alibaba/dubbo/releases下载最新的2.4.11 解压到D:\proje

Linux0.11内核源码——内核态进程切换的改进

由于Linux0.11的内核态进程切换使用的方式是用ljmp来进行TSS的跳转,效率较低,因此考虑对其进行优化,改为后面版本使用的kernel stack栈的切换 需要做的任务 1.重写schedule,switch_to函数 2.将修改过的函数接在一起 3.修改fork函数 目前 Linux 0.11 中工作的 schedule() 函数是首先找到下一个进程的数组位置 next,而这个 next 就是 GDT 中的 n,所以这个 next 是用来找到切换后目标 TSS 段的段描述符的,一旦获得

Mysql 5.7.11源码安装方法

环境:Centos 6.6 64位mysql 5.7.11 1.安装依赖包 yum -y install gcc-c++ ncurses-devel cmake make perl gcc autoconf automake zlib libxml libgcrypt libtool bison 2.安装boost库(对应版本高于或低于这个版本都有问题) wget http://nchc.dl.sourceforge.net/project/boost/boost/1.59.0/boost_1_

mysql 5.7.11 源码安装

mysql5.711安装 1.安装boost包下载地址http://sourceforge.net/projects/boost/files/boost/ 2.解压boost_1_59_0.tar.gz cp -a boost_1_59_0 /usr/local/boost 3.cmake编译(要在源码目录下面执行cmake命令)cmake -DCMAKE_INSTALL_PREFIX=/exapp/mysql -DMYSQL_UNIX_ADDR=/exapp/mysql/mysql.sock

从Spring-Session源码看Session机制的实现细节

Re:从零开始的Spring Session(一) Re:从零开始的Spring Session(二) Re:从零开始的Spring Session(三) 去年我曾经写过几篇和 Spring Session 相关的文章,从一个未接触过 Spring Session 的初学者视角介绍了 Spring Session 如何上手,如果你未接触过 Spring Session,推荐先阅读下「从零开始学习Spring Session」系列(https://www.cnkirito.moe/categori

网狐棋牌6.6完整源码+内核源码+105款游戏源码下载

本资源仅用于研究和学习,请勿用于商业用途,否则后果自负.请下载后24小时之内删除 资源来源于互联网,如果侵犯了您的权益,我们会第一时间删除. 网狐棋牌6.6,里面没有深海捕鱼,其它的还是比较全的,这个已经有网友验证了 我没有亲测,喜欢的朋友自己测试一下吧! 代码图: 开发环境:VS2003 + SQL Server 2005 + IIS 下载地址:自己百度下吧! 下载地址: http://www.yxkfw.com/thread-17-1-1.html 说明文档齐全: 积分数据库的创建 建库 1

EMQ ---v2.3.11源码成熟度

从原作者那边了解到,总体还可以,但是做不到99.99%稳定.主要是连接内存占用没有保护. pubsub均衡时很稳定,但是集群或大量消息向少量订阅发布时会崩溃,小概率情况. EMQ中CPU是公平分配给MQTT会话,大量pub消息到一个订阅,订阅不会拿到更多cpu,最终导致消息累积,内存溢出宕机. 崩溃可能发生在网络波动,大量消息向少量订阅发布,容量不够,集群脑裂,出现异常订阅发布等情况. 所以,客户端要做好连接back off,即连接退避,防止连接风暴.就是说服务器崩溃了,重启时,要防止海量客户端

Stl源码剖析读书笔记之Alloc细节

阅读基础: Foo *pf = new Foo; 执行了两个步骤: 1)::operator new 向系统申请内存. 2) 调用Foo::Foo()构造函数构造实例.  ==> 申请内存,构造实例. delete pf; delete; 执行了两个步骤: 1)调用Foo::~Foo()析构函数. 2). ::operator delete释放内存.         ==> 析构实例,释放内存. Stl Alloc实现: Stl为了高效利用内存,将这两部分分开,分成了四个操作( 构造::con