C/C++不使用汇编的函数定义

发布于 2024-08-25 11:05:48 字数 308 浏览 2 评论 0原文

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

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

I always thought that functions like printf() are, in the last step, defined using inline assembly. That deep in the bowels of stdio.h is buried some asm code that actually tells CPU what to do. For example, in dos, I remember it was implemented by first moving the beginning of the string to some memory location or register and than calling an intterupt.

However, since the x64 version of Visual Studio doesn't support inline assembler at all, it made me wonder how there could be no assembler-defined functions at all in C/C++. How does a library function like printf() get implemented in C/C++ without using assembler code? What actually executes the right software interrupt? Thanks.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(7

孤星 2024-09-01 11:05:48

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

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

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

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

First, you have to understand the concept of rings.
A kernel runs in ring 0, meaning it has a full access to memory and opcodes.
A program runs usually in ring 3. It has a limited access to memory, and cannot use all the opcodes.

So when a software need more privileges (for opening a file, writing to a file, allocating memory, etc), it needs to asks the kernel.
This can be done in many ways. Software interrupts, SYSENTER, etc.

Let's take the example of software interrupts, with the printf() function:
1 - Your software calls printf().
2 - printf() processes your string, and args, and then needs to execute a kernel function, as writing to a file can't be done in ring 3.
3 - printf() generates a software interrupt, placing in a register the number of a kernel function (in that case, the write() function).
4 - The software execution is interrupted, and the instruction pointer moves to the kernel code. So we are now in ring 0, in a kernel function.
5 - The kernel process the request, writing to the file (stdout is a file descriptor).
6 - When done, the kernel returns to the software's code, using the iret instruction.
7 - The software's code continues.

So functions of the C standard library can be implemented in C. All it has to do is to know how to call the kernel when it need more privileges.

橪书 2024-09-01 11:05:48

在 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)                           = ?

在倒数第二个中给出了 The rubs the road (排序,见下文)跟踪的调用:write(1,"x",1x)。此时,控制权从用户态 printx 传递到 Linux 内核,由 Linux 内核处理剩下的事情。 write() 是在 unistd.h 中声明的包装函数

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

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

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

In Linux, strace utility allows you to see what system calls are made by a program. So, taking a program like this

    int main(){
    printf("x");
    return 0;
    }

Say, you compile it as printx, then strace printx gives

    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)                           = ?

The rubber meets the road (sort off, see below) in the next to last call of the trace: write(1,"x",1x). At this point the control passes from user-land printx to the Linux kernel which handles the rest. write() is a wrapper function declared in unistd.h

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

Most system calls are wrapped in this way. The wrapper function, as its name suggests, is little more than a thin code layer that places the arguments in the correct registers and then executes a software interrupt 0x80. The kernel traps the interrupt and the rest is history. Or at least that's the way it used to work. Apparently, the overhead of interrupt trapping was quite high and, as an earlier post pointed out, modern CPU architectures introduced sysenter assembly instruction, which accomplishes the same result at speed. This page System Calls has quite a nice summary of how system calls work.

I feel that you will probably be a bit disappointed with this answer, as was I. Clearly, in some sense, this is a false bottom as there are still quite a few things that have to happen between the call to write() and the point at which the graphics card frame buffer is actually modified to make the letter "x" appear on your screen. Zooming in on the point of contact (to stay with the "rubber against the road" analogy) by diving into the kernel is sure to be educational if a time consuming endeavor. I am guessing you would have to travel through several layers of abstraction like buffered output streams, character devices, etc. Be sure to post the results should you decide to follow up on this:)

左秋 2024-09-01 11:05:48

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

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

The standard library functions are implemented on an underlying platform library (e.g. UNIX API) and/or by direct system calls (that are still C functions). The system calls are (on platforms that I know of) internally implemented by a call to a function with inline asm that puts a system call number and parameters in CPU registers and triggers an interrupt that the kernel then processes.

There are also other ways of communicating with hardware besides syscalls, but these are usually unavailable or rather limited when running under a modern operating system, or at least enabling them requires some syscalls. A device may be memory mapped, so that writes to certain memory addresses (via regular pointers) control the device. I/O ports are also often used and depending the architecture these are accessed by special CPU opcodes or they, too, may be memory mapped to specific addresses.

故事未完 2024-09-01 11:05:48

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

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

Well, all C++ statements except the semicolon and comments end up becoming machine code that tells CPU what to do. You can write your own printf function without resorting to assembly. The only operations that must be written in assembly are input and output from ports, and things that enable and disable interrupts.

However, assembly is still used in system level programming for performance reasons. Even though inline assembly is not supported, there is nothing that prevents you from writing a separate module in assembly and linking it to your application.

霞映澄塘 2024-09-01 11:05:48

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

In general, library function are precompiled and distribute ad object. Inline assembler is used only in particular situation for performance reasons, but it's the exception, not the rule. Actually, printf doesn't seems to me a good candidate to be inline-assembled. Insetad, functions like memcpy, or memcmp. Very low-level functions may be compiled by a native assembler (masm? gnu asm?), and distribute as object in a library.

℡Ms空城旧梦 2024-09-01 11:05:48

库函数的真正定义不在xxx.h或xxx.c中,而是在xxx.o/.obj/.dll/.libc中,它是一个二进制文件。该函数的声明位于xxx.h中。

也许你是对的,有些库函数是用汇编编写的,但汇编代码不在xxx.h中,而是在那些二进制文件中。

The real definition of a library function is not in xxx.h or xxx.c, but in xxx.o/.obj/.dll/.libc instead, which is a binary file. The declaration of the function is in xxx.h.

Maybe you're right that some of library functions are written in assembly, but the assembly code is not in xxx.h, but in those binary files.

眼前雾蒙蒙 2024-09-01 11:05:48

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

The compiler generates the assembly from the C/C++ source code.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文