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

Jac*_*ack 70 c++ inline-assembly

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

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

Hos*_*ork 131

如何在不使用汇编程序代码的情况下在C/C++中实现类似printf()的库函数?什么实际执行正确的软件中断?

对于大多数实际用途,您无法从LinuxWindows中真正调用BIOS .实际上,您根本不想与BIO​​S交互 - 除非您正在编写操作系统或引导程序.

因为你特别询问C函数printf(),我在这里提供的是一点点跟踪,我找到了" GNU的libc中橡胶符合道路的地方". 扰流器警报:它以syscall()结束.

系统调用不是BIOS,而只是一个编号函数表,其中包含OS用于执行基本服务的预期参数.在某种程度上它是相似的,意思是"按数字调用某些参数的约定".虽然这就是所有软件的类型,但我们应该强调差异:你在谈论操作系统,而不是真实模式下的硬件.

因此,printf对于那些不容易厌倦的人来说,这是专门针对GCC的钻研:

第一步

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

extern int printf (__const char *__restrict __format, ...);
Run Code Online (Sandbox Code Playgroud)

但是,您找不到名为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;
}
Run Code Online (Sandbox Code Playgroud)

同一文件中的宏设置关联,以便将此函数定义为非下划线printf的别名:

ldbl_strong_alias (__printf, printf);
Run Code Online (Sandbox Code Playgroud)

有意义的是,printf将是一个使用stdout调用vfprintf的薄层.实际上,格式化工作的内容是在vfprintf中完成的,您可以在其中找到它libc/stdio-common/vfprintf.c.这是一个相当冗长的功能,但你可以看到它仍然都在C!

深入了解兔子洞......

vfprintf神秘地调用outchar和outstring,这是在同一个文件中定义的奇怪的宏:

#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)
Run Code Online (Sandbox Code Playgroud)

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

#define PUTC(C, F) IO_putwc_unlocked (C, F)
Run Code Online (Sandbox Code Playgroud)

当你进入IO_putwc_unlockedin 的定义时libc/libio/libio.h,你可能会开始认为你不再关心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)))
Run Code Online (Sandbox Code Playgroud)

但是尽管有点难以阅读,但它只是做缓冲输出.如果文件指针的缓冲区中有足够的空间,那么它只会将字符粘贴到其中......但如果没有,则调用它__woverflow.因为当你用完缓冲区时唯一的选择是刷新屏幕(或你的文件指针代表的任何设备),我们可以希望在那里找到神奇的咒语.

C中的Vtables?

如果你猜到我们将要经历另一个令人沮丧的间接水平,你就是对的.查看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);
}
Run Code Online (Sandbox Code Playgroud)

基本上,文件指针在GNU标准库中实现为对象.它们有数据成员,但也有函数成员,您可以使用JUMP宏的变体调用它们.在文件中,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 (i.e. the 'this' parameter).
 */
Run Code Online (Sandbox Code Playgroud)

所以,当我们发现IO_OVERFLOWlibc/libio/genops.c,我们发现它是其中调用宏“1-parameter” __overflow的文件指针的方法:

#define IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)
Run Code Online (Sandbox Code Playgroud)

各种文件指针类型的跳转表位于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)
Run Code Online (Sandbox Code Playgroud)

还有这相当于使用#define _IO_new_file_overflow_IO_file_overflow,并且前者相同的源文件中定义.(注意:INTUSE只是一个标记内部使用函数的宏,它并不意味着"此函数使用中断")

我们到了吗?!

_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)
Run Code Online (Sandbox Code Playgroud)

我们现在正处于_IO_do_write可能是橡胶实际遇到道路的地方:对I/O设备的无缓冲,实际,直接写入.至少我们可以希望!它由宏映射到_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;
}
Run Code Online (Sandbox Code Playgroud)

可悲的是,我们再次陷入困境...... _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)
Run Code Online (Sandbox Code Playgroud)

所以在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;
}
Run Code Online (Sandbox Code Playgroud)

现在它只是打电话写!那么实现在哪里呢?你会发现写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;
Run Code Online (Sandbox Code Playgroud)

(注意:__wur是一个宏__attribute__ ((__warn_unused_result__)))

从表生成的函数

这只是写作的原型.您将无法在GNU标准库中找到适用于Linux的write.c文件.相反,您将找到以各种方式连接到OS写入功能的特定于平台的方法,所有这些方法都在libc/sysdeps /目录中.

我们将继续关注Linux如何做到这一点.有一个文件被调用sysdeps/unix/syscalls.list,用于自动生成写入功能.该表的相关数据是:

File name: write
Caller: “-” (i.e. Not Applicable)
Syscall name: write
Args: Ci:ibn
Strong name: __libc_write
Weak names: __write, write
Run Code Online (Sandbox Code Playgroud)

不是那么神秘,除了Ci:ibn.C表示"可取消".冒号将返回类型与参数类型分开,如果您想要更深入地解释它们的含义,那么您可以在shell脚本中看到生成代码的注释libc/sysdeps/unix/make-syscalls.sh.

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

#define SYS_ify(syscall_name) __NR_##syscall_name
Run Code Online (Sandbox Code Playgroud)

啊,旧的令牌贴纸:P.所以基本上,这个的实现__libc_write只不过是使用一个名为参数的syscall函数的代理调用__NR_write,以及其他参数.

人行道结束的地方......

我知道这是一段令人着迷的旅程,但现在我们已经到了GNU libc的末尾.该数字__NR_write由Linux定义.对于32位X86架构,它将使您linux/arch/x86/include/asm/unistd_32.h:

#define __NR_write 4
Run Code Online (Sandbox Code Playgroud)

那么,唯一需要关注的是syscall的实现.我可能会在某些方面做些什么,但是现在我只想指出一些如何向Linux添加系统调用的参考资料.

  • 请将其作为答案发布,不要只链接到外部来源.(特别是不是博客,因为博客臭名昭着地进行了大修并丢失了内容.) (16认同)
  • 我认为我们对博客的信心比对博客更有信心,因为它可以说有更大的利害关系.更重要的是,不要将它放在这里使得它不能通过这些机制进行搜索.如果您希望将来可编辑,请将其设为社区维基.也许你应该在这里链接你的博客,以保持一致性?只是一个想法. (2认同)

Mac*_*ade 16

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

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

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

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

  • printf()适用于没有内核或基于环的架构的系统 (9认同)

Tro*_*nic 5

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

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

  • 全部正确,但仅供参考,供在此线程中发布的其他人使用,大多数现代操作系统和体系结构现在使用特殊的操作码来实际执行系统调用(例如 x86 上的 sysenter 和 sysexit),而不是使用软件中断来提高性能。 (3认同)

Dan*_*nin 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”出现在屏幕上。如果要花很多时间,那么通过深入内核来放大接触点(以与“橡皮筋对路”类似)是很有教育意义的。我猜想您将不得不经历多个抽象层,例如缓冲的输出流,字符设备等。请确保发布结果,以决定继续进行此操作:)


Ter*_*fey -7

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