20190526のLinuxに関する記事は6件です。

カーネルクラッシュダンプの解析方法

はじめに

前回記事の続きとして、カーネルクラッシュダンプを解析する方法について書きました。"カーネルクラッシュダンプを解析した経験のない人向け" の記事です。

この記事では、カーネルクラッシュダンプ (以降、クラッシュダンプと表現) の解析に必要なツール類を導入したあと、実際に解析をしてみるという流れになっています。

なお、解析方法 (確認観点) はクラッシュの原因により変わりますので、この記事では一例として、NULL ポインタ参照により意図的にクラッシュさせた環境のダンプ(前回記事と同じ Magic SysRq Key)を用いて解析します。原因は判明していますが、解析のなんとなくのイメージを掴むには良いと思いました。

1 解析環境を整える

解析には、次の3つが必要です。

  • クラッシュダンプ (vmcore)
  • カーネルのデバッグ情報
  • crash コマンド

クラッシュダンプ

まずはクラッシュダンプ (vmcore) が必要です。

もしクラッシュダンプの取り方を知らない場合は、前回記事を参考に取得してください。

カーネルのデバッグ情報

解析には、クラッシュダンプを取得した環境のカーネルに対応するデバッグ情報が必要です。クラッシュダンプを取得した環境のカーネル (バージョン) は、strings コマンドから確認できます。

[root@localhost ~]# strings <vmcore 配置パス>/vmcore | head
KDUMP   
Linux
localhost.localdomain
3.10.0-957.el7.x86_64

...(snip)...

デバッグ情報は、次のコマンドからインストールできます (3.10.0-957.el7.x86_64 の場合)。

[root@localhost ~]# yum install --enablerepo=base-debuginfo kernel-debuginfo-3.10.0-957.el7

インストールに成功すると、/usr/lib/debug/usr/lib/modules/<カーネルバージョン>/ ディレクトリ配下に vmlinux が格納されます (後で使います)。

[root@localhost ~]# ls /usr/lib/debug/usr/lib/modules/3.10.0-957.el7.x86_64/
kernel  vdso  vmlinux

crash コマンド

解析ツールとして crash コマンドが必要です。次のコマンドからインストールできます。

[root@localhost ~]# yum install crash

2 クラッシュダンプを解析する

解析するには、次のようにデバッグ情報とクラッシュダンプを crash コマンドに渡して実行します。

[root@localhost ~]# crash /usr/lib/debug/usr/lib/modules/<カーネルバージョン>/vmlinux <vmcore 配置パス>/vmcore

しらばらくすると (読み込みに成功すると)、次のように表示されます。

crash 7.2.3-8.el7

...(snip)...

      KERNEL: /usr/lib/debug/usr/lib/modules/3.10.0-957.el7.x86_64/vmlinux
    DUMPFILE: /var/crash/127.0.0.1-2019-04-25-21:24:51/vmcore  [PARTIAL DUMP]
        CPUS: 1
        DATE: Thu Apr 25 21:24:41 2019
      UPTIME: 14:22:25
LOAD AVERAGE: 0.00, 0.01, 0.05
       TASKS: 457
    NODENAME: localhost.localdomain
     RELEASE: 3.10.0-957.el7.x86_64
     VERSION: #1 SMP Thu Nov 8 23:39:32 UTC 2018
     MACHINE: x86_64  (2599 Mhz)
      MEMORY: 3 GB
       PANIC: "SysRq : Trigger a crash"
         PID: 5013
     COMMAND: "bash"
        TASK: ffff927722ec1040  [THREAD_INFO: ffff92771e70c000]
         CPU: 0
       STATE: TASK_RUNNING (SYSRQ)

crash> 

各文字列の意味は次表のとおりです。

文字列 意味
KERNEL: crash の引数に与えた vmlinux のパス
DUMPFILE: crash の引数に与えた vmlinux のパス
CPUS: クラッシュしたマシンの CPU 数
DATE: クラッシュ発生日時
UPTIME: クラッシュしたマシンの稼働時間
LOAD AVERAGE: クラッシュ時のロードアベレージ
TASKS: クラッシュ時のタスク数
NODENAME: クラッシュしたマシンのノード名
RELEASE: クラッシュしたマシンのカーネルバージョン
VERSION: 上記カーネル (RELEASE:) のリリース日
MACHINE: クラッシュしたマシンの
CPU のアーキテクチャ
MEMORY: クラッシュしたマシンの物理メモリサイズ
PANIC: クラッシュの種類やメッセージなどの情報
PID: クラッシュ時に動作していたプロセスの ID
COMMAND: PID: に対応するプロセス名
TASK: PID: に対応するプロセスのアドレス
CPU: PID に対応するプロセスが
実行されていた CPU コア番号
STATE: PID に対応するプロセスの状態

mod -S でモジュール情報を読み込む

本格的に解析を始める前に crash> mod -Sで、ロードされている全てのカーネルモジュールのデバッグ情報を読み込んでおいた方が良いです。これをしておかないと、特定のモジュールに関連した関数などの情報を確認する際に対応するデバッグ情報を読み込まなくてはいけない (crash> mod -s <モジュール名>) ので面倒です。

crash> mod -S
     MODULE       NAME                            SIZE  OBJECT FILE
ffffffffc02f5880  dm_mod                        124407  /usr/lib/debug/usr/lib/modules/3.10.0-957.el7.x86_64/kernel/drivers/md/dm-mod.ko.debug 
ffffffffc0302000  dm_region_hash                 20813  /usr/lib/debug/usr/lib/modules/3.10.0-957.el7.x86_64/kernel/drivers/md/dm-region-hash.ko.debug 
ffffffffc030f160  dm_log                         18411  /usr/lib/debug/usr/lib/modules/3.10.0-957.el7.x86_64/kernel/drivers/md/dm-log.ko.debug

