LoginSignup
269
285

More than 3 years have passed since last update.

【読解入門】Linuxカーネル (概要編)

Last updated at Posted at 2019-02-14
  • (2/18(月)):スタック退避手順に関して追記しました。Armv7辺りの32bitアーキを対象にしています。

はじめに

はじめまして。
これまでは個人的に興味があったRTOSのZephyrに関する記事を書いてきましたが、今回からは実際に仕事で扱っているLinuxに関して投稿していこうと思います。(Zephyrは一旦休憩します)
OSの役割は上記リンク先の先頭にある「ハードウェアの抽象化 & 隠蔽化」と「システムを効率良く利用する」を参照ください。

Linux OSとは

皆さん、Linux OSと言われて何を想像しますか?
RHEL、CentOS、Debianなどのディストリビューションでしょうか。
それともその中に含まれているコアの部分、つまりカーネルでしょうか。
おそらく、前者をイメージされる人の方が多いと思いますが、本記事では後者のLinuxカーネルにスコープにあてて記載していきます。

Linuxカーネルとは

Linuxカーネルはソースコードを理解することは比較的難易度が高いと思います。
・規模が大きい
・関数ポインタが多すぎでクロスリファレンサで追えない
・H/Wの知識も必要
など理由は様々ですが、途中で挫折する方も多いのではないでしょうか。
そういった方々の一助となれば幸いです。

なお、私も「Linuxカーネルを理解しているか?」と言われれれば、(規模や日々のコードの変化などが理由で)回答に困ります。ただし、対象コードを読めば理解はできます。

Linuxカーネルの入手方法、バージョン名と開発/保守スタイル

ここからはメインライン(主流)のカーネルについて述べます。各ディストリビューションの大元となるカーネルです。

Linuxカーネルの入手方法

ポータルサイトはThe Linux Kernel Archivesになります。2019/2/13現在でLatest Stable Kernelは4.20.8です。
HTTP https://www.kernel.org/pub/
GIT https://git.kernel.org/
RSYNC rsync://rsync.kernel.org/pub/
上記で取得できますので、ご自分の使用環境に合わせてダウンロードして下さい。

Linuxカーネルのバージョン名

現在の4.20.8を例で示すと4がメジャーバージョン、20がマイナーバージョン、8がリビジョン番号です。
2.4や2.6が出始めた頃は、マイナーバージョンが偶数の版数を「安定板」、奇数のバージョンを「開発版」と呼んでいましたが、現在はそのような区別はありません。
リビジョン番号は、Bug Fix/Security Fixで機能追加はありません。

開発/保守スタイル

Linuxカーネルのバージョンリリースの間隔は約65日です。新バージョンがリリースされた直後の2週間をマージウインドウと言って次版で取り込む機能を各サブシステムのツリーからLinusがpullする期間があります。ウインドウが閉じた後はデバッグ期間でrc(release candidate)版がリリースされます。通常はrc7からrc8までリリースします。その後、新バージョンを正式リリースします。
正式リリースされた後は保守フェーズに入り、4.20.1、4.20.2、...とインクリメントされていきます。通常のバージョンは次々版のリリース(2、3カ月)までサポートされます。一方、Long Term Support(LTS)版もあり、これはGreg-KH氏が年に1度、特定のバージョンを採用して長期サポートを行います。
現在サポートしているLTS版を示します。

Longterm release kernels Version Maintainer Released Projected EOL
4.19 Greg Kroah-Hartman 2018-10-22 Dec, 2020
4.14 Greg Kroah-Hartman 2017-11-12 Jan, 2024
4.9 Greg Kroah-Hartman 2016-12-11 Jan, 2023
4.4 Greg Kroah-Hartman 2016-01-10 Feb, 2022
3.16 Ben Hutchings 2014-08-03 Apr, 2020

余談ですが、東芝や日立が中心となって取り組んでいるCivil Infrastructure ProjectというThe Linux Foundation傘下のコラボラティブプロジェクトがあり、このプロジェクトではSLTS(Super Long Term Support)を実現しています。
交通機関や発電所などの社会基盤では十年以上サポートを必要とする一方で、影響範囲が大きいカーネルの版数を上げることは現実的に困難です。こういった市場に対してCIPでは十年以上の長期サポート(不具合改修パッチのバックポート)を行います。

