如何从用户空间访问系统调用?

我读了LKD 1中的一些段落,我不能理解下面的内容:

从用户空间访问系统调用

通常,C库提供对系统调用的支持。 用户应用程序可以从标准头文件中获取函数原型,并与C库链接以使用系统调用(或者库函数,然后使用您的系统调用调用)。 但是,如果你只是写系统调用,glibc已经支持它了,这是值得怀疑的!

值得庆幸的是,Linux提供了一组包含访问系统调用的macros。 它设置寄存器内容并发出陷阱指令。 这些macros被命名为_syscall n () ,其中n在0和6之间。 该数字对应于传入系统调用的参数数量,因为macros需要知道期望的参数数量,并因此推入寄存器。 例如,考虑系统调用open() ,定义为

 long open(const char *filename, int flags, int mode) 

系统调用macros使用这个系统调用没有明确的库支持将是

 #define __NR_open 5 _syscall3(long, open, const char *, filename, int, flags, int, mode) 

然后,应用程序可以简单地调用open()

对于每个macros,有2 + 2×n个参数。 第一个参数对应于系统调用的返回types。 第二个是系统调用的名称。 接下来是按照系统调用的顺序跟随每个参数的types和名称。 __NR_open定义在<asm/unistd.h> ; 这是系统呼叫号码。 _syscall3macros通过内联汇编扩展为C函数; 程序集将执行上一节中讨论的步骤,将系统调用号和参数推入正确的寄存器,并发出软件中断以捕获到内核中。 将这个macros放在应用程序中是使用open()系统调用所需要的。

让我们编写macros来使用我们精彩的新foo()系统调用,然后编写一些testing代码来展示我们的努力。

 #define __NR_foo 283 __syscall0(long, foo) int main () { long stack_size; stack_size = foo (); printf ("The kernel stack size is %ld\n", stack_size); return 0; } 

应用程序可以简单地调用open()是什么意思?

另外,对于最后一段代码, foo()的声明在哪里? 我怎样才能使这段代码编译和运行? 什么是我需要包含的头文件?

__________
1罗伯特·爱的Linux内核开发 。 在wordpress.com上的PDF文件 (转到第81页); Google图书结果 。

Solutions Collecting From Web of "如何从用户空间访问系统调用?"

你首先应该明白linux内核的作用,应用程序只能通过系统调用与内核进行交互。

实际上,一个应用程序在内核提供的“虚拟机”上运行:它在用户空间中运行,并且只能在最低的机器级执行用户CPU模式允许的一组机器指令例如SYSENTERINT 0x80 …)用于进行系统调用。 所以,从用户级应用角度来看,一个系统调用是一个原子伪指令。

Linux Assembly Howto解释了如何在组件(即机器指令)级别上完成系统调用。

GNU libc提供了与系统调用相对应的C函数。 所以例如open函数是在NR__open的系统调用之上的一个很小的粘合(即一个包装器)(它使系统调用然后更新errno )。 应用程序通常在libc中调用这些C函数,而不是执行系统调用。

你可以使用一些其他的libc 例如, MUSL libc是somhow“更简单”,它的代码可能更容易阅读。 它也将原始的系统调用包装到相应的C函数中。

如果你添加你自己的系统调用,你最好也实现一个类似的C函数(在你自己的库中)。 所以你应该也有一个你的库的头文件。

另请参阅介绍(2)和系统调用(2)和系统调用(2)手册页以及VDSO在系统调用中的角色。

请注意, 系统调用不是C函数。 他们不使用调用堆栈(甚至可以在没有任何堆栈的情况下调用它们)。 系统调用基本上是一个类似于<asm/unistd.h> NR__open的数字, SYSENTER机器指令具有关于哪些寄存器保存在系统调用的参数之前,哪些寄存器保存在系统调用的结果(包括故障结果,在包装系统调用的C库中设置errno )。 系统调用的约定不是ABI规范中C函数的调用约定(例如x86-64 psABI )。 所以你需要一个C包装。

起初我想提供一些系统调用的定义。 系统调用是从用户空间应用程序同步显式请求特定内核服务的过程。 同步意味着系统调用的行为是通过执行指令序列来预先确定的。 中断是异步系统服务请求的一个例子,因为它们完全独立于处理器上执行的代码到达内核。 与系统调用相比,异常是对内核服务的同步但隐含的请求。

