深入理解Linux内核day09--系统调用

系统调用

操作系统为在用户态运行的进程与硬件设备(如CPU、磁盘、打印机等等)进行交互提供了一组接口。

Unix系统通过向内核发出系统调用(system call)实现用户态进程和硬件设备之间的大部分接口。

POSIX API和系统调用

让我们先强调下应用编程接口(API)与系统调用之间的不同。前者只是一个函数定义,说明了如何获得一个给定的服务;而后者是通过软中断向内核态发出一个明确地请求。

Unix系统给程序员提供了很多API的库函数。libc的标准C库所定义的一些API引用了封装例程(wrapper routine)。通常情况下,每个系统调用对应一个封装例程,而封装例程定义了应用程序使用的API。

系统调用处理程序及服务例程

当用户态的进程调用一个系统调用时,CPU切换到内核态并开始执行一个内核函数。

因为内核实现了很多不同的系统调用,因此进程必须传递一个名为系统调用号(system claa number)的参数来识别所需的系统调用,eax寄存器就用作此目的。当调用一个系统调用时通常还要传递另外的参数。

所有系统调用都返回一个整数值。这些返回值与封装例程返回值的约定是不同的。

在内核中,整数和0表示系统调用成功结束,而负数表示一个出错条件。

在后一种情况下,这个值就是存放在errno变量中必须返回给应用程序的负出错码。内核没有设置和使用errno变量,而封装例程从系统调用返回之后设置这个变量。

系统调用处理程序与其他异常处理程序的结构类似,执行下列操作:

在内核态栈保存大多数寄存器的内容

调用名为系统调用服务例程的相应的C函数来处理系统调用。

退出系统调用处理程序:用保存在内核栈中的值加载寄存器,CPU从内核态切换到用户态。

xyz()系统调用对应的服务例程的名字通常是sys_xyz()。不过也有例外。

为了把系统调用号与相应的服务例程关联起来,内核利用了一个系统调用分派表(dispatch table)。这个表存放在sys_call_table数组中,由NR_syscalls个表项(在Linux2.6.11内核中是289):第n个表项包含系统调用号为n的服务例程的地址。

NR_syscalls宏只是对可实现的系统调用最大个数的静态限制,并不表示实际已实现的调用个数。

进入和退出系统调用

本地应用可以通过两种不同的方式调用系统调用:

1.执行int $0x80汇编语言指令。在Linux老版本中,这是从用户态切换到内核态的唯一方式。

2.执行sysenter汇编语言指令。

同样,内核可以通过两种方式从系统调用退出,从而使CPU切换回到用户态:

1.执行iret汇编语言指令。

2.执行sysexit汇编语言指令。

通过int $0x80指令发出系统调用

调用系统调用的传统方法是使用汇编语言指令int。

向量128(十六进制0x80)对应于内核入口点。在内核初始化阶段调用的函数trap_init(),用下面的方式建立对于向量128的中断描述符表表项:

set_system_gate(SYSCALL_VECTOR,&system_call);

该调用把下列值存入这个门描述符的相应字段:

Segment Selector:内核代码段__KERNEL_CS的段选择符

Offset:指向system_call()系统调用处理程序的指针

Type:置为15.表示这个异常是一个陷阱,相应的处理程序不禁止可屏蔽中断

DPL:置为3.这就允许用户态进程调用这个异常处理程序

因此当用户态进程发出int $0x80指令时,CPU切换到内核态并开始从地址system_call处开始执行命令。

system_call()函数首先把系统调用好和这个异常处理程序可以用到的所有CPU寄存器保存到相应的栈中。

ENTRY(system_call)

pushl %eax            # save orig_eax

SAVE_ALL

GET_THREAD_INFO(%ebp)

# system call tracing in operation

testb $(_TIF_SYSCALL_TRACE|_TIF_SYSCALL_AUDIT),TI_flags(%ebp)

jnz syscall_trace_entry

cmpl $(nr_syscalls), %eax

jae syscall_badsys

syscall_call:

call *sys_call_table(,%eax,4)    //调用eax中所包含的系统调用号对应的特定服务例程

movl %eax,EAX(%esp)        # store the return value

syscall_exit:

cli                # make sure we don‘t miss an interrupt

# setting need_resched or sigpending

# between sampling and the iret

movl TI_flags(%ebp), %ecx

testw $_TIF_ALLWORK_MASK, %cx    # current->work

