Administrator
发布于 2024-01-23 / 7 阅读
0
0

系统调用

系统调用

什么是系统调用?

系统调用 是内核提供给应用程序使用的功能函数,由于应用程序一般运行在 用户态,处于用户态的进程有诸多限制(如不能进行 I/O 操作),所以有些功能必须由内核代劳完成。而内核就是通过向应用层提供 系统调用,来完成一些在用户态不能完成的工作

说白了,系统调用其实就是函数调用,只不过调用的是内核态的函数。但与普通的函数调用不同,系统调用不能使用 call 指令来调用,而是需要使用 软中断 来调用。

系统调用号

每个系统调用都对应着一个系统调用号,用户执行一个系统调用时,通过调用号找到 调用函数,调用号一旦确定了,就无法变更了,一些编译好的程序只认这个调用号,要是底层了变更了,程序无法通过调用号找到相匹配的函数。

那系统调用号在哪里呢?可以找到呢?

想想也知道,肯定是有个表的记录着的,在内核代码里搜下,果然真的有

linux-5.4.34\tools\perf\arch\x86\entry\syscalls\syscall_64.tbl


#
# 64-bit system call numbers and entry vectors
#
# The format is:
# <number> <abi> <name> <entry point>
#
# The __x64_sys_*() stubs are created on-the-fly for sys_*() system calls
#
# The abi is "common", "64" or "x32" for this file.
#
0	common	read			__x64_sys_read
1	common	write			__x64_sys_write
2	common	open			__x64_sys_open
...
547	x32	pwritev2		__x32_compat_sys_pwritev64v2

总共有547个系统调用, 从上面也可以看到常用的 系统函数

怎么是调用的呢?

我们先先写一段代码简单的代码 ,将字符 dd 写到 终端(fd 为1 是标准输出), 这里我们主要关注下write 这个调用


#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
    count = write(1,"dd\n",3);
}

################

root@ubuntu:/home# ./write
dd

.c 文件好像看不出啥,于是把可执行文件反编译成汇编文件 ,继续观察

从main 开始看,调用了 <write@plt> (这里就不讨论 汇编原理了… 展开来的话讲太多了)


objdump -S write > write.S
#----
000000000000064a <main>:
64a:   55                      push   %rbp
64b:   48 89 e5                mov    %rsp,%rbp
64e:   ba 03 00 00 00          mov    $0x3,%edx
653:   48 8d 35 9a 00 00 00    lea    0x9a(%rip),%rsi        # 6f4 <_IO_stdin_used+0x4>
65a:   bf 02 00 00 00          mov    $0x2,%edi
65f:   e8 bc fe ff ff          callq  520 <write@plt>
664:   b8 00 00 00 00          mov    $0x0,%eax
669:   5d                      pop    %rbp
66a:   c3                      retq
66b:   0f 1f 44 00 00          nopl   0x0(%rax,%rax,1)

看看 <write@plt> , 跳转到了 [write@GLIBC\_2.2.5](mailto:write@GLIBC_2.2.5) 里面,这怎么看呢??? 还能继续深入吗?


0000000000000520 <write@plt>:
 520:   ff 25 aa 0a 20 00       jmpq   *0x200aaa(%rip)        # 200fd0 <write@GLIBC_2.2.5>
 526:   68 00 00 00 00          pushq  $0x0
 52b:   e9 e0 ff ff ff          jmpq   510 <.plt>

不如直接用静态编译,这样内容会多一点?


gcc -o write write.c  -static
objdump -S write > write.S

果然,多了很多内容! 还一样从main 开始看


0000000000400b6d <main>:
  400b6d:       55                      push   %rbp
  400b6e:       48 89 e5                mov    %rsp,%rbp
  400b71:       ba 03 00 00 00          mov    $0x3,%edx
  400b76:       48 8d 35 c7 13 09 00    lea    0x913c7(%rip),%rsi        # 491f44 <_IO_stdin_used+0x4>
  400b7d:       bf 02 00 00 00          mov    $0x2,%edi
  400b82:       e8 f9 7e 04 00          callq  448a80 <__libc_write>
  400b87:       b8 00 00 00 00          mov    $0x0,%eax
  400b8c:       5d                      pop    %rbp
  400b8d:       c3                      retq
  400b8e:       66 90                   xchg   %ax,%ax

发现是跳转到 \_\_libc\_write

发现 \_\_libc\_write 实现里面有个syscall , 查了下 syscall 上面一行是系统调用号,从libc\_write 实现看,将0x1 赋值给寄存器 eax,调用号 1 ,在 syscall\_64.tbl 中就是对应的 write 的实现!

common write __x64_sys_write


