執筆者 : 箕浦 真
1. vmlinuxのELFセクション
前回記事は、もう1年半も前に書いて、それなりに多くの方に読んでいただけたようだ。今回記事は、コード例もなく、単にLinuxという巨象の枝葉末節をああでもないこうでもないと闇雲に撫で回すだけの記事だ。
ELFセクションは、リンク時に、同名のセクションのコードやデータを、すべてのオブジェクトファイルからかき集める、という効果があるものであった。前回例に出した.cpuidle.textセクションは、vmlinuxからは消えて.textセクションの中に組み込まれるものの、かき集めてリンクされており、その先頭には__cpuidle_text_start、末尾には__cpuidle_text_endというシンボルが付与されていた。これを利用して、cpu_in_idle()という関数では、引数として渡された値が.cpuidle.textセクション内か否かを返す。同様に、in_lock_functions()という関数は、アドレスを引数とし、そのアドレスが.spinlock.textというセクション内か否かを返す。誰かがアイドルなのか、またはspinlock関連の処理を実行中なのかを知ることができる。
今回記事では、こうした使い方以外でどのようにELFセクションが利用されているのかを見てみよう。
2. initセグメント
前回紹介したldscriptのセグメントの定義を再掲しよう。
PHDRS { text PT_LOAD FLAGS(5); data PT_LOAD FLAGS(6); percpu PT_LOAD FLAGS(6); init PT_LOAD FLAGS(7); note PT_NOTE FLAGS(0); }
percpuというのも興味深いが、per_cpuデータの扱いはそれなりに複雑なので機会を改めたい (が、後で少し出てきてしまう)。ここでは、initセグメントを取り上げる。FLAGS(7)とあるように、読み、書き、実行いずれもできるメモリセグメントである。textセグメントは書き込みが、dataセグメントは実行ができないが、initセグメントでは全部許容されているとは *1。
initセグメントに対応するセクションは、readelfの結果に出ている。これも再掲しよう。
Section to Segment mapping: Segment Sections... 00 .text .notes __ex_table .rodata .pci_fixup __ksymtab __ksymtab_gpl __kcrctab __kcrctab_gpl __ksymtab_strings __param __modver 01 .data .BTF __bug_table .orc_unwind_ip .orc_unwind .orc_lookup .vvar 02 .data..percpu 03 .init.text .altinstr_aux .init.data .x86_cpu_dev.init .parainstructions .altinstructions .altinstr_replacement .iommu_table .apicdrivers .exit.text .smp_locks .data_nosave .bss .brk .init.scratch 04 .notes
00がtext、01がdata、02がpercpuセグメントで、initセグメントは03である。たくさんのセクションがinitセグメントにロードされることがわかる。よく見ると、.bssセクションまでがinitセグメントに含まれている。さっそく定義を見てみよう。
まずは、以下だ。
/* Init code and data - will be freed after init */ . = ALIGN(PAGE_SIZE); .init.begin : AT(ADDR(.init.begin) - LOAD_OFFSET) { __init_begin = .; /* paired with __init_end */ } (中略) /* freed after init ends here */ .init.end : AT(ADDR(.init.end) - LOAD_OFFSET) { __init_end = .; }
コメントにいきなり答えがあった。Initつまり初期化の後解放される。その先頭に、__init_begin、末尾に__init_endというシンボルがつけられる。__init_beginと__init_endの間が読み込まれた部分のメモリは、初期化後に解放されるのだ。
解放しているのはここ:
free_init_pages("unused kernel", (unsigned long)(&__init_begin), (unsigned long)(&__init_end));
free_init_pages()を追いかけると、Freeing unused kernel memory: %ldkという記録がdmesgに残るようだとわかる。
$ dmesg | grep 'unused kernel' [ 1.570567] Freeing unused kernel memory: 2492K [ 1.575342] Freeing unused kernel memory: 2012K [ 1.575418] Freeing unused kernel memory: 320K
__init_beginと__init_endの具体的な値は、System.mapに出ているので、この間のメモリ量を調べられる。
$ sudo egrep '__init_(begin|end)' /boot/System.map-$(uname -r) ffffffff82bea000 D __init_begin ffffffff82e59000 R __init_end $ echo 'ibase=16 ; FFFFFFFF82E59000-FFFFFFFF82BEA000' | bc 2551808 $ echo $((2551808/1024)) 2492
というわけで、3回のFreeing unused kernel memoryのうち、最初のものと一致した (ちなみに他の2つはアラインメントのための空き領域)。
それでは、この間の各セクションが、本当に解放しても良いものか見ていってみよう。
2.1. percpuセグメント
ところで、この2つのシンボルの間がinitセグメントか、というと実はそうではない。__init_beginの次の#ifを展開し、リンカのコマンドASSERT (リンク時エラー検出) を飛ばすと、以下だ。
/* * percpu offsets are zero-based on SMP. PERCPU_VADDR() changes the * output PHDR, so the next output section - .init.text - should * start another segment - init. */ PERCPU_VADDR(INTERNODE_CACHE_BYTES, 0, :percpu) INIT_TEXT_SECTION(PAGE_SIZE) :init
PERCPU_VADDRはマクロで、vmlinux.lds.hに定義されている。
#define PERCPU_VADDR(cacheline, vaddr, phdr) \ __per_cpu_load = .; \ .data..percpu vaddr : AT(__per_cpu_load - LOAD_OFFSET) { \ PERCPU_INPUT(cacheline) \ } phdr \ . = __per_cpu_load + SIZEOF(.data..percpu);
セグメントはphdrつまり:percpuで、先ほどのセグメント定義のpercpuセグメントは__init_beginの後ろにある。__init_beginは、その名に反してinitセグメントだけではなくpercpuセグメントも含めた解放可能な領域の開始アドレス、ということだ。ATはリンカのキーワードで、このセクションをロードするアドレスである *2。
percpuセグメントには、実はper-CPU変数の初期値が書いてあって、記憶域そのものは初期化時にCPUごとに払い出しを受け、percpuセグメントの内容が (上記で定義されている__per_cpu_loadというアドレスからサイズ分だけ) コピーされる (このときCPUがホットプラグされる可能性も考慮されている)。なので、percpuセグメントのデータは初期化が終わったら解放しても良いわけだ。このことから、per-CPU変数の初期値としてper-CPU変数のポインターのようなものを設定することはできないこともわかる。
そして、よく見ると、末尾側も__init_endより前にあるのは.exit.textまでで、.smp_locksや.bssなどは__init_endより後にあり、つまり解放されるわけではない。
2.2. .init.text、.init.data、.init.rodataセクション
次のINIT_TEXT_SECTIONもマクロで、同じファイルの少し後ろに定義されている。マクロを展開すると、以下だ。
. = ALIGN(PAGE_SIZE); .init.text : AT(ADDR(.init.text) - LOAD_OFFSET) { _sinittext = .; *(.init.text .init.text.*) *(.text.startup) _einittext = .; } :init
すべての入力ファイルの.init.textセクション、.init.text.*セクション、.text.startupセクションを、出力ファイルの.init.textというセクションにかき集め、これをinitセグメントにロードする。先頭にsinittext、末尾にeinittextというシンボルを付与する。このうち、.text.startupセクションは、特定のコンパイルオプションをつけたときにコンパイラが自動的に出力するもので、ここでは触れない。
.init.textセクションにはどのようなものが出力されているのだろう。このセクションに出力するためのマクロが<linux/init.h>にある。
/* These are for everybody (although not all archs will actually discard it in modules) */ #define __init __section(.init.text) __cold __latent_entropy __noinitretpoline #define __initdata __section(.init.data) #define __initconst __section(.init.rodata) #define __exitdata __section(.exit.data) #define __exit_call __used __section(.exitcall.exit) (中略) #define __exit __section(.exit.text) __exitused __cold notrace
ついでなので、後に出てくる部分も一緒に引用した。.init.textに出すには、__initというものをくっつける*3。
この__initは、あちこちで使われているが、たとえばこの辺りで、初期化時にしか実行されない関数についている。同様に、そのようなコードからしか参照されないデータ、定数には、それぞれ__initdata、__initconstが付与され、それぞれ.init.data、.init.rodataというセクションに出力される。これらは、少し後のINIT_DATA_SECTIONというマクロの中に含まれる。
2.3. .exit.text、.exit.dataセクション
__exitdataと__exitはどうだろう。.exit.data、.exit.textは、vmlinux.lds.Sの少し後ろに書かれている。もちろん、__init_endの前だ。
. = ALIGN(8); /* * .exit.text is discard at runtime, not link time, to deal with * references from .altinstructions and .eh_frame */ .exit.text : AT(ADDR(.exit.text) - LOAD_OFFSET) { EXIT_TEXT } .exit.data : AT(ADDR(.exit.data) - LOAD_OFFSET) { EXIT_DATA }
initの対であれば、最後に実行されるもの? そんなもの削除してしまっていいの?
__exitや__exitdataは、カーネルのコアの部分にはほとんど使われておらず、検索をかけるとnetやfs、driversといったあたりの下にあるものが多い。たとえば、bridgeドライバーのここだ。何やら色々と登録解除したり資源を解放したりしているようだ。このbr_deinit()を直接呼んでいる部分はなく、直後の
module_exit(br_deinit)
というので参照されているのみだ。
このmodule_exit()というのはマクロで、モジュールをアンロードする (modprobe -r) ときに呼ばれる関数を宣言するものだ。やはりELFセクションの機能を用いているので興味のある方は追っかけてみてほしい。vmlinuxに含まれる__exitの関数は、いずれも静的リンクされているので、決して呼ばれることはない。だからいつでも解放しても良いのだ。__initが起動時に呼ばれるのであれば、__exitはシャットダウン時に呼ばれるのではないか、という気がするかもしれないが、カーネル内の情報については別に後始末をしないでシャットダウンしても特に問題はない。後始末が必要なのは、ユーザーデータであって、これらをシャットダウン時にきっちり保存するのはプロセスの仕事だし、最後にファイルシステムをアンマウント (または、read-onlyでremount) するのは/sbin/init (systemd) の仕事だ。
2.4. .x86_cpu_dev.initセクション
続く.altinstr_aux、少し飛ばして.parainstructions、.altinstructionsについては、大掛かりなので後で説明しよう。
次は、.x86_cpu_dev.initセクションだ。
.x86_cpu_dev.init : AT(ADDR(.x86_cpu_dev.init) - LOAD_OFFSET) { __x86_cpu_dev_start = .; *(.x86_cpu_dev.init) __x86_cpu_dev_end = .; }
これは、Cのソースコードでは以下の使い方がなされる。
#define cpu_dev_register(cpu_devX) \ static const struct cpu_dev *const __cpu_dev_##cpu_devX __used \ __attribute__((__section__(".x86_cpu_dev.init"))) = \ &cpu_devX;
構造体のポインターを、このセクションに置くようになっている。たとえば、arch/x86/kernel/cpu/intel.cで登場している。直前のstruct cpu_dev intel_cpu_devという構造体について、
static const struct cpu_dev *const __cpu_dev_intel_cpu_devX = &intel_cpu_dev;
という感じでポインター型の大域変数を定義し、これを.x86_cpu_dev.initセクションに置いているわけだ。結果的に、.x86_cpu_dev.initセクションには、struct cpu_devへのポインターが並ぶことになる。この様子は、System.mapにも現れている。ソート順がみにくいので順序を変えて引用すると、
ffffffff82e2f5f8 R __x86_cpu_dev_start ffffffff82e2f5f8 r __cpu_dev_intel_cpu_dev ffffffff82e2f600 r __cpu_dev_amd_cpu_dev ffffffff82e2f608 r __cpu_dev_centaur_cpu_dev ffffffff82e2f610 R __x86_cpu_dev_end
ポインターなのでそれぞれ8バイト、3つのポインターが並んでいることがわかる。それぞれ、同じディレクトリにあるintel.c、amd.c、centaur.cからかき集めてきてリンクしたものだが、ポインターの配列的にアクセスできる、ということだ。これを参照しているのは、CPU初期化部分だ。
for (cdev = __x86_cpu_dev_start; cdev < __x86_cpu_dev_end; cdev++) { const struct cpu_dev *cpudev = *cdev; if (count >= X86_VENDOR_NUM) break; cpu_devs[count] = cpudev; count++; // (中略) }
ここでは、__x86_cpu_dev_startから__x86_cpu_dev_endまでのポインターを大域変数にコピーしているだけではあるが、このセクションにあるデータはここでしか参照されていないので、ここの処理が終われば解放しても構わないとわかる。
このように、複数のファイルに散らばった、特定の型のポインターを集めたセクションは、他にもいくつもある。先ほどこっそりスルーしてしまったが、実はINIT_DATA_SECTIONというマクロの中には、たとえばINIT_CALLSという別なマクロを呼び出している部分があり、これがその一つである。これは、幾つものファイルに散らばった初期化コードを、初期化が進むに従って段階的に呼び出していくための仕組みだ。<linux/init.h>あたりからにコメントとともに説明と定義方法があり、呼び出し処理はこの辺なので、興味のある方は追いかけてみてほしい。INIT_CALLS呼び出しの様子は、起動オプションにinitcall_debugを付けるとdmesgで見ることができる。
2.5 .iommu_tableセクション
メインメモリは、CPUがプログラムやデータを読み書きするのに利用されるが、その他にデバイスがデータを読み書きするのにも利用される。たとえばストレージにデータを書き込む場合、デバイスドライバーがストレージコントローラーにメモリアドレスを指定し、ストレージコントローラーはそのアドレスから情報を読み出してストレージに書き込むし、ネットワークからパケットを受信する場合は、デバイスドライバーがNICに指定したメモリアドレスに受信データが書き込まれる。デバイスが直接メインメモリにアクセスするため、これをDMA: Direct Memory Accessと呼ぶ。
IOMMU (Input/Output Memory Management Unit) は、デバイスとメインメモリの間にいて、デバイスから見たメモリアドレスとメインメモリの物理アドレスとのマッピングを行う。MMUがCPUとメインメモリの間 (現在は通常CPUに内蔵されている) にいて、プロセスごとの仮想アドレスと物理アドレスとの間でマッピングを行なっていたのと同じように、デバイスごとの仮想アドレスと物理アドレスとのマッピングを行う、という感じだ。
IOMMUは、32bitから64bitへの過渡期によく使われた。物理メモリアドレスが拡大されたにもかかわらず、多くのデバイスが32bitのメモリアドレスしか扱えなかったため、IOMMUなしでは最初の4GB (32bit) のメモリに対してしかDMAできない状態であった。IOMMUは、32bitのデバイスごとのアドレス空間と、64bitの物理アドレス空間のマッピングに使われた。
現在では、仮想マシン (VM) にデバイスをアサインした際に、ホストや他のVMのメモリを壊さないよう隔離するために使われる。これは、プロセス間でメモリ空間を隔離するMMUの役割に近いといえるだろう。また、DPDKのようにデバイスドライバーがユーザースペースに実装されている場合に、ユーザースペースドライバーが他のメモリを破壊しないような制御をするためにも用いられている。
.iommu_tableセクションには、このIOMMUの検出・初期化ルーチンなどのポインターがかき集められる。INIT_CALLSではいかんのか、というと、実はIOMMUには特有の事情があって、単純なINIT_CALLSではうまくいかない、ということのようだ。たとえば、前者のユースケースのために、IOMMUをソフトウェアでエミュレーションするswiotlbという機構がある。これはソフトウェアなので遅く、ハードウェアのIOMMUのある環境には不要だ。またXenはハイパーバイザーが挟まっていて、デバイスドライバーを持つドメイン (dom0など) の扱うメモリアドレスは物理アドレスではないため、別な管理が必要である。こうした特殊なIOMMUのために、検出・初期化は一定の順序で行わなければならない。ELFセクションに集められる順序は、リンク順などにも依存して制御できないため、検出・初期化ルーチン呼び出しに先立って、テーブルをソートする、ということをやっている。これが、.iommu_tableセクションが分かれている理由と思われる。
2.6 .apicdriversセクション
.apicdriversセクションも、.iommu_tableセクションと似ている。ここでいうAPIC (Advanced Programmable Interrupt Controller) は、Local APICというものだ。CPU側に立って割り込みを受け付けるほか、他のCPU (というかLocal APIC) に割り込みを送る (IPI: Inter-Processor Interrupt)、内蔵タイマーから自身で割り込みを発生させる、という働きがある。
Local APICは、CPUや割り込みを発生するデバイスの数が増えてきた歴史に沿って、(後方互換性を保って、あるいは、サードパーティによって別途) 拡張されてきた。このため、検出ルーチンは新しいものから古いものへと順番に、また独自のチップは初めの方に、それぞれ呼んでいく必要がある。ただし、APICドライバは、アーキテクチャ依存部とデバイスドライバとに分散していたIOMMUとは違い、特定のディレクトリにまとめられているため、リンク順を制御することは容易である (Makefileに新しい順に書けば良い)。このため、ソートはしていない。INIT_CALLSの仕組みを使って登録し、登録順に検出ルーチンを呼んで、というやり方でも良い気がするが、ここでは独自のセクションを使う、という選択をしたようだ。理由はわからないが、仕様上initcallには順序保証がないためだろうか*4。
ここでも一度初期化が済んでしまえばこのセクションに書かれた情報は不要であるため、解放の対象にできる。
3. 休憩
ちょっと長くなってしまったので、中途半端だが今回の記事はここまで。「2」なのにその1とその2があるとはこれいかに!?
「その2」では、initセグメントにあったaltinstructionsなどのセクションと、textセグメントにある__ex_tableセクションあたりを取り上げたいと思う。
*1:実はtextセグメントもkprobeなどを利用すると書き込みが行われる。またそもそもvmlinuxをロードするのはフル機能のELFローダーではないので、こうしたFLAGSを解釈して設定を行うことはない。
*2:フル機能のELFローダーがロードするわけではないので、すべてのセクションのロードアドレスは0始まりになっていて、実際にはgzip圧縮されたvmlinuxがLOAD_OFFSETである0xffffffff80000000以降にベタッと展開・ロードされる。
*3:__coldは、あまり実行されないよ、とコンパイラに指示するもので、たとえば最適化は速度ではなくコードサイズを小さくすることを目標にするようになる。__latent_entropyはこの辺り。また__noinitretpolineは、間接分岐にretpolineを使わないようにすることをコンパイラに指示する。初期化コードには不要、という判断と思われる。
*4:現状実際にはリンク順に呼ばれると思う。