Linux内核分析-4/5/系统调用

时间:2021-10-08 15:47:50

《Linux内核分析》MOOC课程

Linux内核分析-4/系统调用

Linux内核分析-5/系统调用


文章详解

1/解释了系统调用所在的层次

2/系统调用接口的过程(无代码)

3/系统调用的不常用的两种调用方式

4/linux-0.11的调用路径

5/glibc-2.25和linux-3.10中的调用路径


系统调用所在层次


上层

系统调用
1/由于安全的问题,出现了系统调用,系统调用是内核提供的 唯一的 上层访问内核的 接口
2/把用户从底层的硬件编程中解放出来
3/使用户程序具有可移植性

内核

系统调用接口的使用


系统调用不同的是他仅仅在我们开来就是一个函数,而系统调用是通过软中断向内核发出一个明确的功能请求

那么还有一个问题就是所谓的软中断的入口参数是什么呢,实际上就和函数的参数列表一样,我们除了知道他的中断号还要知道功能代码还有就是要知道他的输入是什么(见linux内核分析一)这就是入口参数。那么系统调用 通过软中断或系统调用指令向内核发出一个明确的请求,内核将调用内核相关函数来实现(如sys_read() , sys_write() , sys_fork())。用户程序不能直接调用这些Sys_read,sys_write等函数。这些函数运行在内核态。

理论:
就如同调用了一个命令
int 80 intrupt_num argv1 argv2 ...
就像这样的一个指令会陷入中断,并且会获取参数.
根据不同的中断号 进行相应的中断处理函数

实例:
好比上层我调用了一个
open file1 //open 对应的中断号是60

那么 到底层就对应
int 80 60 file1
然后下面调用 60 对应的中断处理函数,sys_open

系统调用过程

通常API函数库(如glibc)中的函数会调用封装例程,封装例程负责发起系统调用(通过发软中断或系统调用指令),这些都运行在用户态。内核开始接收系统调用后,cpu从用户态切换到内核态(cpu处于什么状态,程序就叫处于什么状态,所以很多地方也说程序从用户态切换到内核态,实际是cpu运行级别的切换,通常cpu 运行在3级表示用户态,cpu 运行在0级表示内核态),内核调用相关的内核函数来处理再逐步返回给封装例程,cpu进行一次内核态到用户态的切换,API函数从封装例程拿到结果,再处理完后返回给用户。

但是PI函数不一定需要进行系统调用,如某些数学函数,没有必要进行系统调用,直接glibc里面就给处理了,整个过程运行在用户态。所以作为我们编写linux用户程序的时候,是不能直接调用内核里面的函数的,内核里面的函数位于进程虚拟地址空间里面的内核空间,用户空间函数及函数库都处于进程虚拟地址空间里面的用户空间,用户空间调用内核空间的函数只有一个通道,这个通道就是系统调用指令,所以通常要调用glibc等库的接口函数,glibc也是用户空间的,但glibc自己实现了调用特殊的宏汇编系统调用指令进行cpu运行状态的切换,把进程从用户空间切换到内核空间。

例子:

fopen 调用 open,调用 一条指令
这里的指令就是 通过发软中断或者系统调用指令实现的.
例如int 80 intrupt_num argv1 argv2 ...
例如

这一句过后,进入cpu从用户态切换到内核态
然后进入一个 system_call 函数
system_call 调用与中断号相关的中断处理函数,中断函数返回
system_call函数返回
然后直接进入用户态?
然后open返回
然后fopen返回

xyz()函数执行的过程中会执行SYSCALL汇编指令,此指令将会把cpu从用户态切换到内核态。

-----------------------------------------------------------------------------------
内核
SYACALL汇编指令中会包含将要调用的内核函数的系统调用号和参数,内核在上图系统调用处理程序中去查一个sys_call_talbe数组来找到这个系统调用号对应的服务例程(如sys_xyz())函数的地址,然后调用这个地址的函数执行。(这里glibc里面的系统调用号和内核里面的系统调用号必须完全相等,当然,这是约定好的)

