ptraceシステムコール入門 ― プロセスの出力を覗き見してみよう!

他のプロセスを中断せずに、その出力をミラーリングして新しくパイプで繋ぐ、そんなことはできるのでしょうか。 straceやgdbといったコマンドは一体どういう仕組みで動いているのでしょうか。 ptraceシステムコールを使い、プロセスが呼ぶシステムコールを調べて出力を覗き見するコマンドを実装してみたいと思います。

ptraceシステムコール

Linuxを触っていると、いかにプロセスを組み合わせるか、組み合わせる方法をどれだけ知っているかが重要になってきます。 パイプやリダイレクトを使ってプロセスの出力結果を制御したり、コードの中からコマンドを実行して、終了ステータスを取得したりします。 プロセスツリーやプロセスグループを理解し、シグナルやnohupコマンドを使ったりします。

プロセスの扱いに慣れると疑問に持つのがstracegdbの仕組みです。 プロセスの実行しているシステムコールを出力したり、メモリーを書き換えたりできるこれらのコマンドは、まるで魔法みたいです。 一体全体どんな仕組みで動いているのでしょうか。 誰に許しを得て他のプロセスのメモリーにアクセスしたりしているのでしょうか。 これらのコマンドは、標準出力を読んだりプロセスにシグナルを送ったりするのとは全く別の次元のことをやっているかのようです。

ptraceシステムコールは、straceやgdbのようなコマンドの実装の中で核となるシステムコールです。 このシステムコールを使うと、実行中の他のプロセスの動作を覗き見したり、メモリーを書き換えたりすることができます。 魔法を使っているんじゃなかったんですね。 ちょっぴり残念な気分です。

[試して理解]Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識

[試して理解]Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識

  • 作者:武内 覚
  • 発売日: 2018/02/23
  • メディア: 単行本(ソフトカバー)

procoutコマンド

使っているシステムコールがわかれば、それを使って何かを作りたくなってきます。 ptraceシステムコールを使うと、原理上はstraceやgdbを自前で実装することができます。 しかし、最初から複雑なデバッガを目指そうとすると大変です。

私はまず手始めに、procoutというコマンドを作ってみました。

中断して途中から再開したりできないプロセスの出力が端末に流れている時に、後からgrepしたりteeしたりしたくなることがあると思います。 そんな時にプロセス自体を中断せず、またコードの変更も行わずに、別のターミナルからプロセスの出力を流すことができるコマンドです。 プロセスの出力を覗き見るという感じなので、procoutと名付けてみました。

次のように、引数にpidを渡して実行します。

 $ sudo procout [pid]

そうすると、対象となるプロセスにアタッチし、コマンドの標準出力をそのまま真似して出力してくれます。 エディタのプロセスに対して使うと、まるで端末がミラーリングされているかのような挙動になります。

f:id:itchyny:20170731003438g:plain

procoutコマンドは、コマンド自体の便利さやおもしろさよりも、それ自体を実装することに意義があります。 ptraceシステムコールの基礎に触れることができるからです。 ptraceの最初の練習問題として、そしてLinuxシステムコールがどのように呼ばれているかについて理解するために、このエントリーではprocoutコマンドを一緒に作っていきたいとおもいます。

免責: 私は一週間前にptraceシステムコールについて学びはじめた素人です。素人だからこそ、わからないところを一つずつ潰しながらこの記事を書きました。もし誤っている記述があれば、お気軽にコメントいただければと思います。

この記事のコード及び上記procoutコマンドは、以下の環境で動作確認をしています。macOSでは動きませんが、仮想マシン上でも簡単に試せますので、是非挑戦していただけたらと思います。

vagrant@vagrant-ubuntu-trusty-64:~/$ uname -srmo
Linux 3.13.0-125-generic x86_64 GNU/Linux

プロセスにアタッチしてみよう

ptraceシステムコールは、引数によって様々なことを行うことができます。 man 2 ptraceでマニュアルを引いてみましょう。

NAME
       ptrace - process trace

SYNOPSIS
       #include <sys/ptrace.h>

       long ptrace(enum __ptrace_request request, pid_t pid,
                   void *addr, void *data);

第一引数にはプロセスに対してどういうptraceリクエストを行うか、第二引数にはプロセスのpidを指定します。 リクエストの内容によって、第三・四引数の意味は変わってくるので、これらについてはおいおい見ていきましょう。 他にはsys/ptrace.hをincludeすること、返り値がlongであることがわかりました。

まずは、ptraceの基本であるアタッチ・デタッチから始めましょう。

#include <stdio.h>
#include <stdlib.h>
#include <sys/ptrace.h>

int main(int argc, char *argv[])
{
  long ret;
  if (argc < 2) {
    fprintf(stderr, "specify pid\n");
    exit(1);
  }

  pid_t pid = atoi(argv[1]);
  printf("attach to %d\n", pid);

  ret = ptrace(PTRACE_ATTACH, pid, NULL, NULL);
  if (ret < 0) {
    perror("failed to attach");
    exit(1);
  }
  printf("attached to %d (ret: %ld)\n", pid, ret);
  sleep(5);

  ret = ptrace(PTRACE_DETACH, pid, NULL, NULL);
  if (ret < 0) {
    perror("failed to detach");
    exit(1);
  }
  printf("detached from %d (ret: %ld)\n", pid, ret);

  return 0;
}

