2017年05月

ptraceとELFとLinuxレジスタ

ptrace(2) は Linux を含む Unix 系OS にあるシステムコールで、実行中のプロセスに対して、メモリ上のデータやレジスタの値を抜き出したり、書き換えたりすることができる。

これを使ってごにょごにょすると、実行中の関数とその引数を取り出して、プロセスを止めずにスタックトレースを取得したり、デバッガを作ったり標準出力を横取りして audit log を取ったりオンラインでパッチをあてて脆弱性対応したりできる。夢が広がる。

例えば、普通のやつらの下を行け: ptrace で実行中のプロセスにちょっかいを出す では、32bit executable なバイナリに対して、実行中に出力文字列を置き換える例を紹介している。

strace コマンドは ptrace(2) を利用して、システムコールを追って出力している。これについては udzura さんの straceがどうやってシステムコールの情報を取得しているか の記事に情報があった。

参考図書

ptrace 入門のために、書籍またはウェブサイトがないのか探していたのだが、古い時代のものしかみつからない。サンプルも 32bit が基本で、入門したい身なのに色々置き換えながら読まないといけなくてツライ。

と思っていたところ、Learning Linux Binary Analysis という本が良さそうと教えてもらった。2016/2/29 出版で新しい。ptrace(2) についても ELF についても載っていて、欲しかったやつだった。

バイナリやアセンブリ周りは、和書は古いものしかないが、洋書だったら最近出版されているものもあるようで、次は最初から洋書を探そうと思うなどした。

※ この本は基本的にはリバースエンジニアリングの本で、Chapter 4 からは virus がどのようにバイナリが実行可能なまま自分自身を盛り込むのか、virus に injection されたことをどのように発見するのか、という内容になる。興味深いが自分はまだ読んでない。

Learning Linux Binary Analysis
Learning Linux Binary Analysis
posted with amazlet at 17.05.30
Ryan O'neil 
Packt Publishing (2016-02-29)
売り上げランキング: 69,854

https://www.packtpub.com/networking-and-servers/learning-linux-binary-analysis

Linux システムコールの ABI

Linux のシステムコールを呼び出すには ABI (Application Binary Interface) が決まっていて、CPUの決まったレジスタに値を書き込んで INT 0x80 (Interrupt) 命令を投げると、カーネルに割り込みをしてシステムコールを実行してもらうことができる。

以下のようにアーキテクチャごとに定まっているレジスタに、システムコール番号と引数の値を書き込み、INT 0x80 命令を投げると呼び出すことができる。i386 だと eax レジスタに、x86_64 だと rax レジスタにシステムコール番号を書き込む。

arch/ABI      syscall# retval arg1  arg2  arg3  arg4  arg5  arg6  arg7  Notes
──────────────────────────────────────────────────────────────
i386          eax      eax    ebx   ecx   edx   esi   edi   ebp   -
x86_64        rax      rax    rdi   rsi   rdx   r10   r8    r9    -

ref. man : syscall(2)

例えば x86_64 アーキテクチャで sys_write して sys_exit するコードをアセンブリで書くと次のようになる。システムコールの番号は linux のヘッダから取ってくる。

;------------------------------------
; hellol.s
;   nasm -f elf64 hellol.s
;   ld -o hellol hellol.o
;   ./hellol
;------------------------------------

bits 64
section .text
global _start

_start:
        mov rax, 1      ; sys_write
        mov rdi, 1      ; stdout
        mov rsi, msg    ; address
        mov rdx, len    ; length (13)
        int 0x80

        mov rax, 60     ; sys_exit
        xor rdi, rdi    ; 0
        int 0x80

section .data
        msg     db      'hello, world', 0x0A
        len     equ     $ - msg

ref. Linux で64bitアセンブリプログラミング (01) - hello world

References:

ptrace でシステムコールを追う

ptrace(2)で対象プロセスのシステムコールを追うC言語プログラムはざっくり言うと以下の手順になる

  • #include <sys/ptrace.h>
  • ptrace(PTRACE_ATTACH, pid, NULL, NULL);
  • システムコール直前、または直後に停止した状態で
    • int e = ptrace(PTRACE_GETREGS, pid, 0, &regs);
  • ptrace(PTRACE_DETACH, pid, NULL, NULL);

regs の定義は x86_64 であれば struct user_regs_struct であり、以下となる。

struct user_regs_struct
{
  unsigned long r15;
  unsigned long r14;
  unsigned long r13;
  unsigned long r12;
  unsigned long rbp;
  unsigned long rbx;
  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;
  unsigned long rip;
  unsigned long cs;
  unsigned long eflags;
  unsigned long rsp;
  unsigned long ss;
  unsigned long fs_base;
  unsigned long gs_base;
  unsigned long ds;
  unsigned long es;
  unsigned long fs;
  unsigned long gs;
};

