73

printf()のような関数は、最後のステップで、インライン アセンブリを使用して定義されていると常に考えていました。stdio.h の奥深くには、実際に CPU に何をすべきかを伝える asm コードが埋め込まれています。たとえば、DOS では、 teruptmovを呼び出すよりも、最初に文字列の先頭をメモリ ロケーションまたはレジスタに ing することによって実装されたことを覚えています。int

しかし、Visual Studio の x64 バージョンはインライン アセンブラをまったくサポートしていないため、C/C++ にアセンブラ定義の関数がまったくないのはなぜだろうと思いました。printf()アセンブラ コードを使用せずに C/C++ で実装されるようなライブラリ関数はどのようになりますか? 適切なソフトウェア割り込みを実際に実行するのは何ですか? ありがとう。

4

6 に答える 6

19

まず、リングの概念を理解する必要があります。
カーネルはリング0で実行されます。つまり、カーネルはメモリとオペコードに完全にアクセスできます。
プログラムは通常、リング3で実行されます。メモリへのアクセスが制限されており、すべてのオペコードを使用できるわけではありません。

したがって、ソフトウェアがより多くの特権(ファイルを開く、ファイルへの書き込み、メモリの割り当てなど)を必要とする場合、カーネルに問い合わせる必要があります。
これは多くの方法で行うことができます。ソフトウェア割り込み、SYSENTERなど。printf

()関数を使用したソフトウェア割り込みの例を見てみましょう。1-
ソフトウェアはprintf()を呼び出します。
2-printf()は文字列と引数を処理し、リング3ではファイルへの書き込みを実行できないため、カーネル関数を実行する必要があります。
3-printf()はソフトウェア割り込みを生成し、カーネル関数(この場合はwrite()関数)の番号をレジスタに配置します。
4-ソフトウェアの実行が中断され、命令ポインタがカーネルコードに移動します。これで、カーネル関数のリング0になりました。
5-カーネルはリクエストを処理し、ファイルに書き込みます(stdoutはファイル記述子です)。
6-完了すると、カーネルはiret命令を使用して、ソフトウェアのコードに戻ります。
7-ソフトウェアのコードは続行されます。

したがって、C標準ライブラリの機能はCで実装できます。必要なのは、より多くの特権が必要なときにカーネルを呼び出す方法を知ることだけです。

于 2010-03-14T17:26:32.373 に答える
5

Linuxでは、straceユーティリティを使用すると、プログラムによって行われたシステムコールを確認できます。だから、このようなプログラムを取る

    int main(){
    printf( "x");
    0を返します。
    }

たとえば、次のようにコンパイルしてからprintxstrace 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 = 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)=?

ゴムは、トレースの最後から2番目の呼び出しで道路に出会う(分類、以下を参照)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」の文字が画面に表示されるように変更されています。カーネルに飛び込むことによって(「道路に対するゴム」のアナロジーにとどまるために)接点にズームインすることは、時間がかかる努力であれば教育的であることは確実です。バッファリングされた出力ストリーム、文字デバイスなど、抽象化のいくつかのレイヤーを移動する必要があると思います。これをフォローアップすることにした場合は、必ず結果を投稿してください:)

于 2012-11-29T18:48:33.497 に答える
4

標準ライブラリ関数は、基盤となるプラットフォーム ライブラリ (UNIX API など) や直接システム コール (依然として C 関数) によって実装されます。システム コールは (私が知っているプラ​​ットフォームでは) システム コール番号とパラメータを CPU レジスタに格納し、カーネルが処理する割り込みをトリガーするインライン asm を使用した関数への呼び出しによって内部的に実装されます。

syscall 以外にもハードウェアと通信する方法は他にもありますが、最新のオペレーティング システムで実行している場合、これらは通常利用できないか、むしろ制限されています。デバイスは、(通常のポインターを介して) 特定のメモリーアドレスへの書き込みがデバイスを制御するように、メモリーマップされている場合があります。I/O ポートもよく使用され、アーキテクチャに応じて、これらは特別な CPU オペコードによってアクセスされるか、特定のアドレスにマップされたメモリである場合があります。

于 2010-03-14T17:11:15.283 に答える
1

セミコロンとコメントを除くすべての C++ ステートメントは、最終的に CPU に何をすべきかを指示するマシン コードになります。アセンブリに頼らずに、独自の printf 関数を作成できます。アセンブリで書かなければならない操作は、ポートからの入出力と、割り込みの有効化と無効化だけです。

ただし、アセンブリは、パフォーマンス上の理由から、システム レベルのプログラミングで引き続き使用されます。インライン アセンブリはサポートされていませんが、アセンブリで別のモジュールを作成してアプリケーションにリンクすることを妨げるものは何もありません。

于 2010-03-14T17:13:44.163 に答える
0

通常、ライブラリ関数はプリコンパイルされ、広告オブジェクトを配布します。インライン アセンブラは、パフォーマンス上の理由から特定の状況でのみ使用されますが、これは例外であり、規則ではありません。実際には、printf はインライン アセンブルするのに適しているとは思えません。挿入、memcpy、または memcmp のような関数。非常に低レベルの関数は、ネイティブ アセンブラ (masm? gnu asm?) によってコンパイルされ、ライブラリ内のオブジェクトとして配布される場合があります。

于 2010-03-14T17:16:15.397 に答える
-7

コンパイラは、C/C++ ソース コードからアセンブリを生成します。

于 2010-03-14T17:09:29.633 に答える