topコマンドを実行し、そのプロセスに対してアタッチしてみましょう。

f:id:itchyny:20170730182701g:plain

あら、アタッチできませんでした。 Operation not permittedと表示されていることから想像がつきますが、ptraceで他のプロセスにアタッチするには、root権限が必要です。 そうですよね、一般ユーザーで他のプロセスを自由に操作できたら怖いですよね。

f:id:itchyny:20170730182642g:plain

うまく動きました。 プロセスにアタッチしてsleepしている間、左側のtopコマンドが停止しているのがわかります。

なぜtopコマンドは止まってしまったのでしょうか。 man 2 ptraceでは次のように説明されています。

While being traced, the tracee will stop each time a signal is delivered, even if the signal is being ignored. (An exception is SIGKILL, which has its usual effect.) The tracer will be notified at its next call to waitpid(2) (or one of the related "wait" system calls); that call will return a status value containing information that indicates the cause of the stop in the tracee.

tracerがptraceするプロセスで、traceeがptraceされるプロセスです (employerとemployeeと同じ)。 ptraceされるプロセスはシグナル毎にいちいち止まるから、ptraceするプロセスはwaitpidを使ってねと書かれています。

straceのようなptraceの典型的な用途では、システムコールが呼ばれるところで処理を行います。 PTRACE_SYSCALLを使ってプロセスを再開すると、次のシステムコールでプロセスが停止し、ptraceするプロセスはwaitpidを使ってその停止を検知することができます。

#include <stdio.h>
#include <stdlib.h>
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <sys/user.h>
#include <sys/syscall.h>

int main(int argc, char *argv[])
{
  int status;

  if (argc < 2) {
    fprintf(stderr, "specify pid\n");
    exit(1);
  }

  pid_t pid = atoi(argv[1]);
  printf("attach to %d\n", pid);

  if (ptrace(PTRACE_ATTACH, pid, NULL, NULL) < 0) {
    perror("failed to attach");
    exit(1);
  }

  while (1) {
    waitpid(pid, &status, 0);
    if (WIFEXITED(status)) {
      break;
    } else if (WIFSIGNALED(status)) {
      printf("terminated by signal %d\n", WTERMSIG(status));
    } else if (WIFSTOPPED(status)) {
      printf("stopped by signal %d\n", WSTOPSIG(status));
    }

    ptrace(PTRACE_SYSCALL, pid, NULL, NULL);
  }

  return 0;
}

f:id:itchyny:20170728000028g:plain

大量に表示されるsignal 5とはどういう意味でしょうか。 プロセスにシグナルを送るコマンドであるkillに聞いてみましょう。

 $ kill -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
...

5番のシグナルがSIGTRAPだということがわかりました。 PTRACE_SYSCALLで再開するとSIGTRAPによって止まるという動作が確認できました。 これは期待されている動作なのでしょうか。 man 2 ptraceを引き、適当に検索しながら該当しそうな記述を探します。

Syscall-enter-stop and syscall-exit-stop are observed by the tracer as waitpid(2) returning with WIFSTOPPED(status) true, and WSTOPSIG(status) giving SIGTRAP.

From the tracer's perspective, the tracee will appear to have been stopped by receipt of a SIGTRAP.

実際の挙動を確かめながら、manを読み込み少しずつ知識を蓄えていくことは楽しいことですね。

レジスタの状態を取得してみよう

プロセスの出力を覗き見するには、プロセスが呼ぶwriteシステムコールの引数を解析する必要があります。 システムコールが呼ばれるときのレジスタの中身を見てみましょう。 ptraceの引数にPTRACE_GETREGSを使ってみます。

  struct user_regs_struct regs;

  while (1) {
    waitpid(pid, &status, 0);

    if (WIFEXITED(status)) {
      break;
    } else if (WIFSTOPPED(status)) {
      ptrace(PTRACE_GETREGS, pid, NULL, &regs);
      printf("%lld %lld %lld %lld\n", regs.orig_rax, regs.rsi, regs.rdx, regs.rdi);
    }

    ptrace(PTRACE_SYSCALL, pid, NULL, NULL);
  }

f:id:itchyny:20170728000012g:plain

なんだか出力が賑やかになってきました。 他のプロセスにアタッチし、システムコールが呼ばれる時のレジスタを出力しているだけですが、これはすでに簡易straceのようなものです。

一番左に出力したorig_raxは、システムコールの番号を表します。 システムコール番号はsys/syscall.hで定義されています。

#include <stdio.h>
#include <sys/syscall.h>

int main(int argc, char const* argv[])
{
  printf("%d\n", SYS_write);
  return 0;
}

