Linux/x86_64の割り込み処理 その1

Linuxが割り込みを受けた際に実行するコードを見ていきます。なお今回はRHEL5系と6系とで大きく異なる部分がありますが、いずれも5系で説明していきます。

デバイスから割り込みが入ると、CPUが自動的に以下の処理を行ないます (割り込みゲート)。

  • ユーザーモード (ring 3) の場合、ring 0に遷移
    割り込みハンドラを実行すべき特権モード (ring) は、IDTに記されています。Linuxの割り込み処理は、ring 0で実行されますので、ユーザーモードでの割り込み時にはこのring 0への遷移が発生します。
  • ユーザーモード (ring 3) の場合、SS/RSPの設定 (TSSより)
    TSS (Task State Segment) は、x86/x86_64アーキテクチャで現在実行中のタスクを示すメモリ上のデータ構造ですが、LinuxではCPU毎に用意されたTSSを全タスク (スレッド) で使い回します。
    TSSにはring 0~ring 2へ遷移する際に利用するスタックのセグメントセレクタとスタックポインタが格納されています。カーネルモード (ring 0) で割り込みを受けた場合は、スタックはそのまま使います。
  • ユーザーモード (ring 3) の場合、SS/RSPの保存 (ring 0スタックへpush)
    割り込みを受けた時点でのSSとRSPの各レジスタの値を、スタックへpushします。保存される値はユーザーモードのSS/RSPのもの、保存先はカーネルモードのスタックです。カーネルモードで割り込みを受けた場合は、スタックは使い続けるので保存の必要はありません。
  • EFLAGS/CS/RIPの保存 (スタックへpush)
    call命令などのときと異なり、フラグレジスタ (EFLAGS) も保存します。
  • 割り込みの禁止
    Linuxでは割り込みゲートを使いますので、ここでEFLAGSのIF (Interrupt Flag) ビットをクリアし、以後の割り込みを禁止します。トラップゲートを使う場合にはこの処理は行なわれません。
  • IDTで指定されたCS/RIPへジャンプ
    割り込みハンドラの呼出しです。

IDTはLinuxが起動時に作成しており、通常の割り込みハンドラの入口は、arch/x86_64/kernel/entry.S内にあります。アセンブラコードでは、以下の処理を行ないます。

  • レジスタ退避
    CPUが退避したRIPなど以外のレジスタを退避します。
  • 割り込みスタックの設定
    CPU毎に割り込みスタックが用意されており、スタックポインタをここに設定します。ただし、割り込みを受けた時点で既に割り込み処理中であった場合 (多重割り込み)、そのまま使い続けます。CPUが割り込み禁止にしていますが、後で再度許可するため、このような事態もあり得ます。
  • Cコードの呼び出し
    Cで書かれたコードを呼び出します。
  • (ユーザーモード時、必要に応じて) コンテキストスイッチ、シグナル配信など
    他に優先度の高いスレッドが発生していれば、そのままユーザーモードに戻るかわりにその高優先のスレッドに移行します。またシグナルが届いていれば、その配信処理を行ないます。

PDA (Processor Data Area)

PDAは、GSセグメントに置かれる構造体です。GSレジスタは特権命令1つ (swapgs) で裏レジスタと交換することができるため、PDAにはカーネル内のプロセッサ毎の基本的な情報が置かれます。

割り込み関連のものは以下のとおりです。

  • int irqcount; プロセスコンテキストでは-1、割り込みハンドラの入り口で+1、出口で-1される
  • char *irqstackptr; 割り込みスタック
  • unsigned int __softirq_pending; (どの) softirqがあるか
  • unsigned apic_timer_irqs; APICタイマ割り込みのカウンタ

この他に、現在実行中のプロセスや、カーネルスタックの情報などが置かれます。

割り込みハンドラの入り口での処理

entry.Sにある割り込みハンドラの入口から順にコードを見て行きます。entry.Sは、マクロの嵐で読みにくいので逆アセンブルリストと対照して読むとよいでしょう。
以下に多くのマクロを展開して示しています。

まず、IDTに登録されている入り口は、IRQ0x00_interrupt、IRQ0x01_interrupt、…、IRQ0xee_interrupt (i8259.c) となります。
以下のように、IRQ0xXX_interruptは、0xXX-256 (64bit値) をスタックにpushして、common_interruptへ飛んで行きます。

IRQ0x00_interrupt:
	push	$0x00-256
	jmp	common_interrupt

IRQ0x01_interrupt:
	push	$0x01-256
	jmp	common_interrupt

	(略)

common_interruptでは、以下のようにレジスタのスタックへの退避を行います。