i386 の場合は、struct i386_user_regs_struct であり、以下となる。

struct i386_user_regs_struct {
    uint32_t ebx;
    uint32_t ecx;
    uint32_t edx;
    uint32_t esi;
    uint32_t edi;
    uint32_t ebp;
    uint32_t eax;
    uint32_t xds;
    uint32_t xes;
    uint32_t xfs;
    uint32_t xgs;
    uint32_t orig_eax;
    uint32_t eip;
    uint32_t xcs;
    uint32_t eflags;
    uint32_t esp;
    uint32_t xss;
}; 

PTRACE_GETREGS でレジスタの値を取り出して、i386 であれば eax レジスタ、x86_64 であれば rax レジスタからシステムコール番号を取り出せるので、どのシステムコールを呼んでいるのかがわかる。

strace はシステムコール番号を取得する以上のことをやっていて、システムコールごとに引数の型がなにかを定義して、適切に値を取り出して表示している。特に、レジスタにポインタのアドレスが書き込まれている場合、アドレスが示すメモリ領域から値を取り出す必要もある。このような処理をシステムコールごとに地道にコードを書いて対応しているとのこと。頭が下がる。

ref. straceがどうやってシステムコールの情報を取得しているか

ELF

ELF は Linux のような Unix 系OSで標準的なバイナリフォーマットで、実行ファイル、共有ライブラリ(.so)、オブジェクトファイル(.o)、コアダンプなどに使われている。

ELFバイナリは、実行するためにメモリに読み込まれた場合でもフォーマットはほとんど変わらないので、ELFバイナリフォーマットについて知識があれば値を取り出せる。このELFフォーマットについては、最初に紹介した「Learning Linux Binary Analysis」で詳細に解説があった。

通常はこのメモリ領域は、データを書き換えようとすると SEGV が起きるわけだけど、ptrace(2) を使うとなんと書き換えることができる。

PTRACE_POKEDATA を使って hello, world を hippo, world に置き換えるサンプルが 普通のやつらの下を行け: ptrace で実行中のプロセスにちょっかいを出すにあったので、やってみると面白い。記事が 32bit 時代のものなので、64 bit に置き換えて動かすのも良い練習になる。

おわりに

ptrace(2)に入門した。これを使いつつさらにごにょごにょすれば、生きているプロセスにアタッチして、Cレベルのスタックトレースを出しつつ、Rubyレベルのスタックトレースを出すなんてこともできるだろう。まぁ、sigdumpでいいんだけど。

