linux下的系统调用函数到内核函数的追踪
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了linux下的系统调用函数到内核函数的追踪相关的知识,希望对你有一定的参考价值。
使用的 glibc : glibc-2.17 使用的 linux kernel :linux-3.2.07
系统调用是内核向用户进程提供服务的唯一方法,应用程序调用操作系统提供的功能模块(函数)。
用户程序通过系统调用从用户态(user mode)切换到核心态(kernel mode ),从而可以访问相应的资源。这样做的好处是:
为用户空间提供了一种硬件的抽象接口,使编程更加容易。
有利于系统安全。
有利于每个进程度运行在虚拟系统中,接口统一有利于移植。
运行模式、地址空间、上下文:
运行模式(mode)
Linux 使用了其中的两个:特权级0和特权级3 ,即内核模式(kernel mode) 和用户模式(user mode )
地址空间(space )
a)每个进程的虚拟地址空间可以划分为两个部分:用户空间和内核空间
b)在用户态下只能访问用户空间;而在核心态下,既可以访问用户空间,又可以访问内核空间。
c)内核空间在每个进程的虚拟地址空间中都是固定的(虚拟地址为3G~4G的地址空间)。
上下文(context )
一个进程的上下文可以分为三个部分:用户级上下文、寄存器上下文以及系统级上下文。
a)用户级上下文:正文、数据、用户栈以及共享存储区;
b)寄存器上下文:通用寄存器、程序寄存器(IP )、处理机状态寄存器(EFLAGS)、栈指针(ESP);
c)系统级上下文:进程控制块task_struct 、内存管理信息(mm_struct 、vm_area_struct、pgd 、pmd、pte 等)、核心栈等。
系统调用、API和C 库
a)Linux 的应用编程接口(API)遵循POSIX标准
b)Linux 的系统调用作为c库的一部分提供。c库中实现了Linux 的主要API,包括标准c库函数和系统调用。
c)应用编程接口(API)其实是一组函数定义,这些函数说明了如何获得一个给定的服务;而系统调用是通过软中断向内核发出一个明确的请求,每个系统调用对应一个封装例程(wrapper routine,唯一目的就是发布系统调用)。一些API应用了封装例程。API还包含各种编程接口,如:C库函数、OpenGL 编程接口等
d)系统调用的实现是在内核完成的,而用户态的函数是在函数库中实现的
系统调用与操作系统命令
a)操作系统命令相对应用编程接口更高一层,每个操作系统命令都是一个可执行程序,比如ls 、hostname 等,
b)操作系统命令的实现调用了系统调用
c)通过 strace 命令可以查看操作系统命令所调用的系统调用,如:
strace ls
strace hostname
系统调用与内核函数
a)内核函数在形式上与普通函数一样,但它是在内核实现的,需要满足一些内核编程的要求
b)系统调用是用户进程进入内核的接口层,它本身并非内核函数,但它是由内核函数实现的
c)进入内核后,不同的系统调用会找到各自对应的内核函数,这些内核函数被称为系统调用的“服务例程 ”
系统调用处理程序及服务例程
a)当用户态的进程调用一个系统调用时,CPU切换到内核态并开始执行一个内核函数
b)系统调用处理程序执行下列操作:
@[email protected] 在内核栈保存大多数寄存器的内容
@[email protected] 调用名为系统调用服务例程(system call service routine)的相应的C函数来处理系统调用
@[email protected] 通过ret_from_sys_call( ) 函数从系统调用返回
系统调用流程
系统调用中参数传递
a)每个系统调用至少有一个参数,即通过 eax 寄存器传递来的系统调用号
b)用寄存器传递参数必须满足两个条件:
@[email protected] 每个参数的长度不能超过寄存器的长度
@[email protected] 参数的个数不能超过6 个(包括eax 中传递的系统调用号),否则,
用一个单独的寄存器指向进程地址空间中这些参数值所在的一个内存区
c)在少数情况下,系统调用不使用任何参数
d)服务例程的返回值必须写到eax 寄存器中
很多系统调用需要不止一个参数
普通C函数的参数传递是通过把参数值写入堆栈(用户态堆栈或内核态堆栈)来实现的。
但因为系统调用是一种特殊函数,它由用户态进入了内核态,所以既不能使用用户态的堆栈
也不能直接使用内核态堆栈
在int $0x80汇编指令之前,系统调用的参数被写入CPU的寄存器。然后,在进入内核态调用系统调用服务例程之前,内核再把存放在CPU寄存器中的参数拷贝到内核态堆栈中。因为毕竟服务例程是C函数,它还是要到堆栈中去寻找参数的
系统调用小结
程序执行系统调用大致可归结为以下几个步骤:
1、程序调用libc 库的封装函数。
2、调用软中断int 0x80 进入内核。
3、在内核中首先执行system_call 函数(首先将系统调用号(eax)和可以用到的所有CPU寄存器保存到相应的堆栈中(由SAVE_ALL完成),接着根据系统调用号在系统调用表中查找 到对应的系统调用服务例程。
4、执行该服务例程。
5、执行完毕后,转入ret_from_sys_call 例程,从系统调用返回
在深入讨论内核和用户空间库如何实现系统调用的技术细节之前,简要看一下内核以系统调用形式实际提供的各个函数是很有用处的。每个系统调用都通过一个符号常数标识,符号常数的定义是平台相关的,在内核源码中指定,XX表示平台相关,有些是 asm_arch,有的是 asm_generic 等
用于实现系统调用的处理程序函数,在形式上有如下几个共同的特性:
1,每个函数的名称前缀都是 sys_ ,将该函数唯一地标识为一个系统调用,更精确的说,标识为一个系统调用的处理程序函数。
2,所有的处理程序函数都最多接收 5 个参数。否则,用一个单独的寄存器指向进程 地址空间中这些参数值所在的一个内存区即可。
3,所有的系统调用都在内核态执行。
系统调用由内核分配的一个编号唯一标识(系统调用号)。
所有的系统调用都由一处中枢代码处理,根据调用编号和一个静态表,将调用分派到具体的函数。传递的参数也是由中枢代码处理,这样参数的传递独立于实际的系统调用。从用户态到内核态,以及调用分派和参数传递,都是由汇编语言代码实现的。
为容许用户态和内核态之间的切换,用户进程必须通过一条专用的机器指令,引起处理器/内核对该进程的关注,这需要 C 标准库的协助。内核也必须提供一个例程,来满足切换请求并执行相关操作。该例程不能在用户空间中实现,因为其中需要执行普通应用程序不允许执行的命令。
系统调用表 (armV7)
1 /* 2 * linux/arch/arm/kernel/calls.S 3 * 4 * Copyright (C) 1995-2005 Russell King 5 * 6 * This program is free software; you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 2 as 8 * published by the Free Software Foundation. 9 * 10 * This file is included thrice in entry-common.S 11 */ 12 /* 0 */ CALL(sys_restart_syscall) 13 CALL(sys_exit) 14 CALL(sys_fork_wrapper) 15 CALL(sys_read) 16 CALL(sys_write) 17 /* 5 */ CALL(sys_open) 18 CALL(sys_close) 19 CALL(sys_ni_syscall) /* was sys_waitpid */ 20 CALL(sys_creat) 21 CALL(sys_link) 22 /* 10 */ CALL(sys_unlink) 23 CALL(sys_execve_wrapper) 24 CALL(sys_chdir) 25 CALL(OBSOLETE(sys_time)) /* used by libc4 */
以系统调用 open() 函数为例:
1,X86 平台:
1,用户空间
1,函数 open() 的声明
@[email protected] 在使用 open()函数时,要 include
<glibc-2.17\include\fcntl.h>
1 #ifndef _FCNTL_H 2 #include 3 #ifndef _ISOMAC 4 /* Now define the internal interfaces. */ 5 extern int __open64 (const char *__file, int __oflag, ...); 6 libc_hidden_proto (__open64) 7 extern int __libc_open64 (const char *file, int oflag, ...); 8 extern int __libc_open (const char *file, int oflag, ...); 9 libc_hidden_proto (__libc_open) 10 extern int __libc_creat (const char *file, mode_t mode); 11 extern int __libc_fcntl (int fd, int cmd, ...); 12 ...
<glibc-2.17\io\fcntl.h>
1 ... 2 /* Open FILE and return a new file descriptor for it, or -1 on error. 3 OFLAG determines the type of access used. If O_CREAT is on OFLAG, 4 the third argument is taken as a `mode_t‘, the mode of the created file. 5 This function is a cancellation point and therefore not marked with 6 __THROW. */ 7 #ifndef __USE_FILE_OFFSET64 8 extern int open (const char *__file, int __oflag, ...) __nonnull ((1)); 9 #else 10 # ifdef __REDIRECT 11 extern int __REDIRECT (open, (const char *__file, int __oflag, ...), open64) 12 __nonnull ((1)); 13 # else 14 # define open open64 15 # endif 16 #endif 17 #ifdef __USE_LARGEFILE64 18 extern int open64 (const char *__file, int __oflag, ...) __nonnull ((1)); 19 #endif 20 ...
1 /* Define a macro which expands inline into the wrapper code for a system 2 call. */ 3 # undef INLINE_SYSCALL 4 # define INLINE_SYSCALL(name, nr, args...) 5 ({ 6 unsigned long int resultvar = INTERNAL_SYSCALL (name, , nr, args); 7 if (__builtin_expect (INTERNAL_SYSCALL_ERROR_P (resultvar, ), 0)) 8 { 9 __set_errno (INTERNAL_SYSCALL_ERRNO (resultvar, )); 10 resultvar = (unsigned long int) -1; 11 } 12 (long int) resultvar; })
2,内核空间
(1)系统启动时,对INT 0x80进行一定的初始化。
使用汇编子程序setup_idt(linux/arch/i386/kernel/head.S)初始化idt表(中断描述符表),这时所有的入口函数偏移地址都被设为ignore_int ,如下图所示。
2)用户程序需要系统提供服务的时候,会通过系统调用产生一个int 0x80的软中断,就会进入到系统调用的入口函数,入口函数存放在以下文件当中
<arch\x86\kernel\entry_32.s>
1 ENTRY(system_call) 2 RING0_INT_FRAME # cant unwind into user space anyway 3 pushl %eax # save orig_eax ,将系统调用号压入栈中 4 CFI_ADJUST_CFA_OFFSET 4 5 SAVE_ALL #将寄存器的值压入堆栈当中,压入堆栈的顺序对应着结构体struct pt_regs , 6 #当出栈的时候,就将这些值传递到结构体struct pt_regs里面的成员, 7 #从而实现汇编代码向C程序传递参数 8 9 GET_THREAD_INFO(%ebp) 10 # system call tracing in operation / emulation 11 #GET_THREAD_INFO宏获得当前进程的thread_info结构的地址,获取当前进程的信息。 12 #thread_inof结构中flag字段的_TIF_SYSCALL_TRACE或_TIF_SYSCALL_AUDIT 13 #被置1。如果发生被跟踪的情况则转向相应的处理命令处。 14 testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%ebp) 15 jnz syscall_trace_entry #比较结果不为零的时候跳转。 16 #对用户态进程传递过来的系统调用号的合法性进行检查 17 #如果不合法则跳到syscall_badsys标记的命令处。 18 cmpl $(nr_syscalls), %eax 19 jae syscall_badsys #比较结果大于或者等于最大的系统调用号的时候跳转,不合法 20 #合法则跳转到相应系统调用号所对应的服务例程当中, 21 #也就是在sys_call_table表中找到了相应的函数入口点。 22 #由于sys_call_table表的表项占4字节字节字节字节,因此获得服务例程指针的具体方法 23 #是将由eax保存的系统调用号乘以4再与sys_call_table表的基址相加。 24 syscall_call: 25 call *sys_call_table(,%eax,4) 26 movl %eax,PT_EAX(%esp) # store the return value 将保存的结果返回。
<arch\x86\include\asm\ptrace.h>
1 struct pt_regs { 2 unsigned long bx; 3 unsigned long cx; 4 unsigned long dx; 5 unsigned long si; 6 unsigned long di; 7 unsigned long bp; 8 unsigned long ax; 9 unsigned long ds; 10 unsigned long es; 11 unsigned long fs; 12 unsigned long gs; 13 unsigned long orig_ax; 14 unsigned long ip; 15 unsigned long cs; 16 unsigned long flags; 17 unsigned long sp; 18 unsigned long ss; 19 };
接下来,会进入到系统调用表查找到系统调用服务程序的入口函数的地址,再进行跳转,
整个过程如下图所示:
1,系统调用号:
<arch\x86\include\asm\unistd_32.h>
1 #ifndef _ASM_X86_UNISTD_32_H 2 #define _ASM_X86_UNISTD_32_H 3 /* 4 * This file contains the system call numbers. 5 */ 6 #define __NR_restart_syscall 0 7 #define __NR_exit 1 8 #define __NR_fork 2 9 #define __NR_read 3 10 #define __NR_write 4 11 #define __NR_open 5 12 #define __NR_close 6 13 #define __NR_waitpid 7
系统调用原型:
<include\linux\syscalls.h>
asmlinkage long sys_open(const char __user *filename, int flags, int mode);
其中这里使用了一个宏asmlinkage ,我们再看一下它在系统里的定义:
<arch\x86\include\asm\linkage.h>
#ifdef CONFIG_X86_32 #define asmlinkage CPP_ASMLINKAGE __attribute__((regparm(0)))
后面的 __attribute__((regparm(0)))表示的是不通过寄存器来传递参数,通过栈来传递
所以系统调用的入口函数里面参数的传递:
<arch\x86\kernel\entry_32.s>
ENTRY(system_call)
SAVE_ALL #将寄存器的值压入堆栈当中,压入堆栈的顺序对应着结构体struct pt_regs ,
#当出栈的时候,就将这些值传递到结构体struct pt_regs里面的成员,
#从而实现从汇编代码向C程序传递参数。
定义 SAVE_ALL 是将参数压到堆栈中,然后通过堆栈来进行参数的传递
3,获取系统调用入口函数:
<arch\x86\kernel\entry_32.s>
syscall_call: call *sys_call_table(,%eax,4)
sys_call_table 每一项占用4个字节。system_call函数可以读取 eax 寄存器,获取当前系统调用的系统调用号,将其乘以 4 生成偏移地址,然后以 sys_call_table 为基址,基址加上偏移地址所指向的内容,既是应该调用的服务程序的地址。
<arch\x86\kernel\syscall_table_32.s>
ENTRY(sys_call_table) .long sys_restart_syscall /* 0 - old "setup()" system call, used for restarting */ .long sys_exit .long ptregs_fork .long sys_read .long sys_write .long sys_open /* 5 */ .long sys_close .long sys_waitpid .long sys_creat .long sys_link .long sys_unlink /* 10 */
在本例中,sys_open 是系统调用服务程序的入口地址
4,调用系统调用函数:(在新的内核中,函数的实现并不是直接通过 sys_xxx 函数,而是通过一个宏的封装)
sys_open -> do_sys_open -> do_filp_open ->do_last-> nameidata_to_filp -> __dentry_open
1 SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, int, mode) 2 { 3 long ret; 4 if (force_o_largefile()) 5 flags |= O_LARGEFILE; 6 ret = do_sys_open(AT_FDCWD, filename, flags, mode); 7 /* avoid REGPARM breakage on x86: */ 8 asmlinkage_protect(3, ret, filename, flags, mode); 9 return ret; 10 }
其中宏 SYSCALL_DEFINE3 定义如下:
<include\linux\syscalls.h>
1 #ifdef CONFIG_FTRACE_SYSCALLS 2 #define SYSCALL_DEFINE0(sname) 3 SYSCALL_TRACE_ENTER_EVENT(_##sname); 4 SYSCALL_TRACE_EXIT_EVENT(_##sname); 5 static struct syscall_metadata __used 6 __syscall_meta__##sname = { 7 .name = "sys_"#sname, 8 .syscall_nr = -1, /* Filled in at boot */ 9 .nb_args = 0, 10 .enter_event = &event_enter__##sname, 11 .exit_event = &event_exit__##sname, 12 .enter_fields = LIST_HEAD_INIT(__syscall_meta__##sname.enter_fields), 13 }; 14 static struct syscall_metadata __used 15 __attribute__((section("__syscalls_metadata"))) 16 *__p_syscall_meta_##sname = &__syscall_meta__##sname; 17 asmlinkage long sys_##sname(void) 18 #else 19 #define SYSCALL_DEFINE0(name) asmlinkage long sys_##name(void) 20 #endif 21 #define SYSCALL_DEFINE1(name, ...) SYSCALL_DEFINEx(1, _##name, __VA_ARGS__) 22 #define SYSCALL_DEFINE2(name, ...) SYSCALL_DEFINEx(2, _##name, __VA_ARGS__) 23 #define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__) 24 #define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4, _##name, __VA_ARGS__) 25 #define SYSCALL_DEFINE5(name, ...) SYSCALL_DEFINEx(5, _##name, __VA_ARGS__) 26 #define SYSCALL_DEFINE6(name, ...) SYSCALL_DEFINEx(6, _##name, __VA_ARGS__)
在本例中,结合 sys_open 的定义:
<include\linux\syscalls.h>
asmlinkage long sys_open(const char __user *filename, int flags, int mode);
可以知道,SYSCALL_DEFINE3 中的数字 3 表示这个函数需要传递 3 个参数。
其中 “##”表示宏中字符直接,即:
SYSCALL_DEFINEx(3,_open,__VA_ARGS__)
其中 SYSCALL_DEFINEx 定义如下:
<include\linux\syscalls.h>
#define SYSCALL_DEFINEx(x, sname, ...) \ __SYSCALL_DEFINEx(x, sname, __VA_ARGS__)
其中 __SYSCALL_DEFINEx 定义如下:
<include\linux\syscalls.h>
#define __SYSCALL_DEFINEx(x, name, ...) \ asmlinkage long sys##name(__SC_DECL##x(__VA_ARGS__)); static inline long SYSC##name(__SC_DECL##x(__VA_ARGS__)); asmlinkage long SyS##name(__SC_LONG##x(__VA_ARGS__)) { __SC_TEST##x(__VA_ARGS__); return (long) SYSC##name(__SC_CAST##x(__VA_ARGS__)); } SYSCALL_ALIAS(sys##name, SyS##name); static inline long SYSC##name(__SC_DECL##x(__VA_ARGS__))
在本例中如下:
<include\linux\syscalls.h>
1 #define __SYSCALL_DEFINEx(3, _open, ...) 2 asmlinkage long sys_open(__SC_DECL3(__VA_ARGS__)); 3 static inline long SYSC_open(__SC_DECL3(__VA_ARGS__)); 4 asmlinkage long SyS_open(__SC_LONG3(__VA_ARGS__)) 5 { 6 __SC_TEST3(__VA_ARGS__); 7 return (long) SYSC_open(__SC_CAST3(__VA_ARGS__)); 8 } 9 SYSCALL_ALIAS(sys_open, SyS_open); 10 static inline long SYSC_open(__SC_DECL3(__VA_ARGS__))
当我们自己定义一个不需要传递参数的系统调用的时候,可以这样定义我们的函数:
1 SYSCALL_DEFINE0(mycall) 2 { 3 printk("This is my_sys_call\n"); 4 return 0; 5 }
5,对调用的函数进行解析
1 SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, int, mode) 2 { 3 long ret; 4 if (force_o_largefile()) 5 flags |= O_LARGEFILE; 6 ret = do_sys_open(AT_FDCWD, filename, flags, mode); 7 /* avoid REGPARM breakage on x86: */ 8 asmlinkage_protect(3, ret, filename, flags, mode); 9 return ret; 10 }
open的核心处理在函数 do_sys_open() 中
1 long do_sys_open(int dfd, const char __user *filename, int flags, int mode) 2 { 3 struct open_flags op; 4 int lookup = build_open_flags(flags, mode, &op); 5 /*获取文件名,getname()函数内部首先创建存取文件名的内存空间, 6 然后从用户空间把文件名拷贝到内存空间来*/ 7 char *tmp = getname(filename); 8 int fd = PTR_ERR(tmp); 9 if (!IS_ERR(tmp)) { 10 /*获取一个可用的 fd,该函数通过调用 alloc_fd() 函数从 fd_table 中获取一个可用的 fd, 11 *并做一些简单的初始化。 12 *需要注意的是:文件描述符 fd,只对本进程有效,也就是说这个 fd 只在该进程中可见, 13 在别的进程中可能不没有使用或是表示别的文件。 14 */ 15 fd = get_unused_fd_flags(flags); 16 if (fd >= 0) { 17 /*文件描述符 fd 获取成功,则打开文件,创建一个 file 对象 18 */ 19 struct file *f = do_filp_open(dfd, tmp, &op, lookup); 20 if (IS_ERR(f)) { 21 //打开失败,释放 fd 22 put_unused_fd(fd); 23 fd = PTR_ERR(f); 24 } else { 25 /*如果文件已经打开,根据 inode 所指定的信息进行打开函数,函数(参数为 f)将该文件加入到 26 *文件监控的系统中。该系统是用来监控文件被打开,创建,读写,关闭,修改等操作的。 27 */ 28 fsnotify_open(f); 29 /*将文件指针安装在 fd 数组中, 30 *将 struct file *f 加入到 fd 索引位置处 的数组中。在后续过程中,有对这个文件描述符操作的话, 31 *就会通过查找该数组得到对应的文件结构,然后进行相关的操作 32 */ 33 fd_install(fd, f); 34 } 35 } 36 //释放放置从用户空间拷贝过来的文件名的存储空间 37 putname(tmp); 38 } 39 return fd; 40 }
1 struct nameidata { 2 struct path path; //当前目录的数据结构 3 struct qstr last; //用以保存当前目录的名称 4 struct path root; 5 struct inode *inode; /* path.dentry.d_inode */ 6 unsigned int flags; 7 unsigned seq; 8 int last_type; 9 unsigned depth; //连接文件的深度 10 char *saved_names[MAX_NESTED_LINKS + 1]; 11 /* Intent data */ 12 union { 13 struct open_intent open; 14 } intent; 15 };
do_filp_open()解析
1 struct file *do_filp_open(int dfd, const char *pathname, 2 const struct open_flags *op, int flags) 3 { 4 struct nameidata nd; 5 struct file *filp; 6 /*根据目录打开文件 7 */ 8 filp = path_openat(dfd, pathname, &nd, op, flags | LOOKUP_RCU); 9 if (unlikely(filp == ERR_PTR(-ECHILD))) 10 filp = path_openat(dfd, pathname, &nd, op, flags); 11 if (unlikely(filp == ERR_PTR(-ESTALE))) 12 filp = path_openat(dfd, pathname, &nd, op, flags | LOOKUP_REVAL); 13 return filp; 14 }
path_openat()
1 static struct file *path_openat(int dfd, const char *pathname, 2 struct nameidata *nd, const struct open_flags *op, int flags) 3 { 4 struct file *base = NULL; 5 struct file *filp; 6 struct path path; 7 int error; 8 ... 9 current->total_link_count = 0; 10 11 //对路径进行解析 12 error = link_path_walk(pathname, nd); 13 if (unlikely(error)) 14 goto out_filp; 15 filp = do_last(nd, &path, op, pathname); 16 ... 17 }
link_path_walk()
1 /* 2 * Name resolution. 3 * This is the basic name resolution function, turning a pathname into 4 * the final dentry. We expect ‘base‘ to be positive and a directory. 5 * 6 * Returns 0 and nd will have valid dentry and mnt on success. 7 * Returns error and drops reference to input namei data on failure. 8 */ 9 static int link_path_walk(const char *name, struct nameidata *nd) 10 { 11 struct path next; 12 int err; 13 14 ... 15 //查找文件 16 err = walk_component(nd, &next, &this, type, LOOKUP_FOLLOW); 17 if (err < 0) 18 return err; 19
以上是关于linux下的系统调用函数到内核函数的追踪的主要内容,如果未能解决你的问题,请参考以下文章
Linux内核与内核函数与操作系统,系统调用,这几者的联系是什么?