jne syscall_exit_work

从系统调用退出

当系统调用服务例程结束时,system_call()函数从eax获得它的返回值,并把这个返回值存放在曾保存用户态eax寄存器值的那个栈单元的位置上。

通过sysenter指令发出系统调用

汇编语言指令int由于要执行几个一致性和安全性检查,所以速度较慢。

sysenter指令

汇编语言指令sysenter使用三种特殊寄存器,它们必须装入下列信息:

SYSENTER_CS_MSR:内核代码段选择符

SYSENTER_EIP_MSR:内核入口点的线性地址

SYSENTER_ESP_MSR:内核堆栈指针

vsyscall页

只要CPU和Linux内核都支持sysenter指令,标准库libc中的封装函数就可以使用它。

进入系统调用

当用sysenter指令发出系统调用时,依次执行下述步骤:

1.标准库中封装例程把系统调用号装入eax寄存器,并调用__kernel_vsyscall()函数。

2.函数__kernel_vsyscall()把ebp、edx和ecx的内容保存到用户态堆栈中,把用户战之中拷贝到ebp中,然后执行sysenter指令。

3.CPU从用户态切换到内核态,内核开始执行sysenter_entry()函数。

4.sysenter_entry()汇编语言函数执行

退出系统调用

当系统调用服务例程结束时,sysenter_entry()函数本质上执行与system_call()函数相同的操作。

首先,它从eax获得系统调用服务例程的返回值,并将返回码存入内核栈中保存用户态eax寄存器值的位置。

然后,函数禁止本地中断,并检查current的thread_info结构中的标志。

如果有任何标志被设置,那么在返回到用户态之前还需要完成一些工作。

sysexit指令

sysexit是与sysenter配对的汇编语言指令:它允许从内核态快速切换到用户态。

SYSENTER_RETURN的代码

SYSENTER_RETURN标记处的代码存放在vsyscall页中,当通过sysenter进入的系统调用被iret或sysexit指令终止时,该页框中的代码被执行。

参数传递

和普通函数类似,系统调用通常也需要输入/输出参数,这些参数可能是实际的值,也可能是用户态进程地址空间的变量,甚至是指向用户态函数的指针的数据结构。

在发出系统调用之前,系统调用的参数被写入CPU寄存器,然后在系统调用服务例程之前,内核再把存放在CPU中的参数拷贝到内核态堆栈中,这是因为系统调用服务例程是普通的C函数。

验证参数

在内核打算满足用户的请求之前,必须仔细地检查所有的系统调用参数。检查的类型即依赖于系统调用,也依赖于特定的参数。

有一种检查对所有的系统调用都是通用的。只要一个参数指定的是地址,那么内核必须检查它是否在这个进程的地址空间之内。由两种可能的方式来执行这种检查:

验证这个线性地址是否属于进程的地址空间,如果是,这个线性地址所在的线性区就具有正确的访问权限。(早期使用)

仅仅验证这个线性地址是否小于PAGE_OFFSET。(从Linux2.2内核开始)

这种粗略的检查是至关重要的,它确保了进程地址空间和内核地址空间都不被非法访问。

对系统调用所传递地址的检查是通过access_ok()宏实现,它由两个分别为addr和size的参数。

#define access_ok(type,addr,size) (likely(__range_ok(addr,size) == 0))

访问进程地址空间

系统调用服务例程需要非常频繁地读写进程地址空间的数据。Linux包含一组宏使这种访问更加容易。我们将描述其中两个名为get_user()和put_user()的宏。第一个宏用来从一个地址读取1、2或4个连续字节,而第二个宏用来把这几种大小的内容写入一个地址。

#define get_user(x,ptr)                            \

({    int __ret_gu;                            \

unsigned long __val_gu;                        \

__chk_user_ptr(ptr);                        \

switch(sizeof (*(ptr))) {                    \

case 1:  __get_user_x(1,__ret_gu,__val_gu,ptr); break;        \

case 2:  __get_user_x(2,__ret_gu,__val_gu,ptr); break;        \

case 4:  __get_user_x(4,__ret_gu,__val_gu,ptr); break;        \

default: __get_user_x(X,__ret_gu,__val_gu,ptr); break;        \

}                                \

(x) = (__typeof__(*(ptr)))__val_gu;                \

__ret_gu;                            \

})

#define put_user(x,ptr)                            \

