没有汇编的C / C ++函数定义

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

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

你当然是对的,橡胶必须在一点上与路面相遇。 但是,在find那个地方之前,需要经过很多的层面! 这听起来像你有一些基于DOS日的先入之见,这不再相关了。

这里已经有了一些很好的一般观点,但没有人在源头细节上与精确的魔鬼联系在一起。 所以为了让你足够的抱歉,你问了:)我做了一个关于GNU的libc和Linuxprintf故事的详尽的跟踪..不要手动任何步骤。 在这个过程中,我带来了一些我自己的知识(警告:这不是轻松无聊!):

(原来的链接是http://blog.hostilefork.com/where-printf-rubber-meets-road/ ,它会被保留在那里,但是为了防止链接在这里被腐烂,内容被caching了。)

第一步

我们当然会从printf的原型开始,它是在libc/libio/stdio.h文件中定义的

 extern int printf (__const char *__restrict __format, ...); 

但是,您将无法find名为printf的函数的源代码。 相反,在文件/libc/stdio-common/printf.c您会发现一些与__printf函数关联的代码:

 int __printf (const char *format, ...) { va_list arg; int done; va_start (arg, format); done = vfprintf (stdout, format, arg); va_end (arg); return done; } 

同一个文件中的一个macros设置了一个关联,因此这个函数被定义为非下划线printf的别名:

 ldbl_strong_alias (__printf, printf); 

这是有道理的,printf将是一个薄层,调用vfprintf与标准输出。 确实,格式化工作的核心是在vfprintf中完成的,您可以在libc/stdio-common/vfprintf.cfindlibc/stdio-common/vfprintf.c 。 这是一个相当长的function,但你可以看到,它仍然是在C!

深入兔子洞

vfprintf神奇地调用了outchar和outstring,它们是在同一个文件中定义的奇怪的macros:

 #define outchar(Ch) \ do \ { \ register const INT_T outc = (Ch); \ if (PUTC (outc, s) == EOF || done == INT_MAX) \ { \ done = -1; \ goto all_done; \ } \ ++done; \ } \ while (0) 

撇开为什么这么奇怪的问题,我们看到它依赖于神秘的PUTC,也在同一个文件中:

 #define PUTC(C, F) IO_putwc_unlocked (C, F) 

当你到达libc/libio/libio.hIO_putwc_unlocked的定义时,你可能会开始认为你不再关心printf是如何工作的:

 #define _IO_putwc_unlocked(_wch, _fp) \ (_IO_BE ((_fp)->_wide_data->_IO_write_ptr \ >= (_fp)->_wide_data->_IO_write_end, 0) \ ? __woverflow (_fp, _wch) \ : (_IO_wint_t) (*(_fp)->_wide_data->_IO_write_ptr++ = (_wch))) 

但是,尽pipe有点难以阅读,它只是做缓冲输出。 如果文件指针的缓冲区中有足够的空间,那么它只会将字符粘贴到它中,但是如果没有,则会调用__woverflow 。 由于缓冲区用完的唯一select是刷新到屏幕(或者文件指针所代表的任何设备),我们可以希望在那里find魔法咒语。

V中的C?

如果你猜测我们会跳过另一个令人沮丧的间接水平,那么你是对的。 看看libc / libio / wgenops.c,你会发现__woverflow的定义:

 wint_t __woverflow (f, wch) _IO_FILE *f; wint_t wch; { if (f->_mode == 0) _IO_fwide (f, 1); return _IO_OVERFLOW (f, wch); } 

基本上,文件指针是作为对象在GNU标准库中实现的。 他们有数据成员,但也有function成员,你可以调用JUMPmacros的变种。 在libc/libio/libioP.h文件中,你会发现这个技术的一些文档:

 /* THE JUMPTABLE FUNCTIONS. * The _IO_FILE type is used to implement the FILE type in GNU libc, * as well as the streambuf class in GNU iostreams for C++. * These are all the same, just used differently. * An _IO_FILE (or FILE) object is allows followed by a pointer to * a jump table (of pointers to functions). The pointer is accessed * with the _IO_JUMPS macro. The jump table has a eccentric format, * so as to be compatible with the layout of a C++ virtual function table. * (as implemented by g++). When a pointer to a streambuf object is * coerced to an (_IO_FILE*), then _IO_JUMPS on the result just * happens to point to the virtual function table of the streambuf. * Thus the _IO_JUMPS function table used for C stdio/libio does * double duty as the virtual function table for C++ streambuf. * * The entries in the _IO_JUMPS function table (and hence also the * virtual functions of a streambuf) are described below. * The first parameter of each function entry is the _IO_FILE/streambuf * object being acted on (ie the 'this' parameter). */ 

所以当我们在libc/libio/genops.c发现libc/libio/genops.c ,我们发现它是一个调用文件指针的“1-parameter” __overflow方法的macros:

 #define IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH) 

各种文件指针types的跳转表位于libc / libio / fileops.c中

 const struct _IO_jump_t _IO_file_jumps = { JUMP_INIT_DUMMY, JUMP_INIT(finish, INTUSE(_IO_file_finish)), JUMP_INIT(overflow, INTUSE(_IO_file_overflow)), JUMP_INIT(underflow, INTUSE(_IO_file_underflow)), JUMP_INIT(uflow, INTUSE(_IO_default_uflow)), JUMP_INIT(pbackfail, INTUSE(_IO_default_pbackfail)), JUMP_INIT(xsputn, INTUSE(_IO_file_xsputn)), JUMP_INIT(xsgetn, INTUSE(_IO_file_xsgetn)), JUMP_INIT(seekoff, _IO_new_file_seekoff), JUMP_INIT(seekpos, _IO_default_seekpos), JUMP_INIT(setbuf, _IO_new_file_setbuf), JUMP_INIT(sync, _IO_new_file_sync), JUMP_INIT(doallocate, INTUSE(_IO_file_doallocate)), JUMP_INIT(read, INTUSE(_IO_file_read)), JUMP_INIT(write, _IO_new_file_write), JUMP_INIT(seek, INTUSE(_IO_file_seek)), JUMP_INIT(close, INTUSE(_IO_file_close)), JUMP_INIT(stat, INTUSE(_IO_file_stat)), JUMP_INIT(showmanyc, _IO_default_showmanyc), JUMP_INIT(imbue, _IO_default_imbue) }; libc_hidden_data_def (_IO_file_jumps) 

还有一个#define将_IO_new_file_overflow_IO_file_overflow等同_IO_new_file_overflow ,前者在同一个源文件中定义。 (注意:INTUSE只是一个标记内部使用的函数的macros,并不意味着“这个函数使用中断”)

我们到了吗?!

_IO_new_file_overflow的源代码做了更多的缓冲区操作,但它确实调用了_IO_do_flush

 #define _IO_do_flush(_f) \ INTUSE(_IO_do_write)(_f, (_f)->_IO_write_base, \ (_f)->_IO_write_ptr-(_f)->_IO_write_base) 

我们现在正处于一个可能实际上符合path的地方,即IO_do_write可能是实际上直接写入I / O设备的缓冲区。 至less我们可以希望! 它由macros映射到_IO_new_do_write,我们有这样的:

 static _IO_size_t new_do_write (fp, data, to_do) _IO_FILE *fp; const char *data; _IO_size_t to_do; { _IO_size_t count; if (fp->_flags & _IO_IS_APPENDING) /* On a system without a proper O_APPEND implementation, you would need to sys_seek(0, SEEK_END) here, but is is not needed nor desirable for Unix- or Posix-like systems. Instead, just indicate that offset (before and after) is unpredictable. */ fp->_offset = _IO_pos_BAD; else if (fp->_IO_read_end != fp->_IO_write_base) { _IO_off64_t new_pos = _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1); if (new_pos == _IO_pos_BAD) return 0; fp->_offset = new_pos; } count = _IO_SYSWRITE (fp, data, to_do); if (fp->_cur_column && count) fp->_cur_column = INTUSE(_IO_adjust_column) (fp->_cur_column - 1, data, count) + 1; _IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base); fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_buf_base; fp->_IO_write_end = (fp->_mode <= 0 && (fp->_flags & (_IO_LINE_BUF+_IO_UNBUFFERED)) ? fp->_IO_buf_base : fp->_IO_buf_end); return count; } 

