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()
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-i386とDebian 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をインストールしました。
- Gemfileを作成する
以下の内容でGemfileを作成します。
source :rubygems gem 'pocket_miku', :git => "git://github.com/toshia/pocket_miku.git"
- gemでインストールする
$ sudo gem193 install pocket_miku
- 簡単なサンプルを試す
以下のサンプルを実行すると、初音ミクが「ふぁー ぼー」としゃべります。
#!/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
アセンブラ短歌の詠み方としては、以下のような流れになります。
x86アーキテクチャでは、意外と最初から五・七・五...になっていることも多いです。
機械語命令がうまいこと五・七・五...にならない場合、以下の方法で命令数を調整します。
これ他にも機械語命令数の調整方法はあるかと思います。筆者は上記の3パターンで何とかアセンブラ短歌を詠めています。
皆様も新年はアセンブラ短歌で(プログラムの)書き初めをしてみてはいかがでしょうか。
【1/4追記】環境の記述が抜けていたので追記しました。