73

我一直认为printf(),在最后一步中,像这样的函数是使用内联汇编定义的。在 stdio.h 的深处隐藏着一些实际上告诉 CPU 做什么的 asm 代码。例如,在 dos 中,我记得它是通过首先mov将字符串的开头插入某个内存位置或寄存器然后调用intterupt 来实现的。

但是,由于 x64 版本的 Visual Studio 根本不支持内联汇编程序,这让我想知道 C/C++ 中怎么可能没有汇编程序定义的函数。如何printf()在不使用汇编代码的情况下在 C/C++ 中实现库函数?什么实际上执行了正确的软件中断?谢谢。

4

6 回答 6

19

首先,您必须了解戒指的概念。
内核在环 0 中运行,这意味着它可以完全访问内存和操作码。
一个程序通常在环 3 中运行。它对内存的访问是有限的,并且不能使用所有的操作码。

因此,当软件需要更多权限(打开文件、写入文件、分配内存等)时,它需要询问内核。
这可以通过多种方式完成。软件中断、SYSENTER 等。

让我们以软件中断为例,使用 printf() 函数:
1 - 您的软件调用 printf()。
2 - printf() 处理您的字符串和 args,然后需要执行内核函数,因为在 ring 3 中无法写入文件。
3 - printf() 生成软件中断,将内核函数的编号(在这种情况下为 write() 函数)放入寄存器。
4 - 软件执行中断,指令指针移至内核代码。所以我们现在在环 0 中,在一个核函数中。
5 - 内核处理请求,写入文件(stdout 是文件描述符)。
6 - 完成后,内核使用 iret 指令返回到软件代码。
7 - 软件代码继续。

所以 C 标准库的功能可以用 C 来实现。它所要做的就是知道在需要更多权限时如何调用内核。

于 2010-03-14T17:26:32.373 回答
5

在 Linux 中,strace实用程序允许您查看程序进行了哪些系统调用。所以,采取这样的程序

    诠释主要(){
    printf("x");
    返回0;
    }

说,您将其编译为printx,然后strace printx给出

    execve("./printx", ["./printx"], [/* 49 vars */]) = 0
    brk(0) = 0xb66000
    access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (没有那个文件或目录)
    mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fa6dc0e5000
    access("/etc/ld.so.preload", R_OK) = -1 ENOENT (没有这样的文件或目录)
    打开(“/etc/ld.so.cache”,O_RDONLY|O_CLOEXEC)= 3
    fstat(3, {st_mode=S_IFREG|0644, st_size=119796, ...}) = 0
    mmap(NULL, 119796, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fa6dc0c7000
    关闭(3)= 0
    access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (没有那个文件或目录)
    打开(“/lib/x86_64-linux-gnu/libc.so.6”,O_RDONLY|O_CLOEXEC)= 3
    读(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\200\30 \2\0\0\0\0\0"..., 832) = 832
    fstat(3, {st_mode=S_IFREG|0755, st_size=1811128, ...}) = 0
    mmap(NULL, 3925208, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7fa6dbb06000
    mprotect(0x7fa6dbcbb000, 2093056, PROT_NONE) = 0
    mmap(0x7fa6dbeba000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1b4000) = 0x7fa6dbeba000
    mmap(0x7fa6dbec0000, 17624, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7fa6dbec0000
    关闭(3)= 0
    mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fa6dc0c6000
    mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fa6dc0c5000
    mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fa6dc0c4000
    arch_prctl(ARCH_SET_FS, 0x7fa6dc0c5700) = 0
    mprotect(0x7fa6dbeba000, 16384, PROT_READ) = 0
    mprotect(0x600000, 4096, PROT_READ) = 0
    mprotect(0x7fa6dc0e7000, 4096, PROT_READ) = 0
    munmap(0x7fa6dc0c7000, 119796) = 0
    fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0
    mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fa6dc0e4000
    写(1,“x”,1x)= 1
    退出组(0)=?

橡胶在跟踪的倒数第二个调用中遇到道路(分类,见下文)write(1,"x",1x):。此时,控制权从用户态传递printx到处理其余部分的 Linux 内核。write()是在中声明的包装函数unistd.h

    extern ssize_t write (int __fd, __const void *__buf, size_t __n) __wur;

大多数系统调用都是以这种方式包装的。包装函数,顾名思义,只不过是一个薄代码层,它将参数放在正确的寄存器中,然后执行软件中断 0x80。内核捕获中断,剩下的就是历史了。或者至少这是它过去的工作方式。显然,中断捕获的开销相当高,正如之前的一篇文章所指出的,现代 CPU 架构引入了sysenter汇编指令,它可以快速完成相同的结果。这个页面系统调用对系统调用的工作方式进行了很好的总结。

write()我觉得你可能会对这个答案感到有点失望,就像我一样。显然,从某种意义上说,这是一个错误的底部,因为在调用 to和点之间还有很多事情要做显卡帧缓冲区实际上已修改为使字母“x”出现在您的屏幕上。如果费时费力,通过深入内核来放大接触点(以保持“橡胶对道路”的类比)肯定是有教育意义的。我猜你将不得不经历几个抽象层,比如缓冲输出流、字符设备等。如果你决定跟进,请务必发布结果:)

于 2012-11-29T18:48:33.497 回答
4

标准库函数在底层平台库(例如 UNIX API)和/或直接系统调用(仍然是 C 函数)上实现。系统调用(在我知道的平台上)通过调用具有内联 asm 的函数在内部实现,该函数将系统调用号和参数放入 CPU 寄存器并触发内核然后处理的中断。

除了系统调用之外,还有其他与硬件通信的方式,但在现代操作系统下运行时,这些通常不可用或相当有限,或者至少启用它们需要一些系统调用。设备可能是内存映射的,因此对某些内存地址的写入(通过常规指针)可以控制设备。I/O 端口也经常被使用,并且根据架构,这些端口由特殊的 CPU 操作码访问,或者它们也可能被内存映射到特定地址。

于 2010-03-14T17:11:15.283 回答
1

好吧,除了分号和注释之外的所有 C++ 语句最终都变成了告诉 CPU 要做什么的机器代码。您可以编写自己的 printf 函数,而无需借助汇编。唯一必须用汇编编写的操作是端口的输入和输出,以及启用和禁用中断的东西。

但是,出于性能原因,汇编仍然用于系统级编程。即使不支持内联汇编,也没有什么可以阻止您在汇编中编写单独的模块并将其链接到您的应用程序。

于 2010-03-14T17:13:44.163 回答
0

一般来说,库函数是预编译并分发广告对象的。出于性能原因,内联汇编器仅在特定情况下使用,但它是例外,而不是规则。实际上,在我看来, printf 似乎不是内联汇编的好人选。Insetad,memcpy 或 memcmp 等函数。非常低级的函数可以由本地汇编程序(masm?gnu asm?)编译,并作为对象分发到库中。

于 2010-03-14T17:16:15.397 回答
-7

编译器从 C/C++ 源代码生成程序集。

于 2010-03-14T17:09:29.633 回答