可悲的是,我们再次被卡住了… _IO_SYSWRITE正在做这件事:

 /* The 'syswrite' hook is used to write data from an existing buffer to an external file. It generalizes the Unix write(2) function. It matches the streambuf::sys_write virtual function, which is specific to this implementation. */ typedef _IO_ssize_t (*_IO_write_t) (_IO_FILE *, const void *, _IO_ssize_t); #define _IO_SYSWRITE(FP, DATA, LEN) JUMP2 (__write, FP, DATA, LEN) #define _IO_WSYSWRITE(FP, DATA, LEN) WJUMP2 (__write, FP, DATA, LEN) 

所以在do_write里我们调用文件指针的write方法。 我们从上面的跳转表中知道映射到_IO_new_file_write,那么这是做什么的?

 _IO_ssize_t _IO_new_file_write (f, data, n) _IO_FILE *f; const void *data; _IO_ssize_t n; { _IO_ssize_t to_do = n; while (to_do > 0) { _IO_ssize_t count = (__builtin_expect (f->_flags2 & _IO_FLAGS2_NOTCANCEL, 0) ? write_not_cancel (f->_fileno, data, to_do) : write (f->_fileno, data, to_do)); if (count < 0) { f->_flags |= _IO_ERR_SEEN; break; } to_do -= count; data = (void *) ((char *) data + count); } n -= to_do; if (f->_offset >= 0) f->_offset += n; return n; } 

