【问题标题】:C/C++ function definitions without assembly无需汇编的 C/C++ 函数定义
【发布时间】:2011-01-27 10:18:54
【问题描述】:

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

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

【问题讨论】:

  • 很难知道从哪里开始,因为你认为你知道的一切都是错误的。您需要阅读一些关于编译和链接的维基百科文章。您可能还想查看 stdio.h 的源代码(它只是文本),在其中您不会找到任何 C++ 实现的任何汇编代码。
  • Visual Studio x64 不支持 inline 汇编器。这并不意味着您不能拥有汇编代码。您仍然可以拥有汇编程序,只是不能内联。 Tronic 在下面的回答是正确的。您还应该研究编译器内在函数。
  • @Jack 我并不是要贬低您的语言技能(事实上,就英语用法而言,您的问题已经很好地表达了),只是为了指出您认为代码以某种方式存在的想法在 stdio.h 中是错误的。我现在明白了,这可能不是你的意思。
  • @Jack 这不是一个是/否的问题。有些系统没有操作系统。答案取决于您的特定系统/在 Windows(32 位和 64 位)的情况下,用户级代码通过调用系统 linraries(它们只是您可以编写的 DLL 库)来工作。在某个时刻,在调用层次结构的深处,一些无法用 C 语言表达的代码被执行。该代码是如何生成的并不是很有趣,但通常它是用汇编程序编写的。是内联还是直接 ASM 并不重要
  • 您所知道的一切都没有错。但在开源时代,为自己回答这样一个问题所需要的只是好奇心加上时间。为了向您展示这是可能的,我的答案从 printf 的原型开始挖掘,并且在您到达 syscall 之前不会跳过任何步骤...带有指向实际源的链接他们的存储库中的文件。写了好久,希望对大家有帮助。 :)

标签: c++ inline-assembly


【解决方案1】:

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

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

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

所以C标准库的功能可以用C实现,只要知道需要更多权限时如何调用内核就可以了。

【讨论】:

  • printf() 适用于没有内核或基于环的架构的系统
  • x86 的 ring 3 和 ring 0 在只提供 2 个权限级别的架构(即大多数运行 Unix 或 Linux 的非 x86 CPU)上的工作方式与用户/内核模式完全相同。没有内核,它真的更像你的独立程序内核,或者至少以完全权限运行,所以printf 只是内核中的一个函数。 (就像 Linux 内核的 printk。)
  • @PeterCordes 是的。这就像将 LCD 连接到微控制器并对微控制器进行编程以在其上显示一些字符串。
【解决方案2】:

在 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 汇编指令,它可以快速完成相同的结果。这个页面System Calls 很好地总结了系统调用的工作原理。

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

【讨论】:

  • 链接网页上描述Linux系统调用的信息似乎已经过时了。特别是在 2.6 之后的内核上使用提供的示例代码无法找到 vsyscall 页面,也可能是一些更早的内核。
  • 更具体地说,由于地址空间随机化,vsyscall 页面不再映射到固定地址。通过查找ELF auxv AT_SYSINFO参数(articles.manugarg.com/aboutelfauxiliaryvectors.html)仍然可以得到页面的地址。
【解决方案3】:

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

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

【讨论】:

  • 但这些调用并不深入 stdio.h
  • 添加了有关直接硬件访问的信息。
  • 所有正确,但仅供参考,对于在此线程中发布的其他人来说,大多数现代操作系统和架构现在使用特殊的操作码来实际执行系统调用(例如 x86 上的 sysenter 和 sysexit),而不是使用软件中断来改进性能。
【解决方案4】:

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

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

【讨论】:

  • 你不能在没有汇编的情况下进行系统调用,或者调用用汇编编写的库函数。 C 编译器没有用于在寄存器中设置 args 和运行 x86 syscall / sysenterint 的内置函数/内在函数,因此这是通过手写 asm 完成的。
【解决方案5】:

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

【讨论】:

    【解决方案6】:

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

    【讨论】:

    • 在某些时候有手写或内联汇编来调用底层系统调用。我不知道带有 x86 的 syscallsysenterint 指令的内置或内在编译器。当然这不在stdio.h中,它在已经编译好的libc中
    猜你喜欢
    • 2021-04-23
    • 1970-01-01
    • 2019-08-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-12-03
    • 1970-01-01
    相关资源
    最近更新 更多