NetBSDの/bin/shでコマンドがexecve(2)されるまでの流れを追いかけてみた

NetBSD/bin/shでコマンドがexecve(2)されるまでの流れを追いかけてみた

技評×オングスこんなシェルスクリプトは書いちゃダメだ!にてシェル上でコマンドを実行する際のユーザランドからカーネルまでの流れが説明されていました。

ちょっと興味が出てきたので、NetBSD/bin/shでコマンドを実行した場合のコマンド入力からexecve(2)までの流れをソースコードレベルで追いかけてみました。

NetBSDソースコードにおける/bin/shの場所

NetBSDソースコードは、ユーザランド/カーネル共に/usr/srcの下に置かれます。ユーザランドのコマンドは/usr/src/<コマンドのパス>の位置に用意されており、/bin/shの場合は/usr/src/bin/shソースコードのあるディレクトリになります。

$ which sh
/bin/sh
$ cd /usr/src/bin/sh
$ ls
CVS/            error.h         jobs.h          mktokens        redir.h
Makefile        eval.c          machdep.h       myhistedit.h    sh.1
TOUR            eval.h          mail.c          mystring.c      shell.h
USD.doc/        exec.c          mail.h          mystring.h      show.c
alias.c         exec.h          main.c          nodes.c.pat     show.h
alias.h         expand.c        main.h          nodetypes       syntax.c
arith.y         expand.h        memalloc.c      options.c       syntax.h
arith_lex.l     funcs/          memalloc.h      options.h       trap.c
bltin/          histedit.c      miscbltin.c     output.c        trap.h
builtins.def    init.h          miscbltin.h     output.h        var.c
cd.c            input.c         mkbuiltins      parser.c        var.h
cd.h            input.h         mkinit.sh       parser.h
error.c         jobs.c          mknodes.sh      redir.c

main()からexecve(2)までの流れ

/bin/shを実行し、ユーザがコマンドを入力・実行する場合は、以下の関数を通過しています(細々とした関数は除き、主要な関数のみ抽出しています)。

  • /usr/src/bin/sh/main.c:main()
    • /usr/src/bin/sh/main.c:cmdloop()
      • /usr/src/bin/sh/eval.c:evaltree()
        • /usr/src/bin/sh/eval.c:evalcommand()
          • /usr/src/bin/sh/jobs.c:forkchild()
          • /usr/src/bin/sh/exec.c:shellexec()
            • /usr/src/bin/sh/exec.c:tryexec()
              • execve(2)

main()から順に追いかけてみます。

main()

何やら不穏なコメントがありますが、単純に/bin/shを起動してキーボードからコマンドを入力する場合は、cmdloop()に入って行くようです。

