Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識 読みました

著者の武内さんより「[試して理解]Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識」を献本いただきました。ありがとうございますm( )m

f:id:masami256:20180218160317j:plain:w250

武内さんはQiitaなどでもわかりやすいカーネルの解説記事を書かれているので、記事を読んだことが有る方も多いかと思います。そして、今回「試して理解]Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識」というOS・ハードウェアのしくみを手を動かして学ぶコンセプトの本を書かれました。

gihyo.jp

本書の「実験と図解で学ぶOSとハードウェアの基礎知識」というコンセプトはOSの解説本にしては珍しいと思います。しかし、何かを学ぶ上で実際に手を動かしたほうが勉強になるということを考えるとこのコンセプトは良いですよね。 また、図解の部分も丁寧に解説されています。第5章の仮想記憶、ページングの図解などはここまで細く説明されている文献はそうそうないと思います。もちろん5章だけでなく他の章も図が豊富に使われていて理解がはかどります。

章立ては1〜8章まであって、1章はコンピュータシステムの概要説明となり、以降の章に入る前の基礎知識を説明しています。2〜7章は基本的には他の章への依存は少ないので興味のあるところから読むというのも良いと思います。例えば、2章のプロセス生成では仮想記憶に関する知識も合ったほうがよく、仮想記憶の詳細は5章で解説しますという形になっています。 しかし、2章では仮想記憶の詳細がわからないくて理解できるようにプロセス管理についてを説明されているので、本を読み進めるために必要な前提知識といったハードルは低くなっています。

また、解説は本質的な内容の理解というところに注力していると思います。たとえば、Linuxx86_64ならユーザモードとカーネルモードの2種類のモードがあって、ユーザモードは実行可能な命令が制限されるというのがありますよね。これもcpuの機能だとx86Ring Protection機能が出てきたししますが、そういった詳細な部分には踏み込まずにモード分けることの意味について説明されているといった感じです。細かい内容はともかく一番大事なところの知識を得ることができます。詳細を知りたくなったら、あとがきに書かれている書籍にチャレンジしましょう!

自分はLinuxカーネルもくもく会というネタをLinuxカーネルに絞ったもくもく会を開催しているのですが、そうするとカーネルに興味があるけどどこから手を付ければ良いのか???というような悩みを聞くことがよくあります。カーネルに興味があって、その中でも明確にXXXという機能に興味があるという場合は良いですが、そうでない場合も結構ありますよね。そういう人には本当にオススメできる本だと思います。OSカーネルの解説本はLinuxに限らず色々出版されていてどれも良い内容なんですが、最初にどれを読めばいいか?と聞かれると意外と困ったりします。本を読むための前提知識といったハードルは極力低くしつつ、丁寧に解説がされていて、なおかつ手を動かして実際に試せるというところで本書のようなコンセプトの本は入門に最適です。

本書ではLinuxカーネルのコードの説明はしていなくて、普通のアプリケーションプログラムを使って/作ってLinuxのしくみを実験するという方針を取られています。そのため、Linuxカーネルにも手を出したいという場合は、本書とお好きなLinuxカーネルの解説本やカーネルソースコードを用意して本書を読みながらカーネルの実装を調べるという進め方になっていくと思います。といっても、カーネルに慣れていない場合はこのような読み方はしないで、まずは本書をじっくり読み進め、次に本書とカーネルのソースを読み進めるのがオススメな流れです。

OSやハードウェアについて知りたい方は読んで実験してみましょう!

Linux kernelをgdbでリモートデバッグするときはKASLRをoffにするのを忘れずに(´・ω・`)

カーネルgdbでリモートデバッグできるようにするかーとか思ってやってたわけです。

しかし、下記のような感じでちょっとハマってました。

  • ブレークポイントが効かなかったり
  • vmlinuxとvmlinuzの組が合ってるのにシンボル名が出なかったり
  • Cannot access memory at address 0xffffffff8124d730とかで落ちたり

f:id:masami256:20180215221450p:plain

原因はKASLRが有効だったため、実行時にアドレスがランダマイズされていたのが原因ですね\(^o^)/

解決策はKASLRをoffにすれば良いだけです。そのためにカーネルの再ビルドは不要で、カーネルコマンドラインでnokaslrを渡せばOKです。

そうすればこのようにいい感じに f:id:masami256:20180215221904p:plain

以下はめも

remote接続にシリアルコンソールを使う場合

qemuはこんな感じになる。

$ qemu-system-x86_64 -kernel vmlinuz -initrd initramfs.img -m 2048 -append "nokaslr console=tty0 kgdboc=ttyS0,115200 kgdbwait" -chardev pty,id=pty -device isa-serial,chardev=pty 

そうすると以下のようなメッセージが出るので、gdbではこのデバイスに対してアクセスする。

qemu-system-x86_64: -chardev pty,id=pty: char device redirected to /dev/pts/2 (label pty)                

gdbからリモートアクセスするとこんな感じに。

gdb-peda$ target remote /dev/pts/2                  
Remote debugging using /dev/pts/2                   
Warning: not running or target is remote            
kgdb_breakpoint () at kernel/debug/debug_core.c:1073                                                     
1073            wmb(); /* Sync point after breakpoint */  

remote接続にTCPを使う場合

この場合は普通に-sオプションを使えばOK。

$ qemu-system-x86_64 -kernel vmlinuz -initrd initramfs.img -m 2048 -append "nokaslr" -s -S 

gdbからアクセスするときもこのように。

masami@saga:~/tmp/test$ gdb ./vmlinux               
Reading symbols from ./vmlinux...done.              
gdb-peda$ dir usr/src/debug/kernel-4.15.fc27/linux-4.15.2-300.fc27.x86_64/                               
Source directories searched: /home/masami/tmp/test/usr/src/debug/kernel-4.15.fc27/linux-4.15.2-300.fc27.x86_64:$cdir:$cwd                                                                                          
gdb-peda$ target remote :1234                       
Remote debugging using :1234                        
Warning: not running or target is remote            
0x000000000000fff0 in cpu_hw_events ()              
gdb-peda$ b __kmalloc                               
Breakpoint 1 at 0xffffffff8124d730: file mm/slub.c, line 3746.                                           
gdb-peda$ c                                         
Continuing.                                         
Warning: not running or target is remote            

Breakpoint 1, __kmalloc (size=size@entry=0xa8, flags=flags@entry=0x14080c0) at mm/slub.c:3746            
3746    {                                           
gdb-peda$                  

低レベルプログラミング

低レベルプログラミング

Linuxの/proc/cpuinfoでflagsを表示しているところの仕組みめも

/proc/cpuinfoでcpuの機能を表示するflagsのところってどうやって名称設定してんだろ?と思ったので調べてみたメモです。調べたカーネルのバージョンはv4.14.12です。

↓これですね f:id:masami256:20180110231210p:plain

/proc/cpuinfoのopen処理

これはfs/proc/cpuinfo.cにあるcpuinfo_open()が担当します。と言っても、実際の処理はseq_operations構造体のcpuinfo_opに関数がセットされていて、それらが実行されます。

extern const struct seq_operations cpuinfo_op;
static int cpuinfo_open(struct inode *inode, struct file *file)
{
    return seq_open(file, &cpuinfo_op);
}

ここからはアーキテクチャ固有になります。我らがx86_64ではarch/x86/kernel/cpu/proc.cにて以下のように設定を行っています。

onst struct seq_operations cpuinfo_op = {
    .start  = c_start,
    .next   = c_next,
    .stop   = c_stop,
    .show   = show_cpuinfo,
};

名前からしてshow_cpuinfo()を見れば良さそうに思えるのでこれを見てみます。

show_cpuinfo()

show_cpuinfo()が実際にcpuの情報を表示する関数です。

上から見ていくとこのようなところがみつかります。ここがflagsを出しているとこですね。

 seq_puts(m, "flags\t\t:");
    for (i = 0; i < 32*NCAPINTS; i++)
        if (cpu_has(c, i) && x86_cap_flags[i] != NULL)
            seq_printf(m, " %s", x86_cap_flags[i]);

seq_printf()はフォーマット指定子に%sを使っていて、引数はx86_cap_flags[i]なので、配列のx86_cap_flagsに名称が入ってるのがわかります。 そしたらx86_cap_flagsがどこで設定されてるか調べればOKですね。

x86_cap_flags配列

この配列ですが使ってるファイルは3ファイルです。

f:id:masami256:20180110232436p:plain

で、実際にこの配列に値を設定している箇所はmakeの実行前には存在しません(´・ω・`) なので、elixir.free-electrons.comだと見れないんですね。