※LTSやCIPのアプローチ vs CI/CDのアプローチ、結局は製品形態によってケースバイケースだと思いますが、私は非常に興味があります。

Linuxカーネルのソースコードの読み方

始めに述べておきますが、読み方に正解はないと思います。
私は、下記の2点を意識して読みます。
・関数1から呼び出している関数2は深追いせず、関数1の役割が何をするのか大雑把に概要を理解する。(関数2は関数上部のコメントを読むレベル)
概要を掴めたら、少しづつ詳細を理解していく。
いきなり、一文一文を厳密に理解しようとして、深くまで入り込んでしまって読解が全然進まない、という人もいます。そうならないように上記の読み方をします。
大きい範囲で概要を理解して、その後に各範囲を見ていくイメージです。
・なぜその処理が必要なのかを常に考える。
これは少し難易度が高いので後回しでも良いと思います。

Linuxカーネルの各階層

Linuxカーネルのトップにあるディレクトリをざっと見てみます。

・arch:アーキテクチャ依存部。x86やArm、Power、MIPSなどがディレクトリで用意されている。
・block:ブロックI/Oレイヤ。I/Oスケジューラなどが含まれます。
・crypto:暗号/復号化レイヤ。
・drivers:各種ドライバ。Linuxカーネルの半分の規模を占める。
・fs:ファイルシステム。extやfat、nfsなどがディレクトリで用意されている。
・include:ヘッダファイル群。アーキテクチャ依存部はarch/xxx/includeに配置。
・init:初期化処理。
・ipc:プロセス間通信レイヤ。
・kernel:カーネルコア部。スケジューラやタイマーなどが含まれる。
・lib:共通ライブラリ。対象アーキテクチャで高速化命令が存在しない場合などに使用されることが多い。e.g. memcpy、memcmp、strcmp
・mm:メモリ管理。アーキテクチャ非依存部。アーキテクチャ依存部はarch/xxx/mmに配置。
・net:各種ネットワーク。e.g. ipv4/6、bluetooth、wimax
・security:セキュリティ。SELinuxやtomoyoなど。
・sound:ALSAのカーネル側コード。
・virt:仮想化。KVMコードなど。

Linuxカーネルの呼び出し例

本記事では、ここまでコードを読むことがなかったため、一例でprintf( )を実行した場合を見てみます。ラズパイなどで身近な存在になっているArmアーキテクチャのコードを例で見ていきましょう。
1.glibcでprintf( )を実行します。
2.glibcで内部でprintfに対応するシステムコールwriteシステムコールをsvc(Super Visor Call)命令を用いてソフトウェア例外を発生させます。
このとき、r7にwriteのシステムコール番号、r0からr6までに引数を入れます。
ちなみにwriteのシステムコール番号はカーネルのarch/arm/tools/syscall.tblで4と定義されています。
時間がある方はカーネルの上記ファイルを見てみると良いと思います。
全てのシステムコールの番号とエントリが記載されています。
なお、ここでポイントなのは、通常の関数呼び出しでないということです。
ほとんどのシステムコールはglibcが内部で行ってくれているため、気づかないですが、Linuxカーネルに処理を依頼するときは必ずsvc命令でソフトウェア例外を発生させます
2.カーネルのベクターテーブルの下記4行目にトラップします。(svc命令を実行するとCPUが自動的に4行目に飛ばしてくれます。同時に、CPUのプロセッサモードをユーザーモードからスーパーバイザーモードへ変更します)

arch/arm/kernel/entry_armv.S
1.L__vectors_start:
2        W(b)    vector_rst
3        W(b)    vector_und
4        W(ldr)  pc, .L__vectors_start + 0x1000
5        W(b)    vector_pabt
6        W(b)    vector_dabt
7        W(b)    vector_addrexcptn
8        W(b)    vector_irq
9        W(b)    vector_fiq