/usr/src/bin/sh/main.c:
 95 /*
 96  * Main routine.  We initialize things, parse the arguments, execute
 97  * profiles if we're a login shell, and then call cmdloop to execute
 98  * commands.  The setjmp call sets up the location to jump to when an
 99  * exception occurs.  When an exception occurs the variable "state"
100  * is used to figure out how far we had gotten.
101  */
102
103 int
104 main(int argc, char **argv)
105 {
...中略...
218         if (sflag || minusc == NULL) {
219 state4: /* XXX ??? - why isn't this before the "if" statement */
220                 cmdloop(1);
221         }

cmdloop()では、255行目のparsecmd()の戻り値からEOFの入力の有無を判定し、EOFでなければevaltree()を呼んでいます。

/usr/src/bin/sh/main.c:
230 /*
231  * Read and execute commands.  "Top" is nonzero for the top level command
232  * loop; it turns on prompting if the shell is interactive.
233  */
234
235 void
236 cmdloop(int top)
237 {
...中略...
245         for (;;) {
...中略...
255                 n = parsecmd(inter);
256                 /* showtree(n); DEBUG */
257                 if (n == NEOF) {
258                         if (!top || numeof >= 50)
259                                 break;
260                         if (!stoppedjobs()) {
261                                 if (!Iflag)
262                                         break;
263                                 out2str("\nUse \"exit\" to leave shell.\n");
264                         }
265                         numeof++;
266                 } else if (n != NULL && nflag == 0) {
267                         job_warning = (job_warning == 2) ? 1 : 0;
268                         numeof = 0;
269                         evaltree(n, 0);
270                 }

単純なコマンド実行の場合、evaltree()内の301行のcase文が実行されます。

/usr/src/bin/sh/eval.c:
 214 /*
 215  * Evaluate a parse tree.  The value is left in the global variable
 216  * exitstatus.
 217  */
 218
 219 void
 220 evaltree(union node *n, int flags)
 221 {
...中略...
 235         switch (n->type) {
...中略...
 293         case NNOT:
...中略...
 297         case NPIPE:
...中略...
 301         case NCMD:
 302                 evalcommand(n, flags, NULL);
 303                 do_etest = !(flags & EV_TESTED);
 304                 break;

マクロ定数NCMDはnodes.hで定義されています。nodes.hはmake時に生成され、元になる定義はファイルnodetypesにあり、NCMDについては"a simple command"というコメントがあります。

nodetypesにはNCMDの他に論理演算子やパイプに関する定義があり、evaltree()内のcase文でNNOTやNPIPEをチェックしています。

$ grep NCMD *.h
nodes.h:#define NCMD 1
$ grep -A1 'create.*nodes\.h' my_make.log
#    create  sh/nodes.h
AWK=awk  SED=sed /bin/sh mknodes.sh nodetypes nodes.c.pat /home/fpig/work/sh
$ grep ^N nodetypes
NSEMI nbinary                   # two commands separated by a semicolon
NCMD ncmd                       # a simple command
NPIPE npipe                     # a pipeline
NREDIR nredir                   # redirection (of a complex command)
NBACKGND nredir                 # run command in background
NSUBSHELL nredir                # run command in a subshell
NAND nbinary                    # the && operator
NOR nbinary                     # the || operator
NIF nif                         # the if statement.  Elif clauses are handled
NWHILE nbinary                  # the while statement.  First child is the test
NUNTIL nbinary                  # the until statement
NFOR nfor                       # the for statement
NCASE ncase                     # a case statement
NCLIST nclist                   # a case
NDEFUN narg                     # define a function.  The "next" field contains
NARG narg                       # represents a word
NTO nfile                       # fd> fname
NCLOBBER nfile                  # fd>| fname
NFROM nfile                     # fd< fname
NFROMTO nfile                   # fd<> fname
NAPPEND nfile                   # fd>> fname
NTOFD ndup                      # fd<&dupfd
NFROMFD ndup                    # fd>&dupfd
NHERE nhere                     # fd<<\!
NXHERE nhere                    # fd<<!
NNOT nnot                       # ! command  (actually pipeline)

NCMDの場合はevalcommand()が呼ばれます。この関数の中でプロセスのフォークが行われます(vfork()が呼ばれています)。実行されるコマンドを追いかけたいので、858行目からの子プロセス側の処理を追って行きます。

/usr/src/bin/sh/eval.c:
 672 /*
 673  * Execute a simple command.
 674  */
 675
 676 STATIC void
 677 evalcommand(union node *cmd, int flgs, struct backcmd *backcmd)
 678 {
...中略...
 845                 if (cmdentry.cmdtype == CMDNORMAL) {
 846                         pid_t   pid;
 847
 848                         savelocalvars = localvars;
 849                         localvars = NULL;
 850                         vforked = 1;
 851                         switch (pid = vfork()) {
 852                         case -1:
 853                                 TRACE(("Vfork failed, errno=%d\n", errno));
 854                                 INTON;
 855                                 error("Cannot vfork");
 856                                 break;
 857                         case 0:
 858                                 /* Make sure that exceptions only unwind to
 859                                  * after the vfork(2)
 860                                  */
 861                                 if (setjmp(jmploc.loc)) {
 862                                         if (exception == EXSHELLPROC) {
 863                                                 /* We can't progress with the vfork,
 864                                                  * so, set vforked = 2 so the parent
 865                                                  * knows, and _exit();
 866                                                  */
 867                                                 vforked = 2;
 868                                                 _exit(0);
 869                                         } else {
 870                                                 _exit(exerrno);
 871                                         }
 872                                 }
 873                                 savehandler = handler;
 874                                 handler = &jmploc;
 875                                 listmklocal(varlist.list, VEXPORT | VNOFUNC);
 876                                 forkchild(jp, cmd, mode, vforked);
 877                                 break;
 878                         default:
 879                                 handler = savehandler;  /* restore from vfork(2) */

一見すると、子プロセス側の処理はforkchild()側で完結する(別プロセスがexecされる)ように見えますが、forkchild()は生成したプロセスに対するシグナルハンドリング処理などの便利関数となっていました。なので、今回はforkchild()の中身についてはスキップします。

/usr/src/bin/sh/jobs.c:
 891 void
 892 forkchild(struct job *jp, union node *n, int mode, int vforked)
 893 {
...中略...
 953 }

forkchild()から戻ってきた子プロセス側の処理は、vfork()の戻り値による親・子プロセスの判別を抜け、evalcommand()の916行目以降の処理に入って行きます。

ここからの処理では、シェル関数、組み込みコマンド、通常コマンドのいずれかにより処理が分かれます。今回は通常コマンドなので、1051行目以降の処理となります。

/usr/src/bin/sh/eval.c:
 676 STATIC void
 677 evalcommand(union node *cmd, int flgs, struct backcmd *backcmd)
 678 {
...中略...
 876                                 forkchild(jp, cmd, mode, vforked);
 877                                 break;
...中略...
 916         /* This is the child process if a fork occurred. */
 917         /* Execute the command. */
 918         switch (cmdentry.cmdtype) {
 919         case CMDFUNCTION:
 920 #ifdef DEBUG
 921                 trputs("Shell function:  ");  trargs(argv);
 922 #endif
...中略...
 973 #ifdef DEBUG
 974                 trputs("builtin command:  ");  trargs(argv);
 975 #endif
...中略...
1051         default:
1052 #ifdef DEBUG
1053                 trputs("normal command:  ");  trargs(argv);
1054 #endif
1055                 clearredir(vforked);
1056                 redirect(cmd->ncmd.redirect, vforked ? REDIR_VFORK : 0);
1057                 if (!vforked)
1058                         for (sp = varlist.list ; sp ; sp = sp->next)
1059                                 setvareq(sp->text, VEXPORT|VSTACK);
1060                 envp = environment();
1061                 shellexec(argv, envp, path, cmdentry.u.index, vforked);
1062                 break;
1063         }
1064         goto out;
1065
1066 parent: /* parent process gets here (if we forked) */

通常コマンドの場合はshellexec()が呼ばれ、ここからtryexec()が呼ばれます。

/usr/src/bin/sh/exec.c:
 116 /*
 117  * Exec a program.  Never returns.  If you change this routine, you may
 118  * have to change the find_command routine as well.
 119  */
 120
 121 void
 122 shellexec(char **argv, char **envp, const char *path, int idx, int vforked)
 123 {
 124         char *cmdname;
 125         int e;
 126
 127         if (strchr(argv[0], '/') != NULL) {
 128                 tryexec(argv[0], argv, envp, vforked);
 129                 e = errno;
 130         } else {
 131                 e = ENOENT;
 132                 while ((cmdname = padvance(&path, argv[0])) != NULL) {
 133                         if (--idx < 0 && pathopt == NULL) {
 134                                 tryexec(cmdname, argv, envp, vforked);
 135                                 if (errno != ENOENT && errno != ENOTDIR)
 136                                         e = errno;
 137                         }
 138                         stunalloc(cmdname);
 139                 }
 140         }
 141
 142         /* Map to POSIX errors */
 143         switch (e) {
 144         case EACCES:
 145                 exerrno = 126;
 146                 break;
 147         case ENOENT:
 148                 exerrno = 127;
 149                 break;
 150         default:
 151                 exerrno = 2;
 152                 break;
 153         }
 154         TRACE(("shellexec failed for %s, errno %d, vforked %d, suppressint      %d\n",
 155                 argv[0], e, vforked, suppressint ));
 156         exerror(EXEXEC, "%s: %s", argv[0], errmsg(e, E_EXEC));
 157         /* NOTREACHED */
 158 }

tryexec()内で晴れて(?)execve(2)が呼ばれる運びとなります。

/usr/src/bin/sh/exec.c:
 161 STATIC void
 162 tryexec(char *cmd, char **argv, char **envp, int vforked)
 163 {
...中略...
 169 #ifdef SYSV
 170         do {
 171                 execve(cmd, argv, envp);
 172         } while (errno == EINTR);
 173 #else
 174         execve(cmd, argv, envp);
 175 #endif

まとめ

NetBSD/bin/shでコマンドが実行されるまでの流れをソースコードレベルで追いかけてみました。今回は単純なコマンド実行だけでしたが、パイプやリダイレクトと組み合わせたコマンド実行の場合についても調べてみたいと思います。

pmml(MMLコンパイラ)によるMIDIファイル生成環境を構築してみた

pmml(MMLコンパイラ)によるMIDIファイル生成環境を構築してみた

ポケットミクちゃん本を読む会(その2)で使用するMIDIファイルを用意しておきたいと考え、MIDIファイルの生成環境を構築してみました。

以前開催した、やはり俺の耳コピはまちがっている。(その1) ゆるゆに-ゆるいUNIX勉強会-#6でもMIDIファイル生成環境の構築を行ったりしましたが、改めて備忘録として手順をまとめておこうと思います。

構築するMIDIファイル生成環境について

NetBSD-6.1-i386上に、pmmlとTiMidity++をインストールしてみます。pmmlはMML(Music Macro Language)コンパイラと呼ばれるツールで、MMLで記述した楽譜情報からMIDIファイルを生成します。TiMidityはソフトウェアMIDI音源で、pmmlで生成したMIDIファイルを再生するのに利用します。

PMMLをビルドする

まずはPMMLをビルドします。オリジナルのソースファイルはftp://ftp.u-aizu.ac.jp/u-aizu/pmml/にありますが、pmml-0.2p1.tar.gz(1998/06/17)以降は新しいバージョンがリリースされておらず、そのままの状態では現在の環境でビルドできません。

そのため、pmml-0.2p1.tar.gzをビルドが通るように修正したものを以下のリポジトリに用意しました。

cloneしてmakeするだけでビルドできます。筆者の環境ではNetBSD-6.1-i386Debian GNU/Linux 7.4 (wheezy)でビルドできることを確認できています。

$ git clone https://github.com/furandon-pig/pmml-0.2p1-fork-201405.git
$ cd pmml-0.2p1-fork-201405/

ビルドは単にmakeコマンドを実行するだけであり、./configure --prefix=...のような指定ができません(そもそも現状ではconfigure未対応です...)。インストール先を変更したい場合は、以下のようにMakefile中のLIBDIR,BINDIRを修正します。以下の例では/opt/pmml-0.2p1-fork-201405にインストールする設定に修正しています。

$ diff -u Makefile.orig Makefile
--- Makefile.orig       2014-05-14 15:32:04.000000000 +0900
+++ Makefile    2014-05-14 15:33:04.000000000 +0900
@@ -5,10 +5,10 @@
 #------ configuration parameters ------

 # The directory to which PMML library files are installed.
-LIBDIR  = /usr/local/lib/pmml
+LIBDIR  = /opt/pmml-0.2p1-fork-201405/lib/pmml

 # The directory to which executable files are installed.
-BINDIR  = /usr/local/bin
+BINDIR  = /opt/pmml-0.2p1-fork-201405/bin

 # Command name of the C compiler
 CC     = gcc

あとはmake,make installでビルドとインストールは完了です。

$ make 2>&1 | tee -a _make.log
$ sudo make install 2>&1 | tee -a _make_install.log

TiMidity++をビルドする

TiMidityについてもconfigure,make,make installで完了します。パッケージで提供されているソフトウェアですが、今回は自分でビルドしてみます。インストール先は/opt/TiMidity++-2.13.0としています。

$ cd work
$ wget http://ftp.jaist.ac.jp/pub/sourceforge/t/project/ti/timidity/TiMidity++/TiMidity++-2.13.0/TiMidity++-2.13.0.tar.gz
$ tar zxvf TiMidity++-2.13.0.tar.gz
$ cd TiMidity++-2.13.0
$ ./configure --prefix=/opt/TiMidity++-2.13.0 2>&1 | tee -a _configure.log
$ gmake 2>&1 | tee -a _make.log
$ sudo gmake install 2>&1 | tee -a _make_install.log

音源ファイルを用意する

TiMidityを使用するには、「音源ファイル」と呼ばれる、音色のファイル集が必要です。以下の手順で用意します。

$ cd work
$ wget ftp://ftp.iij.ad.jp/pub/linux/gentoo/distfiles/shominst-0409.zip
$ sudo mkdir -p /opt/TiMidity++-2.13.0/share/timidity
$ cd /opt/TiMidity++-2.13.0/share/timidity
$ sudo unzip /home/fpig/work/shominst-0409.zip  # ディレクトリ直下に展開されるので注意

timidity.cfg内のファイルパスを自分の環境に合わせて修正します。/opt/TiMidity++-2.13にインストールしたtimidiyがデフォルトで参照するファイルパスに一致するよう音源ファイルを置いています(このためTiMidity側での音源ファイルの場所設定は不要です)。

$ diff -u timidity.cfg.orig timidity.cfg
--- timidity.cfg.orig   1996-04-08 03:31:00.000000000 +0900
+++ timidity.cfg        2014-05-14 15:15:47.000000000 +0900
@@ -30,9 +30,9 @@
 #dir /usr/local/lib/timidity
 #

-dir /nethome/sak95/shom/lib/timidity/inst/GUS
-dir /nethome/sak95/shom/lib/timidity/inst
-dir /nethome/sak95/shom/lib/timidity/inst/test
+dir /opt/TiMidity++-2.13.0/share/timidity/inst/GUS
+dir /opt/TiMidity++-2.13.0/share/timidity/inst
+dir /opt/TiMidity++-2.13.0/share/timidity/inst/test

 bank 0
 source default.cfg

pmml,timidityコマンドのパスを設定する

pmml,timidyのコマンドを通常と異なる/opt以下にインストールしているため、環境変数PATHを設定します。

$ export PATH=$PATH:/opt/pmml-0.2p1-fork-201405/bin
$ export PATH=$PATH:/opt/TiMidity++-2.13.0/bin

それぞれのコマンドが見つかることを確認して設定完了です。

$ which pmml timidity
/opt/pmml-0.2p1-fork-201405/bin/pmml
/opt/TiMidity++-2.13.0/bin/timidity

MMLのサンプルからMIDIファイルを生成する

MMLコンパイル環境とMIDI再生環境が構築できたので、MMLのサンプルからMIDIファイルを生成してみます。 PMMLのexamplesディレクトリにMMLのサンプルがあるので、これを利用します。

"pmml <MMLファイル名>"でMMLコンパイルします。MIDIファイルは MMLファイルと同じディレクトリ に生成される点に注意してください(相対パスMMLファイルを指定すると「MIDIファイルが生成されない!」と早とちりしてしまいます...)

$ cd ~/work/pmml-0.2p1-fork-201405/examples
$ pmml menuet1.pml
$ ls
Makefile      grieg/        menuet1.mid   menuet2.pml   rand2.pml
etude1.pml    handel.pml    menuet1.pml   rand1.pml
$ file menuet1.mid
menuet1.mid: Standard MIDI data (format 1) using 3 tracks at 1/480

生成されたMIDIファイルをtimidityで再生してみます。

$ timidity menuet1.mid
Playing menuet1.mid
MIDI file: menuet1.mid
Format: 1  Tracks: 3  Divisions: 480
Track name: (rh)
Track name: (lh)

emacs+pmmlモードを使ってみる

PMMLの配布物にはpmml-mode.elというファイルが同梱されています。これを利用すると、emacs上でのMMLファイル作成が効率よく行えそうです。

pmml-mode.elを~/.emacs.dにコピーし、.emacsに以下の設定を追加します(最近のemacs.emacsがobsoleteだった気がする...)。

$ cp pmml-0.2p1-fork-201405/emacs/pmml-mode.el ~/.emacs.d/
$ cat <<_EOF | tail -a ~/.emacs
;; PMML mode setup
(setq pmml-player-command '("timidity" "-s16000"))
(require 'pmml-mode)
_EOF
$ emacs --version
GNU Emacs 24.3.1
...

上記設定後、emacsで.pmlファイルを開くと自動的にpmmlモードになります。"C-c C-f"と入力すると、現在開いている.pmlファイルのコンパイルと再生が行われます。

基本的には"C-c C-f"だけでも良さそうですが、"M-x describe-bindings"で利用可能なpmmlモードのキーバインディングを確認してみました。

Major Mode Bindings:
key      binding                   備考
---      -------                   ----
C-c C-c  pmml-record-finish        (筆者の環境では利用できず)
C-c C-f  pmml-play-file            pmlファイル全体を演奏する
C-c C-p  pmml-play-from-here-all   カーソル位置から最後まで演奏する
C-c C-q  pmml-stop                 演奏を停止する
C-c C-r  pmml-record               (筆者の環境では"Recording is not supported."と表示される)
C-c C-s  pmml-step-record          (筆者の環境では"Step recording is not supported."と表示される)
C-c C-t  pmml-show-track-summary   MIDIトラックのサマリ情報を表示する
C-c C-y  pmml-play-region-all      選択したリージョンを演奏する
C-c p    pmml-play-from-here-solo  カーソル位置のパートをソロで演奏する
C-c y    pmml-play-region-solo     選択したリージョンをソロで演奏する

MIDIファイルをWAVファイルに変換する

timidityのもう一つの機能として、MIDIファイルをWAVファイルに変換するというものがあります。"-Ow"でWAVファイルへの変換が行えます。

$ timidity -h
...中略...
Available output modes (-O, --output-mode option):
  -Od          NetBSD audio device
  -Ow          RIFF WAVE file
  -Or          Raw waveform data
  -Ou          Sun audio file
  -Oa          AIFF file
  -Ol          List MIDI event
  -OM          MOD -> MIDI file conversion

Output format options (append to -O? option):
  `S'          stereo
  `M'          monophonic
  `s'          signed output
  `u'          unsigned output
  `1'          16-bit sample width
  `2'          24-bit sample width
  `8'          8-bit sample width
  `l'          linear encoding
  `U'          U-Law encoding
  `A'          A-Law encoding
  `x'          byte-swapped output

以下の例では、pmmlでMIDIファイルを生成し、そこからWAVファイルに変換しています。さらにMP3にエンコードするといった活用方法も可能です。

$ pmml menuet1.pml
$ timidity -Ow2 menuet1.mid
Playing menuet1.mid
MIDI file: menuet1.mid
Format: 1  Tracks: 3  Divisions: 480
Track name: (rh)
Track name: (lh)
Output menuet1.wav
Playing time: ~42 seconds
Notes cut: 0
Notes lost totally: 0
$ file menuet1.wav
menuet1.wav: RIFF (little-endian) data, WAVE audio, Microsoft PCM, 24 bit, stereo 44100 Hz

まとめ

pmmlとTiMidityのビルドとインストール手順をまとめてみました。ビルドが通るよう修正したpmmlのソースコードは以前から手元にあったのですが、HDDの奥深くに埋もれてしまうことが何度かあったため、GitHub上のリポジトリで管理することにしました。TiMidityについても、それ自体のインストールと音源ファイルの準備という若干ややこしい手順のため、pmmlと共に一連のインストール手順としてまとめてみました。

dtrace機能を有効にしたNetBSDインストールCDを作成してみた

dtrace機能を有効にしたNetBSDインストールCDを作成してみた

NetBSDのdtrace機能を試してみようと思い立ち、@akachochinさんのNetBSD dtrace格闘記その2を元にdtrace機能を有効にしたインストールCDの作成手順をまとめてみました。

インストールCD作成準備

@akachochinさんのblogでは、インストール済みのNetBSD環境に直接dtraceまわりのカーネルモジュールをインストールしています。私の環境ではdtraceを有効にしたNetBSDのインストールCDを作成したいため、まずはインストールCD作成用のNetBSD環境を準備します。

ソースコードを展開する

NetBSD-6.1.3-i386仮想マシンにインストールした後、ソースコードを展開します。

# mount /dev/cd0a /cdrom
# cd /cdrom/source/sets/
# ls -l *.tgz
-r--r--r--  1 root  wheel   43069713 Jan 19 20:15 gnusrc.tgz
-r--r--r--  1 root  wheel    7661899 Jan 19 20:16 sharesrc.tgz
-r--r--r--  1 root  wheel  203542266 Jan 19 20:14 src.tgz
-r--r--r--  1 root  wheel   40189162 Jan 19 20:15 syssrc.tgz
-r--r--r--  1 root  wheel  158393066 Jan 19 20:18 xsrc.tgz

ソースコードは/usr/src以下に展開します。

# for i in *.tgz; do tar zxvf $i -C /; done

カーネルコンフィグを修正してdtrace機能を有効化する

dtrace機能を有効にするためのカーネルコンフィグを追加します。INSECURE,KDTRACE_HOOKS,MODULARを有効にする必要がありますが、GENERICコンフィグではINSECURE,MODULARは既に有効になっていたため、KDTRACE_HOOKSのみ追加します。

# cd /usr/src/sys/
# diff -u arch/i386/conf/GENERIC.orig arch/i386/conf/GENERIC
--- arch/i386/conf/GENERIC.orig 2012-08-16 00:33:00.000000000 +0900
+++ arch/i386/conf/GENERIC      2014-04-13 02:23:38.000000000 +0900
@@ -74,6 +74,7 @@
 # Standard system options

 options        INSECURE        # disable kernel security levels - X needs this
+options                KDTRACE_HOOKS

 options        RTC_OFFSET=0    # hardware clock is this many mins. west of GMT
 options        NTP             # NTP phase/frequency locked loop

build.shを実行する

build.shを使用して一通りのビルドを行います。ビルドは時間がかかるので、以下のスクリプトを作成し、バッチ処理的にビルドを行うようにしました。

上記のスクリプトMac mini(2.4GHz Intel Core 2 Duo,メモリ 8GB)のVirtualBox上のNetBSDで走らせたところ、全てのビルドが完了するまで15時間程度かかりました。

ビルドが完了すると、以下の場所にdtrace機能が有効になったインストールCDが生成されます。

/usr/obj/distrib/i386/cdroms/installcd/NetBSD-6.1.3-i386.iso 

OSインストール後のdtrace機能を利用するための設定投入

インストールCDが作成できたので、このCDを使用してNetBSDを再度インストールします。インストール後、dtrace用にデバイスファイルを作成します(OSインストール後に一度行うだけで良い)。

# mkdir /dev/dtrace
# mknod /dev/dtrace/dtrace c dtrace 0

そして、OS起動時にdtraceに必要なカーネルモジュールをロードするよう設定します。もっとスマートな方法があると思うのですが、今回は/etc/rc.localに以下の処理を追加しました。

_MODULE="
solaris
dtrace
sdt
fbt
"
for i in ${_MODULE}
do
    echo "modload ${i}"
    modload ${i}
done

OSを再起動するとdtrace機能が利用可能になっています。

# dtrace -l | wc -l
  45326

dtrace機能を試してみる

NetBSDでdtraceが利用できるようになったので、例としてgetpid()システムコールをフックさせてみます。

"getpid"というキーワードにマッチする関数は、sys_getpid(),sys_getpid_with_ppid()の2つがあるようです。

# dtrace -l | grep getpid
21001        fbt            netbsd                        sys_getpid entry
21002        fbt            netbsd                        sys_getpid return
21003        fbt            netbsd              sys_getpid_with_ppid entry
21004        fbt            netbsd              sys_getpid_with_ppid return

どちらがgetpid()システムコールの実体か分からないので、"getpid"でマッチするようにしてみます。dtrace -nを実行すると、その端末内で出力待ちの状態になります。

# dtrace -n fbt:netbsd:*getpid*:entry
dtrace: description 'fbt:netbsd:*getpid*:entry' matched 2 probes

別の端末にて、getpid()を実行するサンプルプログラムを用意します。

# cat _getpid.c
#include <stdio.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
        printf("%d\n", getpid());
        return 0;
}
# gcc -Wall -Werror -g -o _getpid _getpid.c

サンプルプログラムを実行すると、dtraceを実行している端末に以下が出力されます。

# dtrace -n fbt:netbsd:*getpid*:entry
dtrace: description 'fbt:netbsd:*getpid*:entry' matched 2 probes
CPU     ID                    FUNCTION:NAME
  0  21003       sys_getpid_with_ppid:entry
  0  21003       sys_getpid_with_ppid:entry

getpid()の実体はsys_getpid_with_ppid()のようです。プロセスの生成時にもsys_getpid_with_ppid()は呼ばれるようで、出力が2回になっています。

# cat -n /usr/src/sys/kern/kern_prot.c
    81  /* ARGSUSED */
    82  int
    83  sys_getpid_with_ppid(struct lwp *l, const void *v, register_t *retval)
    84  {
    85          struct proc *p = l->l_proc;
    86
    87          retval[0] = p->p_pid;
    88          retval[1] = p->p_ppid;
    89          return (0);
    90  }

まとめ

NetBSDでdtrace機能を試してみました。デフォルトインストールではdtrace機能が有効で無いため、カーネルカーネルモジュールの構築が必要です。インストールCDが欲しかったので、備忘録を兼ねた一通りのビルド手順についてもまとめてみました。

ポケットミクでテキスト読み上げ

ポケットミクでテキスト読み上げ

学研から発売された「歌うキーボード ポケットミク」を動かしてみました。

カーボンキーボードで演奏すると、初音ミクが「どーれーみー」という感じで歌ってくれます。また、MIDIデバイスとして利用でき、例えばNetBSDでは以下のdmesgが得られます(NetBSD-6.1-i386の場合です)。

Apr 29 07:12:32 hayase /netbsd: uaudio0 at uhub3 port 3 configuration 1 interface 0
Apr 29 07:12:32 hayase /netbsd: uaudio0: NSX-39 NSX-39, rev 2.00/1.00, addr 2
Apr 29 07:12:32 hayase /netbsd: uaudio0: audio descriptors make no sense, error=4
Apr 29 07:12:32 hayase /netbsd: umidi0 at uhub3 port 3 configuration 1 interface 1
Apr 29 07:12:32 hayase /netbsd: umidi0: NSX-39 NSX-39, rev 2.00/1.00, addr 2
Apr 29 07:12:32 hayase /netbsd: umidi0: (genuine USB-MIDI)
Apr 29 07:12:32 hayase /netbsd: umidi0: out=1, in=1
Apr 29 07:12:32 hayase /netbsd: midi1 at umidi0: <0 >0 on umidi0

すばらしいことに、GitHub上にはRuby,Python等のスクリプトからポケットミクを操作するためのライブラリが有志によって公開されており、Rubyからポケットミクを操作できるpoket_mikuがあります。

pocket_mikuのインストール

私の環境では、以下の手順でpocket_mikuをインストールしました。

  1. Gemfileを作成する

以下の内容でGemfileを作成します。

source :rubygems
gem 'pocket_miku', :git => "git://github.com/toshia/pocket_miku.git"
  1. gemでインストールする
$ sudo gem193 install pocket_miku
  1. 簡単なサンプルを試す

以下のサンプルを実行すると、初音ミクが「ふぁー ぼー」としゃべります。

#!/usr/bin/env ruby193
# -*- coding: utf-8 -*-

require 'pocket_miku'

PocketMiku.sing '/dev/rmidi1' do
  tempo 240
  ふぁ 75; ぼ 82;
end

ポケットミクでテキストを読み上げてみる

さて、無事にスクリプトから初音ミクにおしゃべりさせることができたワケですが、もう一歩進めて、今度はテキストを読み上げさせてみます。

以下のように、初音ミクにしゃべらせたい音をスクリプトから指定するだけでOKなのですが、「日本語」→「にほんご」のように、平仮名に直して指定する必要があります。

  ふぁ 75; ぼ 82;

ここはサクっとChasenを使ってテキストを平仮名に直すという方法を試してみました。

Chasenによる形態素解析では、以下のような出力が得られます。読みの第一候補(第二カラム)を使うとうまく行きそうです。 (実はこのカラムの名称を知らず、Chasenマニュアルを読む羽目に...)

$ chasen
今日は良い天気です
今日  キョウ    今日    名詞-副詞可能
は    ハ        は      助詞-係助詞
良い  ヨイ      良い    形容詞-自立     形容詞・アウオ段        基本形
天気  テンキ    天気    名詞-一般
です  デス      です    助動詞  特殊・デス      基本形
EOS

スクリプトは以下においてあります。

以下の手順でスクリプトを実行します(現状「!」とかの記号が入ったテキストだとエラーがでます...)。

$ echo 今日は良い天気です | chasen > data.txt
$ ./miksay.rb data.txt

テキストを読み上げたものの...

ポケットミクによるテキスト読み上げまで行えたのですが、いざ発声を聞いてみると、あまりよく聞き取れません。 しばらく聞いていて気がついたのですが、発声の「間」を調節しないと聞き取りにくいようです。

「キョウハ ヨイ テンキデス」という発声だと良いのですが、「キョウハヨイテンキデス」と一気に読み上げると、思っている以上に単語の区切りが分かりにくいです。日々会話する時にはほとんど意識しないのですが、この「間」は重要なのですね。

まとめ

スクリプトからポケットミクをしゃべらせてみました。形態素解析ツールを活用することでうまいこと仮名の切り出しが行えました。テキスト読み上げについては、発声の「間」などの考慮が必要そうで、まだまだ改良の余地ありです。

追記

肝心なことを忘れていました。5月6日に「ポケットミクちゃんに何かいろいろさせる会(その1)」という会を開催する予定です。ご興味のある方はぜひご参加ください。

アセンブラ短歌を詠みつつブログを始めてみた

あけましておめでとうございます。

2014年の幕開けにあわせて、人もすなる日記といふものを始めようと思いたちました。

最初の日記は新年らしく(?)アセンブラ短歌を詠んでみることにしました。

アセンブリ短歌とは、http://kozos.jp/asm-tanka/で紹介されているように、機械語命令がちょうど五・七・五・七・七の並びにおさまるようにしたプログラムのことであります。

さっそく九九の表を出力するアセンブラ短歌を読んでみます。環境はNetBSD-6.1-i386です。
(数値を出力したかったのですが、ASCII文字にマッピングした文字を出力しています...)

コメントの5,7,5...の部分が五・七・五の切れ目になっており、このmain関数はアセンブラ短歌が2首入っています。

.globl  main
main:
        mov     $0x01, %eax     # 5
xloop:  mov     $0x01, %ebx
yloop:  push    %eax
        nop                     # 7
        imul    %ebx
        add     $0x1f, %eax     # 5
        call    my_putchar
        mov     $0x20, %al      # 7
        call    my_putchar
        pop     %eax
        inc     %ebx            # 7
        cmp     $0x0a, %ebx
        jnz     yloop           # 5
ybreak: inc     %eax
        cmp     $0x0a, %al
        jz      xbreak
        push    %eax
        nop                     # 7
        mov     $0xa, %eax      # 5
        call    my_putchar
        pop     %eax
        nop                     # 7
        jmp     xloop
xbreak: nop
        nop
        nop
        nop
        ret                     # 7

.globl  my_putchar
my_putchar:
        sub     $0x10, %esp
        mov     %eax,(%esp)
        mov     %esp,%eax
        movl    $0x1, 0xc(%esp)
        movl    %eax, 0x8(%esp)
        movl    $0x1, 0x4(%esp)
        mov     $0x4, %ax
        int     $0x80
        add     $0x10, %esp
        ret

コンパイルと実行結果は以下のようになります。

$ gcc -Wall -Werror -g kuku.s
$ ./a.out
  ! " # $ % & ' (
! # % ' ) + - / 1
" % ( + . 1 4 7 :
# ' + / 3 7 ; ? C
$ ) . 3 8 = B G L
% + 1 7 = C I O U
& - 4 ; B I P W ^
' / 7 ? G O W _ g
( 1 : C L U ^ g p

1首目の短歌を逆アセンブルして見てみます。五・七・五・七・七のリズムで機械語命令を区切ることができます。

$ objdump -d a.out | grep -A20 '<main>:'
080486a0 <main>:
 80486a0:       b8 01 00 00 00          mov    $0x1,%eax

080486a5 <xloop>:
 80486a5:       bb 01 00 00 00          mov    $0x1,%ebx

080486aa <yloop>:
 80486aa:       50                      push   %eax
 80486ab:       90                      nop
 80486ac:       f7 eb                   imul   %ebx
 80486ae:       83 c0 1f                add    $0x1f,%eax
 80486b1:       e8 28 00 00 00          call   80486de <my_putchar>
 80486b6:       b0 20                   mov    $0x20,%al
 80486b8:       e8 21 00 00 00          call   80486de <my_putchar>
 80486bd:       58                      pop    %eax
 80486be:       43                      inc    %ebx
 80486bf:       83 fb 0a                cmp    $0xa,%ebx
 80486c2:       75 e6                   jne    80486aa <yloop>

080486c4 <ybreak>:
 80486c4:       40                      inc    %eax

アセンブラ短歌の詠み方としては、以下のような流れになります。

  1. アセンブリ言語でプログラムを書く
  2. アセンブルして動かしてみる(この段階では五・七・五は考えない)
  3. objdump -dで逆アセンブルし、五・七・五...になるよう調整する

x86アーキテクチャでは、意外と最初から五・七・五...になっていることも多いです。
機械語命令がうまいこと五・七・五...にならない場合、以下の方法で命令数を調整します。

  • nop(1byte命令)で埋める
  • movで代入するレジスタをeax→al等にして命令数を増減させる
  • 機械語命令の順番を入れ替える(入れ替えても問題がない場合のみ)

これ他にも機械語命令数の調整方法はあるかと思います。筆者は上記の3パターンで何とかアセンブラ短歌を詠めています。

皆様も新年はアセンブラ短歌で(プログラムの)書き初めをしてみてはいかがでしょうか。

【1/4追記】環境の記述が抜けていたので追記しました。