系统调用是一个软中断,中断号是0x80,它是上层应用程序与Linux系统内核进行交互通信的唯一接口。
所以,系统调用过程是这样的:应用程序调用libc中的函数->libc中的函数引用系统调用宏->系统调用宏中使用int 0x80完成系统调用并返回。

汇编实现调用

//所以我们看到了,open 函数只是存储了一些参数到相应的寄存器,然后调用了 int 80,所以如果我们自己实现这个过程,也可以调用底层的中断处理函数,也可以实现一次系统调用.
//所以我们可以说open层其实是封装了中断 的系统调用API
//真正的系统调用的实现在内核中.
//不过我们用的时候,也可以用在系统调用API上 再一次封装的 API
//例如 fopen 封装了 open
void main(){
char *str = "hello world\n";
asm(
"movl $13,%%edx\n\t"
"movl %0,%%ecx\n\t"
"movl $0,%%ebx\n\t"
"movl $4,%%eax\n\t"
"int $0x80\n\t"
:
:"m"(str)
:"edx","ecx","ebx"
);
asm(
"movl $42,%ebx\n\t"
"movl $1,%eax\n\t"
"int $0x80\n\t"
);
}

syscall实现调用

#include <stdio.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <errno.h>

int main()
{
int rc;
rc = syscall(SYS_chmod, "/etc/passwd", 0444);

if (rc == -1)
fprintf(stderr, "chmod failed, errno = %d\n", errno);
else
printf("chmod succeess!\n");
return 0;
}

下面叙述的是linux-0.11中的调用路径

  • 新版本的内核走的也是这个流程,只不过比之前的看起来复杂.

应用程序

open("./test",0775);

系统调用

//这是linux0.11中的open,已经在内核中
int open(const char * filename, int flag, ...)
{
register int res;
va_list arg;

va_start(arg,flag);
__asm__(
"int $0x80"
:"=a" (res)
:"0" (__NR_open),"b" (filename),"c" (flag),"d" (va_arg(arg,int))
);
if (res>=0)
return res;
errno = -res;
return -1;
}

  • 注意:现在open的实现已经放在glibc中了,后续我们对glibc进行观察

main(init目录)  
sched_init
set_system_gate(0x80,&system_call);

然后int 80 的时候,中断处理函数在 system_call (kernel/system_call.s)

_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
cmpw $0x0f,CS(%esp) # was old code segment supervisor ?
jne 3f
cmpw $0x17,OLDSS(%esp) # was stack segment = 0x17 ?
jne 3f
movl signal(%eax),%ebx
movl blocked(%eax),%ecx
notl %ecx
andl %ebx,%ecx
bsfl %ecx,%ecx
je 3f
btrl %ecx,%ebx
movl %ebx,signal(%eax)
incl %ecx
pushl %ecx
call _do_signal
popl %eax
3: popl %eax
popl %ebx
popl %ecx
popl %edx
pop %fs
pop %es
pop %ds
iret

下面的说的是glibc-2.25和linux-3.10中的调用路径


glibc的系统调用路径

  • i386

1/mkdir

2/INLINE_INSTALL

3/INTERNAL_SYSCALL

4/INTERNAL_SYSCALL_MAIN_x

5/INTERNAL_SYSCALL_MAIN_INLINE

6/int $0x80

  • arm

1/mkdir

2/INLINE_SYSCALL

3/INTERNAL_SYSCALL

4/INTERNAL_SYSCALL_RAW

5/swi 0x0 @ syscall ” #name

//sysdeps/unix/sysv/linux/generic/mkdir.c
int
__mkdir (const char *path, mode_t mode)
{
return INLINE_SYSCALL (mkdirat, 3, AT_FDCWD, path, mode);
}
weak_alias (__mkdir, mkdir)
//weak_alias(name1,name2)
//为标号name1定义一个弱化名name2。
//仅当name2没有在任何地方定义时,连接器就会用name1解析name2相关的符号。
//在文件中定义的标号name1也会同样处理。
arm中的INLINE_SYSCALL

//sysdeps/unix/sysv/linux/arm/sysdep.h

