Linux カーネルの copy_{to, from}_user() に関する考察

Linux カーネルの copy_{to, from}_user() に関する考察

1. copy_{to,from}_user() とは何か

カーネル空間とユーザー空間間の通信のためのブリッジです。すべてのデータ操作では、このようなインターフェースを使用する必要があります。しかし、彼の役割は正確には何でしょうか?私たちは次のような疑問を提起します。

  • copy_{to,from}_user() はなぜ必要なのでしょうか。また、それは舞台裏で何を行うのでしょうか。
  • copy_{to,from}_user() と memcpy() の違いは何ですか? memcpy() を直接使用できますか?
  • memcpy() が copy_{to,from}_user() に置き換わると、問題が発生しますか?

注意: この記事のコード分析は Linux-4.18.0 に基づいており、一部のアーキテクチャ関連のコードは ARM64 で表されています。

1. copy_{to,from}_user() と memcpy()

  • memcpy() と比較すると、copy_{to,from}_user() には、受信アドレスの有効性に関する追加チェックがあります。たとえば、ユーザー空間のアドレス範囲に属しているかどうかなどです。理論的には、カーネル空間はユーザー空間から渡されたポインターを直接使用できます。データのコピーが必要な場合でも、memcpy() を直接使用できます。実際、MMU のないアーキテクチャでは、copy_{to,from}_user() の最終的な実装は memcpy() を使用します。しかし、MMU を備えたほとんどのプラットフォームでは、状況は多少異なります。ユーザー空間から渡されるポインターは仮想アドレス空間にあり、それが指す仮想アドレス空間は実際の物理ページに実際にはマップされていない可能性があります。しかし、私たちはそれに対して何ができるでしょうか?ページ フォールトによって発生した例外はカーネルによって透過的に修復され (新しい物理ページがページ フォールトのアドレス空間に送信されます)、ページ フォールトにアクセスした命令は何も起こらなかったかのように実行され続けます。しかし、これはユーザー空間でのページ フォールト例外の動作に過ぎません。カーネル空間では、このページ フォールト例外を明示的に修復する必要があります。これは、カーネルが提供するページ フォールト例外処理機能の設計パターンによって決まります。その背後にある考え方は、カーネル モードでは、プログラムが物理ページにまだコミットされていないユーザー空間アドレスにアクセスしようとすると、カーネルはこれに対して警戒する必要があり、ユーザー空間のように気づかないわけにはいかないというものです。
  • ユーザー モードで渡されるポインターの正確性を確認すれば、copy_{to,from}_user() を memcpy() 関数に完全に置き換えることができます。いくつかの実験テストを行った結果、memcpy() を使用してプログラムを実行しても問題がないことがわかりました。したがって、ユーザーモードポインタの安全性を確保しながら、2つを置き換えることができます。

各種ブログでは、主に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() は、受信アドレスに対して次のような有効性チェックを実行します (簡単に言うと、検証の詳細についてはコードを参照してください)。

  • データがユーザー空間からカーネル空間にコピーされる場合、ユーザー空間アドレス to と to にコピーされるバイトの長さ n を加えたものが、ユーザー空間アドレス空間内にある必要があります。
  • カーネル空間からユーザー空間にデータをコピーする場合は、アドレスの正当性も確認する必要があります。たとえば、範囲外アクセスであるかどうか、コード セグメント内のデータであるかどうかなどです。つまり、すべての違法行為は直ちに停止される必要があるのです。

この簡単な比較の後、他の違いを見て、上記の 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 プラットフォームの場合) です。両方の構成オプションの機能は、カーネル モードがユーザー アドレス空間に直接アクセスするのを防ぐことです。唯一の違いは、 CONFIG_ARM64_SW_TTBR0_PANソフトウェア シミュレーションを通じてこの機能を実装するのに対し、CONFIG_ARM64_PAN はハードウェア (ARMv8.1 拡張機能) を通じてこの機能を実装することです。分析オブジェクトとして CONFIG_ARM64_SW_TTBR0_PAN を使用します (分析を提供するコードがあるのはソフトウェア シミュレーションのみです)。ちなみに、ハードウェアがサポートしていない場合は、CONFIG_ARM64_PAN を設定しても無駄であり、ソフトウェア エミュレーション方式しか使用できません。ユーザー空間アドレスにアクセスする必要がある場合は、copy_{to,from}_user() のようなインターフェースを使用する必要があります。そうしないと、カーネル oops が発生します。

CONFIG_ARM64_SW_TTBR0_PANオプションをオンにした後、上記のコードをテストするとカーネル oops が発生します。その理由は、カーネル状態がユーザー空間アドレスに直接アクセスするためです。したがって、この場合は memcpy() は使用できません。 copy_{to,from}_user() を使用する以外に選択肢はありません。

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
  • ttbr1_el1 はカーネル ページ テーブル ベース アドレスを格納するため、その値は swapper_pg_dir になります。
  • swapper_pg_dir から RESERVED_TTBR0_SIZE を引いたものが、上で説明した特別なページ テーブルです。
  • もちろん、ttbr0_el1 をこの特別なページ テーブル ベース アドレスを指すように変更すると、その後のユーザー アドレスへのアクセスが不正になることが保証されます。