x86_cap_flags配列の設定

ここで手元のカーネルになるので4.15.0-rc7になるんですが、arch/x86/kernel/cpu/capflags.cが設定している感じですね。

masami@saga:~/linux-kernel (test %)$ find arch/x86 -name "*.c" -o -name "*.h" | xargs grep -n "x86_cap_flags"
arch/x86/kernel/cpu/proc.c:102:         if (cpu_has(c, i) && x86_cap_flags[i] != NULL)
arch/x86/kernel/cpu/proc.c:103:                 seq_printf(m, " %s", x86_cap_flags[i]);
arch/x86/kernel/cpu/capflags.c:5:const char * const x86_cap_flags[NCAPINTS*32] = {
arch/x86/boot/mkcpustr.c:32:                    str = x86_cap_flags[i*32+j];
arch/x86/include/asm/cpufeature.h:35:extern const char * const x86_cap_flags[NCAPINTS*32];
arch/x86/include/asm/cpufeature.h:38:#define x86_cap_flag(flag) x86_cap_flags[flag]

このファイルはこんな内容です。

#ifndef _ASM_X86_CPUFEATURES_H
#include <asm/cpufeatures.h>
#endif

const char * const x86_cap_flags[NCAPINTS*32] = {
    [X86_FEATURE_FPU]        = "fpu",
    [X86_FEATURE_VME]        = "vme",
    [X86_FEATURE_DE]         = "de",
    [X86_FEATURE_PSE]        = "pse",
    [X86_FEATURE_TSC]        = "tsc",
    [X86_FEATURE_MSR]        = "msr",
    [X86_FEATURE_PAE]        = "pae",
    [X86_FEATURE_MCE]        = "mce",
    [X86_FEATURE_CX8]        = "cx8",
    [X86_FEATURE_APIC]       = "apic",
    [X86_FEATURE_SEP]        = "sep",
    [X86_FEATURE_MTRR]       = "mtrr",
    [X86_FEATURE_PGE]        = "pge",
    [X86_FEATURE_MCA]        = "mca",
    [X86_FEATURE_CMOV]       = "cmov",
    [X86_FEATURE_PAT]        = "pat",
    [X86_FEATURE_PSE36]      = "pse36",
    [X86_FEATURE_PN]         = "pn",
    [X86_FEATURE_CLFLUSH]        = "clflush",
    [X86_FEATURE_DS]         = "dts",
    [X86_FEATURE_ACPI]       = "acpi",
    [X86_FEATURE_MMX]        = "mmx",
    [X86_FEATURE_FXSR]       = "fxsr",
    [X86_FEATURE_XMM]        = "sse",
    [X86_FEATURE_XMM2]       = "sse2",
    [X86_FEATURE_SELFSNOOP]      = "ss",
    [X86_FEATURE_HT]         = "ht",
    [X86_FEATURE_ACC]        = "tm",
    [X86_FEATURE_IA64]       = "ia64",
    [X86_FEATURE_PBE]        = "pbe",
    [X86_FEATURE_SYSCALL]        = "syscall",
    [X86_FEATURE_MP]         = "mp",
    [X86_FEATURE_NX]         = "nx",
    [X86_FEATURE_MMXEXT]         = "mmxext",
    [X86_FEATURE_FXSR_OPT]       = "fxsr_opt",
    [X86_FEATURE_GBPAGES]        = "pdpe1gb",
    [X86_FEATURE_RDTSCP]         = "rdtscp",
    [X86_FEATURE_LM]         = "lm",
    [X86_FEATURE_3DNOWEXT]       = "3dnowext",
    [X86_FEATURE_3DNOW]      = "3dnow",
    [X86_FEATURE_RECOVERY]       = "recovery",
    [X86_FEATURE_LONGRUN]        = "longrun",
    [X86_FEATURE_LRTI]       = "lrti",
    [X86_FEATURE_CXMMX]      = "cxmmx",
    [X86_FEATURE_K6_MTRR]        = "k6_mtrr",
    [X86_FEATURE_CYRIX_ARR]      = "cyrix_arr",
    [X86_FEATURE_CENTAUR_MCR]    = "centaur_mcr",
    [X86_FEATURE_CONSTANT_TSC]   = "constant_tsc",
    [X86_FEATURE_UP]         = "up",
    [X86_FEATURE_ART]        = "art",
    [X86_FEATURE_ARCH_PERFMON]   = "arch_perfmon",
    [X86_FEATURE_PEBS]       = "pebs",
    [X86_FEATURE_BTS]        = "bts",
    [X86_FEATURE_REP_GOOD]       = "rep_good",
    [X86_FEATURE_ACC_POWER]      = "acc_power",
    [X86_FEATURE_NOPL]       = "nopl",
    [X86_FEATURE_XTOPOLOGY]      = "xtopology",
    [X86_FEATURE_TSC_RELIABLE]   = "tsc_reliable",
    [X86_FEATURE_NONSTOP_TSC]    = "nonstop_tsc",
    [X86_FEATURE_CPUID]      = "cpuid",
    [X86_FEATURE_EXTD_APICID]    = "extd_apicid",
    [X86_FEATURE_AMD_DCM]        = "amd_dcm",
    [X86_FEATURE_APERFMPERF]     = "aperfmperf",
    [X86_FEATURE_NONSTOP_TSC_S3]     = "nonstop_tsc_s3",
    [X86_FEATURE_TSC_KNOWN_FREQ]     = "tsc_known_freq",
    [X86_FEATURE_XMM3]       = "pni",
    [X86_FEATURE_PCLMULQDQ]      = "pclmulqdq",
    [X86_FEATURE_DTES64]         = "dtes64",
    [X86_FEATURE_MWAIT]      = "monitor",
    [X86_FEATURE_DSCPL]      = "ds_cpl",
    [X86_FEATURE_VMX]        = "vmx",
    [X86_FEATURE_SMX]        = "smx",
    [X86_FEATURE_EST]        = "est",
    [X86_FEATURE_TM2]        = "tm2",
    [X86_FEATURE_SSSE3]      = "ssse3",
    [X86_FEATURE_CID]        = "cid",
    [X86_FEATURE_SDBG]       = "sdbg",
    [X86_FEATURE_FMA]        = "fma",
    [X86_FEATURE_CX16]       = "cx16",
    [X86_FEATURE_XTPR]       = "xtpr",
    [X86_FEATURE_PDCM]       = "pdcm",
    [X86_FEATURE_PCID]       = "pcid",
    [X86_FEATURE_DCA]        = "dca",
    [X86_FEATURE_XMM4_1]         = "sse4_1",
    [X86_FEATURE_XMM4_2]         = "sse4_2",
    [X86_FEATURE_X2APIC]         = "x2apic",
    [X86_FEATURE_MOVBE]      = "movbe",
    [X86_FEATURE_POPCNT]         = "popcnt",
    [X86_FEATURE_TSC_DEADLINE_TIMER] = "tsc_deadline_timer",
    [X86_FEATURE_AES]        = "aes",
    [X86_FEATURE_XSAVE]      = "xsave",
    [X86_FEATURE_AVX]        = "avx",
    [X86_FEATURE_F16C]       = "f16c",
    [X86_FEATURE_RDRAND]         = "rdrand",
    [X86_FEATURE_HYPERVISOR]     = "hypervisor",
    [X86_FEATURE_XSTORE]         = "rng",
    [X86_FEATURE_XSTORE_EN]      = "rng_en",
    [X86_FEATURE_XCRYPT]         = "ace",
    [X86_FEATURE_XCRYPT_EN]      = "ace_en",
    [X86_FEATURE_ACE2]       = "ace2",
    [X86_FEATURE_ACE2_EN]        = "ace2_en",
    [X86_FEATURE_PHE]        = "phe",
    [X86_FEATURE_PHE_EN]         = "phe_en",
    [X86_FEATURE_PMM]        = "pmm",
    [X86_FEATURE_PMM_EN]         = "pmm_en",
    [X86_FEATURE_LAHF_LM]        = "lahf_lm",
    [X86_FEATURE_CMP_LEGACY]     = "cmp_legacy",
    [X86_FEATURE_SVM]        = "svm",
    [X86_FEATURE_EXTAPIC]        = "extapic",
    [X86_FEATURE_CR8_LEGACY]     = "cr8_legacy",
    [X86_FEATURE_ABM]        = "abm",
    [X86_FEATURE_SSE4A]      = "sse4a",
    [X86_FEATURE_MISALIGNSSE]    = "misalignsse",
    [X86_FEATURE_3DNOWPREFETCH]  = "3dnowprefetch",
    [X86_FEATURE_OSVW]       = "osvw",
    [X86_FEATURE_IBS]        = "ibs",
    [X86_FEATURE_XOP]        = "xop",
    [X86_FEATURE_SKINIT]         = "skinit",
    [X86_FEATURE_WDT]        = "wdt",
    [X86_FEATURE_LWP]        = "lwp",
    [X86_FEATURE_FMA4]       = "fma4",
    [X86_FEATURE_TCE]        = "tce",
    [X86_FEATURE_NODEID_MSR]     = "nodeid_msr",
    [X86_FEATURE_TBM]        = "tbm",
    [X86_FEATURE_TOPOEXT]        = "topoext",
    [X86_FEATURE_PERFCTR_CORE]   = "perfctr_core",
    [X86_FEATURE_PERFCTR_NB]     = "perfctr_nb",
    [X86_FEATURE_BPEXT]      = "bpext",
    [X86_FEATURE_PTSC]       = "ptsc",
    [X86_FEATURE_PERFCTR_LLC]    = "perfctr_llc",
    [X86_FEATURE_MWAITX]         = "mwaitx",
    [X86_FEATURE_RING3MWAIT]     = "ring3mwait",
    [X86_FEATURE_CPUID_FAULT]    = "cpuid_fault",
    [X86_FEATURE_CPB]        = "cpb",
    [X86_FEATURE_EPB]        = "epb",
    [X86_FEATURE_CAT_L3]         = "cat_l3",
    [X86_FEATURE_CAT_L2]         = "cat_l2",
    [X86_FEATURE_CDP_L3]         = "cdp_l3",
    [X86_FEATURE_INVPCID_SINGLE]     = "invpcid_single",
    [X86_FEATURE_HW_PSTATE]      = "hw_pstate",
    [X86_FEATURE_PROC_FEEDBACK]  = "proc_feedback",
    [X86_FEATURE_SME]        = "sme",
    [X86_FEATURE_PTI]        = "pti",
    [X86_FEATURE_INTEL_PPIN]     = "intel_ppin",
    [X86_FEATURE_INTEL_PT]       = "intel_pt",
    [X86_FEATURE_AVX512_4VNNIW]  = "avx512_4vnniw",
    [X86_FEATURE_AVX512_4FMAPS]  = "avx512_4fmaps",
    [X86_FEATURE_MBA]        = "mba",
    [X86_FEATURE_TPR_SHADOW]     = "tpr_shadow",
    [X86_FEATURE_VNMI]       = "vnmi",
    [X86_FEATURE_FLEXPRIORITY]   = "flexpriority",
    [X86_FEATURE_EPT]        = "ept",
    [X86_FEATURE_VPID]       = "vpid",
    [X86_FEATURE_VMMCALL]        = "vmmcall",
    [X86_FEATURE_FSGSBASE]       = "fsgsbase",
    [X86_FEATURE_TSC_ADJUST]     = "tsc_adjust",
    [X86_FEATURE_BMI1]       = "bmi1",
    [X86_FEATURE_HLE]        = "hle",
    [X86_FEATURE_AVX2]       = "avx2",
    [X86_FEATURE_SMEP]       = "smep",
    [X86_FEATURE_BMI2]       = "bmi2",
    [X86_FEATURE_ERMS]       = "erms",
    [X86_FEATURE_INVPCID]        = "invpcid",
    [X86_FEATURE_RTM]        = "rtm",
    [X86_FEATURE_CQM]        = "cqm",
    [X86_FEATURE_MPX]        = "mpx",
    [X86_FEATURE_RDT_A]      = "rdt_a",
    [X86_FEATURE_AVX512F]        = "avx512f",
    [X86_FEATURE_AVX512DQ]       = "avx512dq",
    [X86_FEATURE_RDSEED]         = "rdseed",
    [X86_FEATURE_ADX]        = "adx",
    [X86_FEATURE_SMAP]       = "smap",
    [X86_FEATURE_AVX512IFMA]     = "avx512ifma",
    [X86_FEATURE_CLFLUSHOPT]     = "clflushopt",
    [X86_FEATURE_CLWB]       = "clwb",
    [X86_FEATURE_AVX512PF]       = "avx512pf",
    [X86_FEATURE_AVX512ER]       = "avx512er",
    [X86_FEATURE_AVX512CD]       = "avx512cd",
    [X86_FEATURE_SHA_NI]         = "sha_ni",
    [X86_FEATURE_AVX512BW]       = "avx512bw",
    [X86_FEATURE_AVX512VL]       = "avx512vl",
    [X86_FEATURE_XSAVEOPT]       = "xsaveopt",
    [X86_FEATURE_XSAVEC]         = "xsavec",
    [X86_FEATURE_XGETBV1]        = "xgetbv1",
    [X86_FEATURE_XSAVES]         = "xsaves",
    [X86_FEATURE_CQM_LLC]        = "cqm_llc",
    [X86_FEATURE_CQM_OCCUP_LLC]  = "cqm_occup_llc",
    [X86_FEATURE_CQM_MBM_TOTAL]  = "cqm_mbm_total",
    [X86_FEATURE_CQM_MBM_LOCAL]  = "cqm_mbm_local",
    [X86_FEATURE_CLZERO]         = "clzero",
    [X86_FEATURE_IRPERF]         = "irperf",
    [X86_FEATURE_XSAVEERPTR]     = "xsaveerptr",
    [X86_FEATURE_DTHERM]         = "dtherm",
    [X86_FEATURE_IDA]        = "ida",
    [X86_FEATURE_ARAT]       = "arat",
    [X86_FEATURE_PLN]        = "pln",
    [X86_FEATURE_PTS]        = "pts",
    [X86_FEATURE_HWP]        = "hwp",
    [X86_FEATURE_HWP_NOTIFY]     = "hwp_notify",
    [X86_FEATURE_HWP_ACT_WINDOW]     = "hwp_act_window",
    [X86_FEATURE_HWP_EPP]        = "hwp_epp",
    [X86_FEATURE_HWP_PKG_REQ]    = "hwp_pkg_req",
    [X86_FEATURE_NPT]        = "npt",
    [X86_FEATURE_LBRV]       = "lbrv",
    [X86_FEATURE_SVML]       = "svm_lock",
    [X86_FEATURE_NRIPS]      = "nrip_save",
    [X86_FEATURE_TSCRATEMSR]     = "tsc_scale",
    [X86_FEATURE_VMCBCLEAN]      = "vmcb_clean",
    [X86_FEATURE_FLUSHBYASID]    = "flushbyasid",
    [X86_FEATURE_DECODEASSISTS]  = "decodeassists",
    [X86_FEATURE_PAUSEFILTER]    = "pausefilter",
    [X86_FEATURE_PFTHRESHOLD]    = "pfthreshold",
    [X86_FEATURE_AVIC]       = "avic",
    [X86_FEATURE_V_VMSAVE_VMLOAD]    = "v_vmsave_vmload",
    [X86_FEATURE_VGIF]       = "vgif",
    [X86_FEATURE_AVX512VBMI]     = "avx512vbmi",
    [X86_FEATURE_UMIP]       = "umip",
    [X86_FEATURE_PKU]        = "pku",
    [X86_FEATURE_OSPKE]      = "ospke",
    [X86_FEATURE_AVX512_VBMI2]   = "avx512_vbmi2",
    [X86_FEATURE_GFNI]       = "gfni",
    [X86_FEATURE_VAES]       = "vaes",
    [X86_FEATURE_VPCLMULQDQ]     = "vpclmulqdq",
    [X86_FEATURE_AVX512_VNNI]    = "avx512_vnni",
    [X86_FEATURE_AVX512_BITALG]  = "avx512_bitalg",
    [X86_FEATURE_AVX512_VPOPCNTDQ]   = "avx512_vpopcntdq",
    [X86_FEATURE_LA57]       = "la57",
    [X86_FEATURE_RDPID]      = "rdpid",
    [X86_FEATURE_OVERFLOW_RECOV]     = "overflow_recov",
    [X86_FEATURE_SUCCOR]         = "succor",
    [X86_FEATURE_SMCA]       = "smca",
};

const char * const x86_bug_flags[NBUGINTS*32] = {
    [X86_BUG_F00F - NCAPINTS*32]        = "f00f",
    [X86_BUG_FDIV - NCAPINTS*32]        = "fdiv",
    [X86_BUG_COMA - NCAPINTS*32]        = "coma",
    [X86_BUG_AMD_TLB_MMATCH - NCAPINTS*32]  = "tlb_mmatch",
    [X86_BUG_AMD_APIC_C1E - NCAPINTS*32]    = "apic_c1e",
    [X86_BUG_11AP - NCAPINTS*32]        = "11ap",
    [X86_BUG_FXSAVE_LEAK - NCAPINTS*32]     = "fxsave_leak",
    [X86_BUG_CLFLUSH_MONITOR - NCAPINTS*32]     = "clflush_monitor",
    [X86_BUG_SYSRET_SS_ATTRS - NCAPINTS*32]     = "sysret_ss_attrs",
    [X86_BUG_NULL_SEG - NCAPINTS*32]    = "null_seg",
    [X86_BUG_SWAPGS_FENCE - NCAPINTS*32]    = "swapgs_fence",
    [X86_BUG_MONITOR - NCAPINTS*32]         = "monitor",
    [X86_BUG_AMD_E400 - NCAPINTS*32]    = "amd_e400",
    [X86_BUG_CPU_MELTDOWN - NCAPINTS*32]    = "cpu_meltdown",
};

そして、このファイルがどのように作られるかと言うと、./arch/x86/kernel/cpu/Makefileの以下の部分です。mkcapflags.shというシェルスクリプトを実行してそこからファイルを作ってます。

ifdef CONFIG_X86_FEATURE_NAMES
quiet_cmd_mkcapflags = MKCAP   $@
      cmd_mkcapflags = $(CONFIG_SHELL) $(srctree)/$(src)/mkcapflags.sh $< $@

cpufeature = $(src)/../../include/asm/cpufeatures.h

targets += capflags.c
$(obj)/capflags.c: $(cpufeature) $(src)/mkcapflags.sh FORCE
    $(call if_changed,mkcapflags)
endif

まとめ

flagsに表示する機能の名前は配列で定義されていて、その配列はカーネルコンパイル中に作られます。

Linux4.14.12(x86_64)のPage Global Directoryの設定を見てみる

Linux 4.14でプロセスをforkした時のPage Global Directoryの設定を見てみます。読むカーネルはv4.14.12です。 前にLinux x86_64のPaging:Page Global Directory辺りの扱いを見てみる - φ(・・*)ゞ ウーン カーネルとか弄ったりのメモ書いてたけど、最新のカーネルで調べてみたので。

プロセス生成時のPage Global Directoryの設定の流れ

Page Global Directory(pgd)の設定はpgd_alloc()で行います。fork()からの流れはこのような形。

_do_fork()
  -> copy_process()
    -> copy_mm()
      ->  dup_mm()
        -> mm_init()
          -> mm_alloc_pgd()
            -> pgd_alloc()

pgd_alloc()

pgd_alloc()はこのような関数です。

pgd_t *pgd_alloc(struct mm_struct *mm)
{
    pgd_t *pgd;
    pmd_t *pmds[PREALLOCATED_PMDS];

    pgd = _pgd_alloc();

    if (pgd == NULL)
        goto out;

    mm->pgd = pgd;

    if (preallocate_pmds(mm, pmds) != 0)
        goto out_free_pgd;

    if (paravirt_pgd_alloc(mm) != 0)
        goto out_free_pmds;

    /*
    * Make sure that pre-populating the pmds is atomic with
    * respect to anything walking the pgd_list, so that they
    * never see a partially populated pgd.
    */
    spin_lock(&pgd_lock);

    pgd_ctor(mm, pgd);
    pgd_prepopulate_pmd(mm, pgd, pmds);

    spin_unlock(&pgd_lock);

    return pgd;

out_free_pmds:
    free_pmds(mm, pmds);
out_free_pgd:
    _pgd_free(pgd);
out:
    return NULL;
}

pgd_tとpmd_tの変数が宣言されてますね。pgdのほうは良いとして、pmdsですが配列となっています。サイズはPREALLOCATED_PMDSですが、これはPAEの設定で変わります。x86_64環境だとPAEは無いのでPREALLOCATED_PMDSは0と定義されています。

/* No need to prepopulate any pagetable entries in non-PAE modes. */
#define PREALLOCATED_PMDS  0

では、処理を見ていきます。

 pgd = _pgd_alloc();

最初に_pgd_alloc()を呼んでメモリを確保してます。_pgd_alloc()もPAEの有無で処理が変わるのですが、x86_64の場合は__get_free_pages()でページを確保します。 ここで最近話題のKPTIに関する分岐がありました。KPTIが有効なら2ページ確保してます。

#ifdef CONFIG_PAGE_TABLE_ISOLATION
/*
 * Instead of one PGD, we acquire two PGDs.  Being order-1, it is
 * both 8k in size and 8k-aligned.  That lets us just flip bit 12
 * in a pointer to swap between the two 4k halves.
 */
#define PGD_ALLOCATION_ORDER 1
#else
#define PGD_ALLOCATION_ORDER 0
#endif

ページが確保できたら mm->pgd にpgdをセットします。

次にpreallocate_pmds()でpmd用のメモリを確保します。

 if (preallocate_pmds(mm, pmds) != 0)
        goto out_free_pgd;

preallocate_pmds()x86_64環境だと特にやることはありません。ここもPAEが有効な場合に主要な処理があるだけです。x86_64だと引数で渡されたmm構造体がinit_mmだったらアカウンティングをしないという設定をする程度です。

 if (mm == &init_mm)
        gfp &= ~__GFP_ACCOUNT;

ここは特になにもありません。

 if (paravirt_pgd_alloc(mm) != 0)
        goto out_free_pmds;

関数はarch/x86/include/asm/paravirt.hで定義されていて、

static inline int paravirt_pgd_alloc(struct mm_struct *mm)
{
    return PVOP_CALL1(int, pv_mmu_ops.pgd_alloc, mm);
}

pgd_allocは__paravirt_pgd_alloc()がセットされてます。

 .pgd_alloc = __paravirt_pgd_alloc,
    .pgd_free = paravirt_nop,

__paravirt_pgd_alloc()は単に0を返してます。

static inline int  __paravirt_pgd_alloc(struct mm_struct *mm) { return 0; }

こちらはコンストラクタ的な処理です。pgd_ctor()は後ほど。

 pgd_ctor(mm, pgd);

最後はpmdの設定です。

 pgd_prepopulate_pmd(mm, pgd, pmds);

x86_64の場合、pmd用のメモリ確保もしていないのでpgd_prepopulate_pmd()も特に処理はありません。 こんな感じになってます。

 if (PREALLOCATED_PMDS == 0) /* Work around gcc-3.4.x bug */
        return;

ここまででエラーが無ければ終了です。

pgd_ctor()

pgd_ctor()はコンストラクタ的な処理ですね。

static void pgd_ctor(struct mm_struct *mm, pgd_t *pgd)
{
    /* If the pgd points to a shared pagetable level (either the
      ptes in non-PAE, or shared PMD in PAE), then just copy the
      references from swapper_pg_dir. */
    if (CONFIG_PGTABLE_LEVELS == 2 ||
        (CONFIG_PGTABLE_LEVELS == 3 && SHARED_KERNEL_PMD) ||
        CONFIG_PGTABLE_LEVELS >= 4) {
        clone_pgd_range(pgd + KERNEL_PGD_BOUNDARY,
                swapper_pg_dir + KERNEL_PGD_BOUNDARY,
                KERNEL_PGD_PTRS);
    }

    /* list required to sync kernel mapping updates */
    if (!SHARED_KERNEL_PMD) {
        pgd_set_mm(pgd, mm);
        pgd_list_add(pgd);
    }
}

最初のif文は、通常のx86_64環境ならCONFIG_PGTABLE_LEVELSは4だと思います。5段階ページングとか有効にしてたら5でしょうけども。それはともかく、clone_pgd_range()の実行があります。 clone_pgd_range()はこのような関数です。

static inline void clone_pgd_range(pgd_t *dst, pgd_t *src, int count)
{
    memcpy(dst, src, count * sizeof(pgd_t));
#ifdef CONFIG_PAGE_TABLE_ISOLATION
    if (!static_cpu_has(X86_FEATURE_PTI))
        return;
    /* Clone the user space pgd as well */
    memcpy(kernel_to_user_pgdp(dst), kernel_to_user_pgdp(src),
           count * sizeof(pgd_t));
#endif
}

最初にアドレスswapper_pg_dir + KERNEL_PGD_BOUNDARYからKERNEL_PGD_PTRS*sizeof(pgd_t)バイトをアドレスpgd + KERNEL_PGD_BOUNDARYにコピーします。KERNEL_PGD_BOUNDARYについてはKASLRの有効無効で値が変わります。KERNEL_PGD_PTRSは以下の用になっていて、PTRS_PER_PGDは512です。

#define KERNEL_PGD_PTRS     (PTRS_PER_PGD - KERNEL_PGD_BOUNDARY)

static_cpu_has()のところはCPUがPTIをサポートしてないならここで終了です。このフラグについては/arch/x86/include/asm/cpufeatures.hに定義があります。 そうでなければユーザー空間のマッピングもコピーしてます。

2018/01/10追記 このフラグはpti_check_boottime_disable()の処理でPTIが無効にセットされていなければ、setup_force_cpu_cap()を使ってセットしてます。

 setup_force_cpu_cap(X86_FEATURE_PTI);

pgd_ctor()に戻って、SHARED_KERNEL_PMDの値をチェックして0ならpgd_set_mm()pgd_list_add()の実行があります。4段階のページングを使ってるならSHARED_KERNEL_PMDの値は0です。

pgd_set_mm()はpgd変数のアドレスに該当するpage構造体のindexメンバ変数にmm構造体をセットしてます。

static void pgd_set_mm(pgd_t *pgd, struct mm_struct *mm)
{
    BUILD_BUG_ON(sizeof(virt_to_page(pgd)->index) < sizeof(mm));
    virt_to_page(pgd)->index = (pgoff_t)mm;
}

pgd_list_add()のほうはarch/x86/mm/fault.cにあるpgd_listにpgdのpage構造体を追加してます。

static inline void pgd_list_add(pgd_t *pgd)
{
    struct page *page = virt_to_page(pgd);

    list_add(&page->lru, &pgd_list);
}

ここまででpgdの設定が終わりです。

最後に

pgd_alloc()はx86_64の場合はpmdの設定はなくて、pgdの設定のみ行います。また、KPTIが入ったことでこの機能関連の処理が追加されていました。

この後、kernel/fork.cのmm_init()まで戻ってinit_new_context()を実行します。init_new_context()はまたの機会に。

( ´ー`)フゥー...

動くメカニズムを図解&実験! Linux超入門 (My Linuxシリーズ)

動くメカニズムを図解&実験! Linux超入門 (My Linuxシリーズ)

Linuxカーネルで一回だけ実行する関数を作る

この記事はLinux Advent Calendar 2017の9日目の記事です。 なんとなくlib/を見ていたらonce.cなんてファイルを見つけて、一度だけ実行したいという時に使う関数を見つけたのでその機能についてのきじになります。

使い方

まず使い方をザクっと見てみましょう。使用するのはDO_ONCEマクロです。 実装はこんな感じです。

サンプルコード

#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/debugfs.h>
#include <asm/uaccess.h>
#include <linux/err.h>
#include <linux/once.h>

MODULE_DESCRIPTION("once test");
MODULE_AUTHOR("masami256");
MODULE_LICENSE("GPL");

static struct dentry *once_test_file;

static void once_test_run_once(int *count)
{
    (*count)++;
    pr_info("%s called\n", __func__);
}

static ssize_t once_test_write(struct file *filp, const char __user *buf, size_t len, loff_t *ppos)
{
    static int called_cnt = 0;

    DO_ONCE(once_test_run_once, &called_cnt);
    pr_info("called_cnt: %d\n", called_cnt);
    return strnlen_user(buf, 8);
}

static struct file_operations once_test_fops = {
    .owner = THIS_MODULE,
    .write = once_test_write,
};

static int once_test_init(void)
{
    once_test_file = debugfs_create_file("once_test", 0200,
                   NULL, NULL,
                   &once_test_fops);

    if (IS_ERR(once_test_file)) {
        WARN_ON(1);
        return PTR_ERR(once_test_file);
    }

    pr_info("setup done.\n");
    return 0;
}

static void once_test_cleanup(void)
{
    pr_info("cleanup\n");
    debugfs_remove(once_test_file);
}

module_init(once_test_init);
module_exit(once_test_cleanup);

使い方的には/sys/kernel/debug/once_testというファイルに何か書き込むとpr_info()で変数の値を表示します。ここで、初回に限りcalled_cnt変数の値をインクリメントしています。

実行結果

実行するとこうなります。想像通りですね( ´∀`)bグッ!

[  998.207119] once_test: setup done.
[ 1004.934061] once_test: once_test_run_once called
[ 1004.934069] once_test: called_cnt: 1
[ 1015.924961] once_test: called_cnt: 1

DO_ONCEの実装

一度だけ実行したい関数と、その関数用の引数を任意の数だけ受け取ります。先のサンプルコードは1個しか引数を受け取ってませんが。

        #define DO_ONCE(func, ...)                           \
   ({                                     \
       bool ___ret = false;                       \
       static bool ___done = false;                  \
       static struct static_key ___once_key = STATIC_KEY_INIT_TRUE; \
       if (static_key_true(&___once_key)) {                 \
           unsigned long ___flags;                    \
           ___ret = __do_once_start(&___done, &___flags);       \
           if (unlikely(___ret)) {                  \
               func(__VA_ARGS__);               \
               __do_once_done(&___done, &___once_key,       \
                          &___flags);           \
           }                          \
       }                              \
       ___ret;                              \
   })

実行したかのチェックは単純なstatic変数のdoneです。で、doneがfalseならまだ未実行なので引数で渡された関数を実行します。funcの実行前後で_do_once_start()__do_once_done()の呼び出しがあります。 do_once_start()のほうはfuncの実行前にロックを取るだけです。__do_once_done()のほうは多少の処理があります。こちらは後ほど。

WARN*ONCEマクロとの違い

自分は一度だけ実行ってことで思い浮かぶのはWARN*ONCE系のマクロだったりします。DO_ONCEマクロはそれらとは違っています。WARN*ONCE系のマクロの実装、カウンタのクリアに関する実装はこちらに書いたので興味のある方は読んでみてくださいm( )m

qiita.com

一度だけというところをチェックする変数の置き場所もWARN*ONCE系マクロとは違っています。WARN*ONCE系マクロの場合、チェックに使用する変数は.dataセクションのstart_onceとend_onceの間に変数が置かれますが、DO_ONCEの場合は単なるstatic変数です。あと、チェックの変数が、doneとonce_keyの2種類があります。doneの方はdo_once_start()でロックを取る時にというか、ロックを取ったあとに値をチェックして、trueだったら即ロックを解放します。その場合、do_once_start()はfalseを返すのでfuncの実行はありません。funcを実行した場合は、do_once_done()でdoneをtrueに変えます。_once_keyのほうは__do_once_done()で使用します。

__do_once_done()の処理

この関数の処理は3つあります。1つはdone変数の値をtrueに変える。2つ目はdo_once_start()で取ったロックの解放です。そして3つ目が__once_keyの処理です。と言っても難しいことはなくて、static_key構造体のenabled変数の値を1から0にするだけです。

___once_keyはこのように初期化されていました。

static struct static_key ___once_key = STATIC_KEY_INIT_TRUE;

これはinclude/linux/jump_label.hを見るとこのようになっています。

#define STATIC_KEY_INIT_TRUE                    \
   { .enabled = { 1 },                    \
     { .entries = (void *)JUMP_TYPE_TRUE } }

そして、値を変えているのはlib/once.cのこの部分です。

static_key_slow_dec(work->key);

static_key_slow_dec()はinclude/linux/jump_label.hにある関数で、enabledメンバ変数の値を減らしているだけです。

static inline void static_key_slow_dec(struct static_key *key)
{
    STATIC_KEY_CHECK_USE();
    atomic_dec(&key->enabled);
}

と、やっていることは簡単です。ただ、do_once_done()で_once_keyの値を変えるのではなくて、値の変更をする関数をワークキューに突っ込んで、スケジューラによってワーカーが実行されたら値を変えるというようになってます。

string_get_size()でサイズのお手軽表示

この記事はLinux Advent Calendar 2017の22日目の記事です。 カーネルのコードを書いていてサイズを表示したい時にstring_get_size()を使うとお手軽に2進接頭辞(KiBとか)とSI接頭辞(KBとか)を使ったサイズの文字列を作ることができます。

関数のプロトタイプはこうです。

void string_get_size(u64 size, u64 blk_size, enum string_size_units units,
             char *buf, int len);

sizeとblk_sizeは使い分けが有ります。バイト数を扱いたい場合はsizeにバイト数、blk_sizeには1を指定します。ブロックデバイスやページなどを扱う場合などはそのサイズをblk_sizeに指定して、それがいくつあるかをsizeで指定します。

たとえば、1024という数字を2進接頭辞にするならsizeには1024、blk_sizeは1をセットします。1ページのサイズが4096バイトで1ページのバイト数を2進接頭辞にするならsizeは1、blk_sizeは4096という感じです。 unitsは2進接頭辞かSI接頭辞を指定します。bufに渡した変数に結果が入ります。lenはbufで利用可能なバイト数ですね。

こんな感じで使えます。

#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/string_helpers.h>

MODULE_DESCRIPTION("size test");
MODULE_AUTHOR("masami256");
MODULE_LICENSE("GPL");

struct size_test_data {
    u64 size;
    u64 blk_size;
    int unit;
};

static struct size_test_data data[] = {
    {
        .size = 1024,
        .blk_size = 1,
        .unit = STRING_UNITS_2,
    },
    {
        .size = 1024,
        .blk_size = 1,
        .unit = STRING_UNITS_10,
    },
    {
        .size = 1,
        .blk_size = 4096,
        .unit = STRING_UNITS_2,
    },
    {
        .size = 1,
        .blk_size = 4096,
        .unit = STRING_UNITS_10,
    }
};

static int size_test_init(void)
{
    int i;

    for (i = 0; i < sizeof(data) / sizeof(data[0]); i++) {
        char buf[16] = { 0 };
        struct size_test_data tmp = data[i];
        string_get_size(tmp.size, tmp.blk_size, tmp.unit, buf, sizeof(buf) - 1);
        pr_info("size:%lld, blk_size: %lld, unit:%d,  %s\n",
            tmp.size, tmp.blk_size,
            tmp.unit, buf);
    }

    return 0;
}

static void size_test_cleanup(void)
{
    pr_info("%s bye\n", __func__);
}

module_init(size_test_init);
module_exit(size_test_cleanup);

実行するとこう表示されます。

[ 1402.100472] size_test: size:1024, blk_size: 1, unit:1,  1.00 KiB                                      
[ 1402.100474] size_test: size:1024, blk_size: 1, unit:0,  1.02 kB                                       
[ 1402.100474] size_test: size:1, blk_size: 4096, unit:1,  4.00 KiB                                      
[ 1402.100475] size_test: size:1, blk_size: 4096, unit:0,  4.10 kB  

地味に便利ですね( ´ー`)フゥー...

argbashでbashスクリプトのオプション引数を受け取る

この記事はShell Script Advent Calendar 2017の15日目の記事です。

bashスクリプトでオプション引数を処理したい時にcaseで処理したりすると思いますが、引数の処理をサポートするargbashというツールがありました。というわけで、試してみます。

argbashでは3種類の引数の形式を使用できます。オプション引数は-が2つのロングオプション形式です。例えば、--fooとかですね。

引数のタイプ argbashでの使い方
bool型。引数を受け取らないタイプ --bool-opt 名前
引数を受け取るタイプ --opt 名前
--で始まるオプションを使わないタイプ。rmコマンドのファイル名とかディレクトリ名みたいなタイプ --pos 名前

argbashの使いかとしてはargbash-initというツールでテンプレートを作って、argbashでシェルスクリプト化するのが基本のようです。また、bool型の引数の場合、--bool-opt fooとすると--fooと--no-fooという2個のオプションが作られます。後者の方は--fooを無効にするってことですね。

試しに--name 文字列という形の引数を受け取るような設定でコマンドを使うとこのような感じになります。

masami@saga:~/codes/argtest$ argbash-init --opt name
#!/bin/bash

# m4_ignore(
echo "This is just a script template, not the script (yet) - pass it to 'argbash' to fix this." >&2
exit 11  #)Created by argbash-init v2.5.0
# ARG_OPTIONAL_SINGLE([name], , [<name's help message goes here>])
# ARG_HELP([<The general help message of my script>])
# ARGBASH_GO

# [ <-- needed because of Argbash

echo "Value of --name: $_arg_name"

# ] <-- needed because of Argbash

この例では標準出力に出力してますが、パイプでargbashにデータを渡しても構いません。では、実行してみます。

masami@saga:~/codes/argtest$ argbash-init --opt name | argbash -o name.sh -
masami@saga:~/codes/argtest$ wc -l name.sh
82 name.sh
masami@saga:~/codes/argtest$ 

82行ほどのファイルができあがります。ヘルプ用に-h/--helpがサポートされています。自分で作った--nameオプションを使うと値が表示されます。あとは自分で弄っていく感じですね。

masami@saga:~/codes/argtest$ ./name.sh -h
<The general help message of my script>
Usage: ./name.sh [--name <arg>] [-h|--help]
        --name: <name's help message goes here> (no default)
        -h,--help: Prints help
masami@saga:~/codes/argtest$ ./name.sh --name foobar
Value of --name: foobar

このスクリプトのオプション引数はこのように解析されます。

parse_commandline ()
{
        while test $# -gt 0
        do
                _key="$1"
                case "$_key" in
                        --name)
                                test $# -lt 2 && die "Missing value for the optional argument '$_key'." 1
                                _arg_name="$2"
                                shift
                                ;;
                        --name=*)
                                _arg_name="${_key##--name=}"
                                ;;
                        -h|--help)
                                print_help
                                exit 0
                                ;;
                        -h*)
                                print_help
                                exit 0
                                ;;
                        *)
                                _PRINT_HELP=yes die "FATAL ERROR: Got an unexpected argument '$1'" 1
                                ;;
                esac
                shift
        done
}

引数3パターン使ってみて、greeting引数には適当なメッセージ、to-upppercaseでname引数の大文字化をするようなことをしてみます。

masami@saga:~/codes/argtest$ argbash-init --pos name --opt greeting --opt-bool to-upppercase | argbash -o test.sh -                                                                                                

そして、test.shをちょろっと弄ります。

name=${_arg_name}
if [ ${_arg_to_upppercase} = "on" ]; then
    name=${_arg_name^^}
fi

echo "Value of --greeting: $_arg_greeting"
echo "to-upppercase is $_arg_to_upppercase"
echo "Value of name: ${name}"

これを実行するとこんなふうになります。

masami@saga:~/codes/argtest$ ./test.sh --greeting hello --to-upppercase foobar
Value of --greeting: hello
to-upppercase is on
Value of name: FOOBAR
masami@saga:~/codes/argtest$ ./test.sh --greeting hello --no-to-upppercase foobar
Value of --greeting: hello
to-upppercase is off
Value of name: foobar
masami@saga:~/codes/argtest$ ./test.sh --greeting hello  foobar
Value of --greeting: hello
to-upppercase is off
Value of name: foobar

argbashはオプションの解析部分・ヘルプメッセージ表示をサポートしてくれるので結構良いですね( ´∀`)bグッ!

シェルプログラミング実用テクニック

シェルプログラミング実用テクニック