__put_user_check((__typeof__(*(ptr)))(x),(ptr),sizeof(*(ptr)))

动态地址检查:修正代码

正如当前所看到的,access_ok()宏对系统调用以参数传递来的线性地址的有效性进行了粗略的检查。该检查值保证用户态进程不会试图侵扰内核地址空间。

但是,由参数传递的线性地址依然可能不属于进程地址空间。在这种情况下,当内核试图使用任何这种错误地址时,将会发生缺页异常。

在内核态引起缺页异常的四种情况:

1.内核师徒访问属于进程地址空间的页,但是,或者是相应的页框不存在,或者是内核试图去写一个只读页。在这种情况下,处理程序必须分配和初始化一个新的页框。

2.内核寻址到属于其地址空间的页,但是相应的页表项还没有被初始化。在这种情况下,内核鼻血在当前进程页表中适当建立一些表项。

3.某一内核函数包含编程错误,当这个函数运行时就引起异常;或者,可能由于瞬间的硬件错误引起异常。当这种情况发生时,处理程序必须执行一个内核漏洞。

4.本章所讨论的一种情况:系统调用服务例程试图读写一个内存区,而该内存区的地址是通过系统调用参数传递来的,但却不属于进程的地址空间。

异常表

决定缺页的来源在于内核使用有线的访问访问进程的地址空间。

把访问进程地址空间每条内核指令的地址放到一个叫异常表(excepyion table)的结构中并不用非多大功夫。

当在内核态发生缺页异常时,do_page_fault()处理程序检查异常表:如果表中包含产生异常的指令地址,那么这个错误就是有非法的系统调用参数引起的,否则,就是由某一更严重的bug引起的。

Linux定义了几个异常表。主要的异常表在建立内核程序映像时由C编译器自动生成。它存放在内核代码的__ex_table节,其起始和终止地址由C编译器产生的两个符号__start__ex_table和__stop__ex_table来标识。

每一个异常表的表项是一个exception_table_entry结构,它有两个字段:

insn:访问进程地址空间的指令的线性地址。

fixup:当存放在insn单元中的指令所触发的缺页异常发生时,fixup就是要调用的汇编语言代码的地址。

生成异常表和修正代码

GNU汇编程序(Assembler)伪指令 .section允许程序员指定可执行文件的那部分包含紧接着要执行的代码。

内核封装例程

尽管系统调用主要有用户态进程使用,但也可以被内核线程调用,内核线程不能使用库函数。为了简化相应封装例程的声明,Linux定义了7个从_syscall0到_syscall6的一组宏。

每个宏名字中的数字0~6对应着系统调用所用的参数个数(系统调用号除外)。

时间: 2024-10-10 09:58:19

深入理解Linux内核day09--系统调用的相关文章

深入理解Linux内核-系统调用

系统调用:用户态进程向内核发出的,实现用户态进程调用硬件设备的函数或者中断:优点:使编程更容易,将用户从学习硬件设备的低级编程特性中解放:提高系统到安全性,内核在满足请求之前可以做正确性检查:提高可移植性: 系统调用与API调用的区别:1.API调用是一个函数定义:系统调用是通过软中断向内核发出的明确请求2.内核不可以访问库函数 内核中,返回值为正数或者0表示系统调用成功结束,否则表示出错条件 进入内核态的两种方式:1.执行 int $0x80 指令 :2.执行 sysenter 指令内核推出系

【深入理解Linux内核】《第一章 绪论》笔记

