无需汇编的C / C ++函数定义


73

我一直认为printf(),在最后一步中,要使用内联汇编来定义类似的函数。stdio.h的深处埋有一些asm代码,该代码实际上告诉CPU该怎么做。例如,在dos中,我记得它是通过首先mov将字符串的开头放置到某个内存位置或寄存器中,而不是调用int中断来实现的。

但是,由于Visual Studio的x64版本根本不支持嵌入式汇编程序,这使我想知道在C / C ++中根本没有汇编程序定义的函数。printf()在不使用汇编代码的情况下,如何在C / C ++中实现类似库的功能?究竟是什么执行正确的软件中断?谢谢。


21
很难知道从哪里开始,因为您认为自己所知道的一切都是错误的。您需要阅读一些有关编译和链接的维基百科文章。您可能还想看看stdio.h的源代码(它只是文本),在其中找不到任何C ++实现的任何汇编代码。

4
Visual Studio x64不支持嵌入式汇编器。这并不意味着您不能拥有汇编代码。您仍然可以拥有汇编程序,只是没有内联。Tronic在下面的回答是正确的。您还应该研究编译器内在函数。

1
@Jack我并不是要贬低您的语言能力(实际上,就英语用法而言,您的问题表达得非常好),而只是指出您的想法是stdio.h中的代码是错误的。我现在知道那可能不是您的意思。

1
@Jack这不是一个是/不是问题。某些系统没有操作系统。答案取决于您的特定系统/在Windows(32位和64位)中,用户级代码通过调用系统程序(您可以编写的DLL库)来工作。有时,在调用层次结构的深处,执行了一些无法用C表达的代码。生成代码的方式并不是很有趣,但是通常它是用汇编器编写的。无论这是行内或直ASM并不重要

7
您所知道的一切都没有错。但是在开源时代,为自己回答这个问题所需要的只是好奇心和时间。为了向您展示这是可能的,我的答案从printf的原型开始挖掘,直到您到达syscall ...并链接到其存储库中的实际源文件之后,才跳过任何步骤。写了很长时间,希望对您有所帮助。:)
HostileFork说不要相信2010年

Answers:


18

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

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

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

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


9
printf()的工作在系统中没有内核,或基于环的结构

x86的第3环和第0环在仅提供2个特权级别的体系结构(即,大多数运行Unix或Linux的非x86 CPU)上类似于用户/内核模式。没有内核,实际上更像是您的独立程序内核,或者至少以完全特权运行,因此printf内核中只是一个函数。(就像Linux内核一样printk。)
Peter Cordes

5

在Linux中,strace实用程序使您可以查看程序进行了哪些系统调用。因此,采用这样的程序

    int main(){
    printf(“ x”);
    返回0;
    }

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

    execve(“ ./ printx”,[“ ./printx”],[/ * 49个变量* /])= 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
    关闭(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
    write(1,“ x”,1x)= 1
    exit_group(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()与到达该点之间仍然有很多事情要做实际上修改了图形卡帧缓冲区,以使字母“ x”出现在屏幕上。如果要花费大量时间,那么通过深入内核来放大接触点(以与“橡皮筋抵挡道路”类似)是很有教育意义的。我猜想您将不得不经历多个抽象层,例如缓冲的输出流,字符设备等。请确保发布结果,以决定继续进行此操作:)


链接网页上描述Linux中系统调用的信息似乎已过时。特别是在2.6或更高版本的内核上,无法使用提供的示例代码找到vsyscall页面。
Daniel Genin 2012年

更具体地说,由于地址空间随机化,vsyscall页面不再映射到固定地址。页面的地址仍然可以通过查找ELF auxv AT_SYSINFO参数(获得articles.manugarg.com/aboutelfauxiliaryvectors.html)。
Daniel Genin 2012年

4

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

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


但是这些调用在stdio.h中并不深入

添加了有关直接硬件访问的信息。
Tronic

3
对于在此线程中发布的其他消息,所有这些都正确,但仅供参考,大多数现代OS和体系结构现在使用特殊的操作码来实际执行系统调用(例如x86上的sysenter和sysexit),而不是使用软件中断来提高性能。
PinkyNoBrain

1

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

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


如果没有汇编,则不能进行系统调用,也不能调用用汇编编写的库函数。C编译器没有用于在寄存器中设置args并运行x86 syscall/sysenter或的内建函数/内在函数int,因此这是通过手写asm完成的。
彼得·科德斯

0

通常,库函数是预编译的,并分发广告对象。出于性能原因,仅在特定情况下使用内联汇编程序,但这是例外,不是常规。实际上,在我看来,printf似乎不是内联汇编的好选择。Insetad,功能类似于memcpy或memcmp。底层汇编器(masm?gnu asm?)可能会编译非常低级的函数,并将其作为对象分发到库中。


-7

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


在某个时刻存在手写或内联汇编,以调用基础系统调用。我不知道有一个内置的编译器的或内在的x86的syscallsysenterint说明。当然,这不在其中stdio.h,而是在已经编译的libc中
Peter Cordes
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.