私の手元ではSYS_write1でした。

上記のコードでは、システムコール番号の他にrsi, rdx, rdiを表示しています。 システムコールが呼ばれる瞬間、各レジスタには何が入っているのでしょうか。 x86_64 syscall registersなどでググって調べてもいいのですが、簡単なコードのアセンブリを見るという方法もあります。

#include <stdio.h>
#include <unistd.h>

void main() {
  write(STDOUT_FILENO, "Hello, world!", 13);
}
 $ gcc -O0 -S write_regs.c
 $ cat write_regs.s
 .file "write_regs.c"
    .section  .rodata
.LC0:
    .string   "Hello, world!"
    .text
    .globl    main
    .type main, @function
main:
.LFB0:
    .cfi_startproc
    pushq    %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq %rsp, %rbp
    .cfi_def_cfa_register 6
    movl $13, %edx
    movl $.LC0, %esi
    movl $1, %edi
    call write
    popq %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size main, .-main
    .ident    "GCC: (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4"
    .section  .note.GNU-stack,"",@progbits

文字列のアドレスはesiに、長さ (writeの第三引数) がedxに、出力先であるfd (=STDOUT_FILENO=1) はediに書かれていることがわかります。 siレジスタは文字列操作のためのSource index, diレジスタDestination indexであることからその名がついていることを思い出すと、それぞれに文字列のアドレスとfdが入ってるというのは自然な挙動です。

やりたいことは「プロセスの出力を覗き見する」だったので、regs.orig_rax == SYS_writeのときにプロセスのメモリーから文字列を読み取り、出力すれば完成です。

syscall-enter-stopとsyscall-exit-stop

これまで「システムコールが呼ばれる時」とごまかしてきましたが、実はこの言い方は正確ではありません。 レジスタの値を出力して様子を見てみましょう。

 $ sudo ./main 24358
attach to 24358
1 6545952 434 1
1 6545952 434 1
1 6545952 1140 1
1 6545952 1140 1
1 6545952 821 1
1 6545952 821 1
1 6545952 1136 1
1 6545952 1136 1
# orig_rax rsi rdx rdi

左からシステムコール番号 (SYS_write), 文字列のアドレス, 書き込んだバイト長, fdです。 同じ値の行が二回ずつ表示されていることがわかります。 これは同じ引数で二回システムコールが呼ばれているのではなく、システムコールが呼ばれる直前と直後の二回表示されているのです。

さらに理解を深めるために、orig_raxレジスタに加えてraxレジスタも表示してみます。

 $ sudo ./main 24358
attach to 24358
1 -38 6545952 1278 1
1 1278 6545952 1278 1
1 -38 6545952 1262 1
1 1262 6545952 1262 1
1 -38 6545952 821 1
1 821 6545952 821 1
1 -38 6545952 1122 1
1 1122 6545952 1122 1
# orig_rax rax rsi rdx rdi

システムコールが呼ばれると、その返り値がraxレジスタに入ります。 writeシステムコールの返り値は書き込んだバイト数ですから、writeの第三引数であるrdxraxが同じ行はシステムコールが呼ばれた後ということになります。 raxレジスタの値が-38となっている行は、システムコールが呼ばれる前の状態ということになります。 この-38が何の値なのかは後で説明します。

PTRACE_SYSCALLによりシステムコールをトラップすると、システムコールに入ったとき (syscall-enter-stop) と終わった時 (syscall-exit-stop) の二回停止するようになっています。 manを見てみましょう。

If the tracee was restarted by PTRACE_SYSCALL or PTRACE_SYSEMU, the tracee enters syscall-enter-stop just prior to entering any system call (中略). No matter which method caused the syscall-entry-stop, if the tracer restarts the tracee with PTRACE_SYSCALL, the tracee enters syscall-exit-stop when the system call is finished, or if it is interrupted by a signal. (That is, signal-delivery-stop never happens between syscall-enter-stop and syscall-exit-stop; it happens after syscall-exit-stop.).

プロセスの出力文字列を覗き見するのは、入ったときでも終わったときでもどっちでも構いません。 ただ二回表示されると困るので、ここではシステムコールに入った時だけ処理を行うようにしましょう。 では、syscall-enter-stopsyscall-exit-stopを区別するにはどうすればいいのでしょうか。 manを順番に読んでいくと、次のような記述に愕然とします。

Syscall-enter-stop and syscall-exit-stop are indistinguishable from each other by the tracer. The tracer needs to keep track of the sequence of ptrace-stops in order to not misinterpret syscall-enter-stop as syscall-exit-stop or vice versa.

関連する記述をmanから抜き出してまとめてみました。

  • syscall-enter-stopの直後はsyscall-exit-stopとは限らない。PTRACE_EVENTによる停止かもしれないし、終了しているかもしれない。
  • PTRACE_O_TRACESYSGOOD を使うと、syscall-{enter,exit}-stopかそれ以外かは区別できる。
  • x86において、syscall-enter-stopではraxレジスタ-ENOSYS (この値が-38) になる。しかし、何らかのシステムコールが同じ値を返すこともあり、rax == -ENOSYS だからといってsyscall-exit-stopではないとは言い切れない。
  • syscall-enter-stopとsyscall-exit-stopは単体で見た時に区別することはできない。前の状態を保持しておいて調べるしかない。

