C 程序中可以进行系统调用吗?

tar*_*nom 6 c function libc system-calls

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

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

    return 0;
}
Run Code Online (Sandbox Code Playgroud)

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

问题

  • 我的结论首先正确吗?
  • C 程序中可以进行系统调用吗?
  • 如果系统调用可以在 C 程序中发生,那么它们什么时候发生?请举个例子。
  • 是否可以通过编译选项来控制使用库函数与直接进行系统调用?例如,我们是否可以使用一些选项编译上面的程序,以便直接进行write和系统调用,而如果我们使用不同的选项编译它,则它会调用库函数?read

Sha*_*hop 8

系统调用背景

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

理解系统调用的另一种方式是,用户空间程序向操作系统内核发出请求,以代表用户空间程序执行某些任务。内核提供的全套系统调用类似于(在某些方面)内核向用户空间提供的 API。

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

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

例子

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

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

#include <stdio.h>

int main() {
    write(1, "Hello world!\n", 13);
}
Run Code Online (Sandbox Code Playgroud)

上面我们有一个非常简单的 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 +++
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

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

这告诉我们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);
}
Run Code Online (Sandbox Code Playgroud)

这里我们直接调用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 +++
Run Code Online (Sandbox Code Playgroud)

我们可以看到write系统调用按照预期进行了。

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

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

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

可能有一种方法可以直接从用户空间C程序进行系统调用,而不需要调用任何库函数,如果有一种方法我相信那将是非常先进的。

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

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

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

是否有编译器选项可以防止调用库包装函数进行系统调用?

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