我一直认为像printf()
这样的函数在最后一步中是使用内联汇编定义的。在stdio.h的内部深处埋藏了一些asm代码,它实际上告诉CPU要做什么。例如,在dos中,我记得它是通过首先mov
将字符串的开头int
实现到某个内存位置或寄存器而不是调用printf()
terupt来实现的。
但是,由于Visual Studio的x64版本根本不支持内联汇编程序,因此我想知道在C / C ++中根本不存在汇编程序定义的函数。如何在不使用汇编程序代码的情况下在C / C ++中实现像{{1}}这样的库函数?什么实际执行正确的软件中断?感谢。
答案 0 :(得分:132)
答案 1 :(得分:16)
首先,您必须了解戒指的概念
内核在ring 0中运行,这意味着它可以完全访问内存和操作码
程序通常在第3环中运行。它对内存的访问受限,不能使用所有操作码。
因此,当软件需要更多权限(用于打开文件,写入文件,分配内存等)时,它需要询问内核。
这可以通过多种方式完成。软件中断,SYSENTER等。
我们以软件中断为例,使用printf()函数:
1 - 您的软件调用printf()。
2 - printf()处理你的字符串和args,然后需要执行内核函数,因为写入文件不能在环3中完成。
3 - printf()生成一个软件中断,在寄存器中放入一个内核函数的编号(在这种情况下,写入()函数)。
4 - 软件执行被中断,指令指针移动到内核代码。所以我们现在在内核函数的0环中
5 - 内核处理请求,写入文件(stdout是文件描述符)
6 - 完成后,内核使用iret指令返回软件代码
7 - 软件代码继续。
因此,C标准库的功能可以在C中实现。它所要做的就是知道如何在需要更多权限时调用内核。
答案 2 :(得分:5)
在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 (No such file or directory) mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fa6dc0e5000 access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) 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 (No such file or directory) 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)
。此时,控件从user-land printx
传递到处理其余部分的Linux内核。 write()
是在unistd.h
extern ssize_t write (int __fd, __const void *__buf, size_t __n) __wur;
大多数系统调用都以这种方式包装。正如其名称所示,包装函数只不过是一个简单的代码层,它将参数放在正确的寄存器中,然后执行软件中断0x80。内核捕获中断,其余的是历史记录。或者至少那是以前的工作方式。显然,中断陷阱的开销非常高,正如之前的帖子指出的那样,现代CPU架构引入了sysenter
汇编指令,它可以快速完成相同的结果。这个页面System Calls对系统调用的工作方式有很好的总结。
我觉得你可能会对这个答案感到有点失望,就像我一样。显然,从某种意义上说,这是一个虚假的底线,因为在调用{{{ 1}}以及实际修改图形卡帧缓冲区以使字母“x”出现在屏幕上的点。通过潜入内核来放大接触点(与“橡胶对抗道路”类比),如果耗费时间的努力,肯定会具有教育意义。我猜你必须经历几个抽象层,如缓冲输出流,字符设备等。如果您决定对此进行跟进,请务必发布结果:)
答案 3 :(得分:4)
标准库函数在底层平台库(例如UNIX API)和/或直接系统调用(仍然是C函数)上实现。系统调用(在我知道的平台上)通过调用带有内联asm的函数在内部实现,该函数将系统调用号和参数放入CPU寄存器中并触发内核然后处理的中断。
除了系统调用之外,还有其他与硬件通信的方式,但这些方法在现代操作系统下运行时通常不可用或相当有限,或者至少启用它们需要一些系统调用。设备可以是存储器映射的,因此对某些存储器地址的写入(通过常规指针)控制设备。 I / O端口也经常使用,并且根据架构,它们可以通过特殊的CPU操作码访问,或者也可以将内存映射到特定的地址。
答案 4 :(得分:1)
好吧,除了分号和注释之外的所有C ++语句最终都成为告诉CPU要做什么的机器代码。您可以编写自己的printf函数,而无需使用汇编。必须在汇编中写入的唯一操作是从端口输入和输出,以及启用和禁用中断的操作。
但是,出于性能原因,仍然在系统级编程中使用汇编。即使不支持内联汇编,也没有什么可以阻止您在汇编中编写单独的模块并将其链接到您的应用程序。
答案 5 :(得分:0)
通常,库函数是预编译的并分发广告对象。内联汇编程序仅出于性能原因仅在特定情况下使用,但它是例外,而不是规则。实际上,printf在我看来并不适合进行内联组装。 Insetad,memcpy或memcmp等功能。非常低级的函数可以由本机汇编程序(masm?gnu asm?)编译,并作为对象在库中分发。
答案 6 :(得分:-7)
编译器从C / C ++源代码生成程序集。