common_interrupt:
	subq    $9*8,%rsp
	movq    %rdi,8*8(%rsp)
	movq    %rsi,7*8(%rsp)
    movq    %rdx,6*8(%rsp)
    movq    %rcx,5*8(%rsp)
	movq    %rax,4*8(%rsp)
	movq    %r8,3*8(%rsp)
	movq    %r9,2*8(%rsp)
	movq    %r10,1*8(%rsp)
	movq    %r11,0*8(%rsp)
	lea	    -ARGOFFSET(%rsp),%rdi   %rdiがCの第1引数
	pushq   %rbp
	movq    %rsp,%rbp

上記の2行目のsubqから11行目のmovq %r11,までは、

push    %rdi
push    %rsi
push    %rdx
push    %rcx
push    %rax
push    %r8
push    %r9
push    %r10
push    %r11

と同義です。

ARGOFFSETは、6*8と定義してあり (<asm/ptrace.h>)、これは、struct pt_regsのメンバr11のオフセットです。

	struct pt_regs {
	unsigned long r15;
	unsigned long r14;
	unsigned long r13;
	unsigned long r12;
	unsigned long rbp;
	unsigned long rbx;
/* arguments: non interrupts/non tracing syscalls only save upto here*/
	unsigned long r11;
	unsigned long r10;	
	unsigned long r9;
	unsigned long r8;
	unsigned long rax;
	unsigned long rcx;
	unsigned long rdx;
	unsigned long rsi;
	unsigned long rdi;
	unsigned long orig_rax;
/* end of arguments */ 	
/* cpu exception frame or undefined */
	unsigned long rip;
	unsigned long cs;
	unsigned long eflags; 
	unsigned long rsp; 
	unsigned long ss;
/* top of stack page */ 
};

%rdiには、%rsp-48が入りますが、これは、struct pt_regsのポインタとみなすことで、r11以降のメンバにアクセスできます。orig_raxは、IRQ0xXX_interruptの先頭でpushした値になります。なお、%rdiは、Cコード呼び出し時に第1引数と解されます。

common_interruptに戻ります。「movq %rsp,%rbp」の次の15行目からです。

	testl   3,CS(%rdi)         カーネルのCS?
	je  1f
	swapgs                  カーネルのGSに変更
1:  incl    %gs:pda_irqcount        PDAのirqcount++
	cmoveq  %gs:pda_irqstackptr,%rsp    結果0ならスタック変更
	push    %rbp
	call    do_IRQ

15行目のCSは17*8と定義されており、これは、struct pt_regsのメンバcsのオフセットです。これにより、CS(%rdi) で、割り込みゲート通過時にCPUがスタックに退避したCSレジスタの値にアクセスできます。

セグメントレジスタの下位2bitは、RPL (Required PriorityLevel) に割り当てられていますが、IDTでRPLの項は0 (ring 0) と設定されています (このセグメントはring 0でないとアクセスできないという意味です。カーネルソースの中で、「KERNEL_CS」をキーに検索してください)。一方、ユーザーモード (ring 3) のセグメントでは、この項は3と設定されます (ring 3であってもアクセス可能であることを示します。「USER_CS」で検索)。したがって、CSレジスタの値と3との論理積 (つまり下位2bit) が0であれば、カーネルのコードセグメントが使われていることが分かり、したがってカーネルモードで動作していることがわかります。

ここでは、割り込みの入った時点でring 0以外、つまりユーザモードで動いていれば、swapgs命令で、GSレジスタをカーネルの値に設定しなおしています。

さらに、PDAのirqcountを1増やしますが、プロセスコンテキストで割り込みが入った場合にはここで値が0となりますので、moveq %gs:pda_irqstackptr,%rspが実行され、スタックポインタが割り込みスタックのものに設定されます。

最後に%rbpを保存し、Cのコードdo_IRQが呼び出されます。do_IRQは、arch/x86_64/kernel/irq.cにあり、

asmlinkage unsigned int do_IRQ(struct pt_regs *regs)

と定義されています。第1引数は%rdiであり、struct pt_regsのポインタであることは先に述べた通りです。

スタックに関する補足

割り込み処理時には、CPU毎に用意されている割り込みスタックが使われます。サイズは、2²ページつまり16KBです。

この他にカーネルモード用のスタックがありますが、これはプロセスがシステムコールを呼び出した場合や、カーネル内スレッドが通常の処理をしているときなどに使われます。ちなみにカーネルがこうした処理をしている状態を「プロセスコンテキスト」、割り込み処理をしている状態を「割り込みコンテキスト」などと呼ぶことがあります。

なお、上記のcommon_interruptは、割り込みコンテキストで実行されますが、cmoveqまでは (プロセスコンテキスト用の) カーネルスタックが使われる点は要注意です。