如何访问用户的系统调用
我阅读了LKD1中的一些段落,但我无法理解以下内容:
从用户空间访问系统调用
通常,C库提供对系统调用的支持。 用户应用程序可以从标准头文件中获取函数原型,并与C库链接以使用系统调用(或者库函数,然后使用您的系统调用调用)。 但是,如果你只是写系统调用,那glibc已经支持它了!
幸运的是,Linux提供了一组用于封装系统调用访问的宏。 它设置寄存器内容并发出陷阱指令。 这些宏被命名为_syscalln()
,其中n
在0和6之间。 该数字对应于传入系统调用的参数数量,因为宏需要知道需要多少个参数,并因此推入寄存器。 例如,考虑系统调用open()
,定义为
long open(const char *filename, int flags, int mode)
没有显式库支持的情况下使用此系统调用的系统调用宏将会是
#define __NR_open 5
_syscall3(long, open, const char *, filename, int, flags, int, mode)
然后,应用程序可以简单地调用open()
。
对于每个宏,有2 + 2×n个参数。 第一个参数对应于系统调用的返回类型。 第二个是系统调用的名称。 接下来按照系统调用的顺序跟随每个参数的类型和名称。 __NR_open
定义在<asm/unistd.h>
; 它是系统呼叫号码。 _syscall3
宏通过内联汇编扩展为C函数; 该程序集将执行上一节中讨论的步骤,将系统调用编号和参数推送到正确的寄存器中,并发出软件中断以陷入内核。 将这个宏放在应用程序中是使用open()
系统调用所需的全部。
让我们编写宏来使用我们精彩的新foo()
系统调用,然后编写一些测试代码来展示我们的努力。
#define __NR_foo 283
__syscall0(long, foo)
int main ()
{
long stack_size;
stack_size = foo ();
printf ("The kernel stack size is %ldn", stack_size);
return 0;
}
应用程序可以简单地调用open()
是什么意思?
此外,对于最后一段代码, foo()
的声明在哪里? 我怎样才能让这段代码编译和运行? 我需要包含哪些头文件?
__________
1罗伯特爱的Linux内核开发。 在wordpress.com上的PDF文件(转到第81页); Google图书结果。
您首先应该了解Linux内核的作用,并且应用程序仅通过系统调用与内核进行交互。
实际上,应用程序在内核提供的“虚拟机”上运行:它在用户空间中运行,并且只能在最低的机器级别上执行用户CPU模式允许的一组机器指令(指令增加例如SYSENTER
或INT 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包装器。
起初我想提供一些系统调用的定义。 系统调用是同步显式请求来自用户空间应用程序的特定内核服务的过程。 同步意味着系统调用的行为是通过执行指令序列来预先确定的。 中断是异步系统服务请求的一个例子,因为它们完全独立于处理器上执行的代码到达内核。 与系统调用相反的例外是对内核服务的同步但隐含的请求。
系统调用由四个阶段组成:
通常,所有这些操作都可以作为一个大型库函数的一部分来实现,该函数会在实际系统调用之前和/或之后进行一些辅助操作。 在这种情况下,我们可以说系统调用被嵌入到这个函数中,但是这个函数通常不是系统调用。 在另一种情况下,我们可以有一个微小的功能,只有这四个步骤,而没有更多。 在这种情况下,我们可以说这个函数是一个系统调用。 实际上,您可以通过手动执行上述所有四个阶段来实现系统调用。 请注意,在这种情况下,您将被迫使用汇编程序,因为所有这些步骤完全取决于体系结构。
例如,Linux / i386环境具有下一个系统调用约定:
includeuapiasm-genericunistd.h
Linux源代码树中找到所有系统服务ID。 在现代版本的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 ebx
, ecx
和edx
包含输入参数。 这些应该可以从内核源码中的每个系统调用的签名中扣除。 另请参阅:x86-64上的UNIX和Linux系统调用以及汇编语言中的Linux系统调用表或汇编表的调用约定是什么 int 0x80
进行调用,但现在有更好的方法:什么更好“int 0x80”或“syscall”? 当然,程序集会很快乏味,你很快就会想要使用glibc / POSIX提供的C包装器,或者当你不能时使用SYSCALL
宏。