C/C++函数定义不包含汇编

77
我一直以为像 printf() 这样的函数最后都是使用内联汇编定义的。在 stdio.h 的深处,会有一些 asm 代码告诉 CPU 要做什么。例如,在 dos 中,我记得它是通过先将字符串的开头 mov 到某个内存位置或寄存器,然后调用 interrupt 来实现的。
然而,自从 Visual Studio 的 x64 版本根本不支持内联汇编后,这让我想知道 C/C++ 中是否真的没有用汇编代码定义的函数。那么像 printf() 这样的库函数是如何在 C/C++ 中实现的,又是什么执行了正确的软件中断呢?谢谢。

22
很难确定从哪里开始,因为你认为的所有东西都是错误的。你需要阅读一些关于编译和链接的维基百科文章。你可能还想看一下stdio.h的源代码(它只是文本),在其中你不会找到任何C++实现的汇编代码。 - anon
4
Visual Studio x64不支持“内联”汇编,但这并不意味着您不能有汇编代码。您仍然可以有汇编代码,只是不能内联使用。下面Tronic的答案是正确的。您还应该研究编译器内置函数。 - user2189331
1
@Jack,我并不是要贬低你的语言能力(事实上,就英语用法而言,你的问题表达得非常好),只是想指出你认为代码在stdio.h中的想法是错误的。现在我明白了,也许那不是你的意思。 - anon
1
@Jack 这不是一个简单的是/否问题。有些系统没有操作系统。答案取决于您具体的系统。在Windows的情况下,无论是32位还是64位,用户级代码都通过调用系统库(这只是您可以编写的DLL库)来工作。在调用层次结构的深处,会执行一些无法用C语言表达的代码。如何生成该代码并不太重要,但通常它将以汇编语言编写。无论是内联还是直接ASM都不重要。 - anon
7
你所知道的并不是错的。但在开源时代,只需要好奇心和时间就能回答这样的问题。为了向你展示这是可行的,我的回答从 printf 的原型开始挖掘,直到达到 syscall,并提供它们存储库中实际源文件的链接,没有跳过任何步骤。我花了很长时间来写这篇文章,希望对你有所帮助 :) - HostileFork says dont trust SE
显示剩余6条评论
7个回答

23
首先,你需要了解“ring”的概念。
内核在ring 0运行,意味着它可以完全访问内存和操作码。
程序通常在ring 3中运行。它仅能有限地访问内存,并且不能使用所有的操作码。

因此,当软件需要更多权限(例如打开文件、写入文件、分配内存等)时,它需要请求内核的帮助。
这可以通过多种方式实现。例如软件中断、SYSENTER等等。

让我们以printf()函数为例来说明软件中断的过程:
1 - 您的软件调用printf()。
2 - printf()处理您的字符串和参数,然后需要执行内核函数,因为在ring 3中无法进行文件写入操作。
3 - printf()生成一个软件中断,在寄存器中放置一个内核函数的编号(在本例中为write()函数)。
4 - 软件执行被中断,并且指令指针移动到内核代码。因此,我们现在处于ring 0中,在内核函数中。
5 - 内核处理请求,写入文件(stdout是一个文件描述符)。
6 - 处理完成后,内核使用iret指令返回到软件的代码。
7 - 软件代码继续执行。

因此,C标准库的函数可以在C中实现。它所需要做的就是知道如何在需要更多权限时调用内核。


11
printf() 可以在没有内核或基于环形结构的系统上运行。 - anon
x86的第三和第零环与仅提供2个特权级别(即运行Unix或Linux的大多数非x86 CPU)的体系结构上的用户/内核模式完全相同。 没有内核,它实际上更像是你的独立程序内核,或者至少以完全特权运行,因此printf只是内核中的一个函数。 (就像Linux内核的printk一样。) - Peter Cordes
@PeterCordes 是的。这就像将LCD连接到微控制器上,并编程微控制器在其上显示一些字符串。 - Sandeep

