NULLポインターのすばらしい旅

今日は、NULLポインタがどのように形成されるかを示しますか?もちろん、オペレーティングシステムを詳しく調べて、NULL命令にアクセスするとセグメントフォールトエラーが報告される理由を確認する必要があります。

おそらく誰もがコンピューターに触れたときに、特にC言語をプレイする人は、NULLポインタープログラムを作成しました。たとえば、メモリ空間を割り当てる前に、初期化されたばかりのint型のポインターがこのポインターに割り当てられ、その後、操作中にセグメント障害エラーが発生します。

#include <stdio.h>

int main()
{
    int*p = NULL;

    *p = 123;
    return 0;
}


root:~/test$ ./a.out
Segmentation fault (core dumped)

ほんの数行のコードですが、オペレーティングシステムで長い「旅」を経験しました。今日は、この素晴らしい旅を探検します。

旅行を始める

プログラムをコンパイルしたら、。/ a.outを使用して実行します。オペレーティングシステムでは、bashを使用して子プロセスが作成されます。この子プロセスはNULLポインタープログラムです。サブプロセスの作成方法については、プロセスによって作成された関連記事を参照できます。子プロセスが作成されると、NULLポインタープログラムの内容がexecプログラムを通じてロードされます。プログラムが実行されると、オペレーティングシステムはNULLポインタープログラムの各セグメントをロードします

プログラムが実行されると、オペレーティングシステムは自動的にプログラムのさまざまなセクションをマウントします。一般的なセクションは次のとおりです。

  • データセグメント:読み取り専用データセグメントと、読み取りおよび書き込み可能なデータセグメントに分割
  • コードセグメント:私たちが書いたコード、一般的な権限はRXです
  • ヒープ:通常、malloによって要求されたメモリ領域またはmmapをマップするために使用されます
  • スタック:通常、関数呼び出しの関数パラメーターを格納するために使用され、関数ジャンプを保存するために使用されます。
  • 共有ライブラリ:これはすべてのプロセスに存在する必要があります。一部のプログラムはgibcにカプセル化された関数を使用する必要があり、Glicライブラリが必要です。

ランニングトリップ

すべての環境がセットアップされたら、プログラムはその使命を実行する必要があります。NULLポインタープログラムを逆アセンブルできます。逆アセンブルの内容は多数あります。ここでは、main関数の逆アセンブルのみを確認します。ここでは、aarch64-linux- gnu-objdumpツールチェーン