0000000000448a80 <__libc_write>:
  448a80:       8b 05 86 3d 27 00       mov    0x273d86(%rip),%eax        # 6bc80c <__libc_multiple_threads>
  448a86:       85 c0                   test   %eax,%eax
  448a88:       75 16                   jne    448aa0 <__libc_write+0x20>
  448a8a:       b8 01 00 00 00          mov    $0x1,%eax
  448a8f:       0f 05                   syscall
  448a91:       48 3d 00 f0 ff ff       cmp    $0xfffffffffffff000,%rax
  448ac0:       b8 01 00 00 00          mov    $0x1,%eax
  448ac5:       0f 05                   syscall
....

  448b19:       0f 1f 80 00 00 00 00    nopl   0x0(%rax)

到目前为止, 我们知道了 执行了 系统调用 ,先从glibc 接口进入,然后汇编函数 syscall 会通过调用号找到对应的系统调用函数

write -> glibc.so -> (syscall number ) -> syscall (系统调用进入内核) -> \_\_x64\_sys\_write

可是我看 lkd ,这书上说 系统调用会发起个软中断,陷入内核,软中断指令是 int &0x80,但是 反汇编都没看到 0x80的字眼,后来查询了下

系统调用(syscall) 是操作系统提供给程序以请求内核服务的一种机制。和 int 0x80 提供相同的服务。

INT 80h:指令 INT 80h 触发软件中断。执行时,CPU 需要在处理中断之前保存程序的当前状态,包括各种寄存器。此过程是一个上下文切换,它涉及大量开销,因为 CPU 实质上是在暂停一个任务以启动另一个任务。

syscall: 该 syscall 指令旨在最大限度地减少需要保存和恢复的状态量,从而减少上下文切换开销。这是从用户模式到内核模式的更直接的过渡,需要更少的 CPU 时间和资源使用

两者功能相同,最后调度程序从 eax (64 位是 %rax) 寄存器中 读取系统调用号。根据该调用号,从 内核系统调用表 中查找到 相应的内核函数

如今我们已经陷入内核了,现在从内核的视角看下,调用流程是怎样的?

从tbl 文件中可以知道调用号 1 对应的是 \_\_x64\_sys\_write, 索性在调式内核的时候 打这个断点函数,然后执行 刚才编译的二进制文件


(gdb) b __x64_sys_write
Breakpoint 1 at 0xffffffff811b7bb6: file fs/read_write.c, line 623.

于是 是有个函数,但是看了对应的代码,SYSCALL\_DEFINE 定义的,


SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
		size_t, count)
{
	return ksys_write(fd, buf, count);
}

那 \_\_x64\_sys\_write 和 SYCALL\_DEFINE 又有什么连续呢?

目前还不知道,那我们继续调试看看堆栈是怎样调用的?


(gdb) bt
#0  __x64_sys_write (regs=0xffffc900001b7f58) at fs/read_write.c:620
#1  0xffffffff81002389 in do_syscall_64 (nr=<optimized out>, regs=0xffffc900001b7f58)
    at arch/x86/entry/common.c:290
#2  0xffffffff81c0007c in entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:175

通过调用栈可以看到 是从 entry\_SYSCALL\_64 开始的,先这个函数的实现,这也是系统调用的入口了,看了这个函数的注释,写的很详细,这里说明了 rax system call number


/*
 * 64-bit SYSCALL instruction entry. Up to 6 arguments in registers.
 *....
 *
 * SYSCALL instructions can be found inlined in libc implementations as
 * well as some other programs and libraries.  There are also a handful
 * of SYSCALL instructions in the vDSO used, for example, as a
 * clock_gettimeofday fallback.
 .....
 *
 * Registers on entry:
 * rax  system call number
 * rcx  return address
 * r11  saved rflags (note: r11 is callee-clobbered register in C ABI)
 * rdi  arg0
 * rsi  arg1
 * rdx  arg2
 * r10  arg3 (needs to be moved to rcx to conform to C ABI)
 * r8   arg4
 * r9   arg5
 * Only called from user space.
 */


ENTRY(entry_SYSCALL_64)
	UNWIND_HINT_EMPTY
	/*
	 * Interrupts are off on entry.
	 * We do not frame this tiny irq-off block with TRACE_IRQS_OFF/ON,
	 * it is too small to ever cause noticeable irq latency.
	 */

	swapgs
...
	TRACE_IRQS_OFF

	/* IRQs are off. */
	movq	%rax, %rdi
	movq	%rsp, %rsi
	call	do_syscall_64		/* returns with IRQs disabled */

	TRACE_IRQS_IRETQ		/* we're about to change IF */
...

从代码上看,中间有调用 do\_syscall\_64,继续看 do\_syscall\_64 如


ENTRY(entry_SYSCALL_64)
	UNWIND_HINT_EMPTY
	/*
	 * Interrupts are off on entry.
	 * We do not frame this tiny irq-off block with TRACE_IRQS_OFF/ON,
	 * it is too small to ever cause noticeable irq latency.
	 */

	swapgs
...
	TRACE_IRQS_OFF

	/* IRQs are off. */
	movq	%rax, %rdi
	movq	%rsp, %rsi
	call	do_syscall_64		/* returns with IRQs disabled */

	TRACE_IRQS_IRETQ		/* we're about to change IF */
