システムコールとカーネルモード
レジスタ・戻り先・権限の変化から、関数呼出し、システムコール、ページフォルトの処理を理解します。
説明
アプリは通常ユーザーモードで実行され、ファイルや装置へのアクセスをOSへ依頼します。LinuxのAArch64環境では通常のアプリがEL0、カーネルがEL1で動きます。ELはCPUの例外レベルで、ユーザーIDのrootかどうかとは別の仕組みです。
3バイトを出力するwriteシステムコールを例に、レジスタとメモリの小さなモデルを操作します。呼出し前の値、例外への入口、戻り先、エラー値を順に確認し、最後にページフォルトと割込みの記録を比較します。レジスタ名と番号はLinuxのAArch64 ABIに合わせます。
1出力するバイトとレジスタ
出力データはASCIIのOKと改行です。x0はファイル記述子、x1はバッファ先頭、x2は長さ、x8はシステムコール番号です。1は標準出力、64はこのABIのwriteに対応します。
data = b'OK\n'出力バイト列; regs = {'x0': 1, 'x1': 0x4000, 'x2': len(data), 'x8': 64}引数と呼出し番号; print(data.hex(' '))バイト表示; print(regs)レジスタ値4f 4b 0aの3バイトです。x1の16384は0x4000の10進表示で、文字そのものではなくユーザー仮想アドレスです。カーネルはこの範囲へのアクセスを検査してデータを参照します。
2通常の関数呼出し
まず同じEL0内の関数呼出しを計算します。0x1000にあるblが0x2000へ分岐する例では、次の命令0x1004がリンクレジスタx30へ入ります。
state = {'EL': 0, 'PC': 0x1000}呼出し前; state['x30'] = state['PC'] + 4戻り先を保存; state['PC'] = 0x2000関数先頭へ; print(state['EL'], hex(state['PC']), hex(state['x30']))EL・現在位置・戻り先ELは0のままです。関数呼出しはプログラム内で制御を移す操作であり、呼び出す関数がライブラリに含まれていても、それだけでカーネル権限になるわけではありません。
3svc命令で例外へ入る
0x1010でsvc #0を実行する場合を計算します。A64命令は4バイトです。通常のEL0からEL1へのSVC例外では、復帰アドレスELR_EL1はsvcの次の命令になります。
state = {'EL': 0, 'PC': 0x1010}svcの実行位置; state['saved_EL'] = state['EL']元のELをモデルに保存; state['ELR_EL1'] = state['PC'] + 4次命令の位置; state['EL'] = 1カーネル側へ; print(state['EL'], hex(state['ELR_EL1']))例外レベルと復帰先EL1へ移り、復帰先は0x1014です。実際には例外ベクタからカーネルの入口処理へ進み、元の状態はSPSR_EL1などにも保存されます。svc #0の0がwrite番号ではなく、x8=64が呼出し番号です。
4指定された範囲の検査
バッファ全体が0x4000〜0x4fffの読取り可能な領域に入るかを検査するモデルです。端のアドレスを含めると混乱するので、終端は含まない区間で扱います。
lo, hi = 0x4000, 0x5000読取り可能な範囲; start, size = regs['x1'], regs['x2']バッファの指定; print(lo <= start and start + size <= hi)範囲内か; print(hex(start), hex(start + size))先頭と終端対象は[0x4000, 0x4003)で領域内です。バッファがユーザー領域にあることだけでなく、対象ページの権限やコピー時の障害も実際のカーネルでは扱います。この計算は一つの連続領域への境界検査に絞っています。
5書込み成功からユーザーモードへ戻る
3バイトすべてを受け付けたとき、writeの戻り値は3です。x0へ戻り値を入れ、例外復帰で元のELと復帰先を使う過程を計算します。
regs['x0'] = 3受け付けたバイト数; state['EL'] = state['saved_EL']元のELへ; state['PC'] = state['ELR_EL1']保存した位置へ; print(regs['x0'], state['EL'], hex(state['PC']))復帰状態戻り値3、EL0、PC=0x1014です。writeは要求より少ない正のバイト数を返す場合もあるため、常に長さ全体が書けたとは限りません。戻り値が何を表すかはシステムコールごとの仕様です。
6カーネルのエラー値とライブラリの返り値
不正なファイル記述子でEBADF=9になった場合を使います。AArch64 Linuxの生の戻り値は負のエラー番号で、libcのラッパーは-1とerrnoへ変換します。
raw = -9生のエラー値; x0_bits = raw & ((1 << 64) - 1)64ビット表現; result, errno_value = (-1, -raw) if -4095 <= raw < 0 else (raw, 0)ラッパーの変換モデル; print(hex(x0_bits), result, errno_value)レジスタ・返り値・errnoレジスタの64ビット表現は0xfffffffffffffff7です。Cから見る戻り値は-1、errnoは9です。成功時にerrnoが必ず0になるという規則はなく、ここでの0はモデルの初期値です。エラー判定はまず戻り値の仕様に従います。
7ページフォルト後に同じ命令を再実行
ページフォルトは、メモリアクセス命令に同期して生じる例外です。対応付けのあるページがまだメモリにない場合、OSが準備してから同じ命令を再開できることがあります。
fault = {'PC': 0x3000, 'address': 0x9000, 'mapped': True, 'present': False}ページ未準備の状態; fault['ELR_EL1'] = fault['PC']障害を起こした命令を保存; fault['present'] = Trueページの準備完了; print(hex(fault['ELR_EL1']), fault['present'])再開位置と状態復帰先は次命令ではなく0x3000です。これは解決可能なフォルトの例です。そもそも許されていないアドレスや権限違反なら同じ処置で解決できず、LinuxではSIGSEGVなどの通知につながります。
8同期例外・割込み・権限違反
命令による例外と外部からの割込みを表にします。EL0で許されないシステムレジスタへのアクセスも、アプリが自分でEL1へ昇格する手段にはなりません。
events = [('svc', 'synchronous', 'system call'), ('page fault', 'synchronous', 'memory access'), ('timer IRQ', 'asynchronous', 'timer'), ('restricted register', 'synchronous', 'access violation')]原因別の記録; print('\n'.join(' | '.join(row) for row in events))原因と同期性の対応svc・ページフォルト・権限違反は実行命令に同期し、タイマー割込みは非同期です。どの入口でもカーネル側の定めた処理が動きます。rootのプロセスも通常の命令はEL0で動き、システムコール時の権限検査では資格情報が別途使われます。
まとめ
通常の関数呼出しはELを変えず、システムコールは定められた例外入口からカーネルへ移ります。
Linux AArch64ではx8が呼出し番号、x0以降が引数で、戻り値もx0を使います。
復帰先は例外の種類に依存し、SVCの次命令と解決したページフォルトの再実行位置は異なります。
操作履歴と解説を振り返れます。
Python 3
data = b'OK\n'; regs = {'x0': 1, 'x1': 0x4000, 'x2': len(data), 'x8': 64}; print(data.hex(' ')); print(regs)4f 4b 0a
{'x0': 1, 'x1': 16384, 'x2': 3, 'x8': 64}state = {'EL': 0, 'PC': 0x1000}; state['x30'] = state['PC'] + 4; state['PC'] = 0x2000; print(state['EL'], hex(state['PC']), hex(state['x30']))0 0x2000 0x1004
state = {'EL': 0, 'PC': 0x1010}; state['saved_EL'] = state['EL']; state['ELR_EL1'] = state['PC'] + 4; state['EL'] = 1; print(state['EL'], hex(state['ELR_EL1']))1 0x1014
lo, hi = 0x4000, 0x5000; start, size = regs['x1'], regs['x2']; print(lo <= start and start + size <= hi); print(hex(start), hex(start + size))True 0x4000 0x4003
regs['x0'] = 3; state['EL'] = state['saved_EL']; state['PC'] = state['ELR_EL1']; print(regs['x0'], state['EL'], hex(state['PC']))3 0 0x1014
raw = -9; x0_bits = raw & ((1 << 64) - 1); result, errno_value = (-1, -raw) if -4095 <= raw < 0 else (raw, 0); print(hex(x0_bits), result, errno_value)0xfffffffffffffff7 -1 9
fault = {'PC': 0x3000, 'address': 0x9000, 'mapped': True, 'present': False}; fault['ELR_EL1'] = fault['PC']; fault['present'] = True; print(hex(fault['ELR_EL1']), fault['present'])0x3000 True
events = [('svc', 'synchronous', 'system call'), ('page fault', 'synchronous', 'memory access'), ('timer IRQ', 'asynchronous', 'timer'), ('restricted register', 'synchronous', 'access violation')]; print('\n'.join(' | '.join(row) for row in events))svc | synchronous | system call page fault | synchronous | memory access timer IRQ | asynchronous | timer restricted register | synchronous | access violation
演習完了