1. copy_{to,from}_user() とは何かカーネル空間とユーザー空間間の通信のためのブリッジです。すべてのデータ操作では、このようなインターフェースを使用する必要があります。しかし、彼の役割は正確には何でしょうか?私たちは次のような疑問を提起します。
注意: この記事のコード分析は Linux-4.18.0 に基づいており、一部のアーキテクチャ関連のコードは ARM64 で表されています。 1. copy_{to,from}_user() と memcpy()
各種ブログでは、主に1点目について意見が集中しています。第一の点は広く認識されているようです。しかし、練習に重点を置く人は、結局のところ、練習をすれば完璧になるという別の見方に至ります。真実は少数の人々の手にあるのでしょうか?それとも人々の目は鋭くなったのでしょうか?もちろん、私は上記の見解を否定するものではありません。また、どの見解が正しいかを保証することもできません。なぜなら、かつては完璧だった理論であっても、時間が経ったり、具体的な状況が変わったりすると、もはや正しくなくなる可能性があると私は信じているからです。たとえば、ニュートンの古典力学の理論(これは少し無理があるように思えます)。人間の言葉で言えば、Linux のコードベースは時間の経過とともに絶えず変化している、ということになります。おそらく、上記の見解はかつては正しかったのでしょう。もちろん、それは今でも正しいかもしれません。以下の分析は私の意見です。同様に、私たちは懐疑的な姿勢を保つ必要があります。 2. 関数の定義まず、memcpy() と copy_{to,from}_user() の関数定義を見てみましょう。パラメータはほぼ同じで、すべて宛先アドレス、送信元アドレス、コピーするバイト サイズが含まれています。 静的 __always_inline 符号なし long __must_check copy_to_user(void __user *to、const void *from、unsigned long n); 静的 __always_inline 符号なし long __must_check copy_from_user(void *to、const void __user *from、unsigned long n); void *memcpy(void *dest, const void *src, size_t len); しかし、確かにわかっていることが一つあります。つまり、memcpy() は渡されたアドレスの正当性をチェックしません。そして、copy_{to,from}_user() は、受信アドレスに対して次のような有効性チェックを実行します (簡単に言うと、検証の詳細についてはコードを参照してください)。
この簡単な比較の後、他の違いを見て、上記の 2 つの点について説明しましょう。 2番目のポイントから始めましょう。練習に関しては、練習すれば完璧になるということを私は今でも信じています。私のテストの結果から、実装結果は 2 つの状況に分かれます。 最初のケースの結果は、memcpy() を使用してテストすると、問題はなく、コードは正常に実行されるということです。テスト コードは次のとおりです (proc ファイル システムの file_operations に対応する読み取りインターフェイス関数のみが表示されます)。 静的ssize_t test_read(構造体ファイル*ファイル、char __user *buf、 size_t 長さ、loff_t *オフセット) { memcpy(buf, "test\n", 5); /* copy_to_user(buf, "test\n", 5) */ 5を返します。 } ファイルの内容を読み込むには cat コマンドを使用します。cat は read システム コールを通じて test_read を呼び出し、渡される buf サイズは 4k です。テストは順調に進み、結果も有望でした。 「テスト」文字列が正常に読み取られました。 2番目の点は正しいようです。しかし、まだ検証と調査を継続する必要があります。最初のポイントで述べたように、「このページ フォールト例外はカーネル空間で明示的に修復される必要があります。」したがって、次の状況も確認する必要があります。buf にユーザー空間の仮想アドレス空間が割り当てられているが、物理メモリとの特定のマッピング関係が確立されていない場合、この場合はカーネル モード ページ フォールトが発生します。まずこの条件を作成し、一致する buf を見つけて、それをテストする必要があります。もちろん私はこれをテストしませんでした。テストの結論があるからです(主に私が怠け者なので、この条件を構築するのが面倒だからです)。このテストは、私の友人で、ソン先生の「アシスタント教師」としても知られるアッカーマンが実施したものです。彼はかつてこの実験を行い、buf と物理メモリの間に特定のマッピング関係がない場合でも、コードは正常に実行できるという結論に達しました。ページ フォールトはカーネル状態で発生し、カーネル状態によって修復されます (特定の物理メモリを割り当て、ページ テーブルを埋め、マッピング関係を確立します)。同時に、コードの観点から分析しましたが、結論は同じでした。 以上の解析の結果、memcpy() も普通に使えるようです。安全性を考えると copy_{to,from}_user() などのインターフェースを使うのが推奨されます。 2 番目のケースの結果、上記のテスト コードが正しく実行されず、カーネル oops がトリガーされます。もちろん、このテストのカーネル構成オプションは前回のテストのものと異なります。この構成項目は、CONFIG_ARM64_SW_TTBR0_PAN または CONFIG_ARM64_PAN (ARM64 プラットフォームの場合) です。両方の構成オプションの機能は、カーネル モードがユーザー アドレス空間に直接アクセスするのを防ぐことです。唯一の違いは、 PAN (Privileged Access Never) 機能が必要なのはなぜですか?その理由は、ユーザー空間とカーネル空間間のデータのやり取りによってセキュリティ上の問題が簡単に発生する可能性があるため、カーネル空間がユーザー空間に簡単にアクセスできないようにしているためです。そうする必要がある場合は、特定のインターフェイスを介して PAN を閉じる必要があります。一方、PAN 機能は、カーネル モードとユーザー モードのデータ相互作用のためのインターフェイスの使用をさらに標準化できます。 PAN 機能を有効にすると、カーネルまたはドライバー開発者は、システムのセキュリティを向上させるために、copy_{to,from}_user() などのセキュリティ インターフェイスを使用するように強制される可能性があります。 memcpy() のような非標準の操作の場合、カーネルがエラーを出します。 不適切なプログラミングによりセキュリティ上の脆弱性が生じます。たとえば、Linux カーネルの脆弱性 CVE-2017-5123 により、権限が昇格される可能性があります。この脆弱性が導入される理由は、ユーザーが渡したアドレスの正当性をチェックする access_ok() が不足していることです。したがって、独自のコードによってもたらされるセキュリティ上の問題を回避するには、カーネル空間とユーザー空間データ間の相互作用に特に注意する必要があります。 2. CONFIG_ARM64_SW_TTBR0_PANの原則CONFIG_ARM64_SW_TTBR0_PAN設計の背後にある原則。 ARM64 の特殊なハードウェア設計により、2 つのページ テーブル ベース アドレス レジスタ ttbr0_el1 と ttbr1_el1 を使用します。プロセッサは、64 ビット アドレスの上位 16 ビットに基づいて、アクセスされたアドレスがユーザー空間に属するかカーネル空間に属するかを判断します。ユーザー空間アドレスの場合は ttbr0_el1 を使用し、それ以外の場合は ttbr1_el1 を使用します。したがって、ARM64 プロセスを切り替える場合は、ttbr0_el1 の値を変更するだけで済みます。すべてのプロセスが同じカーネル空間アドレスを共有するため、ttbr1_el1 は変更しないことを選択できます。 プロセスがカーネル状態 (割り込み、例外、システム コールなど) に切り替わるときに、カーネル状態がユーザー状態アドレス空間にアクセスするのを防ぐにはどうすればよいですか?実際、ttbr0_el1 の値を不正なマッピングを指すように変更するだけでよいことは簡単にわかります。そのため、この目的のために特別なページテーブルを用意します。ページテーブルのサイズは 4k メモリで、その値はすべて 0 です。プロセスがカーネル モードに切り替わるときに、ttbr0_el1 の値をページ テーブルのアドレスに変更すると、ユーザー空間アドレスへのアクセスが不正にならないようにすることができます。ページ テーブルの値が不正であるためです。この特別なページ テーブル メモリは、リンカー スクリプトによって割り当てられます。 #RESERVED_TTBR0_SIZE (PAGE_SIZE) を定義します セクション { reserved_ttbr0 = .; . += RESERVED_TTBR0_SIZE; swapper_pg_dir = .; . += SWAPPER_DIR_SIZE; swapper_pg_end = .; } この特別なページ テーブルは、カーネル ページ テーブルと一緒に配置されます。 swapper_pg_dir とのサイズ差はわずか 4k です。 reserved_ttbr0 アドレスから始まる 4k メモリ空間の内容がクリアされます。 カーネル状態に入ると、__uaccess_ttbr0_disable を介して ttbr0_el1 を切り替えてユーザー空間アドレス アクセスを無効にし、アクセスが必要なときに _uaccess_ttbr0_enable を介してユーザー空間アドレス アクセスを有効にします。 2 つのマクロ定義は複雑ではありません。原理を説明するために、_uaccess_ttbr0_disable を例に挙げてみましょう。その定義は次のとおりです。 マクロ __uaccess_ttbr0_disable、tmp1 mrs \tmp1, ttbr1_el1 // swapper_pg_dir (1) ビック \tmp1, \tmp1, #TTBR_ASID_MASK sub \tmp1, \tmp1, #RESERVED_TTBR0_SIZE // reserved_ttbr0 直前 // スワッパー_pg_dir (2) msr ttbr0_el1, \tmp1 // 予約済みのTTBR0_EL1を設定 (3) イスブ \tmp1、\tmp1、#RESERVED_TTBR0_SIZE を追加 msr ttbr1_el1, \tmp1 // 予約済みのASIDを設定 イスブ .endm
__uaccess_ttbr0_disable に対応する C 言語実装はここにあります。カーネル モードでユーザー空間アドレスにアクセスできるようにするにはどうすればよいでしょうか?これも非常に単純で、__uaccess_ttbr0_disable の逆操作であり、ttbr0_el1 に有効なページ テーブル ベース アドレスを与えます。ここで繰り返す必要はありません。ここで知っておくべきことは、CONFIG_ARM64_SW_TTBR0_PAN が設定されている場合、copy_{to,from}_user() インターフェイスにより、コピー前にカーネル モードがユーザー空間にアクセスでき、コピーが完了した後にカーネル モードがユーザー空間にアクセスする機能が無効になるということです。したがって、copy_{to,from}_user() を使用するのが正統的なアプローチです。主にセキュリティチェックやセキュリティアクセス処理に反映されます。これは memcpy() よりも優れている最初の機能であり、別の重要な機能は後で紹介されます。 これで、前のセクションで残された質問に答えることができます。 memcpy() を引き続き使用するにはどうすればよいですか?これで非常に簡単になりました。memcpy() を呼び出す前に、uaccess_enable_not_uao() を介してカーネル モードがユーザー空間アドレスにアクセスできるようにし、memcpy() を呼び出し、最後に uaccess_disable_not_uao() を介してカーネル モードがユーザー空間にアクセスする機能を無効にします。 3. テスト上記のテスト ケースはすべて、ユーザー空間で正当なアドレスを渡すテストに基づいています。正当なユーザー空間アドレスとは何でしょうか?システム コールを通じてユーザー空間から要求された仮想アドレス空間に含まれるアドレス範囲は、有効なアドレスです (マッピング関係を確立するために物理ページが割り当てられているかどうかに関係なく)。インターフェース プログラムを作成しているため、プログラムの堅牢性も考慮する必要があります。ユーザーによって渡されるすべてのパラメーターが正当であると想定することはできません。参加者の不正送信の発生を予測し、事前に準備しておくこと、つまり雨の日に備えておくことが重要です。 まず、ランダムな無効なアドレスを渡して、memcpy() のテスト ケースを使用します。テストの結果、カーネル oops がトリガーされることが判明しました。 memcpy() テストの代わりに copy_{to,from}_user() を引き続き使用します。テストの結果、read() はエラーを返すだけで、カーネル oops はトリガーされないことがわかりました。これが私たちが望んでいる結果です。結局のところ、アプリケーションはカーネル oops をトリガーできないようにする必要があります。このメカニズムの実装原理は何ですか? copy_to_user() を例に挙げてみましょう。関数呼び出しフローは次のとおりです。 _arch_copy_to_user() は ARM64 プラットフォーム上のアセンブリ コードで実装されており、この部分のコードは重要です。 終了 .req x5 エントリ(__arch_copy_to_user) uaccess_enable_not_uao x3、x4、x5 は無効です 終了、x0、x2 を追加 #include "copy_template.S" uaccess_disable_not_uao x3、x4 です 移動 x0, #0 戻る ENDPROC(__arch_copy_to_user) .section .fixup,"ax" .align 2 9998: sub x0, end, dst // バイトはコピーされません 戻る 。前の
前回の分析の結果と比較すると、実際には _arch_copy_to_user() は次の関係とほぼ同等であると言えます。 uaccess_enable_not_uao(); memcpy(ubuf, kbuf, サイズ); == __arch_copy_to_user(ubuf, kbuf, サイズ); uaccess_disable_not_uao(); まず、copy_template.S が memcpy() である理由を説明するメッセージを挿入します。 memcpy() は、ARM64 プラットフォーム上のアセンブリ コードによって実装されています。これは、arch/arm64/lib/memcpy.S ファイルで定義されています。 .弱いメモリコピー エントリ(__memcpy) エントリ(memcpy) #include "copy_template.S" 戻る ENDPIPROC(memcpy) ENDPROC(__memcpy) したがって、明らかに、memcpy() と __memcpy() 関数の定義は同じです。また、memcpy() 関数は弱い関数として宣言されているため、memcpy() 関数は書き換えることができます (少し無理があります)。もう少し詳しく説明しましょう。なぜアセンブリを使用するのでしょうか? lib/string.c ファイルで memcpy() 関数を使用しないのはなぜですか?もちろん、これは memcpy() の実行速度を最適化するためです。 lib/string.c ファイルの memcpy() 関数はバイトをコピーします (最良のハードウェアでも粗雑なコードによって台無しになる可能性があります)。ただし、最近のプロセッサのほとんどは 32 ビットまたは 64 ビットなので、4 バイト、8 バイト、さらには 16 バイトをコピーすることも可能です (アドレス アライメントを考慮)。実行速度を大幅に向上できます。したがって、ARM64 プラットフォームではアセンブリ実装が使用されます。この部分の知識については、こちらのブログ「memcpy の最適化と ARM64 の実装」を参照してください。 要点に戻って繰り返します。カーネル状態がユーザー空間アドレスにアクセスしてページ フォールトがトリガーされると、ユーザー空間アドレスが正当である限り、カーネル状態は何も起こらなかったかのように例外を修復します (物理メモリを割り当て、ページ テーブル マッピング関係を確立します)。ただし、不正なユーザー空間アドレスにアクセスする場合は、パス 2 を選択して、回復を試みてください。この方法は、 静的void __do_kernel_fault(unsigned long addr, unsigned int esr, 構造体pt_regs *regs) { /* * このカーネル障害に対処する準備はできていますか? * 命令の誤りを処理する準備はほぼ確実に整っていません。 */ if (!is_el1_instruction_abort(esr) && fixup_exception(regs)) 戻る; /* ... */ } fixup_exception() は search_exception_tables() を呼び出し、_extable セクションを検索します。 __extable セグメントには例外テーブルが格納され、各エントリには例外アドレスとそれに対応する修復アドレスが格納されます。例えば、前述の 9998:subx0,end,dst 命令のアドレスを見つけ、do_page_fault() 関数の戻りアドレスを変更することで、ジャンプ修復機能を実現します。実際、検索プロセスは、問題のアドレス addr に基づいて、_extable セグメント (例外テーブル) 内に対応する例外テーブル エントリがあるかどうかを調べることです。ある場合は、修復できることを意味します。 32 ビット プロセッサと 64 ビット プロセッサの実装方法は異なるため、まず 32 ビット プロセッサの例外テーブルの実装原理から説明します。 _extable セグメントの最初と最後のアドレスは、__start___ex_table と __stop___ex_table です (include/asm-generic/vmlinux.lds.h で定義されています)。このメモリ セグメントは配列と見なすことができ、各要素は struct exception_table_entry 型で、例外が発生したアドレスとそれに対応する修復アドレスを記録します。 例外テーブル __start___ex_table --> +---------------+ | エントリー | +---------------+ | エントリー | +---------------+ ... | +---------------+ | エントリー | +---------------+ | エントリー | __stop___ex_table --> +---------------+ 32 ビット プロセッサでは、struct exception_table_entry は次のように定義されます。 構造体例外テーブルエントリ{ 符号なしロング insn、修正; }; 1 つ明確にしておく必要があるのは、32 ビット プロセッサでは、unsigned long は 4 バイトであるということです。 insn と fixup には、それぞれ例外発生アドレスとそれに対応する修正アドレスが格納されます。例外アドレス ex_addr に従って対応する修復アドレスを検索します (見つからない場合は 0 を返します)。回路図コードは次のとおりです。 符号なしロング search_fixup_addr32(符号なしロング ex_addr) { 定数構造体 exception_table_entry *e; (e = __start___ex_table; e < __stop___ex_table; e++) の場合 (ex_addr == e->insn) の場合 e->fixup を返します。 0を返します。 }
64 ビット プロセッサの開発が始まったときに、この方法を使い続けると、例外テーブルを格納するために 32 ビット プロセッサの 2 倍のメモリが必要になります (アドレスを格納するのに 8 バイト必要になるため)。したがって、カーネルは別の方法を使用してそれを実装します。 64 プロセッサでは、 構造体例外テーブルエントリ{ int insn、修正; }; 各例外テーブル エントリが占有するメモリは 32 ビット プロセッサと同じなので、メモリ使用量は変わりません。しかし、insn と fixup の意味は変わりました。 insn と fixup はそれぞれ、例外が発生したアドレスと、現在の構造体メンバー アドレスを基準とした修復アドレスのオフセットを格納します (少しわかりにくいです)。たとえば、例外アドレス ex_addr に従って、対応する修復アドレスが検索され (見つからない場合は 0 が返されます)、回路図コードは次のようになります。 符号なしlong search_fixup_addr64(符号なしlong ex_addr) { 定数構造体 exception_table_entry *e; (e = __start___ex_table; e < __stop___ex_table; e++) の場合 (ex_addr == (unsigned long)&e->insn + e->insn) の場合 (unsigned long)&e->fixup + e->fixup を返します。 0を返します。 } したがって、ここでは exception_table_entry の構築方法に焦点を当てます。ユーザー空間アドレスへのメモリアクセスごとに例外テーブルエントリを作成し、それを _extable セグメントに挿入する必要があります。例えば、次のアセンブリ命令です(アセンブリ命令に対応するアドレスは任意に記述されているので、正しいか間違っているかは気にしないでください。原則を理解することが鍵となります)。 0xffff000000000000: ldr x1、[x0] 0xffff000000000004: x1、x1、#0x10 を加算 0xffff000000000008: ldr x2、[x0、#0x10] /* ... */ 0xffff000040000000: 移動 x0、#0xffffffffffffffff2 // -14 0xffff000040000004: 戻り x0 レジスタがユーザー空間アドレスを保持していると仮定すると、アドレス 0xffff0000000000000 のアセンブリ命令の例外テーブル エントリを作成する必要があり、x0 が不正なユーザー空間アドレスである場合、ジャンプによって返される修復アドレスは 0xffff000040000000 になると予想されます。計算を簡単にするために、これが最初のエントリの作成であり、__start___ex_table の値が 0xffff000080000000 であると仮定します。最初の例外テーブルエントリの insn および fixup メンバーの値は、0x80000000 および 0xbffffffc (両方の値は負) になります。したがって、copy{to,from}user() アセンブリ コード内の各ユーザー空間アドレス アクセス命令に対してエントリが作成されます。したがって、アドレス 0xffff0000000000008 のアセンブリ命令でも例外テーブル エントリを作成する必要があります。 では、カーネル モードが不正なユーザー空間アドレスにアクセスすると、具体的に何が起こるのでしょうか?上記の分析プロセスは次のように要約できます。
IV. 結論ここで、復習とまとめを行い、copy_{to,from}_user() についての考察はここで終わります。この記事をまとめで終わりにしたいと思います。 カーネル モードまたはユーザー モードで正当なユーザー空間アドレスにアクセスする場合、仮想アドレスが物理アドレスとのマッピング関係を確立しないと、ページ フォールト プロセスはほぼ同じになり、物理メモリに適用してマッピング関係を作成するのに役立ちます。したがって、この場合、memcpy() と copy_{to,from}_user() は似ています。 カーネル状態が不正なユーザー空間アドレスにアクセスすると、例外アドレスに基づいて修復アドレスが見つかります。この例外修復方法では、アドレス マッピング関係は確立されませんが、do_page_fault() の戻りアドレスが変更されます。 memcpy() ではこれができません。 最後に、場合によっては memcpy() が正常に動作することもあるということを述べておきます。ただし、これも推奨されておらず、適切なプログラミング方法でもありません。ユーザー空間とカーネル空間のデータのやり取りでは、copy_{to,from}_user() に似たインターフェースを使用する必要があります。なぜ似ているのでしょうか?カーネル空間とユーザー空間のデータのやり取りには他のインターフェースもありますが、copy_{to,from}_user() ほど有名ではありません。たとえば、{get,put}_user() です。 copy_{to, from}_user() に関するこの記事はこれで終わりです。より関連性の高いコピーとユーザーコンテンツについては、123WORDPRESS.COM の過去の記事を検索するか、以下の関連記事を引き続き参照してください。今後とも 123WORDPRESS.COM をよろしくお願いいたします。 以下もご興味があるかもしれません:
|
<<: CSS を使用して波状のウォーターボール効果を実装するためのサンプルコード
>>: 4種類のMySQL接続とマルチテーブルクエリの詳細な説明
序文トップメニューを作成する場合、ポップアップのセカンダリメニューを作成する必要があります。 以前の...
目次1. 準備2. 減圧3. 統合を開始する1. 準備Ckeditor_4.5.7_full + C...
私たちの日常的な開発作業では、テキストのオーバーフロー、切り捨て、省略は、考慮する必要がある非常に一...
みなさんこんにちは。今日は12連休ですが、何かお買い物はしましたか?今日は「Linux View S...
Vue $set 配列コレクションオブジェクトの割り当てVue カスタム配列オブジェクト コレクショ...
1. イメージをプルするdocker pull レジストリ.cn-hangzhou.aliyuncs...
1. Docker環境を構築する1. Dockerfileを作成する Centos:latest か...
一般的に使用される Oracle10g パーティションは、範囲 (範囲パーティション)、リスト (リ...
最近、nginx-ingress-controller のアプリケーションについて説明した公開アカウ...
Windows10 Home Edition でHyper-vを有効にする方法をまだ探していますか?...
フロントエンドのデザイン案では、「X」や「>」の形をした閉じるボタンや、他の 3 方向の白抜き...
フレームセットと本文は同じレベルにあるため、本文にフレームセットを配置することはできません。まずペー...
コード例: パブリッククラスJDBCDemo3 { パブリック静的voiddemo3_1(){ bo...
目次解決策1: EUIの転送コンポーネントをコピーして変更し、プロジェクトディレクトリに導入する解決...
目次1.まずネットワークカードの設定ディレクトリに入る2. ifcfg-ens33ネットワークカード...