系统调用可以发生在 C 程序中吗?

Can a system call happen in a C program?

C 程序中可以进行系统调用吗? 考虑一下:

int main()
{
    int f = open("/tmp/test.txt", O_CREAT | O_RDWR, 0666);
    write(f, "hello world", 11);
    close(f);

    return 0;
}

在此示例代码中,openwriteclose是库函数。在我的搜索过程中,我得出结论,它们是函数而不是系统调用。这些函数(openwriteclose)中的每一个都进行系统调用。

问题

直接在系统调用中编程是一项乏味且非常艰巨的任务。调用printf、sprintf、vprintf、fopen、open等,都是用到里面的系统调用。 Glibc 库已经为您实现了这些系统调用的包装器。

如果你想直接使用系统调用,假设你正在使用Linux,包括sys/syscall.h,你可以直接调用syscall函数。请注意,这不太安全,仅当您的 libc 实现没有该系统调用的包装器时才应使用。

“在计算中,系统调用是一种编程方式,计算机程序通过这种方式向执行它的操作系统的内核请求服务。这可能包括与硬件相关的服务(例如,访问硬盘驱动器),新进程的创建和执行,以及与进程调度等集成内核服务的通信。系统调用提供了进程和操作系统之间的基本接口。” - Wikipedia

close系统调用是内核用来关闭文件描述符的系统调用。对于大多数文件系统,程序使用关闭系统调用终止对文件系统中文件的访问。

int close(int fd);

close() 关闭一个文件描述符,这样它就不再引用任何文件并且可以被重用。 如您所见,当您需要访问内核中的某些内容时使用系统调用 space 并且这仅可能是系统调用。

系统调用后台

根据 Wikipedia,系统调用是一种“计算机程序从其执行所在的操作系统内核请求服务的编程方式”。

另一种理解系统调用的方式是user space program making a request to the operating system kernel代表用户space程序执行一些任务。内核提供的全套系统调用类似于(在某些方面)内核向用户 space.

提供的 API

由于系统调用是内核的低级接口,因此正确提供它们的参数可能容易出错甚至是危险的。由于这些原因,C 库作者为内核的系统调用集的重要部分提供了更简单、更安全的包装函数。

这些包装函数采用简化的参数集,然后导出适当的值以传递给内核,以便可以执行系统调用。

例子

注意:此示例基于在Linux 上使用gcc 编译和运行ning 一个C 程序。系统调用、库函数和输出在其他 POSIX 或非 POSIX 操作系统上可能不同。

我将尝试通过一个简单的示例来展示如何查看何时进行系统调用。

#include <stdio.h>

int main() {
    write(1, "Hello world!\n", 13);
}

上面我们有一个非常简单的 C 程序,它将字符串 Hello world!\n 写入 stdout。如果我们用 strace 编译然后执行这个程序,我们会看到以下内容(注意输出在其他计算机上可能看起来不同):

$ strace ./hello > /dev/null
execve("./hello", ["./hello"], 0x7fff083a0630 /* 58 vars */) = 0
<a bunch of output we aren't interested in>
write(1, "Hello world!\n", 13)          = 13
exit_group(0)                           = ?
+++ exited with 0 +++

strace 是一个 Linux 程序,它拦截并显示程序发出的所有系统调用,以及提供给系统调用的参数及其 return 值。

我们可以在这里看到,正如预期的那样,write 系统调用是使用预期参数进行的。还没有什么奇怪的。

另一个 Linux 跟踪程序是 ltrace,它拦截程序进行的动态库调用,并显示它们的参数和 return 值。

如果我们 运行 与 ltrace 相同的程序,我们会看到:

$ ltrace ./hello > /dev/null
write(1, "Hello world!\n", 13)                                = 13
+++ exited (status 0) +++

这告诉我们执行了write库函数。这意味着 C 代码首先调用 write 库函数,然后依次调用 write 系统调用。

现在假设我们要显式地进行 write 系统调用而不调用 write 库函数。 (这在正常使用中是不可取的,但有助于说明。)

这是新代码:

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

int main() {
    syscall(SYS_write, 1, "Hello world!\n", 13);
}

这里我们直接调用syscall库函数,告诉它我们要执行write系统调用

重新编译后,这里是strace的输出:

$ strace ./hello > /dev/null 
execve("./hello", ["./hello"], 0x7ffe3790a660 /* 58 vars */) = 0
<a bunch of output we aren't interested in>
write(1, "Hello world!\n", 13)          = 13
exit_group(0)                           = ?
+++ exited with 0 +++

我们可以看到 write 系统调用如期进行。

如果我们 运行 ltrace 我们会看到以下内容:

$ ltrace ./hello > /dev/null 
syscall(1, 1, 0x560b30e4d704, 13)                             = 13
+++ exited (status 0) +++

所以write库函数不再被调用,但我们仍在进行库函数调用。现在我们正在调用 syscall 库函数而不是 write 库函数。

可能有一种方法可以直接从用户space C程序中进行系统调用而不调用任何库函数,如果有这种方法我相信它会很先进。

检测 C 程序何时进行系统调用

一般来说,几乎每个重要的 C 程序都至少进行一次系统调用。这是因为用户 space 没有直接访问内核内存或计算机硬件的权限。用户 space 程序可以通过系统调用间接访问内核内存和硬件。

要识别已编译的 C 程序(或 Linux 上的任何其他程序)是否进行系统调用,并识别它进行了哪些系统调用,只需使用 strace.

是否有编译器选项来防止为系统调用调用库包装函数?

您可以使用 -nostdlib 选项编译您的 C 程序(假设您使用的是 gcc)。这将阻止链接 C 标准库作为生成可执行文件的一部分。但是,那么您将需要编写自己的代码来进行系统调用。