现在只是叫写! 那么在那里执行? 你会发现写在libc/posix/unistd.h

 /* Write N bytes of BUF to FD. Return the number written, or -1. This function is a cancellation point and therefore not marked with __THROW. */ extern ssize_t write (int __fd, __const void *__buf, size_t __n) __wur; 

(注意: __wur__attribute__ ((__warn_unused_result__)))的macros__attribute__ ((__warn_unused_result__)))

从表生成的函数

这只是写作的原型。 在GNU标准库中,你不会findLinux的write.c文件。 相反,您会find特定于平台的方法,以各种方式连接到OS写入函数,所有这些方法都在libc / sysdeps /目录中。

我们将继续跟随Linux如何做。 有一个名为sysdeps/unix/syscalls.list的文件,用于自动生成写入function。 表中的相关数据是:

 File name: write Caller: “-” (ie Not Applicable) Syscall name: write Args: Ci:ibn Strong name: __libc_write Weak names: __write, write 

不是所有这些神秘的,除了Ci:ibn 。 C表示“可取消”。 冒号将返回types与参数types分隔开,如果您想更深入地解释它们的含义,则可以在生成代码的shell脚本中看到注释libc/sysdeps/unix/make-syscalls.sh

所以现在我们期望能够链接到由这个shell脚本生成的名为__libc_write的函数。 但是,正在产生什么? 一些通过名为SYS_ify的macros实现写入的C代码,您可以在sysdeps / unix / sysdep.h中find它

 #define SYS_ify(syscall_name) __NR_##syscall_name 

啊,很好的老令牌粘贴:P。 所以基本上,这个__libc_write的实现变成仅仅是一个名为__NR_write的参数和其他参数的代理调用syscall函数。

人行道终点在哪里

我知道这是一个迷人的旅程,但现在我们已经到了GNU libc的末尾。 这个数字__NR_write是由Linux定义的。 对于32位X86体系结构,它会让你到linux/arch/x86/include/asm/unistd_32.h

 #define __NR_write 4 

剩下唯一要看的就是系统调用的实现。 我可能在某个时候做了些什么,但现在我只是指出了一些关于如何将系统调用添加到Linux的参考 。

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

所以当一个软件需要更多的权限(打开文件,写入文件,分配内存等)时,需要向内核请求。
这可以通过许多方式来完成。 软件中断,SYSENTER等

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

所以C标准库的function可以在C中实现。只需要知道如何在需要更多权限时调用内核。

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

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

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

但是,由于性能原因,程序集仍然用于系统级编程。 即使内联汇编不受支持,也没有任何东西阻止您在汇编中编写单独的模块并将其链接到应用程序。

在Linux中, strace实用程序允许您查看程序进行的系统调用。 所以,这样的程序


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

printx说,你把它编译成printx ,然后把strace printx给出


     execve(“./ printx”,[“./printx”],[/ * 49 vars * /])= 0
     brk(0)= 0xb66000
    访问(“/ etc / ld.so.nohwcap”,F_OK)= -1 ENOENT(没有这样的文件或目录)
     mmap(NULL,8192,PROT_READ | PROT_WRITE,MAP_PRIVATE | MAP_ANONYMOUS,-1,0)= 0x7fa6dc0e5000
    访问(“/ 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
    closures(3)= 0
    访问(“/ 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
    closures(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
     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。 内核陷阱中断,其余的是历史。 或者至less是以前的工作方式。 显然,中断陷阱的开销是相当高的,正如之前的文章指出的,现代CPU架构引入了sysenter汇编指令,从而达到了同样的速度。 这个页面系统调用对系统调用的工作有很好的总结。

我觉得你可能会对这个答案有点失望,就像我一样。显然,从某种意义上说,这是一个错误的底部,因为在write()和call write()之间还有很多事情必须发生。指向graphics卡帧缓冲区被实际修改,使字母“x”出现在您的屏幕上。 通过深入内核来放大接触点(保持与“橡胶反对路线”类比),肯定会受到教育,如果一个耗时的努力。 我猜你必须通过缓冲输出stream,字符设备等几个抽象层次旅行。一定要张贴结果,如果你决定跟进这个:)

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

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

Interesting Posts