#undef INLINE_SYSCALL
#define INLINE_SYSCALL(name, nr, args...) \
({ unsigned int _sys_result = INTERNAL_SYSCALL (name, , nr, args); \
if (__builtin_expect (INTERNAL_SYSCALL_ERROR_P (_sys_result, ), 0)) \
{ \
__set_errno (INTERNAL_SYSCALL_ERRNO (_sys_result, )); \
_sys_result = (unsigned int) -1; \
} \
(int) _sys_result; })

//sysdeps/unix/sysv/linux/arm/sysdep.h
#undef INTERNAL_SYSCALL
#define INTERNAL_SYSCALL(name, err, nr, args...) \
INTERNAL_SYSCALL_RAW(SYS_ify(name), err, nr, args)


//sysdeps/unix/sysv/linux/arm/sysdep.h
# undef INTERNAL_SYSCALL_RAW
# define INTERNAL_SYSCALL_RAW(name, err, nr, args...) \
({ \
register int _a1 asm ("r0"), _nr asm ("r7"); \
LOAD_ARGS_##nr (args) \
_nr = name; \
asm volatile (\
"swi 0x0 @ syscall " #name \
: "=r" (_a1) \
: "r" (_nr) ASM_ARGS_##nr \
: "memory"\
); \
_a1; \
})
i386中的INLINE_SYSCALL

//sysdeps/unix/sysv/linux/i386/sysdep.h
# define INLINE_SYSCALL(name, nr, args...) \
({ \
unsigned int resultvar = INTERNAL_SYSCALL (name, , nr, args); \
__glibc_unlikely (INTERNAL_SYSCALL_ERROR_P (resultvar, )) \
? __syscall_error (-INTERNAL_SYSCALL_ERRNO (resultvar, )) \
: (int) resultvar; })
//sysdeps/unix/sysv/linux/i386/sysdep.h
#define INTERNAL_SYSCALL(name, err, nr, args...) \
({ \
register unsigned int resultvar; \
INTERNAL_SYSCALL_MAIN_##nr (name, err, args); \
(int) resultvar; })
//sysdeps/unix/sysv/linux/i386/sysdep.h
#define INTERNAL_SYSCALL_MAIN_0(name, err, args...) \
INTERNAL_SYSCALL_MAIN_INLINE(name, err, 0, args)
#define INTERNAL_SYSCALL_MAIN_1(name, err, args...) \
INTERNAL_SYSCALL_MAIN_INLINE(name, err, 1, args)
#define INTERNAL_SYSCALL_MAIN_2(name, err, args...) \
INTERNAL_SYSCALL_MAIN_INLINE(name, err, 2, args)
#define INTERNAL_SYSCALL_MAIN_3(name, err, args...) \
INTERNAL_SYSCALL_MAIN_INLINE(name, err, 3, args)
#define INTERNAL_SYSCALL_MAIN_4(name, err, args...) \
INTERNAL_SYSCALL_MAIN_INLINE(name, err, 4, args)
#define INTERNAL_SYSCALL_MAIN_5(name, err, args...) \
INTERNAL_SYSCALL_MAIN_INLINE(name, err, 5, args)