__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() を例に挙げてみましょう。関数呼び出しフローは次のとおりです。

copy_to_user()->_copy_to_user()->raw_copy_to_user()->__arch_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 // バイトはコピーされません 
        戻る 
        。前の
  • uaccess_enable_not_uao と uaccess_disable_not_uao は、上記のユーザー空間にアクセスするためのカーネル モードのスイッチです。
  • copy_template.S ファイルは、memcpy() 関数のアセンブリ実装です。後で memcpy() の実装コードを見ると、それがよくわかります。
  • .section.fixup,“ax”権限 ax ('a' 再配置可能セグメント、'x' 実行可能セグメント) を持つ ".fixup" という名前のセクションを定義します。ラベル 9998 の命令は後処理用です。 copy_{to,from}_user() の戻り値の意味を覚えていますか?コピーが成功した場合は 0 を返し、それ以外の場合はコピーが残っているバイト数を返します。このコード行は、コピーされていない残りのバイト数を計算します。不正なユーザー空間アドレスにアクセスすると、ページフォールトが必ず発生します。この場合、カーネル状態で発生したページフォールトは復帰時に修復されていないため、例外が発生したアドレスに戻って実行を継続することは絶対に不可能です。したがって、システムには 2 つの選択肢があります。最初の選択肢はカーネル OOP であり、現在のプロセスに SIGSEGV シグナルを送信します。2 番目の選択肢は、例外が発生したアドレスに戻るのではなく、修復されたアドレスを選択して戻ることです。 memcpy() を使用している場合は、最初のオプションのみ使用できます。しかし、copy_{to,from}_user() には 2 番目のオプションがあります。この修復機能を実装するには、.fixup セグメントが使用されます。コピー処理中に不正なユーザー空間アドレスにアクセスすると、do_page_fault() によって返されるアドレスは 9998 番になります。このとき、コピーされていない残りのバイトの長さを計算でき、プログラムは実行を継続できます。

前回の分析の結果と比較すると、実際には _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 を選択して、回復を試みてください。この方法は、 .fixup__ex_tableセクションを使用することです。状況を救う方法がない場合、現在のプロセスに SIGSEGV シグナルを送信することしかできません。さらに、エラーはカーネル oops またはパニックである可能性があります (カーネル構成オプション CONFIG_PANIC_ON_OOPS によって異なります)。カーネル モードで不正なユーザー空間アドレスにアクセスすると,do_page_fault()最終的に no_context ラベルの do_kernel_fault() にジャンプします。

静的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を返します。 
}


32 ビット プロセッサでは、例外テーブル エントリの作成は比較的簡単です。 copy{to,from}user() アセンブリ コード内のユーザー空間アドレスにアクセスする命令ごとにエントリが作成され、insn には現在の命令に対応するアドレスが格納され、fixup には修復命令に対応するアドレスが格納されます。

64 ビット プロセッサの開発が始まったときに、この方法を使い続けると、例外テーブルを格納するために 32 ビット プロセッサの 2 倍のメモリが必要になります (アドレスを格納するのに 8 バイト必要になるため)。したがって、カーネルは別の方法を使用してそれを実装します。 64 プロセッサでは、 struct exception_table_eは次のように定義されます。

