printf()
のような関数は、最後のステップでインラインアセンブリを使用して定義されるといつも思っていました。 stdio.hの奥深くには、CPUに実際に何をすべきかを指示するasmコードが埋め込まれています。たとえば、dosでは、文字列の先頭をメモリ位置またはレジスタに最初にmov
してから、int
teruptを呼び出すことによって実装されたことを覚えています。
ただし、x64バージョンのVisual Studioはインラインアセンブラをまったくサポートしていないため、C/C++にアセンブラ定義関数がまったくないのではないかと思いました。 printf()
のようなライブラリ関数は、アセンブラコードを使用せずにC/C++でどのように実装されますか?何が実際に正しいソフトウェア割り込みを実行しますか?ありがとう。
Printf()のようなライブラリ関数は、アセンブラコードを使用せずにC/C++でどのように実装されますか?何が実際に正しいソフトウェア割り込みを実行しますか?
ほとんどの実用的な目的では、BIOSを実際に呼び出すことはできません Linuxから または Windowsから 。実際、オペレーティングシステムやブートローダーを作成している場合を除いて、BIOSを操作する必要はまったくありません。
printf()
のようなC関数について具体的に質問しているので、ここで提供するのは、GNUの 「ゴムが道路と出会う場所」 を見つけるために行った小さなトレースです。 libc。 ネタバレ注意:syscall() で終了します。
システムコール はBIOSではなく、基本的なサービスを実行するためにOSが持つと予想されるパラメーターを含む番号付き関数のテーブルです。ある意味では、「いくつかのパラメーターを使用して合意された規則である番号で何かを呼び出す」という意味で、同様です。それはすべてのソフトウェアの一種ですが、おそらく違いを強調する必要があります:リアルモードのハードウェアではなく、OSと話しているのです。
それで、ここに、GCCのprintf
...を特に掘り下げて、簡単に退屈しない人のために:
もちろん、ファイル_libc/libio/stdio.h
_で定義されているprintfのプロトタイプから始めます。
_extern int printf (__const char *__restrict __format, ...);
_
ただし、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;
}
_
同じファイル内のマクロは、この関数が下線のないprintfのエイリアスとして定義されるように関連付けを設定します。
_ldbl_strong_alias (__printf, printf);
_
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)
_
なぜそんなに奇妙なのかという質問を避けて、同じファイルにある謎めいたPUTCに依存していることがわかります。
_#define PUTC(C, F) IO_putwc_unlocked (C, F)
_
_IO_putwc_unlocked
_の_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)))
_
しかし、少し読みにくいにもかかわらず、バッファリングされた出力を実行しているだけです。ファイルポインタのバッファに十分なスペースがある場合は、文字をそのバッファに貼り付けるだけですが、そうでない場合は、___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);
}
_
基本的に、ファイルポインタは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).
*/
_
したがって、_IO_OVERFLOW
_で_libc/libio/genops.c
_を見つけると、ファイルポインタで_“1-parameter” __overflow
_メソッドを呼び出すマクロであることがわかります。
_#define IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)
_
さまざまなファイルポインタタイプのジャンプテーブルは、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)
_
__IO_new_file_overflow
_を__IO_file_overflow
_と同等にする#defineもあり、前者は同じソースファイルで定義されています。 (注: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)
_
現在、_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;
}
_
悲しいことに、私たちは再び立ち往生しています…__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;
}
_
今ではwriteを呼び出すだけです!さて、その実装はどこにありますか? _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__)))
のマクロです
これは書き込みのプロトタイプにすぎません。 Linux用のwrite.cファイルはGNU標準ライブラリにはありません。代わりに、さまざまな方法で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
_
_Ci:ibn
_を除いて、それほど神秘的ではありません。 Cは「キャンセル可能」を意味します。コロンは戻り値の型を引数の型から分離します。それらの意味をより深く説明したい場合は、コード_libc/sysdeps/unix/make-syscalls.sh
_を生成するシェルスクリプトのコメントを参照してください。
そのため、このシェルスクリプトによって生成される__libc_writeという関数にリンクできることが期待されています。しかし、何が生成されているのでしょうか。 sysdeps/unix /sysdep.hにあるSYS_ifyと呼ばれるマクロを介して書き込みを実装するいくつかのCコード
_#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
_
したがって、残っているのはsyscallの実装だけです。これはいつか行うかもしれませんが、今のところは、 Linuxにシステムコールを追加する方法 に関するいくつかのリファレンスを紹介します。
まず、リングの概念を理解する必要があります。
カーネルはリング0で実行されます。つまり、メモリとオペコードへのフルアクセスがあります。
プログラムは通常リング3で実行されます。メモリへのアクセスが制限されており、すべてのオペコードを使用できるわけではありません。
したがって、ソフトウェアがより多くの特権(ファイルを開く、ファイルへの書き込み、メモリの割り当てなど)を必要とする場合、カーネルに問い合わせる必要があります。
これはさまざまな方法で実行できます。ソフトウェア割り込み、SYSENTERなど。
printf()関数を使用したソフトウェア割り込みの例を見てみましょう。
1-ソフトウェアがprintf()を呼び出します。
2-printf()は文字列と引数を処理し、リング3ではファイルへの書き込みを実行できないため、カーネル関数を実行する必要があります。
3-printf()はソフトウェア割り込みを生成し、カーネル関数(この場合はwrite()関数)の番号をレジスタに配置します。
4-ソフトウェアの実行が中断され、命令ポインタがカーネルコードに移動します。これで、カーネル関数のリング0になりました。
5-カーネルはリクエストを処理し、ファイルに書き込みます(stdoutはファイル記述子です)。
6-完了すると、カーネルはiret命令を使用して、ソフトウェアのコードに戻ります。
7-ソフトウェアのコードは続行されます。
したがって、C標準ライブラリの関数はCで実装できます。必要なのは、より多くの特権が必要なときにカーネルを呼び出す方法を知ることだけです。
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(そのようなファイルまたはディレクトリはありません) 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 close(3)= 0 access( "/ 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 = 1 811128、...})= 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、MAP__NO 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)= 0x7fa6__4000 Arch_SET_FS、0x7fa6dc0c5700)= 0 mprotect(0x7fa6dbeba000、16384、PROT_READ)= 0 mprotect(0x600000、4096、PROT_READ)= 0 mprotect(0x7fa6dc0e7000、 OT_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」が画面に表示されるポイント。カーネルに飛び込むことによって(「道路に対するゴム」のアナロジーにとどまるために)接点にズームインすることは、時間がかかる努力であれば教育的であることは確実です。バッファリングされた出力ストリーム、文字デバイスなど、抽象化のいくつかのレイヤーを通過する必要があると思います。これをフォローアップすることにした場合は、必ず結果を投稿してください:)
標準ライブラリ関数は、基盤となるプラットフォームライブラリ(UNIX APIなど)に実装されているか、直接システムコール(C関数のまま)によって実装されています。システムコールは(私が知っているプラットフォームでは)インラインasmを使用した関数の呼び出しによって内部的に実装され、システムコール番号とパラメーターをCPUレジスターに入れ、カーネルが処理する割り込みをトリガーします。
システムコール以外にもハードウェアと通信する方法は他にもありますが、最新のオペレーティングシステムで実行している場合、これらは通常利用できないか、制限されています。少なくとも、それらを有効にするには、いくつかのシステムコールが必要です。デバイスはメモリマップされている可能性があるため、特定のメモリアドレスへの書き込み(通常のポインタを介して)がデバイスを制御します。 I/Oポートもよく使用され、アーキテクチャによっては、特別なCPUオペコードによってアクセスされるか、特定のアドレスにメモリマップされている場合があります。
セミコロンとコメントを除くすべてのC++ステートメントは、CPUに何をすべきかを指示するマシンコードになります。 Assemblyを使用せずに、独自のprintf関数を作成できます。アセンブリで記述しなければならない操作は、ポートからの入力と出力、および割り込みを有効または無効にする操作だけです。
ただし、アセンブリは、パフォーマンス上の理由から、システムレベルのプログラミングで引き続き使用されます。インラインアセンブリはサポートされていませんが、アセンブリで別のモジュールを作成し、それをアプリケーションにリンクすることを妨げるものは何もありません。
一般に、ライブラリ関数はプリコンパイルされ、広告オブジェクトを配布します。インラインアセンブラは、パフォーマンス上の理由から特定の状況でのみ使用されますが、これは例外であり、規則ではありません。実際、printfはインラインアセンブルするのに適した候補ではないようです。 Insetad、memcpy、またはmemcmpのような機能。非常に低レベルの関数は、ネイティブアセンブラ(masm?gnu asm?)によってコンパイルされ、ライブラリ内のオブジェクトとして配布される場合があります。