0000000000400530 <main>:
  400530:       d10043ff        sub     sp, sp, #0x10
  400534:       f90007ff        str     xzr, [sp,#8]
  400538:       f94007e0        ldr     x0, [sp,#8]
  40053c:       52800f61        mov     w1, #0x7b                       // #123
  400540:       b9000001        str     w1, [x0]
  400544:       52800000        mov     w0, #0x0                        // #0
  400548:       910043ff        add     sp, sp, #0x10
  40054c:       d65f03c0        ret

主な機能に行くことができるすべては、オペレーティングシステムがいくつかのことをするのを手伝うことです、今のところこの部分に注意を払っていません。メイン関数に到達すると、最初にスタックをプッシュし、次にCPUがstr w1、[x0]命令を実行します対応するC言語は* p = 123です。CPUがこのステートメントを実行すると、次の操作が発生します。

  • CPUは最初に仮想アドレスをMMUに送信し、MMUハードウェアユニットに仮想アドレスから物理アドレスへのルックアップテーブルを実行させ、それを変換させます。
  • 同時に、MMUハードウェアユニットは、仮想アドレスアクセスが境界を超えているかどうかを確認するために、いくつかの仮想アドレス許可チェックや、読み取りおよび書き込み許可なども行います。
  • 仮想アドレスと物理アドレスのマッピング関係がMMUハードウェアユニットにすでに存在する場合、CPUがアクセスを実行するために物理アドレスが直接返されます。
  • MMUハードウェアユニット内の仮想アドレスと物理アドレスの間にマッピングがない場合、ページフォールト例外がトリガーされ、仮想-リアルマッピングが確立されます。
  • 同時に、仮想-リアルマッピングの方が時間がかかるため、TLBは最近アクセスされた仮想-リアルマッピング関係をキャッシュするために使用され、変換速度を上げるためにテーブルルックアップの前にTLBがアクセスされます。
  • この例では、* pのアドレスはNULLです。CPUがアクセスを実行すると、MMUはアドレスが不正であると判断し、データアボート例外がトリガーされます。
  • 例外をトリガーすると、対応するアーキテクチャの例外ベクターテーブルにジャンプして実行されます。ここでは、ARM64を例として取り上げます

異常な旅行

CPUはNULLアドレスにアクセスし、MMUが不正にアクセスされたことを検出すると、例外をトリガーし、ARM64例外ベクタテーブルにジャンプして実行します。

/*
 * Exception vectors.
 */
	.pushsection ".entry.text", "ax"

	.align	11
ENTRY(vectors)
	kernel_ventry	1, sync_invalid			// Synchronous EL1t
	kernel_ventry	1, irq_invalid			// IRQ EL1t
	kernel_ventry	1, fiq_invalid			// FIQ EL1t
	kernel_ventry	1, error_invalid		// Error EL1t

	kernel_ventry	1, sync				// Synchronous EL1h
	kernel_ventry	1, irq				// IRQ EL1h
	kernel_ventry	1, fiq_invalid			// FIQ EL1h
	kernel_ventry	1, error			// Error EL1h

	kernel_ventry	0, sync				// Synchronous 64-bit EL0
	kernel_ventry	0, irq				// IRQ 64-bit EL0
	kernel_ventry	0, fiq_invalid			// FIQ 64-bit EL0
	kernel_ventry	0, error			// Error 64-bit EL0

ARM64アーキテクチャでは、EL0、EL1、EL2、EL3の4つの異常レベルが定義されています。EL0はユーザー空間、EL1はLinuxカーネル、El2はハイパー、EL3はセキュアモードです。現在、例外はEL0からトリガーされ、EL0例外処理ハンドラーにジャンプします

/*
 * EL0 mode handlers.
 */
	.align	6
el0_sync:
	kernel_entry 0
	mrs	x25, esr_el1			// read the syndrome register
	lsr	x24, x25, #ESR_ELx_EC_SHIFT	// exception class
	cmp	x24, #ESR_ELx_EC_SVC64		// SVC in 64-bit state
	b.eq	el0_svc
	cmp	x24, #ESR_ELx_EC_DABT_LOW	// data abort in EL0
	b.eq	el0_da
	cmp	x24, #ESR_ELx_EC_IABT_LOW	// instruction abort in EL0
	b.eq	el0_ia
	cmp	x24, #ESR_ELx_EC_FP_ASIMD	// FP/ASIMD access
	b.eq	el0_fpsimd_acc
	cmp	x24, #ESR_ELx_EC_SVE		// SVE access
	b.eq	el0_sve_acc
	cmp	x24, #ESR_ELx_EC_FP_EXC64	// FP/ASIMD exception
	b.eq	el0_fpsimd_exc
	cmp	x24, #ESR_ELx_EC_SYS64		// configurable trap
	ccmp	x24, #ESR_ELx_EC_WFx, #4, ne
	b.eq	el0_sys
	cmp	x24, #ESR_ELx_EC_SP_ALIGN	// stack alignment exception
	b.eq	el0_sp_pc
	cmp	x24, #ESR_ELx_EC_PC_ALIGN	// pc alignment exception
	b.eq	el0_sp_pc
	cmp	x24, #ESR_ELx_EC_UNKNOWN	// unknown exception in EL0
	b.eq	el0_undef
	cmp	x24, #ESR_ELx_EC_BREAKPT_LOW	// debug exception in EL0
	b.ge	el0_dbg
	b	el0_inv

データ例外DateAbort、命令例外IABort、スタックアラインメント例外、PCアラインメント例外など、多くの種類の例外があることがわかります。そして、どのような異常が存在するのかをどのようにして知っていますか?これは、対応する例外タイプを取得するためにESRレジスタを読み取ることによるものです。

  • ビット[31:26]は、例外のタイプ、例外クラスを決定するために使用されます
  • ビット[25]:異常な命令の長さを決定するために使用、0は16ビットの異常な命令を表し、1は32ビットの異常を表す
  • ビット[24:0]:特定の例外を決定するために使用され、各例外タイプはこのフィールドを個別に定義します
  • 詳細については、ARMマニュアルを参照してください。
el0_da:
	/*
	 * Data abort handling
	 */
	mrs	x26, far_el1
	enable_daif
	ct_user_exit
	clear_address_tag x0, x26
	mov	x1, x25
	mov	x2, sp
	bl	do_mem_abort
	b	ret_to_user

ここで起こったのはデータアボート例外で、el0_daにジャンプし、最終的にdo_mem_abortハンドラー関数にジャンプします

static const struct fault_info fault_info[] = {
	{ do_bad,		SIGKILL, SI_KERNEL,	"ttbr address size fault"	},
	{ do_bad,		SIGKILL, SI_KERNEL,	"level 1 address size fault"	},
	{ do_bad,		SIGKILL, SI_KERNEL,	"level 2 address size fault"	},
	{ do_bad,		SIGKILL, SI_KERNEL,	"level 3 address size fault"	},
	{ do_translation_fault,	SIGSEGV, SEGV_MAPERR,	"level 0 translation fault"	},
	{ do_translation_fault,	SIGSEGV, SEGV_MAPERR,	"level 1 translation fault"	},
	{ do_translation_fault,	SIGSEGV, SEGV_MAPERR,	"level 2 translation fault"	},
	{ do_translation_fault,	SIGSEGV, SEGV_MAPERR,	"level 3 translation fault"	},

asmlinkage void __exception do_mem_abort(unsigned long addr, unsigned int esr,
					 struct pt_regs *regs)
{
	const struct fault_info *inf = esr_to_fault_info(esr);

	if (!inf->fn(addr, esr, regs))
		return;
}

ESRレジスターの値を介して対応する例外タイプを取得し、対応する例外処理関数を、fault_info配列の添え字として例外タイプを使用して取得します。EL0で発生したため、この例に対応する例外処理関数はdo_translation_faultです。アドレス変換エラー。

static int __kprobes do_translation_fault(unsigned long addr,
					  unsigned int esr,
					  struct pt_regs *regs)
{
	if (is_ttbr0_addr(addr))
		return do_page_fault(addr, esr, regs);

	do_bad_area(addr, esr, regs);
	return 0;
}

ここで、例外アドレスに従って、それが現在EL0であるか他のモード例外であるかを判断します。EL0例外に属するaddr = 0x0なので、例外をさらに処理するためにdo_page_faultにジャンプします。do_page_faultは、ページフォールト例外のカーネルの総処理インターフェイスです。あらゆる種類のページ違反例外を処理します。

  • 仮想アドレスが正当である場合、仮想-実マッピングを確立するために、仮想アドレスのページテーブルが作成されます
  • 仮想アドレスアクセスが不正で、アドレスがカーネルアドレス空間に属している場合、直接パニックになります
  • 仮想アドレスが正当な場合は権限も遵守され、仮想アドレスが読み取り専用の場合、書き込まれた場合は例外が発生します。
  • ユーザー空間の仮想不正仮想アドレスの場合、通常、上位層にはプログラムを終了するためのシグナルが通知されます
  • NULLポインタープログラムの場合、SIGSEGVシグナルは最終的にアプリケーションに通知されます
arm64_force_sig_fault(SIGSEGV,fault == VM_FAULT_BADACCESS ? SEGV_ACCERR : SEGV_MAPERR,
		(void __user *)addr, inf->name);

カーネルは最終的にarm64_force_sig_faultを呼び出してアプリケーションに通知します。ここでの信号タイプはSIGSEGV、不正アクセスです。

信号受信トラベル

シグナルは非同期通信方式であり、プロセスは別のプロセスにシグナルを送ることができますが、シグナル処理はカーネルに実装されています。信号のタイプは次のとおりです。

 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

この例で発生するシグナルはSIGSEGVです。シグナルの通常の方法は次のとおりです。

  • プロセスインストールシグナルは、sigactionシステムで呼び出すことができます。インストールシグナルは、シグナルが発生したときにシグナルを処理するために、シグナルのコールバック関数を設定する必要があります。
  • たとえば、Kill -9 PIDはプロセスを強制終了できますが、同時にプロセスはシグナルを受信し、シグナルのインストール機能を処理します。

信号受信のプロセス、ここではコード分析なし:

  • sigactionがシグナルをインストールすると、システムコールをトリガーし、カーネルスペースにトラップして、このプロセスのシグナルアクションを設定します。
  • このプロセスがSIGSEGVなどのシグナルを受信すると、シグナルの損失を防止しないために、sigqueue構造を使用してシグナルが管理されます。
  • これは信号受信キューとして理解でき、受信信号はエンキューによって管理されます。もちろん優先順位のような戦略があります
  • シグナルがキューに入ると、保留中のキューに入れられて処理されます。このとき、シグナルを処理する必要のあるプロセスが起こされます。

信号処理旅行

信号はいつでも処理できません。ユーザー空間に戻るときに信号処理があるかどうかを確認してください。

/*
 * Ok, we need to do extra processing, enter the slow path.
 */
work_pending:
	mov	x0, sp				// 'regs'
	bl	do_notify_resume
	ldr	x1, [tsk, #TSK_TI_FLAGS]	// re-check for single-step
	b	finish_ret_to_user
/*
 * "slow" syscall return path.
 */
ret_to_user:
	disable_daif
	ldr	x1, [tsk, #TSK_TI_FLAGS]
	and	x2, x1, #_TIF_WORK_MASK
	cbnz	x2, work_pending
finish_ret_to_user:
	enable_step_tsk x1, x2
	kernel_exit 0
ENDPROC(ret_to_user)

ret_to_userがユーザースペースに戻ると、処理する追加の項目があるかどうかを確認し、ある場合はdo_notify_resumeにジャンプし、thread_infoのフラグフラグを判断して、追加の処理があるかどうかを判断します。

asmlinkage void do_notify_resume(struct pt_regs *regs,
				 unsigned long thread_flags)
{

	do {
		/* Check valid user FS if needed */
		addr_limit_user_check();

		if (thread_flags & _TIF_NEED_RESCHED) {
			/* Unmask Debug and SError for the next task */
			local_daif_restore(DAIF_PROCCTX_NOIRQ);

			schedule();
		} else {
			if (thread_flags & _TIF_SIGPENDING)
				do_signal(regs);

		}

	} while (thread_flags & _TIF_WORK_MASK);
}
  • ユーザー空間に戻るときに処理する必要がある2つの一般的なこと
    • 1つは、NEED_RESCHEdフラグが設定されているかどうかを確認することにより、現在のプロセスがスケジューリングを必要とするかどうかを確認することです
    • 1つは、保留中のシグナルがあるかどうかを確認することです。ある場合は、do_signalでシグナルを処理します。

一般的なプロセスは、get_signalを介して優先度の高い信号処理を見つけ、対応する信号処理ハンドラー(sigactionを介して設定されたコールバック関数)を返すことです。最後にhanle_signal関数を呼び出して信号を処理します。

static void setup_return(struct pt_regs *regs, struct k_sigaction *ka,
			 struct rt_sigframe_user_layout *user, int usig)
{
	__sigrestore_t sigtramp;

	regs->regs[0] = usig;
	regs->sp = (unsigned long)user->sigframe;
	regs->regs[29] = (unsigned long)&user->next_frame->fp;
	regs->pc = (unsigned long)ka->sa.sa_handler;

	if (ka->sa.sa_flags & SA_RESTORER)
		sigtramp = ka->sa.sa_restorer;
	else
		sigtramp = VDSO_SYMBOL(current->mm->context.vdso, sigtramp);

	regs->regs[30] = (unsigned long)sigtramp;
}

ここでシグナルスタックの概念を確立する必要がありますが、信号処理関数をユーザー空間に戻るPCポインタに設定することで、ユーザー空間に戻るときに信号処理関数が呼び出されます。処理後、sigreturnシステムコールを介してカーネルクリーンスタックフレーム操作に戻ります。

                                            

旅行に登録する

私たちのNULLポインタープログラムから、インストールシグナルはありません、なぜセグメンテーション違反を受け取るのですか?実際、これはglibCが私たちのために行うことです。glibcコードをダウンロードする。

/* Standard signals  */
  init_sig (SIGHUP, "HUP", N_("Hangup"))
  init_sig (SIGINT, "INT", N_("Interrupt"))
  init_sig (SIGQUIT, "QUIT", N_("Quit"))
  init_sig (SIGILL, "ILL", N_("Illegal instruction"))
  init_sig (SIGTRAP, "TRAP", N_("Trace/breakpoint trap"))
  init_sig (SIGABRT, "ABRT", N_("Aborted"))
  init_sig (SIGFPE, "FPE", N_("Floating point exception"))
  init_sig (SIGKILL, "KILL", N_("Killed"))
  init_sig (SIGBUS, "BUS", N_("Bus error"))
  init_sig (SIGSEGV, "SEGV", N_("Segmentation fault"))

glibcがフラグ信号の処理関数をいくつかインストールしていることがわかります。したがって、NULL命令にアクセスした後、セグメンテーション違反が発生します。

旅行の概要

  • アプリケーションが起動すると、sigactionシステムがglibcで呼び出され、フラグ信号の信号処理関数を設定します
  • CPUが仮想アドレスに0x0にアクセスすると、データアボート例外がトリガーされ、カーネル状態になります。
  • カーネルモードは、ESRレジスタに従って対応する例外タイプを取得し、対応する例外処理関数do_translation_faultを呼び出します。
  • アドレスが処理できないユーザー空間アドレスの場合、SIGSEGVシグナルがsigqueueキューに送信され、対応するシグナル処理機能がウェイクアップされます。
  • ユーザー空間に戻ると、信号処理があるかどうかを確認し、ある場合はdo_signal関数にジャンプして信号を処理します。
  • do_signal関数で、get_signal関数を介して信号に対応するコールバック処理関数を取得し、信号のスタックフレームを確立します。
  • 信号処理関数ハンドラーをアプリケーションのPCポインターに設定し、ユーザー層に戻ると信号のコールバック関数を処理します
  • このとき、glibcで設定したSIGSEGV信号に対応するコールバック関数が呼び出され、「セグメンテーション違反」エラーが発生します。
  • 処理後、カーネルスペースによって作成されたスタックフレームに戻り、sigreturnシステムコールを介してクリーンになり、その後、ユーザースペースに戻って実行されます。
  • この時点で、単純なNULLポインターの旅は終わりました。これは非常に複雑です。

 

187件の元の記事を公開 108 件を獲得 37万回表示

おすすめ

転載: blog.csdn.net/longwang155069/article/details/104789808