構造体例外テーブルエントリ{ 
        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 のアセンブリ命令でも例外テーブル エントリを作成する必要があります。

では、カーネル モードが不正なユーザー空間アドレスにアクセスすると、具体的に何が起こるのでしょうか?上記の分析プロセスは次のように要約できます。

  • 0xffff000000000000:ldrx1,[x0]
  • MMUが例外をトリガーする
  • CPUはdo_page_fault()を呼び出す
  • do_page_fault() は search_exception_table() を呼び出します (regs->pc == 0xffff000000000000)
  • _extable セグメントを調べ、0xffff000000000000 を見つけ、修復アドレス 0xffff000040000000 を返します。
  • do_page_fault()は関数の戻りアドレス(regs->pc = 0xffff000040000000)を変更し、
  • プログラムは実行を継続し、エラーを処理します。
  • 関数の戻り値 x0 = -EFAULT (-14) を変更して戻ります (ARM64 は関数の戻り値を x0 経由で渡します)

IV. 結論

ここで、復習とまとめを行い、copy_{to,from}_user() についての考察はここで終わります。この記事をまとめで終わりにしたいと思います。

カーネル モードまたはユーザー モードで正当なユーザー空間アドレスにアクセスする場合、仮想アドレスが物理アドレスとのマッピング関係を確立しないと、ページ フォールト プロセスはほぼ同じになり、物理メモリに適用してマッピング関係を作成するのに役立ちます。したがって、この場合、memcpy() と copy_{to,from}_user() は似ています。

カーネル状態が不正なユーザー空間アドレスにアクセスすると、例外アドレスに基づいて修復アドレスが見つかります。この例外修復方法では、アドレス マッピング関係は確立されませんが、do_page_fault() の戻りアドレスが変更されます。 memcpy() ではこれができません。

CONFIG_ARM64_SW_TTBR0_PANまたはCONFIG_ARM64_PANが有効になっている場合 (ハードウェアがサポートしている場合のみ有効)、copy_{to,from}_user() インターフェイスのみを使用できます。memcpy() を直接使用することはできません。

最後に、場合によっては memcpy() が正常に動作することもあるということを述べておきます。ただし、これも推奨されておらず、適切なプログラミング方法でもありません。ユーザー空間とカーネル空間のデータのやり取りでは、copy_{to,from}_user() に似たインターフェースを使用する必要があります。なぜ似ているのでしょうか?カーネル空間とユーザー空間のデータのやり取りには他のインターフェースもありますが、copy_{to,from}_user() ほど有名ではありません。たとえば、{get,put}_user() です。

copy_{to, from}_user() に関するこの記事はこれで終わりです。より関連性の高いコピーとユーザーコンテンツについては、123WORDPRESS.COM の過去の記事を検索するか、以下の関連記事を引き続き参照してください。今後とも 123WORDPRESS.COM をよろしくお願いいたします。

以下もご興味があるかもしれません:
  • Linux カーネルプログラミングにおけるコンテナの of() 関数の紹介
  • Linuxカーネルマクロcontainer_ofの詳細な分析
  • Linuxカーネルのcontainer_of関数の詳細な説明
  • VMware Workstation のインストール (Linux カーネル) Kylin グラフィック チュートリアル
  • LinuxカーネルマクロContainer_Ofの詳細な説明

<<:  CSS を使用して波状のウォーターボール効果を実装するためのサンプルコード

>>:  4種類のMySQL接続とマルチテーブルクエリの詳細な説明

推薦する

変換を使用して純粋な CSS ポップアップ メニューを実装するためのサンプル コード

序文トップメニューを作成する場合、ポップアップのセカンダリメニューを作成する必要があります。 以前の...

Ckeditor + Ckfinderを使用したJavaScriptファイルアップロードケースの詳細な説明

目次1. 準備2. 減圧3. 統合を開始する1. 準備Ckeditor_4.5.7_full + C...

純粋な CSS で「テキストオーバーフローの切り捨てと省略」を実装するいくつかの方法

私たちの日常的な開発作業では、テキストのオーバーフロー、切り捨て、省略は、考慮する必要がある非常に一...

Linuxでサーバーのハードウェア情報を表示する方法

みなさんこんにちは。今日は12連休ですが、何かお買い物はしましたか?今日は「Linux View S...

vue $setは配列コレクションオブジェクトへの値の割り当てを実装します

Vue $set 配列コレクションオブジェクトの割り当てVue カスタム配列オブジェクト コレクショ...

Docker で Oracle 11g イメージ構成をプルダウンする際の問題を分析する

1. イメージをプルするdocker pull レジストリ.cn-hangzhou.aliyuncs...

docker を使用して Redis マスター/スレーブを構築する方法

1. Docker環境を構築する1. Dockerfileを作成する Centos:latest か...

Oracle10パーティションとMySQLパーティションの違いの詳細な説明

一般的に使用される Oracle10g パーティションは、範囲 (範囲パーティション)、リスト (リ...

nginx-ingress-controller ログ永続化ソリューションのソリューション

最近、nginx-ingress-controller のアプリケーションについて説明した公開アカウ...

Hyper-V なしで Windows 10 を動作させるソリューション

Windows10 Home Edition でHyper-vを有効にする方法をまだ探していますか?...

after疑似要素を使用して中空の三角矢印とXアイコンを実装する例

フロントエンドのデザイン案では、「X」や「>」の形をした閉じるボタンや、他の 3 方向の白抜き...

iframe を介してフレームセットを本体に配置する

フレームセットと本文は同じレベルにあるため、本文にフレームセットを配置することはできません。まずペー...

SQL インジェクション脆弱性プロセスの例と解決策

コード例: パブリッククラスJDBCDemo3 { パブリック静的voiddemo3_1(){ bo...

大量のデータを含むエレメントのシャトルボックスで「すべて選択」をクリックするとスタックする問題の解決方法

目次解決策1: EUIの転送コンポーネントをコピーして変更し、プロジェクトディレクトリに導入する解決...

LinuxでIPアドレスを手動で設定するための詳細な手順

目次1.まずネットワークカードの設定ディレクトリに入る2. ifcfg-ens33ネットワークカード...