//sysdeps/unix/sysv/linux/i386/sysdep.h
# define INTERNAL_SYSCALL_MAIN_INLINE(name, err, nr, args...) \
LOADREGS_##nr(args) \
asm volatile
(\ \
"int $0x80" \
: "=a" (resultvar) \
: "a" (__NR_##name) ASMARGS_##nr(args) \
: "memory", "cc"\
)

内核中调用路径

  • 从上面来看,已经进入中断,所以就只能进入中断处理函数来处理了.

中断处理函数

//

start_kernel//init/main.c
trap_init
#ifdef CONFIG_X86_32
set_system_trap_gate(SYSCALL_VECTOR, &system_call);
set_bit(SYSCALL_VECTOR, used_vectors);
#endif

//arch/x86/include/asm/irq_vectors.h
#ifdef CONFIG_X86_32
# define SYSCALL_VECTOR 0x80
#endif
$ grep system_call * -nr  | grep x86
arch/x86/kernel/traps.c:70:asmlinkage int system_call(void);
arch/x86/kernel/traps.c:768: set_system_trap_gate(SYSCALL_VECTOR, &system_call);
arch/x86/kernel/entry_64.S:608:ENTRY(system_call)
arch/x86/kernel/entry_64.S:620:GLOBAL(system_call_after_swapgs)
arch/x86/kernel/entry_64.S:635:system_call_fastpath:
arch/x86/kernel/entry_64.S:720: jmp system_call_fastpath
arch/x86/kernel/entry_64.S:829:END(system_call)
arch/x86/kernel/cpu/common.c:1122: wrmsrl(MSR_LSTAR, system_call);
arch/x86/kernel/entry_32.S:506:ENTRY(system_call)
arch/x86/kernel/entry_32.S:604:ENDPROC(system_call)
arch/x86/xen/xen-asm_64.S:131: jmp system_call_after_swapgs
arch/x86/include/asm/proto.h:8:void system_call(void);

//arch/x86/kernel/entry_32.S
/*
* syscall stub including irq exit should be protected against kprobes
*/

.pushsection .kprobes.text, "ax"
# system call handler stub
ENTRY(system_call)
RING0_INT_FRAME # can't unwind into user space anyway
ASM_CLAC
pushl_cfi %eax # save orig_eax
SAVE_ALL
GET_THREAD_INFO(%ebp)
# system call tracing in operation / emulation
testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%ebp)
jnz syscall_trace_entry
cmpl $(NR_syscalls), %eax
jae syscall_badsys
syscall_call:
call *sys_call_table(,%eax,4)
movl %eax,PT_EAX(%esp) # store the return value
syscall_exit:
LOCKDEP_SYS_EXIT
DISABLE_INTERRUPTS(CLBR_ANY) # make sure we don't miss an interrupt
# setting need_resched or sigpending
# between sampling and the iret
TRACE_IRQS_OFF
movl TI_flags(%ebp), %ecx
testl $_TIF_ALLWORK_MASK, %ecx # current->work
jne syscall_exit_work
restore_all:
TRACE_IRQS_IRET
restore_all_notrace:
movl PT_EFLAGS(%esp), %eax # mix EFLAGS, SS and CS
# Warning: PT_OLDSS(%esp) contains the wrong/random values if we
# are returning to the kernel.
# See comments in process.c:copy_thread() for details.
movb PT_OLDSS(%esp), %ah
movb PT_CS(%esp), %al
andl $(X86_EFLAGS_VM | (SEGMENT_TI_MASK << 8) | SEGMENT_RPL_MASK), %eax
cmpl $((SEGMENT_LDT << 8) | USER_RPL), %eax
CFI_REMEMBER_STATE
je ldt_ss # returning to user-space with LDT SS
restore_nocheck:
RESTORE_REGS 4 # skip orig_eax/error_code
irq_return:
INTERRUPT_RETURN


//arch/x86/kernel/syscall_32.c
const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
/*
* Smells like a compiler bug -- it doesn't work
* when the & below is removed.
*/

[0 ... __NR_syscall_max] = &sys_ni_syscall,
#include <asm/syscalls_32.h>
};

sys_open

//在内核中是没有open的,只有sys_open
//下面这一句在open.c中
SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)

根据
#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)
转换成


SYSCALL_DEFINEx(3, _open, const char __user *, filename, int, flags, umode_t, mode)

根据
#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__))
转换成

asmlinkage long sys_open(__SC_DECL3(const char __user *, filename, int, flags, umode_t, mode)); \
static inline long SYSC_open(__SC_DECL3(const char __user *, filename, int, flags, umode_t, mode)); \
asmlinkage long SyS_open(__SC_LONG3(const char __user *, filename, int, flags, umode_t, mode)) \
{ \
__SC_TEST3(const char __user *, filename, int, flags, umode_t, mode); \
return (long) SYSC_open(__SC_CAST3(const char __user *, filename, int, flags, umode_t, mode)); \
} \
SYSCALL_ALIAS(sys_open, SyS_open); \
static inline long SYSC_open(__SC_DECL3(const char __user *, filename, int, flags, umode_t, mode))

参考资料

Linux 下系统调用的三种方法