Jac*_*ack 70 c++ inline-assembly
我一直认为printf()
在最后一步中使用内联汇编定义函数.在stdio.h的内部深处埋藏了一些asm代码,它实际上告诉CPU要做什么.例如,在dos中,我记得它是通过首先mov
将字符串的开头添加到某个内存位置或寄存器而不是调用int
terupt来实现的.
但是,由于Visual Studio的x64版本根本不支持内联汇编程序,因此我想知道在C/C++中根本不会有汇编程序定义的函数.库函数如何printf()
在不使用汇编代码的情况下在C/C++中实现?什么实际执行正确的软件中断?谢谢.
Hos*_*ork 131
如何在不使用汇编程序代码的情况下在C/C++中实现类似printf()的库函数?什么实际执行正确的软件中断?
对于大多数实际用途,您无法从Linux或Windows中真正调用BIOS .实际上,您根本不想与BIOS交互 - 除非您正在编写操作系统或引导程序.
因为你特别询问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_unlocked
in 的定义时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
.因为当你用完缓冲区时唯一的选择是刷新屏幕(或你的文件指针代表的任何设备),我们可以希望在那里找到神奇的咒语.
如果你猜到我们将要经历另一个令人沮丧的间接水平,你就是对的.查看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_OVERFLOW
在libc/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添加系统调用的参考资料.
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中实现.它所要做的就是知道如何在需要更多权限时调用内核.
标准库函数在底层平台库(例如UNIX API)上和/或通过直接系统调用(仍然是C 函数)来实现。系统调用(在我知道的平台上)是通过调用具有内联汇编的函数来内部实现的,该函数将系统调用号和参数放入 CPU 寄存器中,并触发内核随后处理的中断。
除了系统调用之外,还有其他与硬件通信的方式,但在现代操作系统下运行时,这些方式通常不可用或相当有限,或者至少启用它们需要一些系统调用。设备可以是内存映射的,以便对某些内存地址的写入(通过常规指针)控制设备。I/O 端口也经常被使用,并且根据架构,这些端口由特殊的 CPU 操作码访问,或者它们也可能被内存映射到特定地址。
在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”出现在屏幕上。如果要花很多时间,那么通过深入内核来放大接触点(以与“橡皮筋对路”类似)是很有教育意义的。我猜想您将不得不经历多个抽象层,例如缓冲的输出流,字符设备等。请确保发布结果,以决定继续进行此操作:)
归档时间: |
|
查看次数: |
9493 次 |
最近记录: |