...(snip)...

log から状況を把握する

クラッシュ時のリングバッファからは様々な情報が確認できます。まずはここからクラッシュ時の状況を把握するのが良いと思います。

crash> log
[    0.000000] Initializing cgroup subsys cpuset

...(snip)...

[51745.467192] SysRq : Trigger a crash

次の2行からNULL ポインタ参照が直接の原因であることと、sysrq_handle_crash 関数がその引き金となったことが分かります 1

[51745.467352] BUG: unable to handle kernel NULL pointer dereference at           (null)
[51745.467356] IP: [<ffffffff9d461bf6>] sysrq_handle_crash+0x16/0x20

次の行は参照しようとしたアドレスのページテーブル情報です。

[51745.467403] PGD 8000000098f52067 PUD a9306067 PMD 0

Oops: の後ろにある 0002 (2進数: 0010) はエラーコードです。

2進数表記にした時の各 bit の意味を左から順に説明すると次のとおりです。

  • 1 bit 目 ... 1 なら命令フェッチに成功、0 なら失敗
  • 2 bit 目 ... 1 ならユーザモード、0 ならカーネルモード
  • 3 bit 目 ... 1 なら書き込みアクセス、0 なら読み取り or 実行アクセス
  • 4 bit 目 ... 1 なら無効なアクセス、0 ならページが存在しなかった