少し話が逸れますが、上記は例外ベクタテーブルであり、全ての例外を定義しています。
2行目:リセット例外
3行目:未定義命令例外
4行目:ソフトウェア例外
5行目:プリフェッチアボート例外
6行目:データアボート例外
7行目:アドレス例外
8行目:irq割込み例外(irq割込みが発生すると本エントリから処理されます)
9行目:fiq割込み例外

この__vectors_start(ベクターテーブル)はリンカで0xffff0000に配置されています。
従ってldr命令でpcに0xffff1000を入れています。
次にざっくり飛ばしますが(笑)、重要な箇所は以下のvector_swiです。
なお、r0-r6の引数を保持したレジスタ、r7のシステムコール番号を保持したレジスタは使用(破壊)していないことに注目してください。writeシステムコール関数で使用するため、システムコールの前処理でこれらのレジスタを触ることはありません。2行目でスタックポインタで指すアドレスを更新したあとに2行目でr0からr12の値をスタックに退避しています。この退避した値はシステムコール処理からユーザー空間に復帰するときにスタックからレジスタに復元して整合性を保ちます。

arch/arm/kerne/entry-common.S
/*=============================================================================
 * SWI handler
 *-----------------------------------------------------------------------------
 */

        .align  5
ENTRY(vector_swi)
#ifdef CONFIG_CPU_V7M
 1       v7m_exception_entry