1.商用Unix操作系统包括: - AT&T公司开发的(System V Release 4) SVR4. - 加州伯克利分校发布的4.4BSD - Dec公司(现属于HP)的Digital Unix - IBM公司的AIX - HP公司的HP-UX - Sun公司的Solaris   - Apple公司的Mac OS X 所有商业版本都是SVR4或4.4BSD的变体,并且都趋向于遵循某些通用标准:如IEEE的POSIX(Portable Operating Systems based on U

20150514我读《深入理解linux内核》之虚拟文件系统笔记

20150514我读<深入理解linux内核>之虚拟文件系统笔记 2015-05-14 Lover雪儿 虚拟文件系统所隐含的思想就是把很多不同种类的文件系统的共同信息放入内核,其中有一个字段或者函数来支持Linux所支持的所有实际文件系统所提供的任何操作.对所调用的每个读.写或者其他函数,内核都能把他们替换成支持本地Linux文件系统.NTFS文件系统,或者文件所在的任何其他文件系统的实际函数. 虚拟文件系统可以称为虚拟文件系统转换,是一个内核软件层,用来处理与Unix标准文件系统相关的所有系

【笔记】深入理解Linux内核--内存寻址(一)

<深入理解Linux内核>中关于内存管理一共有三章,这是其中的一章,还有第八章,讨论内核怎样给自己分配主存,以及第九章,考虑怎样给进程分配线性地址. 内存地址 -- (P40) 以下三种地址是相对与8086处理器来说的. 逻辑地址(logical address) 包含在机器语言指令中用来指定一个操作数或一条指令的地址.比如下面反汇编代码中最左边的地址即逻辑地址. 1 40052d: 55 push %rbp 2 40052e: 48 89 e5 mov %rsp,%rbp 3 400531:

向linux内核添加系统调用新老内核比较

2.6内核 1>修改linux-source-2.6.31/kernel/sys.c文件,在文件末尾添加系统响应函数.函数实现如下: asmlinkage int sys_mycall(int number) { printk("这是我添加的第一个系统调用"); return number; } 2>在linux-source-2.6.31/arch/x86/kernel/syscall_table_32.S 中添加:.long sys_mycall 如: .long sy

【深入理解Linux内核】《第二章 内存寻址》笔记 (2014-06-28 12:38)

2.1 内存地址 逻辑地址:段+偏移 线性地址(虚拟地址) 物理地址 2.2硬件中的分段 2.2.1 段选择符和段寄存器 15                                                3  2  1   0 ------------------------------------------------|                                                    |TI |RPL  ||         索引号 

【读书笔记::深入理解linux内核】内存寻址

我对linux高端内存的错误理解都是从这篇文章得来的,这篇文章里讲的 物理地址 = 逻辑地址 – 0xC0000000:这是内核地址空间的地址转换关系. 这句话瞬间让我惊呆了,根据我的CPU的知识,开启分页之后,任何寻址都要经过mmu的转换,也就是一个二级查表的过程(386) 难道内核很特殊,当mmu看到某个逻辑地址是内核传来的之后,就不查表了,直接减去0xC0000000,然后就传递给内存控制器了??? 我发现网上也有人和我问了同样的问题,看这个问题 这句话太让人费解了,让人费解到以至于要怀疑

深入理解Linux内核 第一章 绪论

Unix 文件系统概述 Unix的每个进程都有一个当前工作目录. 为标识一个特定的文件,进程使用路径名.如果路径名第一个字符是斜杠,那么这个路径是绝对路径,其起点是根目录:如果第一项是目录名或者文件名,那么这个路径就是相对路径,其起点是进程的当前目录. 硬链接的限制 1)不允许用户给目录创建硬链接,因为这可能把目录的树形结构变成环形结构. 2)只有在同一文件系统中的文件之间才能创建硬链接.此限制较大,因为现代Unix系统可能包含多种文件系统,这些文件系统位于不同的磁盘和/或分区,用户也无法知道他

[linux内核]ARM-Linux系统调用

1,系统调用的概念: 是用户空间访问内核的唯一手段,系统调用依靠软件中断实现,每个系统调用被赋予一个系统调用号,用来指明要执行哪个系统调用. 2,系统调用的实现:系统调用是通过软中端(SWI)实现的,SWI指令SWI指令的格式为:SWI{条件} 24位的立即数SWI指令用于产生软件中断,以便用户程序能调用操作系统的系统例程.操作系统在SWI的异常处理程序中提供相应的系统服务,指令中24位的立即数指定用 户程序调用系统例程的类型,相关参数通过通用寄存器传递,当指令中24位的立即数被忽略时,用户程序

linux内核增加系统调用--Beginner‘s guide

Linux内核中设置了一组用于实现系统功能的子程序,称为系统调用.系统调用和普通库函数调用非常相似明知是系统调用由操作系统核心提供,运行于核心态,而普通的函数调用由函数库或用户自己提供,运行于用户态. 一般的,进程是不能访问内核的,它不能访问内核所占用内存空间也不能调用内核函数.这被称为保护模式.为了和用户空间上运行的进程进行交互,内核提供一组接口.通过该接口应用程序可以访问硬件设备和其他操作系统资源. 实际上提供这组接口主要是为了保证系统稳定可靠,避免应用程序肆意妄为. 系统调用在用户空间和硬