[51745.467411] Oops: 0002 [#1] SMP

Modules linked in: 以降の文字列はロード済みモジュールの情報です。

[51745.467420] Modules linked in: tcp_lp fuse uinput xt_CHECKSUM ipt_MASQUERADE nf_nat_masquerade_ipv4 tun devlink ip6t_rpfilter ipt_REJECT nf_reject_ipv4 ip6t_REJECT nf_reject_ipv6 xt_conntrack ip_set nfnetlink ebtable_nat ebtable_broute bridge stp llc ip6table_nat nf_conntrack_ipv6 nf_defrag_ipv6 nf_nat_ipv6 ip6table_mangle ip6table_security ip6table_raw iptable_nat nf_conntrack_ipv4 nf_defrag_ipv4 nf_nat_ipv4 nf_nat nf_conntrack iptable_mangle iptable_security iptable_raw ebtable_filter ebtables ip6table_filter ip6_tables iptable_filter sunrpc ppdev iosf_mbi crc32_pclmul ghash_clmulni_intel snd_hda_codec_generic aesni_intel lrw gf128mul glue_helper ablk_helper cryptd joydev pcspkr snd_hda_intel virtio_balloon snd_hda_codec snd_hda_core sg snd_hwdep snd_seq snd_seq_device snd_pcm parport_pc parport
[51745.467499]  snd_timer snd soundcore i2c_piix4 ip_tables xfs libcrc32c sr_mod cdrom virtio_blk virtio_console virtio_net crct10dif_pclmul crct10dif_common crc32c_intel ata_generic serio_raw qxl pata_acpi drm_kms_helper syscopyarea sysfillrect sysimgblt fb_sys_fops ttm floppy drm ata_piix libata virtio_pci virtio_ring virtio drm_panel_orientation_quirks dm_mirror dm_region_hash dm_log dm_mod

次の行にある情報は crash コマンドの起動時に表示される情報とほとんど同じです。この中で注目すべき箇所は Not tainted という文字列です。これは、カーネルが汚染されていないことを示します。汚染要因は様々ですが、汚染されている場合は、Tainted: P のように表示されます (詳細は割愛)。

[51745.467550] CPU: 0 PID: 5013 Comm: bash Kdump: loaded Not tainted 3.10.0-957.el7.x86_64 #1

次の行は文字通りハードウェアの名の情報です (今回は KVM 上でクラッシュさせました)。なお、ハードウェア名は dmicode コマンドなどでも確認できます。

[51745.467555] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.10.2-1ubuntu1 04/01/2014

次の行はクラッシュ時に動作していたプロセスのアドレスなどの情報です。

[51745.467559] task: ffff927722ec1040 ti: ffff92771e70c000 task.ti: ffff92771e70c000

RIP から CR2 はクラッシュ時のレジスタの情報です。

[51745.467563] RIP: 0010:[<ffffffff9d461bf6>]  [<ffffffff9d461bf6>] sysrq_handle_crash+0x16/0x20
[51745.467576] RSP: 0018:ffff92771e70fe58  EFLAGS: 00010246
[51745.467580] RAX: ffffffff9d461be0 RBX: ffffffff9dce4c60 RCX: 0000000000000000
[51745.467583] RDX: 0000000000000000 RSI: ffff92773fc13898 RDI: 0000000000000063
[51745.467585] RBP: ffff92771e70fe58 R08: ffffffff9dfe38bc R09: ffffffff9dfee37b
[51745.467591] R10: 000000000000025c R11: 000000000000025b R12: 0000000000000063
[51745.467594] R13: 0000000000000000 R14: 0000000000000004 R15: 0000000000000000
[51745.467598] FS:  00007fbedb513740(0000) GS:ffff92773fc00000(0000) knlGS:0000000000000000
[51745.467601] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[51745.467603] CR2: 0000000000000000 CR3: 000000009ce18000 CR4: 00000000001606f0

コールトレースの情報です。

[51745.467610] Call Trace:
[51745.467617]  [<ffffffff9d46241d>] __handle_sysrq+0x10d/0x170
[51745.467628]  [<ffffffff9d462888>] write_sysrq_trigger+0x28/0x40
[51745.467645]  [<ffffffff9d2b7f30>] proc_reg_write+0x40/0x80
[51745.467650]  [<ffffffff9d2410a0>] vfs_write+0xc0/0x1f0
[51745.467654]  [<ffffffff9d241ebf>] SyS_write+0x7f/0xf0
[51745.467680]  [<ffffffff9d774ddb>] system_call_fastpath+0x22/0x27

RIP が示すアドレスから 64 バイト分までのコードの情報です。

[51745.467682] Code: eb 9b 45 01 f4 45 39 65 34 75 e5 4c 89 ef e8 e2 f7 ff ff eb db 0f 1f 44 00 00 55 48 89 e5 c7 05 21 57 7e 00 01 00 00 00 0f ae f8 <c6> 04 25 00 00 00 00 01 5d c3 0f 1f 44 00 00 55 31 c0 c7 05 9e

前述したレジスタの情報と同じです (内容が重複していることもあるようです)。

[51745.467750] RIP  [<ffff92771e70fe58>] sysrq_handle_crash+0x16/0x20
[51745.467755]  RSP <ffff92771e70fe58>
[51745.467757] CR2: 0000000000000000

bt でバックトレースを確認する

クラッシュに至るまでの処理の流れなどを確認してみます。

(内容的には前述のコールトレース部分とほとんど同じです)

crash> bt
PID: 5013   TASK: ffff927722ec1040  CPU: 0   COMMAND: "bash"
 #0 [ffff92771e70fa98] machine_kexec at ffffffff9d063674
 #1 [ffff92771e70faf8] __crash_kexec at ffffffff9d11ce12
 #2 [ffff92771e70fbc8] crash_kexec at ffffffff9d11cf00
 #3 [ffff92771e70fbe0] oops_end at ffffffff9d76c758
 #4 [ffff92771e70fc08] no_context at ffffffff9d75aa7e
 #5 [ffff92771e70fc58] __bad_area_nosemaphore at ffffffff9d75ab15
 #6 [ffff92771e70fca8] bad_area at ffffffff9d75ae25
 #7 [ffff92771e70fcd0] __do_page_fault at ffffffff9d76f821
 #8 [ffff92771e70fd40] trace_do_page_fault at ffffffff9d76f9c6
 #9 [ffff92771e70fd80] do_async_page_fault at ffffffff9d76ef42
#10 [ffff92771e70fda0] async_page_fault at ffffffff9d76b788

[exception RIP: sysrq_handle_crash+22] からは、関数のどの行 (オフセット) でクラッシュしたのかが分かります (実際にクラッシュさせているのは、async_page_fault などのそれより先にある処理)。この行と対応するソースは crash> dis sysrq_handle_crash or crash> dis sysrq_handle_crash+22 から確認できます (この辺は後述)。

    [exception RIP: sysrq_handle_crash+22]
    RIP: ffffffff9d461bf6  RSP: ffff92771e70fe58  RFLAGS: 00010246
    RAX: ffffffff9d461be0  RBX: ffffffff9dce4c60  RCX: 0000000000000000
    RDX: 0000000000000000  RSI: ffff92773fc13898  RDI: 0000000000000063
    RBP: ffff92771e70fe58   R8: ffffffff9dfe38bc   R9: ffffffff9dfee37b
    R10: 000000000000025c  R11: 000000000000025b  R12: 0000000000000063
    R13: 0000000000000000  R14: 0000000000000004  R15: 0000000000000000

CS: 0010 (2進数: 10000) からは、クラッシュ時に動作していた CPU の特権レベル (CPL: Current Privilege Level) を確認できます。CPL は CS レジスタの下位 2bit から判断できます。00 (0) ならカーネル (特権) モード、11 (3) ならユーザ (非特権) モードの意味です。

今回の場合は下位 2bit が 00 なのでカーネルモードで動作していたと判断できます。

    ORIG_RAX: ffffffffffffffff  CS: 0010  SS: 0018
#11 [ffff92771e70fe60] __handle_sysrq at ffffffff9d46241d
#12 [ffff92771e70fe90] write_sysrq_trigger at ffffffff9d462888
#13 [ffff92771e70fea8] proc_reg_write at ffffffff9d2b7f30
#14 [ffff92771e70fec8] vfs_write at ffffffff9d2410a0
#15 [ffff92771e70ff08] sys_write at ffffffff9d241ebf
#16 [ffff92771e70ff50] system_call_fastpath at ffffffff9d774ddb
    RIP: 00007fbedabfefd0  RSP: 00007ffd4a5b6e38  RFLAGS: 00010202
    RAX: 0000000000000001  RBX: 0000000000000002  RCX: 0000000000000063
    RDX: 0000000000000002  RSI: 00007fbedb523000  RDI: 0000000000000001
    RBP: 00007fbedb523000   R8: 000000000000000a   R9: 00007fbedb513740
    R10: 00007fbedb513740  R11: 0000000000000246  R12: 00007fbedaed7400
    R13: 0000000000000002  R14: 0000000000000001  R15: 0000000000000000
    ORIG_RAX: 0000000000000001  CS: 0033  SS: 002b

dis で関数の中身を確認する

crash> bt から sysrq_handle_crash+22 の行が引き金となり、クラッシュしたことが分かりました。この行では何をしているのか crash> dis sysrq_handle_crash で関数を確認すると、+22 の行は movb $0x1,0x0 でした。0 番地への書き込みなので、これにより NULL ポインタ参照によるクラッシュが発生したと考えられます。

crash> dis sysrq_handle_crash
0xffffffff9d461be0 <sysrq_handle_crash>:        nopl   0x0(%rax,%rax,1) [FTRACE NOP]
0xffffffff9d461be5 <sysrq_handle_crash+5>:      push   %rbp
0xffffffff9d461be6 <sysrq_handle_crash+6>:      mov    %rsp,%rbp
0xffffffff9d461be9 <sysrq_handle_crash+9>:      movl   $0x1,0x7e5721(%rip)        # 0xffffffff9dc47314
0xffffffff9d461bf3 <sysrq_handle_crash+19>:     sfence 
0xffffffff9d461bf6 <sysrq_handle_crash+22>:     movb   $0x1,0x0
0xffffffff9d461bfe <sysrq_handle_crash+30>:     pop    %rbp
0xffffffff9d461bff <sysrq_handle_crash+31>:     retq 

実際のカーネルソースとも比較してみます。この中で 1 を書き込もうとしているのは、143 行目と145 行目で、書き込み先が NULL となるのは 145 行目です。145 行目の *killer には 135 行目で NULL が渡されています。また位置的に考えても 145 行目の *killer = 1movb $0x1,0x0 の対応行であるのは間違いなさそうです (より確実な確認方法については後述)。

このことを踏まえると、今回の事象は NULL を指している *killer1 を書き込もうとしたところ NULL ポインタ参照となり、アクセス例外でクラッシュが発生...という流れだったと考えられます。

drivers/tty/sysrq.c
 133 static void sysrq_handle_crash(int key)
 134 {
 135         char *killer = NULL;
 136 
 137         /* we need to release the RCU read lock here,
 138          * otherwise we get an annoying
 139          * 'BUG: sleeping function called from invalid context'
 140          * complaint from the kernel before the panic.
 141          */
 142         rcu_read_unlock();
 143         panic_on_oops = 1;      /* force panic */
 144         wmb();
 145         *killer = 1;
 146 }

なお、カーネルソースとの対応を確認するには、次のような方法もあります。

  • crash> dis -s sysrq_handle_crash を実行。
    -> 指定した関数と対応するソースコードをそのまま表示。

  • crash> dis -l sysrq_handle_crash を実行。
    -> 指定した関数と対応するソースコードの行数のみを表示。

  • 通常のターミナル上で <カーネルソース>/scripts/faddr2line /usr/lib/debug/usr/lib/modules/<カーネルバージョン>/vmlinux sysrq_handle_crash+22 を実行。
    -> 指定した関数とオフセット値(+22)に対応するソースコードの行数をピンポイントで表示 (これをあえて使う場面は思いつきませんが、こんな方法もあるというメモ)。

3 その他、トピックス

解析的には以上で終了ですが、crash コマンドで確認できることはその他にも色々あるため、今回の事象に関連していそうなものをいくつか紹介します。

RDI レジスタを確認する

crash> bt などで確認できる RDI レジスタには関数に渡される第一引数の値が入っています。この値を確認することで、クラッシュ時に実行された関数の引数 (第1引数) を知ることができます 2

sysrq_handle_crash の第一引数は int 型の key でした。構造体などの場合はアドレスが入っているため、値を確認するのが面倒 (crash> p ((struct hoge *) address)->member など)ですが、値がそのまま入っている場合は、crash> ascii で16進数を ascii に変換すれば確認できます。

drivers/tty/sysrq.c
 133 static void sysrq_handle_crash(int key)

...(snip)...

第一引数には c が入っていたようです。

crash> ascii 0000000000000063
0000000000000063: c<NUL><NUL><NUL><NUL><NUL><NUL><NUL>

ps でプロセスを確認する

クラッシュしたプロセス (5013) の状態も確認してみます。

crash> ps 5013
   PID    PPID  CPU       TASK        ST  %MEM     VSZ    RSS  COMM
>  5013   5005   0  ffff927722ec1040  RU   0.1  116832   3688  bash

メモリ使用量などに高騰は見られません。普通です。親プロセス (5005) も見てみます。

crash> ps 5005
   PID    PPID  CPU       TASK        ST  %MEM     VSZ    RSS  COMM
   5005      1   0  ffff927726bce180  IN   0.8  681112  26560  gnome-terminal-

こちらも特に異常は見られません (gnome-terminal- が途中で切れていますが、gnome-terminal-server のことだと思います)。

files でオープンファイルを確認する

クラッシュしたプロセスである bash (5013) が何をオープンしていたのか見てみます。

FD が 1 (標準出力) の行に /proc/sysrq-trigger があります。クラッシュ時には、bash プロセスの標準出力先が /proc/sysrq-trigger になっています。

crash> files 5013
PID: 5013   TASK: ffff927722ec1040  CPU: 0   COMMAND: "bash"
ROOT: /    CWD: /var/crash
 FD       FILE            DENTRY           INODE       TYPE PATH
  0 ffff92771e6c9600 ffff9277394e79c0 ffff927739c2b720 CHR  /dev/pts/0
  1 ffff927731f8c200 ffff9276d9cc3840 ffff9276d9d259e0 REG  /proc/sysrq-trigger
  2 ffff92771e6c9600 ffff9277394e79c0 ffff927739c2b720 CHR  /dev/pts/0
 10 ffff92771e6c9600 ffff9277394e79c0 ffff927739c2b720 CHR  /dev/pts/0
255 ffff92771e6c9600 ffff9277394e79c0 ffff927739c2b720 CHR  /dev/pts/0

fuser でファイルや inode を使用しているプロセスを確認する

files とは逆に指定のファイルや inode を使用しているプロセスを確認できます。/proc/sysrq-trigger を使用しているのは bash のようです。

crash> fuser /proc/sysrq-trigger
 PID         TASK        COMM             USAGE
 5013  ffff927722ec1040  "bash"           fd 

help を眺める

解析に詰まったら、help を眺めてみて、色々試してみるのもありです。新しい手がかりが掴めるかもしれません。

crash> help

*              extend         log            rd             task           
alias          files          mach           repeat         timer          
ascii          foreach        mod            runq           tree           
bpf            fuser          mount          search         union          
bt             gdb            net            set            vm             
btop           help           p              sig            vtop           
dev            ipcs           ps             struct         waitq          
dis            irq            pte            swap           whatis         
eval           kmem           ptob           sym            wr             
exit           list           ptov           sys            q        

...(snip)...

help bt のようにすることで、bt のオプションなどを確認することもできます。

crash> help bt

NAME
  bt - backtrace

SYNOPSIS
  bt [-a|-c cpu(s)|-g|-r|-t|-T|-l|-e|-E|-f|-F|-o|-O|-v] [-R ref] [-s [-x|d]]
     [-I ip] [-S sp] [pid | task]

DESCRIPTION
  Display a kernel stack backtrace.  If no arguments are given, the stack
  trace of the current context will be displayed.

       -a  displays the stack traces of the active task on each CPU.
           (only applicable to crash dumps)
       -A  same as -a, but also displays vector registers (S390X only).

...(snip)...

4 crash コマンドを自動で実行させる

logbt などよく使うコマンドを crash の起動と共に自動で実行させる方法を紹介します。

まずは、次のように bt などのコマンドを列挙したファイルを用意します。

crash_cmd.txt
log
bt
q

crash コマンド実行時、-i オプションと共に上記のファイルを指定すると、ファイルに列挙されている crash コマンドを自動で実行させることができます。

[root@localhost ~]# crash -i crash_cmd.txt /usr/lib/debug/usr/lib/modules/<カーネルバージョン>/vmlinux <vmcore 配置パス>/vmcore

...(snip)...

crash> log

...(snip)...

crash> bt

...(snip)...

crash> q
[root@localhost ~]#

補足情報

検証環境

  • カーネル: 3.10.0-957.el7.x86_64
  • ディストリビューション: CentOS Linux release 7.6.1810 (Core)
  • crash: crash-7.2.3-8.el7.x86_64

参考

Web

書籍


  1. 2行目にある IP: とは、Instruction Pointer のことです。実行中のプログラムの位置を指す実行ポインタを表しています。sysrq_handle_crash+0x16 で止まっているということは、ここでクラッシュしたということです。ちなみに sysrq_handle_crash+0x16/0x20 にある 0x16 は関数内の位置、0x20 は関数サイズを表しています。 

  2. x86_64 Linux では、第1引数:RDI、第2引数:RSI、第3引数:RDX、第4引数:RCX、第5引数:R8、第6引数:R9、第7引数以降:スタック...という具合にレジスタを使います。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

パイプしたときの終了ステータスの取得方法

コマンドの実行でエラーが起こってないか監視ツールで監視したいため、
コマンドの実行が成功したか失敗するか取得する方法を調べました。

正常 or エラー

Linux(Unix)では、すべてのコマンドは終了コード(exit code)を返します。
0が成功、1以上の値はエラーです。
エラー時の終了ステータスは以下のように予約されています。
128+nのエラー内容はコマンドやソフトウェアの実装によって異なります。

終了ステータス 意味
1 一般的なエラー
2 ビルトインコマンドの誤用
126 コマンドを実行できなかった(実行権限がなかった)
127 コマンドが見つからなかった
128 exit に不正な値を渡した(例えば浮動小数点数)
128+n シグナル n で終了
255 範囲外の終了ステータス

引用:
https://ja.wikipedia.org/wiki/%E7%B5%82%E4%BA%86%E3%82%B9%E3%83%86%E3%83%BC%E3%82%BF%E3%82%B9#bash

特殊変数 $?

直前に実行したコマンドの終了コードは$?という特殊変数に格納されています。
echo $?というコマンドを実行することで直前の終了コードを表示できます。
echoは引数の文字列や変数を表示するコマンドです。

パイプの終了ステータス

# exit 2 | exit 1 | exit 0

こういうケース。
3つのコマンドをパイプで繋いでいます。
1つめと2つめはエラーを返すようになっています。

上記のコマンドの実行後にecho $?とすると、0(正常終了)が返ってしまいました。

パイプ元でエラーが発生していても正常終了扱いになってしまうので注意が必要です。
(たぶん、最後に終了したコマンドの終了コードが帰ってきます。)

簡単な解決策は2つあります。
ただし、Bash,Zsh以外のシェルでも使えるか不明なので注意すること。

PIPESTATUS

# exit 2 | exit 1 | exit 0
# echo  ${PIPESTATUS[@]}
2 1 0

PIPESTATUSという配列変数で取得できます。

set -o pipefail

pipefailを有効にすると、Pipeでエラーが有ったときそのエラーコードを返してくれます。

# set -o pipefail
# exit 2 | exit 1 | exit 0
# echo $?
2

関連:リダイレクトの終了ステータス

リダイレクト元かリダイレクト先のどちらかが終了ステータスを返したら
その終了ステータスを返します。

# echo aaa > .
# echo $?
1

#  cat . > file.txt
# echo $?
1

.はディレクトリなので、書き込めずエラーになります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Linuxの権限確認・変更とスーパーユーザ

ファイルのオーナーとグループ

catコマンド(既存コマンド)のオーナーの確認

$ ls -l /bin/cat
-rwxr-xr-x  1 root root 54160 Nov 30 15:16 /bin/cat

最初のrootはファイルの所有者、2つ目のrootはファイルが所属するグループを意味します。
/bin/catファイルは、rootユーザがオーナーで、rootグループに属しています。

新規作成したファイルのオーナーの確認

$ touch test.txt
$ ls -l test.txt
-rw-rw-r--  1 banri banri 54160 Nov 30 15:16 test.txt

以上の例では、test.txtというファイルを作成し、そのオーナーを確認しています。
→ユーザbanriがファイルを作成したため、banriがオーナーになっています。

グループ

グループとは、ユーザの集合を意味します。
自分がどこにグループに属しているかは以下のコマンドで確認できます。

$ groups
banri wheel

banriはbanriwheelというグループに属しています。

ファイル権限(パーミッション)確認

ファイルタイプ

$ ls -l /bin/cat
-rwxr-xr-x  1 root root 54160 Nov 30 15:16 /bin/cat

もう1度、catコマンドの例を挙げます。-rwxr-xr-xの箇所に注目です。
一番左の-はファイルタイプの一種で、ファイルを意味します。以下はファイルタイプとそれらの意味となる対応表です。

ファイルタイプ 意味
- ファイル
d ディレクトリ
l シンボリックリンク

ファイルタイプを除いた部分rwxr-xr-xの部分に注目です。
これら9文字はファイルモードを意味し、rwxr-xr-xと3文字ごとのブロックに分けられます。
catコマンドでは、以下のようになります。

オーナー グループ その他ユーザ
rwx r-x r-x

ファイル権限の記号と意味は以下のようになります。

記号 意味
r 読み取り(read)
w 書き込み(write)
x 実行(execute)

なお、-となっているところは、権限がないという意味です。
そして、ファイル権限の記号はディレクトリ関しにも同様です。

記号 意味
r 読み取り→ディレクトリに含まれるファイル一覧取得
w 書き込み→ディレクトリ下にあるファイル・ディレクトリの作成・削除
x 実行(execute)→ディレクトリをカレントディレクトリにする

ファイル権限(パーミッション)変更

ファイル権限の変更方法は以下の2つです。

1.シンボルモード

シンボルモードは、変更対象にどのような権限を追加・禁止するかが明確です。

chmod [ugoa]  [+-=]  [rwx]  <file name>
    ------  -----  -----  -----------
      変更対象  演算子   権限   ファイル名

変更対象はそれぞれ以下のような意味になります。

記号 意味
u ユーザ(=オーナー)(user)
g グループ(group)
o その他(others)
a すべて(all)

演算子はそれぞれ以下のような意味になります。

記号 意味
= 指定した権限と等しくする
+ 権限を付与する
- 権限を禁止する

先ほど、作成したtest.txtのファイルの権限を変更してみます。オーナーやグループと同様に、その他ユーザへファイルの書き込み権限を付与します。

$ ls -l test.txt
-rw-rw-r--.  1 banri banri 54160 Nov 30 15:16 test.txt
$ chmod o+w test.txt
$ ls -l test.txt
-rw-rw-rw-.  1 banri banri 54160 Nov 31 02:16 test.txt

その他ユーザの権限r--rw-に変更されましたね。

次に、グループのファイルへの書き込み権限を禁止します。

$ ls -l test.txt
-rw-rw-rw-.  1 banri banri 54160 Nov 31 02:16 test.txt
$ chmod g-w test.txt
$ ls -l test.txt
-rw-r--rw-.  1 banri banri 54160 Nov 31 02:17 test.txt

グループの権限rw-r--に変更されましたね。

複数のユーザの権限を一括指定する場合は以下のようにします。

$ ls -l test.txt
-rw-rw-rw-.  1 banri banri 54160 Nov 31 02:17 test.txt
$ chmod ug=x test.txt
$ ls -l test.txt
---x--xrw-.  1 banri banri 54160 Nov 31 02:18 test.txt

2.数値モード

chmod <8進数の数値> <file name>
数字 意味
4 読み取り(read)
2 書き込み(write)
1 実行(e x ecute)

権限は以上の8進数の足し算で決定できます。例えばrwxr-xr-xのような権限を一括指定したい場合は以下のようになります。権限の一括指定には、シンボルモードより数値モードの方が利用されるそうです。私も権限設定で「700」や「754」などをよく使用します。

オーナー グループ その他ユーザ
rwx rw- r-x
4+2+1 4+2 4+1
7 6 5

以上のように、それぞれの足し算の結果を繋げた765が権限の数値となります。 

$ ls -l test.txt
-rwxr-xrw-.  1 banri banri 54160 Nov 31 02:17 test.txt
$ chmod 765 test.txt
$ ls -l test.txt
-rwxrw-r-x.  1 banri banri 54160 Nov 31 02:18 test.txt

スーパーユーザ

スーパーユーザは、あらゆる権限が付与されたユーザのことで、システムの設定ファイルの変更やアプリケーションをインストールする権限を持っています。rootユーザとも呼ばれています。スーパーユーザに対して、ユーザbanriなどは一般ユーザに分類されます。以下はスーパーユーザになる方法とスーパーユーザで実行する方法です。

1.スーパーユーザになる - suコマンド

[banri@localhost]$ su
Password:
[root@localhost]#

一般ユーザからスーパーユーザになる(ログインする)と、プロンプトが$から#に変更されます。

[root@localhost]# exit
exit
[banri@localhost]$

スーパーユーザから一般ユーザに戻る(ログアウトする)には、exitコマンドを入力します。プロンプトが#から$に戻りました。

2.スーパーユーザで実行する - sudoコマンド

sudoは設定ファイルを実行したい時やアプリケーションをインストール・起動したい時など、アプリケーションのインストール・起動などに利用されます。以下は、sudoを使用したnginxのインストールと起動に関するコマンドです。

# nginxをインストール
$ sudo yum install nginx

# nginxを起動
$ sudo systemctl start nginx
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ThriftをC言語で使ってみる

この記事について

C言語でThriftを使いたいと思ったのですが、結構面倒だったので備忘録&情報共有のため投稿します。

なぜThrift?

C++だけでなくC言語にも対応しているからです。
最近がgRPC(Protocol Buffers)の方が勢いがある気もしますが、今回はThriftを使ってみます。

前提

dockerとThriftの知識が少し必要です。
dockerに関しては、dockerイメージの部分を純粋なCentOSと読み替えれば大丈夫です。
この記事で使用するソース等はここに配置していますので参考にしてください。

なぜC言語?

筆者はここ20年くらい、Java中心の仕事をしていますが久しぶりにC言語が使いたくなったのです。そしてこれがやりたい!

スクリーンショット 2019-05-12 23.00.03.png

Berkeley DBのフル機能を使うためにはJava EditionではなくC言語版が必要でした。
そして、最終的これやってみたい。
スクリーンショット 2019-05-19 14.40.48.png

やるとは言ってません。やってみたいだけです。

開発環境の準備

C言語やC++を使う時に一番面倒なのはこれです。
多くの依存モジュールをインストールする必要があります。

以前はMacに直接環境を作っていましたが、今はContainerが便利ですね。
CentOSのDockerイメージで環境を作ります。もちろん純粋なCentOSでも構いません。

docker run -it -d --name="mycentos" centos /bin/bash
docker attach mycentos

基本的に、Thriftのマニュアル(https://thrift.apache.org/docs/install/centos)通りにインストールすればよいのですが、C言語で動作させる場合は少し追加が必要です。
以下に追加の環境設定手順を記載します。
同じことを行っているDockerfileもご活用ください。

不足モジュールのインストール

※テストを簡単に行うため、Pythonも使えるようにしておきます。

#Install glib2
yum -y install glib2-devel

#Install Pip(Pythonを使う場合)
wget https://thrift.apache.org/docs/install/centos
python get-pip.py
python -m pip -V

#Install six (Python modules)
pip install six

ビルド時にpythonとc_glibのライブラリが使えるよに指定します。

#Build and Install Thrift Compiler
git clone https://github.com/apache/thrift.git
cd thrift
RUN ./bootstrap.sh
RUN ./configure --with-lua=no --with-python=yes --with-c_glib=yes 
RUN make
RUN make install

ライブラリパスも忘れずに!

export LD_LIBRARY_PATH=/usr/local/bin

サンプルコード

ping() と hello() のインターフェースを実装します。
Thrift公式ページのTutorial(https://thrift.apache.org/tutorial)を参考にしていますが、もっとシンプルにしました。
ソースはこちら

インターフェースはsample.thriftに定義する必要があります。
仕様はこちら

sample.thrift
namespace c_glib  sample 
namespace py sample

# hello              :Sample implementation
service Basic
{
      void ping(), 
      string hello(1:string arg)
}

サーバ側の実装

サンプル3.png

|-- Makefile
|-- sample.thrift
|-- server.c

スケルトンの生成

  • まず、I/Fが定義されているsample.thriftをThriftコンパイラでコンパイルし、必要なスケルトンソースを生成します。
thrift --gen c_glib sample.thrift

gen-c_glibディレクトリが作成されています。

|-- Makefile
|-- sample.thrift
|-- server.c
|-- gen-c_glib
|   |-- sample_basic.c
|   |-- sample_basic.h
|   |-- sample_sample_types.c
|   `-- sample_sample_types.h

関数の実装

- 生成されたsample_basic.cに ping(),hello()の処理を記述します。

gboolean sample_basic_handler_ping (sampleBasicIf * iface, GError ** error)
{
  /* TODO: Implement your own code. */
  THRIFT_UNUSED_VAR (iface);
  THRIFT_UNUSED_VAR (error);
  puts ("ping()");
  return TRUE;
}

gboolean sample_basic_handler_hello (sampleBasicIf * iface, gchar ** _return, const gchar * arg, GError ** error)
{
  /* TODO: Implement your own code. */
  THRIFT_UNUSED_VAR (iface);
  THRIFT_UNUSED_VAR (error);
  GString *retValue;
  retValue = g_string_new( NULL );
  g_string_printf( retValue ,"Server received: %s \n" , arg );
  puts( retValue->str );
  *_return = retValue->str;
  return TRUE; 
}

サーバ実行

dockerイメージで実行してみます
/root/dev/c_thrift_sample の下にソースがあることを想定しています

docker run -it mycentos /bin/bash
# cd /root/dev/c_thrift_sample
# make idl (or #thrift --gen c_glib sample.thrift)
# make 
# ./server
Starting the server...

9090ポートでListenしています

クライアント側

クライアントのコードはpythonで記述します。
サンプル4.png

クライアント実装

ソースコードはこちら

client.py
print "Call ping() = %s " % (client.ping(),)
print "Call hello() = %s " % (client.hello("Hey guys!"),)

必要なPythonモジュールの生成

thrift --gen py sample.thrift  #<--- C言語のスケルトン生成時に使用したファイル

クライアント実行

docker exec -it 232db73154c2  /bin/bash  #<---  232db73154c2 はサーバ動作中のCONTAINER IDです
# cd /root/dev/c_thrift_sample/test/python  #<---  テストコードはここにある想定
# python client.py
Call ping() = None
Call hello() = Server received: Hey guys!

pythonクライアントから送った文字列がC言語で作成したサーバで受信できました。

おまけ

thriftコンパイラで生成したソースを変更しましたが、もう一度生成すると、上書きされて消えてしまいます。
そこで、変更したhandler関数を外出ししました。
ソース類をここに置きます。興味のある方はご覧ください。

まとめ

今更ですがC言語は難しい!
BerkeleyDBも少し使ってみたのですが、久しぶりに使ってみて痛感しました。
最近の言語では、あまり意識する必要がない「メモリ管理」や「UNICODE文字列の操作」などを、自分で作るか外部ライブラリに頼ることになります。
モジュールやライブラリの管理も微妙で難易度がさらに上がります。
しかし、C言語はとてもシンプルなので、性能や実行モジュールサイズはC言語に敵うものはありません。
これからも職人のツールとして残るでしょう。
最後に
 C言語は計算機の仕組みを理解するには一番良い言語です
 これを理解していれば、新しい言語や技術に対応できる能力が身につきます
筆者のようなおじさんだけでなく、若い技術者にもトライしてほしいと思います

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

LinuxのオリジナルコマンドをMacに移植した話

経緯

僕の大学では入学時にPCを買うとVirtualBoxを入れさせられ、学校の授業用に何やら設定を施されたubuntuが配布されます。
1、2年生の間は特に配布されたubuntuにありがたみは感じないものの、3年生になると「sep3asm」というオリジナルプログラムを授業内で使うことになります。
これは我が校の教授が教育用に作ったオリジナルCPU、「SEP-3」(モデルは過去の名機PDP-11らしい)で用いるアセンブラのプログラムをアセンブルするもので、いうまでもなく学校で配布されたubuntuにしか入っていません。

ここまでの話は正直どうでも良いという人がほとんどだと思いますが、僕はこの「sep3asm」をMacでどうにか使えないかと考えました。
言い忘れていましたがこの「sep3asm」はコマンドで動くものでLinux用バイナリでした。
したがってMacで動かすには少々工夫が必要でしたが、今回の設定は他の場面でも使えると感じたので方法をシェアしたいと思います。

作業の流れ

コマンドの場所を確認(Linux上)

コマンドのファイルをMacへ移動

実行権限を加える(Mac上)

noahをインストール

コマンドのファイルを所定の位置に移動

rootでnoahを起動

コマンド実行

コマンドの場所を確認

コマンドをMacに移すにはまずコマンドのファイルの場所を特定しなければいけません。

$ where sep3asm
/usr/local/bin/sep3asm

上記のコマンドで探すことができます。

コマンドのファイルを移動

方法はお任せします。
僕の場合はubuntuがVirtualBoxに入っているので共有フォルダでWindowsに移してからLineでMacに送りました。

実行権限を加える。

$ chmod a+x sep3asm

chmodコマンドは、ファイルやディレクトリのアクセス権を変更するコマンドです。引数のa+xの意味を一応解説しておきます。
自分なりの認識なので間違っていたらすみません。
chmodの引数は
<誰に> - <何の>  権限を与える、という構造になっているようです。

<誰に>の部分は4種類

記号 意味
u 所有者(user)
g グループ(group)
o その他(other)
a 上の三つ全て(all)

<何の>の部分は3種類

記号 意味
r 読み込み権限
w 書き込み権限
x 実行権限

今回の場合は<全ての者に> - <実行権限>を与えているわけです。

noahをインストール

Macでは、Linux用バイナリを実行できません。したがってこれに関しては何らかの対策を講じる必要があるのですが、色々調べてみるとすごく便利なものを見つけてしまいました。

Noahというアプリケーションは、MacOS上でLinuxOS用アプリケーションを動かすことのできる優れものです。
apt-getやLinux向けgcc、自作のLinuxアプリケーションが、そのままMacOSで動作します。

ということで、早速インストールします。

$ brew install linux-noah/noah/noah

初めて起動するとこんな感じ

$ noah
Noah is installing the initial filesystem in ~/.noah/tree. Proceed? [Y/n] Y
Password:

(中略)

Noah is still under development, so please enable it at your own risk! [y/N] y

(中略)

I have no name!@username:~$

適当にyを押しておきます。

コマンドを所定の位置に移動させる

sep3asmを所定のいちに移動させます。

$ sudo cp sep3asm /bin/sep3asm

コマンドのファイルを/bin に入れる動作はrootでしかできないのでsudoでやります。

一応、移動させたら確認してみてください。

$ which sep3asm
/bin/sep3asm

こんな感じになっていたらOK。

rootでnoahを起動

$ sudo noah
Password:
groups: cannot find name for group ID 61
groups: cannot find name for group ID 80
groups: cannot find name for group ID 701
groups: cannot find name for group ID 98
root@username:~# 

起動画面はこんな感じです。
もうこのままコマンドが打てる状態です。
試しにプログラムをアセンブルしてみましょう。

今回アセンブルするプログラム

sample.s
 . = 0x1FF
 . WORD 0x0A0A, 0xF0F0
 MOV #0x200, R6
 MOV R6, R0
 HLT

アセンブルします。

$ sep3asm sample.s

アセンブル結果がこちら

sample.bin
000001FF: 0A0A
00000200: F0F0
00000201: 43E6
00000202: 0200
00000203: 40C0
00000204: 0000

ちゃんと動作していますね!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

pdfの内容をtxt形式へ一括変換する(Linux)

pdfのテキスト部分を抜き出す

フォルダ以下の .pdfファイルのテキスト部分を抜き出し、.pdf.txt というファイル名で保存する。

$ find -name '*.pdf' -print0 | xargs -0 -i pdftotext {} {}.txt

同じフォルダ内に、.pdf.txtというファイル名で、pdfをpdftotextを使って、txt形式に変換した物が保存される。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む