6
在Linux中,strace实用程序允许您查看程序所调用的系统调用。因此,以这样的程序为例:
    int main(){
    printf("x");
    return 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 (找不到文件或目录)
    open("/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
    close(3)                                = 0
    access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (找不到文件或目录)
    open("/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
    read(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
    close(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
    write(1, "x", 1x)                        = 1
    exit_group(0)                           = ?
在跟踪的倒数第二个调用中,由write(1,"x",1x)处理(有所不同,请参见下文),这时控制权从用户空间的printx传递到了Linux内核,由内核处理其余部分。大多数系统调用都以这种方式进行封装。正如其名称所示,封装函数只���一个薄薄的代码层,它将参数放置在正确的寄存器中,然后执行软件中断0x80。内核捕获中断,剩下的就成为历史了。或者至少过去是这样的。显然,中断捕获的开销相当高,正如早期的一篇帖子指出的那样,现代CPU架构引入了sysenter汇编指令,以便以更快的速度实现相同的结果。这个页面System Calls非常好地总结了系统调用的工作原理。
我感觉你可能会对这个答案有点失望,就像我一样。很明显,在某种意义上,这是一个错误的底部,因为在调用write()并使字母“x”出现在屏幕上的图形卡帧缓冲区实际上被修改之间仍然有很多事情要发生。如果深入研究内核,聚焦联系点(保持“橡胶和道路”比喻),这肯定是一次值得学习的而且也需要耗费时间的努力。我猜你必须要穿过几层抽象,如缓冲输出流、字符设备等。如果你决定跟进此问题,请务必发布结果 :)

似乎链接的网页上描述Linux系统调用的信息已经过时。特别是在新于2.6内核版本的系统中,使用提供的示例代码无法找到vsyscall页面,可能也适用于一些早期版本。 - Daniel Genin
更具体地说,由于地址空间随机化,vsyscall页面不再映射到固定地址。仍然可以通过查找ELF auxv AT_SYSINFO参数(http://articles.manugarg.com/aboutelfauxiliaryvectors.html)来获取页面的地址。 - Daniel Genin

5
标准库函数是在底层平台库(例如UNIX API)上实现的,或者通过直接系统调用(仍然是C函数)实现。系统调用(在我所知道的平台上)是通过调用一个带有内联asm的函数来实现的,该函数将系统调用号和参数放置在CPU寄存器中并触发中断,然后内核进行处理。
除了系统调用之外,还有其他与硬件通信的方式,但在运行在现代操作系统下时,这些方式通常不可用或受到限制,至少需要一些系统调用才能启用它们。设备可以被映射到内存,以便通过对特定内存地址的写入(通过常规指针)控制设备。I/O端口也经常使用,根据体系结构,这些可以通过特殊的CPU操作码访问,或者它们也可以被映射到特定地址的内存中。

但是这些调用并不深入 stdio.h。 - anon
添加了有关直接硬件访问的信息。 - Tronic
3
所有的都正确,但是为了提供参考,对于在这个讨论串中发帖的其他人,大多数现代操作系统和架构现在使用特殊的操作码来执行系统调用(例如,在x86体系结构上使用的sysenter和sysexit),而不是使用软中断,以提高性能。 - PinkyNoBrain

1

所有的C++语句,除了分号和注释,都会变成机器码告诉CPU该做什么。你可以编写自己的printf函数而不必使用汇编语言。必须用汇编语言编写的唯一操作是从端口输入和输出以及启用和禁用中断等操作。

然而,在系统级编程中仍然使用汇编语言来提高性能。即使不支持内联汇编,也没有任何阻止你编写一个单独的汇编模块并将其链接到应用程序中。


1
你不能在没有汇编语言的情况下进行系统调用,或者调用一个用汇编语言编写的库函数。C编译器没有内置/内部函数来设置寄存器中的参数并运行x86 syscall / sysenterint,因此需要手写汇编代码来完成这个过程。 - Peter Cordes

0
一般来说,库函数是预编译并分发为对象。内联汇编仅在特定情况下出于性能原因使用,但这是例外而不是规律。实际上,printf 看起来不是内联汇编的好选择。相反,像 memcpy 或 memcmp 这样的函数更适合。非常低级别的函数可以由本地汇编器(masm?gnu asm?)编译,并作为库中的对象分发。

-1
真正的库函数定义不在xxx.h或xxx.c中,而是在xxx.o/.obj/.dll/.libc等二进制文件中。函数的声明在xxx.h中。
也许你是对的,一些库函数是用汇编语言编写的,但汇编代码不在xxx.h中,而是在那些二进制文件中。

请阅读[答案]。特别是,请确保您发布的答案实际上回答了所提出的问题。 - undefined

-7
编译器从 C/C++ 源代码生成汇编代码。

某些情况下会有手写或内联汇编来调用底层系统调用。我不知道是否有一个内置的x86的syscallsysenterint指令的编译器。当然,这不在stdio.h中,而是在已编译的libc中。 - Peter Cordes
实际上,MSVC确实有一个用于int 0x2c的内嵌函数,用于32位Windows系统调用向量。https://learn.microsoft.com/en-us/cpp/intrinsics/int2c?view=msvc-170。但是,我没有看到64位`syscall`或32位`sysenter`的内嵌函数,用于快速系统调用,所以我推测Windows DLLs可能有一些手写的.asm文件。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接