FYI: sigdump は対象 ruby プロセスに sigdump gem を入れて require しておかないといけない。ptrace(2) ベースでアプローチすれば、何も入れておく必要はなくなる。ただし、そのツールはCRubyのバイナリレベルの変更に追随する必要がある(´・ω・`)

debianパッケージ周りでよく使うコマンドとオプション

後進のためのメモシリーズ。自分がよく使うやつ。

debian パッケージのインストール

$ sudo apt-get install --no-install-recommends <パッケージ名>

デフォルトだと recommends しているだけの必須ではないパッケージも一緒に入って時間がかかるので --no-install-recommends をつけるのが常套手段

/etc も含めて完全に消す

$ sudo apt-get --purge remove <パッケージ名>

remove だけだと /etc などが残る


インストール候補の debian パッケージのバージョンとレポジトリを調べる

$ sudo apt-cache policy linux-image-extra-virtual
linux-image-extra-virtual:
  Installed: 3.13.0.112.120
  Candidate: 3.13.0.112.120
  Version table:
 *** 3.13.0.112.120 0
        500 http://us-east-1.ec2.archive.ubuntu.com/ubuntu/ trusty-updates/main amd64 Packages
        500 http://security.ubuntu.com/ubuntu/ trusty-security/main amd64 Packages
        100 /var/lib/dpkg/status
     3.13.0.24.28 0
        500 http://us-east-1.ec2.archive.ubuntu.com/ubuntu/ trusty/main amd64 Packages

debian パッケージがインストール済みかを調べる

dpkg -l (--list) パッケージ名、または dpkg -l | grep パッケージ名

$ dpkg -l linux-headers-$(uname -r)
Desired=Unknown/Install/Remove/Purge/Hold
| Status=Not/Inst/Conf-files/Unpacked/halF-conf/Half-inst/trig-aWait/Trig-pend
|/ Err?=(none)/Reinst-required (Status,Err: uppercase=bad)
||/ Name                             Version        Architecture  Description
+++-================================-==============-=============-==========================================================
ii  linux-headers-3.13.0-112-generic 3.13.0-112.159 amd64         Linux kernel headers for version 3.13.0 on 64 bit x86 SMP

変にカスると終了コードが 1 (エラー) ではなく 0 (正常) になることがあるので注意

$ dpkg -l linux-headers
Desired=Unknown/Install/Remove/Purge/Hold
| Status=Not/Inst/Conf-files/Unpacked/halF-conf/Half-inst/trig-aWait/Trig-pend
|/ Err?=(none)/Reinst-required (Status,Err: uppercase=bad)
||/ Name          Version Architecture Description
+++-=============-=======-============-==============================
un  linux-headers <none>  <none>       (no description available)
$ echo $?
0
$ dpkg -l unknown
dpkg-query: no packages found matching unknown
$ echo $?
1

終了コードでインストール済みかチェックする場合は dpkg -p (--print-avail) のほうが確実

$ dpkg -p linux-headers-$(uname -r)
Package: linux-headers-3.13.0-112-generic
Priority: optional
Section: devel
Installed-Size: 13185
Maintainer: Ubuntu Kernel Team <kernel-team@lists.ubuntu.com>
Architecture: amd64
Source: linux
Version: 3.13.0-112.159
Provides: linux-headers, linux-headers-3.0
Depends: linux-headers-3.13.0-112, libc6 (>= 2.14)
Size: 694058
Description: Linux kernel headers for version 3.13.0 on 64 bit x86 SMP
 This package provides kernel header files for version 3.13.0 on
 64 bit x86 SMP.
 .
 This is for sites that want the latest kernel headers.  Please read
 /usr/share/doc/linux-headers-3.13.0-112/debian.README.gz for details.
 $ echo $?
 0
$ dpkg -p linux-headers
dpkg-query: package 'linux-headers' is not available
Use dpkg --info (= dpkg-deb --info) to examine archive files,
and dpkg --contents (= dpkg-deb --contents) to list their contents.
$ echo $?
1

インストールした packge に含まれていたファイルを一覧する

dpkg -L (--list-files)

$ dpkg -L wget
/.
/etc
/etc/wgetrc
/usr
/usr/bin
/usr/bin/wget
/usr/share
/usr/share/info
/usr/share/info/wget.info.gz
/usr/share/doc
/usr/share/doc/wget
/usr/share/doc/wget/MAILING-LIST
/usr/share/doc/wget/NEWS.gz
/usr/share/doc/wget/AUTHORS
/usr/share/doc/wget/copyright
/usr/share/doc/wget/changelog.Debian.gz
/usr/share/doc/wget/README
/usr/share/man
/usr/share/man/man1
/usr/share/man/man1/wget.1.gz

指定のファイルがどのパッケージに含まれているか探す

インストール済みのパッケージから探すだけであれば dpkg -S (--search) を使える

$ dpkg -S libelf
libelf1:amd64: /usr/share/doc/libelf1
libelf1:amd64: /usr/lib/x86_64-linux-gnu/libelf.so.1
libelf1:amd64: /usr/lib/x86_64-linux-gnu/libelf-0.158.so
libelf1:amd64: /usr/share/doc/libelf1/changelog.Debian.gz
libelf1:amd64: /usr/share/doc/libelf1/copyright

libelf1 だ。

インストール済みでないパッケージから探す場合は、
https://www.debian.org/distrib/packages から検索するか、apt-file search を使う

$ sudo apt-get install apt-file
$ sudo apt-file update
$ apt-file search libelf
...
libelf-dev: /usr/include/libelf.h
libelf-dev: /usr/lib/x86_64-linux-gnu/libelf.a
libelf-dev: /usr/lib/x86_64-linux-gnu/libelf.so
libelf-dev: /usr/share/doc/libelf-dev/changelog.Debian.gz
libelf-dev: /usr/share/doc/libelf-dev/copyright
...

依存しているパッケージの表示

apt-cache depends

$ apt-cache depends vim
vim
  Depends: vim-common
  Depends: vim-runtime
  Depends: libacl1
  Depends: libc6
  Depends: libgpm2
  Depends: libpython2.7
  Depends: libselinux1
  Depends: libtinfo5
  Suggests: <ctags>
    exuberant-ctags
  Suggests: vim-doc
  Suggests: vim-scripts

再帰的に取得するには apt-rdepends を使う

パッケージソースを持ってくる

apt-get source

$ apt-get source ttyrec

debian ディレクトリ内にあてているパッチがおいてある

/proc/[PID]/environ が見づらい

後進のためのメモシリーズ。

/proc/[PID]/environ でプロセスの環境変数を横から見ることができるが、区切り文字が改行文字ではなく\0 (NULL文字) になっているので見づらい。

$ cat /proc/[PID]/environ
LANG=en_US.UTF-8USER=foobarLOGNAME=foobarHOME=/home/foobar

sed でNULL文字を置換すれば良い

$ cat /proc/[PID]/environ | sed 's/\x0/\n/g'
LANG=en_US.UTF-8
USER=foobar
LOGNAME=foobar
HOME=/home/foobar

と思ってたけど、strings のほうが楽なことに気づいた。大体うまくいく

$ strings /proc/[PID]/environ
LANG=en_US.UTF-8
USER=foobar
LOGNAME=foobar
HOME=/home/foobar

詳解システムパフォーマンス輪読会 6章 CPU および perf stat

社内有志で集まって詳解システムパフォーマンスの輪読会をしている。現在11章まで完了。6章 CPUを担当したので要点だけを書いておく。 資料もあるのだけど、公開すると引用の枠を超えているような気がするので自重しておく。

勉強メモなので、なにか間違いがあるかもしれません。見つけたら教えてください。

詳解 システム・パフォーマンス
Brendan Gregg 
オライリージャパン 
売り上げランキング: 36,763


要点

(本に書いてあったものより自分で調べて追記したものが多いかも)

  • top などで見れるCPU使用率はメモリストールサイクル(メモリI/O待ち)を含む
    • 純粋にCPUサイクルだけを見れているわけではない!
  • Linuxのロードアベレージは割り込み不能(I/O待ち)状態のタスクを含む
    • CPU使用率だけでもメモリI/O待ちを含んでいたのに、ロードアベレージはさらにディスク、ネットワークI/O待ちを含むので、純粋なCPUの負荷を表現していない。参考程度にしかならない。
  • psの%CPUは生存期間中のCPU使用率を表現するので監視には役に立たない
    • topのCPU使用率は一定期間中の使用率
  • perf stat で cpc (cpu performance counter, cpuレベルのパフォーマンス計測) をプロファイリングできる
    • LLC-load-misses: 最後のレベルのキャッシュのロードミス。メインメモリからのロードの計測値になる
    • IPC(instructions per cycle): 命令幅4のCPUなら4までいくはず(パイプライン考えるともっと伸びるはず?) ref. https://en.wikipedia.org/wiki/Instructions_per_second
  • クロック周波数×IPCがCPUの速さの指標
    • Intelはクロック周波数をあげる方向
    • AMDはIPCをあげる方向
  • 昨今のCPUはHT(ハイパースレッディング)をデフォルトで有効にしているはず、かつ1物理コアで2論理コアになるはずなので、OSからCPUコアが例えば24コアに見えても物理的には12コアしかない
    • スレッド数12とスレッド数24でCPUバウンドなアプリを動かした場合、ほとんど速度が変わらないはず
    • スレッド数24で動かした場合、topでみるとそれぞれのコアが50%程度の使用率になっているかもしれないが、物理的には100%出ているかもしれない
  • 最近の linux スケジューラは NUMA アーキテクチャやキャッシュも考慮するので、taskset ないし は cpuset で CPU affinity (特定のプロセスを特定のCPUコアで動かす)の設定をしても、おそらくあまり効果がない


perf stat

個々のCPUは、イベントを記録するようにプログラムできる少数のレジスタ (通常2個から8個)を持っていて、そこに CPU パフォーマンスカウンタ (CPC) の値を保存している。libcpc ないし perf は、この CPU の値を参照できる。

この値はCPUごとに持っているものや持っていないものがあり、例えば stalled-cycles-frontend および stalled-cycles-backend は Intel Xeon X5650 では表示できたが、最近のCPUだと表示されなかったりするようだ。

パフォーマンスカウンタについては the Intel® 64 and IA-32 Architectures Optimization Reference Manual の Appendix B に詳細が書いてある。


使い方

perf list で見れるもの一覧し、perf stat -e で指定する。

$ perf stat -e instructions,cycles,cache-misses,L1-dcache-load-misses,L1-dcache-store-misses,LLC-load-misses,LLC-store-misses,dTLB-load-misses,dTLB-store-misses ls

 Performance counter stats for 'ls':

         1,546,781      instructions              #    0.90  insns per cycle
         1,720,435      cycles
             3,736      cache-misses
            25,534      L1-dcache-load-misses
   <not supported>      L1-dcache-store-misses
             1,806      LLC-load-misses
     <not counted>      LLC-store-misses
     <not counted>      dTLB-load-misses           (0.00%)
     <not counted>      dTLB-store-misses          (0.00%)

       0.002274088 seconds time elapsed

4-wide スーパースカラなら 4 insns per cycle までいくはず(パイプライン考えたらもっといくはず?)なので、0.90 は遅いといえそう。 LLC-load-misses が最後のレベル(LLC: Last Level Cache)、つまりL3キャッシュのロードミス ≒ メインメモリへのアクセスの回数となる(結局、何 cycle になるのかいまいちわからんが)

$ perf stat sysbench --num-threads=8 --test=cpu --cpu-max-prime=1000 run

 Performance counter stats for 'sysbench --num-threads=8 --test=cpu --cpu-max-prime=1000 run':

        527.029020 task-clock                #    7.483 CPUs utilized
               113 context-switches          #    0.214 K/sec
                18 cpu-migrations            #    0.034 K/sec
               750 page-faults               #    0.001 M/sec
     1,539,575,025 cycles                    #    2.921 GHz                     [83.46%]
       882,133,872 stalled-cycles-frontend   #   57.30% frontend cycles idle    [83.59%]
       624,548,115 stalled-cycles-backend    #   40.57% backend  cycles idle    [67.16%]
       523,014,992 instructions              #    0.34  insns per cycle
                                             #    1.69  stalled cycles per insn [83.35%]
       142,563,177 branches                  #  270.503 M/sec                   [83.36%]
         5,987,335 branch-misses             #    4.20% of all branches         [83.07%]

       0.070432542 seconds time elapsed

cycles はマルチコア全体で使用した合計サイクル数。試しに sysbench の並列度を --num-threads=1 に減らしても実行時間は伸びるが cycles の値はたいして変わらなかった。

1.69 stalled cycles per insn がメモリにアクセスするなどしてストールした CPI  (cycles per instructions) の値。 stalled-cycles-backend が主にメモリアクセスでストールしたサイクル数で、stalled-cycles-frontend が主に Fetch 命令でストールしたサイクル数。下図参照。

なお、vm guest で実行すると以下のように <not supported> だらけになるようだ。vmware だと起動時に有効にできるっぽいのだが、kvm や ec2 は無理そう ...? ec2 の場合は vm ではないハードウェア専有インスタンスを使うとかするしかなさそう。

$ perf stat ls
          0.528094 task-clock                #    0.547 CPUs utilized
                 0 context-switches          #    0.000 K/sec
                 0 cpu-migrations            #    0.000 K/sec
               138 page-faults               #    0.261 M/sec
   <not supported> cycles
   <not supported> stalled-cycles-frontend
   <not supported> stalled-cycles-backend
   <not supported> instructions
   <not supported> branches
   <not supported> branch-misses

lsof の FD 列

後進のためのメモシリーズ

lsof ファイル名で、あるファイルを開いているプロセスとユーザを調べることができる。

$ sudo lsof ~/.config/gcloud/credentials
COMMAND   PID   USER   FD   TYPE DEVICE SIZE/OFF   NODE NAME
python2 10517 system    4uW  REG  202,1     3643 823637 /home/system/.config/gcloud/credentials
python2 10704 system    3u   REG  202,1     3643 823637 /home/system/.config/gcloud/credentials

PID でプロセス番号、USER でユーザ名を調べられる。 これは皆さんご存知のとおり。

で、FD 列。

FD列は「FD番号 読み書きモード ロックモード」を意味していて、man によると以下のようになる。

FD is followed by one of these characters, describing the mode under which the file is open:

  r for read access;
  w for write access;
  u for read and write access;
The mode character is followed by one of these lock characters, describing the type of lock applied to the file:

  N for a Solaris NFS lock of unknown type;
  r for read lock on part of the file;
  R for a read lock on the entire file;
  w for a write lock on part of the file;
  W for a write lock on the entire file;
  u for a read and write lock of any length;
  U for a lock of unknown type;
  x for an SCO OpenServer Xenix lock on part      of the file;
  X for an SCO OpenServer Xenix lock on the      entire file;

たとえば、4uW の場合は 4=> FD番号 4、u => 読み書きモード(rw)、W => 書き込みロック(LOCK_EX) ということになる。

蛇足であるが、w for a write lock on part of the file は flock(2) ではできないが、fcntl(2) で F_SETLKW を使って実現できる。個人的にはあまり使ってる事例はみないがカーネルハッカーなどは使うのだろうか?

A Ruby and Fluentd committer working at DeNA. 記事本文および記事中のコード片は引用および特記あるものを除いてすべて修正BSDライセンスとします。 #ruby #fluentd #growthforecast #haikanko #yohoushi #specinfra #serverspec #focuslight
はてぶ人気エントリー