系统调用

Last updated on 8 months ago

什么是系统调用?

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

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

系统调用号

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#
# 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 这个调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#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> (这里就不讨论 汇编原理了… 展开来的话讲太多了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 里面,这怎么看呢??? 还能继续深入吗?

1
2
3
4
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>

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

1
2
3
gcc -o write write.c  -static
objdump -S write > write.S

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

1
2
3
4
5
6
7
8
9
10
11
12
13

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14

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, 索性在调式内核的时候 打这个断点函数,然后执行 刚才编译的二进制文件

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

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

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

那 __x64_sys_write 和 SYCALL_DEFINE 又有什么连续呢?

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

1
2
3
4
5
6
(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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

/*
* 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.
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

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 如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#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 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 是做什么用的? )

1
2
3
4
5
6
7
8
9
10
11
12
(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 里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#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