以上を踏まえて、syscall-enter-stopでのみレジスタ値を表示するように実装してみました。

  ptrace(PTRACE_SETOPTIONS, pid, NULL, PTRACE_O_TRACESYSGOOD);

  int is_enter_stop = 0;
  long prev_orig_rax = -1;
  while (1) {
    waitpid(pid, &status, 0);

    if (WIFEXITED(status)) {
      break;
    } else if (WIFSTOPPED(status) && WSTOPSIG(status) == (SIGTRAP | 0x80)) {
      ptrace(PTRACE_GETREGS, pid, NULL, &regs);
      is_enter_stop = prev_orig_rax == regs.orig_rax ? !is_enter_stop : 1;
      prev_orig_rax = regs.orig_rax;
      if (is_enter_stop && regs.orig_rax == SYS_write) {
        printf("%lld %lld %lld %lld %lld\n", regs.orig_rax, regs.rax, regs.rsi, regs.rdx, regs.rdi);
      }
    }

PTRACE_SETOPTIONSはptraceリクエストの1つで、PTRACE_O_TRACESYSGOODを指定することで、システムコールによる停止かどうかを正確に判定できるようになります。 システムコール番号が前から変化したときにenter-stopだと判定するようにしました。 ずっと同じシステムコールが呼ばれ続けるならば、ずっとexit-stopで出力する可能性は否定できませんが、多くの現実的なコマンドそういうことはなさそうですし、仮にそうなったとしても出力を覗き見するコマンドとしての動作には影響しません。 だんだん精度良くシステムコールをトレースできるようになってきましたね。

文字列をメモリーから読み取ろう

プロセスがwriteシステムコールを呼ぶ時の引数から、出力されているバイト列を読み取ることができます。 PTRACE_PEEKDATAを使ってptraceを呼ぶと、プロセスの管理しているメモリーの値を取得することができます。

        peek_and_output(pid, regs.rsi, regs.rdx, (int)regs.rdi);

/* ... */

void peek_and_output(pid_t pid, long long addr, long long size, int fd)
{
  if (fd != 1 && fd != 2) {
    return;
  }
  char* bytes = malloc(size + sizeof(long));
  int i;
  for (i = 0; i < size; i += sizeof(long)) {
    long data = ptrace(PTRACE_PEEKDATA, pid, addr + i, NULL);
    if (data == -1) {
      printf("failed to peek data\n");
      free(bytes);
      return;
    }
    memcpy(bytes + i, &data, sizeof(long));
  }
  bytes[size] = '\0';
  write(fd == 2 ? 2 : 1, bytes, size);
  fflush(fd == 2 ? stderr : stdout);
  free(bytes);
}

ptrace(PTRACE_PEEKDATA, pid, {addr}, NULL)の返り値が、そのアドレスにある値です。 標準エラー出力ならエラー出力に出すように実装してみました。

さっそく実行してみましょう。

やったー! topコマンドのプロセスにアタッチすると、まるでこちらでもtopコマンドを打ったような動きになりました。 もちろん、topコマンドでなくてどんなコマンドに対しても使えます。 Vimのプロセスにアタッチしてみましょう。 Vimの画面がミラーリングされていておもしろい!

今回実装するprocoutコマンドはここまでとします。 ただ、ここから様々な発展したコマンドが実装できると思います。 上のコードでは諸事情でfd = 1, 2のみ扱っていますが、この処理の必然性はありません (こう制限しないとエディタにアタッチした時にゴミが出力される)。 open, readなど、対応するシステムコールを増やしていくと、より便利なデバッガとなるでしょう。

straceやgdbそのものをそっくり実装しようとする必要はありません。 既にこれらのコマンドはあるじゃないですか、うまく動いているじゃないですか。 それでもなお、これらのコマンドの仕組みを理解することは重要な意義があると考えています。 ptraceシステムコールについて学ぶこと、それを使って実際に動くコマンドを作ってみること、簡単なデバッガを書いてみること。 そして、何をどのように実装すればstraceやgdbなどを作れるかをイメージできるようになること。 コマンドやツールの使い方を学ぶだけで満足するのではなく、それらの仕組みを深く知り、実装方法をイメージできるようになると、技術者としての知識がより幅広くそして深くなり、エンジニアリングを楽しめるようになっていくのだと思います。

Rustで書き直そう

Linuxシステムコールについて学ぶためには、C言語が最適の言語だと思います。 しかし、それらを組み合わせて大きなプログラムを組んだり、複雑なTUIを作ったりする必要が生じたときにふさわしい言語であるかどうかはかなり疑わしいと思います。

Rustはシステムプログラミングを学ぶのに適した言語です。 冒頭でご紹介したprocoutコマンドも、Rustで実装しています。 実は、先にRust版を書いてから、ブログのためにCで書き直しているのが実情です。 ブログを書く時にCを選んだのは、システムコールについて学ぶ時にRustを選ぶことが時に遠回りになりうるとわかったからです。

Programming Rust: Fast, Safe Systems Development

Programming Rust: Fast, Safe Systems Development

Rustを書いていると、実行時のメモリー安全性や、型チェックの厳格さから、「コードの正しさ」をコンパイルのチェックに委ねがちになります。 しかし、低レイヤーを触るとこれはかなり様子が変わってくるのがわかります。 当然のことですが、コンパイルが通っても、システムコールの使い方が適切でなければ全く動きません。 システムコールの呼ぶ順番が間違っていたら、コンパイルが通ったとしても、やってることは全くトンチンカンかもしれません。 また、Rustで書くこととコードがportableかどうかもイコールではありません。 結局のところ、誰かがportabilityの高い素晴らしいライブラリ (ただし中身は涙ぐましい努力で書かれている) を用意しないといけないのです。

Rustで書くことは、メモリー管理と型チェックに関してコードの安全性と大きな安心感をもたらしてくれます。 例えば、ptraceの2つの引数を誤って逆に書いてしまい、数分悩むといったことは起こりえないでしょう (記事を書く過程でやらかしました…)。 Rustの洗練されたエラーのハンドリングや、豊富なライブラリなどエコシステムの恩恵も受けられます。 ただ、低レイヤーを触るときは「正しくないコードはコンパイルが通ったとしても正しく動かない」という当たり前のことを教えてくれます。

さて、procoutのRust実装ですが、これは「読者の課題」としたいと思います。 大した行数じゃないので、簡単に移植できると思います。 私もコードをGitHubにあげていますので、実装できたらコードを見比べてみるとおもしろいかもしれません。 ここではRustで書いたことで得られた知見を簡単に書いておきます。

  • rust-lang/libcnix-rust/nixを使えばだいたいのことはできる。
  • nixパッケージにもmacOSで動くptraceは実装されていない。システムプログラミングでportableにすることは難しい。システムコールの番号すらプラットフォームによって異なる。
  • nixパッケージのptraceはまだ機能が揃っていない。getregsなんかは欲しい。まだ未熟なので、プルリクエストを送ったら簡単に取り込まれるかもしれません。

まとめ

ptraceシステムコールを使い、他のプロセスが呼ぶシステムコールを調べたり、メモリーを読み取ることができるのを確認しました。 プロセスの呼ぶwriteシステムコールの引数を使い、プロセスの出力を覗き見するコマンドprocoutをRustで実装しました。

当初は、普段使っているmacOS上で動くものを作ろうとしたのですが、nixライブラリのptraceのコードを見た時に諦めました。 VagrantUbuntuを立てて、その中で動作確認を行なっています。 普段Cを書くことがほとんどないので、自分にとって貴重な経験になりました。 /usr/includeから素早くファイルを開いて実装を確認するのにも慣れました。 portableなstraceを作るにはかなり大変だということもわかりました。

Rustは書いていてとても楽しい言語です。 今回だと、メモリーからバイト列を読み取って結合するコードはきれいに書けたと思います。 低レイヤーを触るときは何よりもまず、システムコール自体を正しく理解していることが大事です。 実際に動作するCのコードを書けることを確認しておくとよいでしょう。

straceやgdbってどうやって動いているのだろう。 この小さな疑問が浮かんだのが、一週間前のことです。 システムコールについて学び、それを使ったコマンドツールを作るのは、とても楽しい経験でした。 既存のツールの仕組みを調べることで「何を使えば何ができる」というレパートリーを増やし、それらがアイディアの源泉となって、便利なコマンドラインツールを作っていけたらいいなと思います。

参考にしたサイトは以下のとおりです。勉強させていただきました、ありがとうございます。

おまけ: ptraceを使いこなせるようになると、GitHub - nelhage/reptyr: Reparent a running program to a new terminalのような発想が出てくるわけです。いやはや、これはすごいですね。

ソースコード

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <sys/user.h>
#include <sys/syscall.h>

void peek_and_output(pid_t, long long, long long, int fd);

int main(int argc, char *argv[])
{
  int status;
  struct user_regs_struct regs;

  if (argc < 2) {
    fprintf(stderr, "specify pid\n");
    exit(1);
  }

  pid_t pid = atoi(argv[1]);
  printf("attach to %i\n", pid);

  if (ptrace(PTRACE_ATTACH, pid, NULL, NULL) < 0) {
    fprintf(stderr, "failed to attach\n");
    exit(1);
  }
  ptrace(PTRACE_SETOPTIONS, pid, NULL, PTRACE_O_TRACESYSGOOD);

  int is_enter_stop = 0;
  long prev_orig_rax = -1;
  while (1) {
    waitpid(pid, &status, 0);

    if (WIFEXITED(status)) {
      break;
    } else if (WIFSTOPPED(status) && WSTOPSIG(status) == (SIGTRAP | 0x80)) {
      ptrace(PTRACE_GETREGS, pid, NULL, &regs);
      is_enter_stop = prev_orig_rax == regs.orig_rax ? !is_enter_stop : 1;
      prev_orig_rax = regs.orig_rax;
      if (is_enter_stop && regs.orig_rax == SYS_write) {
        peek_and_output(pid, regs.rsi, regs.rdx, (int)regs.rdi);
      }
    }

    ptrace(PTRACE_SYSCALL, pid, NULL, NULL);
  }

  return 0;
}

void peek_and_output(pid_t pid, long long addr, long long size, int fd)
{
  if (fd != 1 && fd != 2) {
    return;
  }
  char* bytes = malloc(size + sizeof(long));
  int i;
  for (i = 0; i < size; i += sizeof(long)) {
    long data = ptrace(PTRACE_PEEKDATA, pid, addr + i, NULL);
    if (data == -1) {
      printf("failed to peek data\n");
      free(bytes);
      return;
    }
    memcpy(bytes + i, &data, sizeof(long));
  }
  bytes[size] = '\0';
  write(fd == 2 ? 2 : 1, bytes, size);
  fflush(fd == 2 ? stderr : stdout);
  free(bytes);
}

音量を調節できるCLIツールをGo言語で作りました!

volumeコマンドを作りました。

音量の調整ってコマンドからどうやるんだろう、ポータブルな形でコマンドラインツールがあれば便利なのでは… と思ったので作りました。 macOSUbuntuで動作確認をしています。

インストール

go get github.com/itchyny/volume-go/cmd/volume

getは今の音量を返します。set (0-100) で音量を設定できます。

 $ volume get
25
 $ volume set 20
 $ volume get
20

up, down, mute, unmuteなど、直感的に使えるサブコマンドを揃えています。

 $ volume down
 $ volume status
volume: 14
muted: false
 $ volume up
 $ volume status
volume: 20
muted: false
 $ volume mute
 $ volume status
volume: 20
muted: true
 $ volume unmute
 $ volume status
volume: 20
muted: false

簡単!OS起動時に音量を下げたり、アプリケーションを切り替えるたびに音量を調節したりするなどのオートメーションに使えて便利ですね。おわり!

コマンドラインがさらに便利になるfillinコマンドを作りました!

fillinというコマンドラインツールを作成しました。

コマンドの一部を変数化して、別の履歴に保存しておけるツールです。 ステージング環境と本番環境のように、同じコマンドで複数の環境を切り替えるのに便利です。

zshの本 (エッセンシャルソフトウェアガイドブック)

zshの本 (エッセンシャルソフトウェアガイドブック)

  • 作者:広瀬 雄二
  • 発売日: 2009/06/17
  • メディア: 単行本(ソフトカバー)

どうして作ったの

コマンド履歴って便利ですよね。 私はよくコマンド履歴からコマンドを選んで実行しています。 シェルに付属しているデフォルトの履歴を使っている方もおられるでしょうし、fzfやpecoのようなインタラクティブな絞り込みを行なっている方もいるでしょう。

私が一番困っていたのが、認証キーの扱いです。 webアプリを作っていてcurlで素早く確認するときに、認証キーやアクセストークンを打つことがあります。 アクセストークンのようなランダムな英数字は、fzfのようなfuzzy searchとかなり相性が悪いものです。 最近fzfに乗り換えてインターフェースは気に入っていたのですが、トークンを入れてcurlする癖を直さないといけないなぁと思っていました。

いや、トークンを直に打つなよ…っていうご意見はごもっともです。 ライブコーディングする前に履歴消さないといけませんし。 Basic認証ならば標準入力で指定できるんですけど、ヘッダーは (たぶん) できませんよね。 認証キーは何かしらの履歴には残っていて簡単に呼び出したいけど、シェルの履歴には入れたくない… この程度のことにシェルスクリプトを書くのも面倒だし、ファイルに一個ずつ保存していちいち展開するのもかわいくない…

トークンをコマンドに直接書きたくない以外にも、コマンドの一部を別管理したいと思うことはたくさんあります。 データベースに繋ぐ時、データベースのホスト名とデータベース名はセットで管理したいですよね。 awsコマンドのプロファイルだってシュッと切り替えて同じコマンドを実行したくなることもあります。

コマンドの一部を「変数」にして、それを「展開して実行」してくれるコマンドがあれば良いのではないか? シェルの履歴が汚れることはないし、履歴をfuzzy searchしてもおかしなことにはなりにくい。 変数に埋めた値の履歴をローカルに保存して矢印キーとかで呼び出せれば便利そう…

はい、それがfillinです。

brew install itchyny/tap/fillin

または

go get github.com/itchyny/fillin

でインストールできます。

導入編

fillinコマンドの使い方はとても簡単です。 いつも打つコマンドにfillinとつけて、「変えたいところ」をテンプレートにしてあげるだけです。 例えば、次のように打ってみましょう。

 $ fillin echo {{message}}
message: 

messageは何かと聞かれました。適当に打ってみます。

message: こんにちは、世界!
こんにちは、世界!

こんにちは! fillinコマンドは、messageの場所をユーザーに入力してもらい、その値を埋めてコマンドを実行するだけです。

そうです、{{message}}というところを「埋めて (fill-in)」から実行する、それがfillinコマンドです。

もちろん変数はいくつも使うことができます。

 $ fillin echo {{foo}} {{bar}} {{baz}}
foo: 変数を
bar: こういうふうに
baz: 入力するよ
変数を こういうふうに 入力するよ

かわいい。

普段使っているコマンドをテンプレート化してfillinとつけるだけなので、例えば環境変数を含んだコマンドも実行できます。

 $ fillin LANG={{lang}} date
lang: en_US
Mon Jun 12 08:22:55 JST 2017
 $ fillin LANG={{lang}} date
lang: ja_JP
2017612日 月曜日 082252秒 JST

便利!

実践編

いつも打つコマンドの中で、ホスト名のように環境によって切り替えるところをテンプレート化することができます。

 $ fillin psql -h {{psql:hostname}} -U {{psql:username}} -d {{psql:dbname}}
[psql] hostname: localhost
[psql] username: example-user
[psql] dbname: example-db

psql (9.6.3)
Type "help" for help.

example-db=>

各値を入力して決定すると、いつものようにコマンドが実行されます。 入力した値はまとめて履歴に残るので、同じコマンドから上矢印キーで呼び出すことができます。

 $ fillin psql -h {{psql:hostname}} -U {{psql:username}} -d {{psql:dbname}}
[psql] hostname, username, dbname: localhost, example-user, example-db

もちろん、ステージング環境用、本番用のように複数の設定を残すことができます。 psql:という同じ「スコープ」を持つ変数は、組になって履歴を呼び出すことができるのが便利なところです。

webアプリのAPIの動作確認でcurlを使う人ならば、次のようにbase-urlaccess-tokenを管理することができます (認証方法はサンプルです)。

 $ fillin curl {{api:base-url}}/api/info -H 'Authorization: Bearer {{api:access-token}}'
[api] base-url, access-token: example.com, accesstokenabcde012345

この例でもapi:というスコープをつけたので、ホストと認証キーがステージングと本番でごっちゃになることはありません。

何が直交したデータなのかということを考えるのはとても大事なことです。 ローカルの環境、ステージング環境、本番環境によって、ベースURLと認証キーは異なります。 そして、API/api/info, /api/service, /api/account ... といくつかあります (URLは適当です)。 どのAPIも、どの環境に対しても叩くことができます。 つまり環境とAPIのエンドポイントは直交した概念です。

「このAPIのエンドポイントにcurlする」ということはシェルの履歴に残します。 「ステージングの認証キーや本番のキー」といった情報は、fillinの履歴に残します。 これによって、シェルにn*m個の履歴が残ることなく、コマンド履歴を探すのが楽になります。 新しいAPIのエンドポイントを作ったときも、開発時にローカル環境に対して打っておけば、後は同じコマンドでステージング、本番と切り替えるだけで動作確認できます。

こういう話はデータベース、API開発以外にもたくさんあると思います。 例えばawsコマンドの--profile引数ですね。

 $ fillin aws --profile {{aws:profile}} ec2 describe-instances
[aws] profile: aws-profile-example

EC2 (など) に何をするか×アカウントのプロファイルが直交概念ですね。 他にも応用が効く考え方だと思うので、是非よい使い方を考えてみてください。

Go言語による並行処理

Go言語による並行処理

まとめ

コマンドには「どこに」「何を」するかの二軸があります。 「どのホストのどのポートに」「Redis CLIでログインする」とか、「AWSのどのプロファイルの管理している」「EC2のインスタンスを一覧する」、「ステージング環境 (あるいは本番環境) の」「APIを叩いてレスンポンスを調べる」といった具合です。 fillinを用いると、この2つをきれいに分離し、コマンド履歴の管理が簡単になります。

コマンドには「何をするか」を打ちます。

ホスト名、ベースURL、認証キー、プロファイルといった「環境 (どこに)」はfillinで切り替えます。

このように運用すると、コマンドを打つのがとても楽になります。 いや、楽になりそうです… 前の木曜にアイディアを思いついて、この土日でようやく動いたばかりなので、まだがっつりは使ってないんです… でも、きっと便利なはずなので、よかったら使ってみてください。 私もしばらくfillin運用すると思います。 沢山の人に使っていただくと私がとても喜びます。

実際に実行されたコマンドが履歴に残らないのは困る? ご安心ください。 fillinコマンドは実行したコマンドをタイムスタンプ付きで~/.config/fillin/.fillin.histfileに保存しています (この場所が適切なのかという話はありそうだけど、まぁとりあえず…)。 zshのコマンド履歴を真似して出力しているので、fillinをやめたいとなったら (そんな…><)、.histfilegrep -v fillinし、.fillin.histfile.histfileに追記しソートするといい感じになるはずです。 よかったですね。

宣伝

はてなでは、開発時に不便なことがあったらツールを自分で作って解消する、そんな情熱あふれるエンジニアを募集しています。

エンジニア以外にも様々な職種のご応募お待ちしております。

こちらの記事もどうぞ。私です。

Vimに自分の書いたパッチが取り込まれた!

Vim 8.0.0623に私の書いたパッチが取り込まれました。 わーい ∩(>◡<*)∩ わーい!

もともとのきっかけは、自分のプラグインを開発している中で、[\u3000-\u4000]という正規表現に対する挙動がset re=1set re=2で違うことに気がついたのです。 Vim正規表現エンジンを2つ積んでいる恐ろしいエディターなのですが、この2つの正規表現エンジンの挙動に微妙な違いがあることに気がつきました。 新しいNFAエンジンではエラーは出ませんが、古いエンジンではエラーが出ます。 古い正規表現エンジンでは、[a-z] みたいなパターンで、[\u3000-\u4000]のように差が大きすぎるとエラーを吐くのです。 この挙動の違いはまぁいいんです、エンジンの仕組みがぜんぜん違うので… ただ、この時のエラー番号E16でヘルプを引くと、コマンドのrangeに関するエラーなんです。 例えば :0buffer のようなケースですね。 ソースコードを覗いてみると、E16は [z-a] のようなパターンに対するエラーにも使われており、コマンドのrangeに関するエラーと正規表現の文字クラスに関するエラーは分けたほうが良さそうだと気が付きました。

エラー番号E16を正規表現のエラーに使いまわすのはおかしいと報告したところ、Bram氏に「昔はメモリーをあまり使わないよう、エラーメッセージが増えすぎないようにしていたんだ」と教えてもらいました。 「どう変えたらよくなるかを提案してね」ということだったので、エラーメッセージを追加したパッチを送りました。 この最初のパッチでは[z-a][\u3000-\u4000]の2つのエラーを同じメッセージにしていたのですが、Bram「メッセージ2つ分けてちょうだい。あとテストも書いてくれたらうれしいな」とのことだったので、メッセージを分けてテストを書いて送り返しました。 それから半月くらい音沙汰なかったのですが、最近ようやくマージされました。 返事がなくても気長に待つことですね。 あと、ヘルプなどのruntimeファイルはまとめて更新されるはずです。

最新バージョンを入れて動作チェックして、自分が書いたエラーメッセージが表示された時はホッとしました。 VimのE944とE945はな、父さんが作ったんだよって息子に自慢できますね、結婚してないけど。 世界中の開発者が使う道具の中のどこかに自分が書いたコードが入っていると思うとワクワクしますね! そんな感じです!じゃあねっ!

lightline.vimのREADME.mdを書き直しました

lightline.vimVimのステータスラインをいい感じにしてくれるプラグインです。 作って四年弱経つんですね。 おかげさまで多くのユーザーさまに使っていただいています。

itchyny.hatenablog.com

github.com

このREADME.mdを最近書き換えました。

………

それだけかいな!って感じなのですが、いろいろと大変でした… 主に精神的に… つらい…

  • プラグインを作った勢いと使って欲しいという強い思いで、プラグインを作ってすぐにREADME.mdをかなり詳しく書いていた
  • そのために当初の設計や実装時の思いが強く出ていたが、初めてプラグインを触る人にとっては読みにくい文章になってしまっていた
  • パッチをあてたフォントを使わないとプラグインが使えないと誤解されることがあった。実際、スクリーンショットの多くがパッチをあてたフォントを使った状態で撮っていた。
  • スクリーンショットの画質がかなり悪かった

とにかくいろいろと書き換えたいと思いつつ何か月も経ってしまったのですが、なかなか手が進みませんでした。 一応プラグインを作ってから三年以上、沢山の方にREADME.mdを読んでいただいていて、そこそこ機能していました。 そこそこ長い英語の文章で書いてしまったので、これをまた全体を書き直すのが億劫になっていたというのもあります。

いや、ほんと、一旦書いた英語の文章を、いい感じに書き直すの大変ですよ… しかもそれが趣味プロダクトのREADMEで、書き換えるのが必須でもなくて…

なんて思っていたのですが、やはりいろいろとよくないことも多かったので、最近一気に書き直しました。

  • 関数コンポーネントを使うことで、短い設定でいろいろと見た目を変更できることを伝えるようにしました。
  • パッチフォントに関する言及は辞めました。引き続き特殊なフォントのグリフを使うことはできますが、ポータビリティは悪いのであまりオススメはしません。
  • 誤解を生みやすい component_visible_condition の説明はやめました。関数コンポーネントを使えば十分です。
  • スクリーンショットをきれいに撮り直しました。

そんなこんなで少しは読みやすくなったかな〜と思います。

ヘルプの方も近いうちに書き換えていきます。 こんな感じです! じゃあね!