...

从代码上看,中间有调用 do\_syscall\_64,继续看 do\_syscall\_64 实现


#ifdef CONFIG_X86_64
__visible void do_syscall_64(unsigned long nr, struct pt_regs *regs)
{
	struct thread_info *ti;

	enter_from_user_mode();
	local_irq_enable();
	ti = current_thread_info();
	if (READ_ONCE(ti->flags) & _TIF_WORK_SYSCALL_ENTRY)
		nr = syscall_trace_enter(regs);

	if (likely(nr < NR_syscalls)) {
		nr = array_index_nospec(nr, NR_syscalls);  // 根据调用号找到对应系统调用函数
		regs->ax = sys_call_table[nr](regs);
#ifdef CONFIG_X86_X32_ABI
	} else if (likely((nr & __X32_SYSCALL_BIT) &&
			  (nr & ~__X32_SYSCALL_BIT) < X32_NR_syscalls)) {
		nr = array_index_nospec(nr & ~__X32_SYSCALL_BIT,
					X32_NR_syscalls);
		regs->ax = x32_sys_call_table[nr](regs);
#endif
	}

	syscall_return_slowpath(regs);
}

看了下大致,大致意思是 nr 是系统调用号,sys\_call\_table应该个指针函数表,为了验证下在 do\_syscall\_64 打个断点,然后执行write 函数


Breakpoint 3, do_syscall_64 (nr=1, regs=0xffffc900001b7f58) at arch/x86/entry/common.c:279
279	{
(gdb) n
283		local_irq_enable();
(gdb) n
284		ti = current_thread_info();
(gdb) n
285		if (READ_ONCE(ti->flags) & _TIF_WORK_SYSCALL_ENTRY)
(gdb)
288		if (likely(nr < NR_syscalls)) {
(gdb)
289			nr = array_index_nospec(nr, NR_syscalls);
(gdb) p nr
$4 = 1
(gdb)	 n
290			regs->ax = sys_call_table[nr](regs);

s进入 sys\_call\_table函数,最后是进入了 SYSCALL\_DEFINE3

(TODO:GENERATE\_THUNK 是做什么用的? )


(gdb) n
regs->ax = sys_call_table[nr](regs);
(gdb) s
__x86_indirect_thunk_rax () at arch/x86/lib/retpoline.S:32
32	GENERATE_THUNK(_ASM_AX)
(gdb) n
__x64_sys_write (regs=0xffffc900001b7f58) at fs/read_write.c:620
620	SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
(gdb) s
__do_sys_write (count=<optimized out>, buf=<optimized out>, fd=<optimized out>) at fs/read_write.c:623
623		return ksys_write(fd, buf, count);

从 SYSCALL\_DEFINE3 -> ksys\_write ,ksys\_write 也是真正系统调用函数,这里不在深挖下去了

那SYSCALL\_DEFINE3 是什么?源码一搜,都在 syscalls.h 里


#define SYSCALL_DEFINE1(name, ...) SYSCALL_DEFINEx(1, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE2(name, ...) SYSCALL_DEFINEx(2, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE5(name, ...) SYSCALL_DEFINEx(5, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE6(name, ...) SYSCALL_DEFINEx(6, _##name, __VA_ARGS__)

#define SYSCALL_DEFINE_MAXARGS	6

#define SYSCALL_DEFINEx(x, sname, ...)				\
	SYSCALL_METADATA(sname, x, __VA_ARGS__)			\
	__SYSCALL_DEFINEx(x, sname, __VA_ARGS__)

#ifndef __SYSCALL_DEFINEx
#define __SYSCALL_DEFINEx(x, name, ...)					\
	__diag_push();							\
	__diag_ignore(GCC, 8, "-Wattribute-alias",			\
		      "Type aliasing is used to sanitize syscall arguments");\
	asmlinkage long sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))	\
		__attribute__((alias(__stringify(__se_sys##name))));	\
	ALLOW_ERROR_INJECTION(sys##name, ERRNO);			\
	static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__));\
	asmlinkage long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__));	\
	asmlinkage long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__))	\
	{								\
		long ret = __do_sys##name(__MAP(x,__SC_CAST,__VA_ARGS__));\
		__MAP(x,__SC_TEST,__VA_ARGS__);				\
		__PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__));	\
		return ret;						\
	}								\
	__diag_pop();							\
	static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))
#endif /* __SYSCALL_DEFINEx */

SYSCALL\_DEFINE3 -> SYSCALL\_DEFINEx -> SYSCALL\_DEFINEx -> \_\_do\_sys

宏展开有点复杂,留个TODO!

对于大多数系统函数来说 ,在内核代码中对应的是

FunName ->do\_sys\_FunName

相关引用

https://blog.csdn.net/weixin\_43356770/article/details/135387868


评论