系统调用由四个阶段组成:

  1. 将控制权交给内核中的特定点,将处理器从用户模式切换到内核模式,并将切换处理器返回到用户模式。
  2. 指定所请求的内核服务的ID。
  3. 传递所请求服务的参数。
  4. 捕获服务的结果。

通常,所有这些动作都可以作为一个大型库函数的一部分来实现,在实际的系统调用之前和/或之后进行一些辅助动作。 在这种情况下,我们可以说系统调用被嵌入到这个函数中,但是这个函数通常不是系统调用。 在另一种情况下,我们可以有一个微小的功能,只有这四个步骤,而不是更多。 在这种情况下,我们可以说这个函数是一个系统调用。 其实你可以通过手动执行上面提到的所有四个阶段来实现系统调用。 请注意,在这种情况下,您将被迫使用汇编程序,因为所有这些步骤完全取决于体系结构。

例如,Linux / i386环境有下一个系统调用约定:

  1. 从用户模式到内核模式的控制可以通过编号为0x80的软件中断(汇编指令INT 0x80)或SYSCALL指令(AMD)或SYSENTER指令(Intel)完成。
  2. 在进入内核模式时,请求的系统服务的Id由存储在EAX寄存器中的整数值指定。 内核服务ID必须以_ NR的形式定义。 您可以在路径include\uapi\asm-generic\unistd.h Linux源代码树中找到所有系统服务ID。
  3. 最多可以通过寄存器EBX(1),ECX(2),EDX(3),ESI(4),EDI(5),EBP(6)传递6个参数。 括号中的数字是参数的序号。
  4. 内核返回在EAX寄存器中执行的服务的状态。 这个值通常由glibc用来设置errno变量。

在现代版本的Linux中,没有任何_syscall宏(据我所知)。 相反,glibc库(这是Linux内核的主要接口库)提供了一个特殊的宏 – INTERNAL_SYSCALL ,它扩展为内嵌汇编指令填充的一小段代码。 这段代码是针对特定的硬件平台的,它实现了系统调用的所有阶段,因此,这个宏本身代表一个系统调用。 还有另一个宏INLINE_SYSCALL 。 最后一个宏提供了类似于glibc的错误处理,根据这个错误处理失败的系统调用-1将被返回并且错误号将被存储在errno变量中。 这两个宏都在glibc包的sysdep.h中定义。

您可以通过下面的方式调用系统调用:

 #include <sysdep.h> #define __NR_<name> <id> int my_syscall(void) { return INLINE_SYSCALL(<name>, <argc>, <argv>); } 

其中<name>必须由系统调用名称字符串<id>替换 – 由系统服务编号id <argc> – 由实际参数数量(从0到6)和<argv> – 由实际参数分隔用逗号(如果参数存在,用逗号开始)。

例如:

 #include <sysdep.h> #define __NR_exit 1 int _exit(int status) { return INLINE_SYSCALL(exit, 1, status); // takes 1 parameter "status" } 

或者另一个例子:

 #include <sysdep.h> #define __NR_fork 2 int _fork(void) { return INLINE_SYSCALL(fork, 0); // takes no parameters } 

最小的可运行装配示例

hello_world.asm :

 section .rodata hello_world db "hello world", 10 hello_world_len equ $ - hello_world section .text global _start _start: mov eax, 4 ; syscall number: write mov ebx, 1 ; stdout mov ecx, hello_world ; buffer mov edx, hello_world_len int 0x80 ; make the call mov eax, 1 ; syscall number: exit mov ebx, 0 ; exit status int 0x80 

编译并运行:

 nasm -w+all -f elf32 -o hello_world.o hello_world.asm ld -m elf_i386 -o hello_world hello_world.o ./hello_world 

从代码中,很容易推断出:

  • eax包含系统调用号码,例如4表示写入。 内核源代码完整的32位列表: https : //github.com/torvalds/linux/blob/v4.9/arch/x86/entry/syscalls/syscall_32.tbl#L13
  • ebxecxedx包含输入参数。 这些应该可以从内核源码中的每个系统调用的签名中扣除。 另请参见: x86-64上的UNIX和Linux系统调用以及汇编语言中的Linux系统调用表或汇编表 的调用约定是什么
  • 返回值主要包含错误代码,并转至eax(为简单起见,未在此代码中显示)
  • int 0x80进行调用,但现在有更好的方法: 什么是更好的“INT 0x80”或“系统调用”?

当然,程序集很快就会变得乏味,你很快就会想要使用glibc / POSIX提供的C封装器,或者当你不能的时候使用SYSCALL宏。