#else
 2       sub     sp, sp, #PT_REGS_SIZE
 3       stmia   sp, {r0 - r12}                  @ Calling r0 - r12
 4ARM(   add     r8, sp, #S_PC           )
 5ARM(   stmdb   r8, {sp, lr}^           )       @ Calling sp, lr
 6THUMB( mov     r8, sp                  )
 7THUMB( store_user_sp_lr r8, r10, S_SP  )       @ calling sp, lr
 8       mrs     saved_psr, spsr                 @ called from non-FIQ mode, so ok.
 9 TRACE( mov     saved_pc, lr            )
10       str     saved_pc, [sp, #S_PC]           @ Save calling PC
11       str     saved_psr, [sp, #S_PSR]         @ Save CPSR
12       str     r0, [sp, #S_OLD_R0]             @ Save OLD_R0
#endif
 :
 :
local_restart:
        ldr     r10, [tsk, #TI_FLAGS]           @ check for syscall tracing
        stmdb   sp!, {r4, r5}                   @ push fifth and sixth args

        tst     r10, #_TIF_SYSCALL_WORK         @ are we tracing syscalls?
        bne     __sys_trace

        invoke_syscall tbl, scno, r10, __ret_fast_syscall★
:
ENDPROC(vector_swi)

3行目から12行目までの処理で呼び出し時のレジスタやCPSRをスタックや変数に保存しています。
この部分を図を示しながら説明します。
3行目でsp(スタックポインタ(現在のスタックの位置を示す役割))をPT_REGS_SIZE分引き下げています。
PT_REGS_SIZEは下記のように定義されており、pt_regs構造体のサイズを示します。

arch/arm/kernel/asm-offsets.c
  DEFINE(PT_REGS_SIZE,          sizeof(struct pt_regs));

そしてpt_regs構造体及びその構造体に設定するエントリは下記の通りです。
サイズは4byte×18の72byte(hexで0x48)です。

arch/arm/include/uapi/asm/ptrace.h
#ifndef __KERNEL__
struct pt_regs {
        long uregs[18];
};
#endif /* __KERNEL__ */

#define ARM_cpsr        uregs[16]
#define ARM_pc          uregs[15]
#define ARM_lr          uregs[14]
#define ARM_sp          uregs[13]
#define ARM_ip          uregs[12]
#define ARM_fp          uregs[11]
#define ARM_r10         uregs[10]
#define ARM_r9          uregs[9]
#define ARM_r8          uregs[8]
#define ARM_r7          uregs[7]
#define ARM_r6          uregs[6]
#define ARM_r5          uregs[5]
#define ARM_r4          uregs[4]
#define ARM_r3          uregs[3]
#define ARM_r2          uregs[2]
#define ARM_r1          uregs[1]
#define ARM_r0          uregs[0]
#define ARM_ORIG_r0     uregs[17]

元々は以下のSP(0xD0001000と仮定)の位置でした。
stack1.jpg
そして2行目で下図のようになります。(SP = 0xD0000FB8)
stack2.jpg

3行目でシステムコール呼び出し時のr0からr12の値を退避します。
stack3.jpg

4行目から12行目でスタックは下図のようになります。全てシステムコール呼出し時のレジスタ値の退避処理です。

stack4.jpg

ここでORIG_r0って?と思われる方もいらっしゃるとおもいます。
Armでは、SPで示しているアドレス(r0を示している箇所)をシステムコールの復帰値として利用します。
従って、そのままではr0の値を復元できないため、ORIG_r0のアドレスにもr0を退避して、復帰時にはr0はこのORIG_r0を復元します。

そして最後の★で下記マクロを実行します。

arch/arm/kernel/entry-header.S
        .macro  invoke_syscall, table, nr, tmp, ret, reload=0
#ifdef CONFIG_CPU_SPECTRE
        mov     \tmp, \nr
        cmp     \tmp, #NR_syscalls              @ check upper syscall limit
        movcs   \tmp, #0
        csdb
        badr    lr, \ret                        @ return address
        .if     \reload
        add     r1, sp, #S_R0 + S_OFF           @ pointer to regs
        ldmccia r1, {r0 - r6}                   @ reload r0-r6
        stmccia sp, {r4, r5}                    @ update stack arguments
        .endif
        ldrcc   pc, [\table, \tmp, lsl #2]      @ call sys_* routine★
#else
        cmp     \nr, #NR_syscalls               @ check upper syscall limit
        badr    lr, \ret                        @ return address
        .if     \reload
        add     r1, sp, #S_R0 + S_OFF           @ pointer to regs
        ldmccia r1, {r0 - r6}                   @ reload r0-r6
        stmccia sp, {r4, r5}                    @ update stack arguments
        .endif
        ldrcc   pc, [\table, \nr, lsl #2]       @ call sys_* routine★
#endif
        .endm

★でsys_write( )を呼び出します。その後、例えば、シリアルコンソールに出力する場合はtty層を通じてdrivers/tty/serial/serial_core.c(抽象化レイヤ)、amba-p1010.c(AMBA PrimeCell UARTドライバ)の流れで処理を行い文字列を出力します。

端折りすぎて載せない方が良いかもと思いましたが、もしかすると雰囲気を理解していただけるかもしれませんので一応載せておきます。
アセンブラなど出てきますので、理解できなくても問題ありません。
ただ、pritnfの場合はこのような流れで実行されているということを大体理解してもらえれば結構です。
以下にポイントを示します。
・printfはwriteシステムコールを発行する
・システムコールの発行はsvc命令で行う
・r0 - r6に引数、r7にシステムコール番号を格納してsvc命令を発行する。
 この時、プロセッサモードはユーザーモードからスーパーバイザーモードに変更される。割込みも禁止される。
・ベクターテーブルに飛んできた後にvector_swiを実行する。
・各レジスタをスタックに退避する(システムコール実行後にユーザー空間に復帰する時にスタックから復元する)
・システムコールはarch/arm/tools/syscall.tblに定義されており、sys_xxxxという形式の関数である。

次回からは体裁を整えて丁寧に書くようにします。

第2回:【読解入門】Linuxカーネル (スケジューラ編その1)
第3回:【読解入門】Linuxカーネル (スケジューラ編その2)
第4回:【読解入門】Linuxカーネル (スケジューラ編その3-1)
第5回:【読解入門】Linuxカーネル (スケジューラ編その3-2)
第6回:【読解入門】Linuxカーネル (スケジューラ編その3-3)
第7回:【読解入門】Linuxカーネル (スケジューラ編その4-1)

番外編:【読解入門】Linuxカーネル (スケジューラと割込みハンドラの関係)

269
285
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
269
285