pty
前回の『バックパッカーズ読本』を読了。ふと発行日を見たら、2014年7月だった。図書館の新刊コーナーに置いてあったので、少なくとも今年に発行されたものと思っていた。 きっと、旅に出たい人がおねだりしたんだろうね。
巻末に、便利な連絡先とかが載ってたけど、新興国で代謝が激しいと、いつまでも有効って事は 無いんじゃないかな。逆に言えば、4-5年たっても変わらない所は、老舗として信用してもいいかも。
オイラー、この間から勝手気ままにソースの海を漂っている。参考資料は、20世紀の最後に、技評が発行した、『Linuxシステムコール』って本。21世紀になって直ぐに購入した。
前書きに、システムコールの説明をLinuxに絞って、実例プログラムと共に解説しましたって、出てた。オイラーはOpenBSD Loveにも関わらず、参考になってる。
そう、細かい事はいいのよ。考え方が取り込めれば。後は、しっかりした今通用する辞書があるから。BSD方言とリナ方言なんて、英語と米語の違いぐらいでしょ。
最近は、由緒正しき英語より通じればそれでOKってのが、席巻してるな。まあ、それでもいいけど。
んな事で、早速ソースの海を泳いでいきます。manと言う素敵な辞書を携えてね。 ああ、新興と言えばgolangがあるけど、manは不親切。C語は歴史がある分、膨大な資料があって、古典研究には向いてるよ。
tty
仮想端末を攻めたいんだけど、その入り口として、コマンドttyを調べてみる。汝の端末名を教えろってやつだ。
ob6$ tty /dev/ttyp1
このコマンドは、中どうなってるの?
char *t; t = ttyname(STDIN_FILENO); puts(t);
要約すれば、これだけ。STDIN_FILENOって何? cppの力を借りてもいいんだけど、今回は正攻法で行ってみる。
ob6$ grep STDIN_FILENO /usr/include/*.h /usr/include/unistd.h:#define STDIN_FILENO 0 /* standard input file descriptor */
0,1,2なんてのは、オイラー的には常識番号になっているぞ。で、ふと疑問。STDOUTとかを指定したらどういう返答を返すかな?
(gdb) set foo = ttyname(0) (gdb) p foo $1 = 0x54f4e290 <error: Cannot access memory at address 0x54f4e290>
どうもOpenBSDのgdbでは、上手く扱えないようなので、臨機応変に、うぶに移動します。
sakae@usvr:/tmp$ tty /dev/pts/1
(gdb) p ttyname(0) $1 = 0x555555756670 "/dev/pts/1" (gdb) p ttyname(1) $2 = 0x555555756670 "/dev/pts/1" (gdb) p ttyname(2) $3 = 0x555555756670 "/dev/pts/1" (gdb) p ttyname(3) $4 = 0x0
ふーん、3つ組が、一つのデバイス名として扱われるのね。割り当てられていないfdに対してはNULLって応答になるんだな。そしてこのデータって、/proc/pid/fd/ 以下に出て来るやつじゃん。
pty
折角、うぶに居るから、ptyの辞書を引いてみたよ。
read by the process that is connected to the master end. Pseudotermi‐ nals are used by applications such as network login services (ssh(1), rlogin(1), telnet(1)), terminal emulators such as xterm(1), script(1), screen(1), tmux(1), unbuffer(1), and expect(1).
ptyの使い処が説明されてた。OpenBSDには、こういう説明無かったぞ。複数辞書を引くと、知見が広まりますなあ。
再びOpenBSDに戻ってptyを引くと、大本は、/dev/ptmに対して、TPMGET っていうioctl(2)を発行する事で得られるとな。普通は、openptyを呼ぶことで、ユーザーが使うfdを得る事が出来ると説明されてた。
openptyを引くと、お仲間にlogin_ttyなんてのが列挙されてた。何処かで見たな。えーっと、gettyだったか。で、TPMGETをioctlする代わりに、getpmfd()も提供されてるとな。
一般ユーザーが使うには、openptyか。どんなアプリで使われているか調べてみる。
ob6$ cd /usr/src ob6$ find usr.bin -name '*.c' | xargs grep -l openpty usr.bin/script/script.c usr.bin/ssh/sshpty.c usr.bin/vi/ex/ex_script.c ob6$ find usr.sbin -name '*.c' | xargs grep -l openpty usr.sbin/pppd/sys-bsd.c usr.sbin/vmd/vmd.c ob6$ find bin -name '*.c' | xargs grep -l openpty ob6$ find sbin -name '*.c' | xargs grep -l openpty sbin/ldattach/ldattach.c
script
openptyの実例を、以前やったscriptで追っかけてみる。
if (isatty(0)) { if (tcgetattr(STDIN_FILENO, &tt) == 0 && ioctl(STDIN_FILENO, TIOCGWINSZ, &win) == 0) istty = 1; } if (openpty(&master, &slave, NULL, &tt, &win) == -1) err(1, "openpty");
これが、masterとslaveデバイスをオープンする部分。masterもslaveも型は、ただのintだ。
cc = read(master, obuf, sizeof (obuf)); : ssize_t n = write(STDOUT_FILENO, obuf + off, cc - off);
これが大雑把なmasterの使われ方。親プロセス側でmasterが使われている。masterから読み出すって事は、対向するslave側からの出力って事だ。読みだしたデータは、親側の標準出力(ログファイル)に書き出している。すなわち、子プロセスの出力をログしてる事になる。
一方、子プロセスはどうなっているかと言うと、
login_tty(slave); : execl(shell, shell, "-i", (char *)NULL);
login_ttyが肝になりそう。manによると
The login_tty() function prepares for a login on the tty fd (which may be a real tty device, or the slave of a pseudo-tty as returned by openpty()) by creating a new session, making fd the controlling terminal for the current process, setting fd to be the standard input, output, and error streams of the current process, and closing fd.
login_tty()
説明だけじゃあれなので、近くまで行ってみる。#include <util.h>と言う案内表示が出ていたので、普通のlibcとかじゃないだろうと見当を付ける。裏を取る為、ldd scriptする。そして、/usr/lib/libutil.so.13.0を発見。もう、rootが確定したね。
ob6$ cd /usr/src/lib/libutil/ ob6$ grep login_tty *.[ch] login_tty.c:/* $OpenBSD: login_tty.c,v 1.9 2014/06/30 00:26:22 deraadt Exp $ */ login_tty.c:login_tty(int fd) pty.c: login_tty(slave); util.h:int login_tty(int);
login_tty.cの中
int login_tty(int fd) { (void) setsid(); if (ioctl(fd, TIOCSCTTY, (char *)NULL) == -1) return (-1); (void) dup2(fd, STDIN_FILENO); (void) dup2(fd, STDOUT_FILENO); (void) dup2(fd, STDERR_FILENO); if (fd > STDERR_FILENO) (void) close(fd); return (0); }
セッションリーダーになる指令を出して、ちゃんと成れたか確認。それからdup2でお馴染みのやつを作っているのか。
master/slave
再び、scriptに戻る。
if (openpty(&master, &slave, NULL, &tt, &win) == -1) err(1, "openpty"); (void)printf("Script started, output file is %s\n", fname); (void)printf("master= %s\n", ttyname(master)); (void)printf("slave= %s\n", ttyname(slave));
script.cをちょいと改変して、マスターとスレーブの名前を出すようにしてみる。
ob6$ ./a.out Script started, output file is typescript master= /dev/ptyp2 slave= /dev/ttyp2 ob6$ tty /dev/ttyp2 ob6$ exit
ほー、manに書いてある通りだ。マスターがptyとな。
(gdb) p tt $5 = { c_iflag = 11010, c_oflag = 7, c_cflag = 19200, c_lflag = 1483, c_cc = "\004\377\377\177\027\025\022\377\003\034\032\031\021\023\026\017\001\0 00\377\377", c_ispeed = 115200, c_ospeed = 115200 } (gdb) p win $6 = { ws_row = 0, ws_col = 0, ws_xpixel = 0, ws_ypixel = 0 }
termios怖くない。続いて、openptyに突入
at /usr/src/lib/libutil/pty.c:58
53│ openpty(int *amaster, int *aslave, char *name, struct termios *termp, 54│ struct winsize *winp) 55│ { 56│ int ptmfd; 57│ 58├───────> if ((ptmfd = getptmfd()) == -1) 59│ return (-1); 60│ if (fdopenpty(ptmfd, amaster, aslave, name, termp, winp) == -1) { 61│ close(ptmfd); 62│ return (-1); 63│ } 64│ close(ptmfd); 65│ return (0); 66│ }
getptmfd()が肝になってるな。
47│ getptmfd(void) 48│ { 49├───────> return (open(PATH_PTMDEV, O_RDWR|O_CLOEXEC)); 50│ }
openに渡すフラグが意味深
O_RDWR Open for reading and writing. O_CLOEXEC Set FD_CLOEXEC (the close-on-exec flag) on the new file descriptor.
読み書き可能でopenしてるよ。
(gdb) s getptmfd () at /usr/src/lib/libutil/pty.c:49 (gdb) s _libc_open_cancel (path=0xb4d9983dfb4 "/dev/ptm", flags=65538) at /usr/src/lib/libc/sys/w_open.c:29 : (gdb) p ptmfd $9 = 9
/dev/ptm ってデバイスが出て来た。
そして、先に進めると、fdopenptyの中で、ioctlして、ptmの構造体が得られる。
(gdb) p ptm $10 = { cfd = 10, sfd = 11, cn = "/dev/ptyp5\000\000\000\000\000", sn = "/dev/ttyp5\000\000\000\000\000" }
この時点で、ユーザーが見てもいい、デバイス名が得られている。
ptm(4)から
The standard way to allocate pty devices is through openpty(3), a function which internally uses a PTMGET ioctl(2) call on the /dev/ptm device. The PTMGET command allocates a free pseudo terminal, changes its ownership to the caller, revokes the access privileges for all previous users, opens the file descriptors for the master and slave devices and returns them to the caller in struct ptmget. struct ptmget { int cfd; int sfd; char cn[16]; char sn[16]; }; The cfd and sfd fields are the file descriptors for the controlling and slave terminals. The cn and sn fields are the file names of the controlling and slave devices.
放浪の旅で、有名観光地を訪れ、ガイド本と同じ光景に出会って、感激してる図だな。まあ、徒歩走破が尊いわけですよ。と、自分を納得させています。
大本は、 kern/tty_pty.c/ptmioctl あたりと関係するのかな?
0,1,2 .. n
#include <unistd.h> #include <stdio.h> int main(){ write(0, "Hello", 5); return puts(""); }
案内本の低水準ファイル入出力に載っていた例。Hello って言う、5文字を出力するよ。システムコールのwriteを使ってね。次の行のputsで空行を出力してるのは、単に改行させたかったから。深い意味は無い。
で、実行すると、当たり前の結果となった。
sakae@fb:/tmp % ./a.out Hello
debian:work$ ./a.out Hello debian:work$ echo $? 1
一応、Debian様でも確認。終了ステータスが1になってるのは、ご愛敬(putsした文字数が出て来ただけ)。
本当か? オイラーが本からコピーする時、手が滑って、writeの第一引数を1とすべき所を0にしちゃったんだけどな(本当は確信犯、愉快犯ですが)。これが何を意味するか?
第一引数は、ファイルディスクリプタである。ファイル名を数字で呼ぶって習慣に則って、openを実行すると得られるものだ。
これが0って、知ってる人は知っている。知らない人は、STDIN_FILENO もっと言うと、標準入力。普通の人は、標準出力に向かってwriteするわな。
これおかしくないかい? 標準入力に向かって書き込んで(表示させたくて)も、エラーにならず、期待通り?に動いた。
#include <unistd.h> #include <stdio.h> int main(){ char buf[128]; read(1, buf, 3); write(1, buf, 3); return puts(""); }
今度は、標準出力から3文字読み込んで、それを出力してみる。
ob6$ ./a.out abc abc ob6$ ob6$ ./a.out 12345 123 ob6$ 45 ksh: 45: not found
一回目は、丁度3文字入力した。3文字出力された。二回目の実行では、5文字入力。出力は3文字するってプログラミングしてるので、3文字出力された。興味深いのは、余った2文字分。shellに渡されて、リダイレクト入力とみなされて、45ってコマンドを実行した。(勿論エラーだけど)
低水準IOを使えば、shell上の約束を反故に出来るって事だね。まあ、login_tty()の所で、ディスクリプタ 0,1,2 の指示の仕方を見れば、当然予想出来る事だけど。
#include <unistd.h> #include <stdio.h> #include <fcntl.h> #include <stdlib.h> int main(){ int fd; char *buf = "hello"; fd = open("hoge", O_RDONLY); write(fd, buf, 3); perror("Write: "); }
今度はhogeなんていうファイルを作っておいて、実験。writeで怒られるはずだから、その言い分を聞くべく、先回りしておく。
ob6$ ./a.out Write: : Bad file descriptor
期待通りの怒られる方をした。O_RDONLY を O_RDWR に変えれば、3文字分、hogeってファイルに書かれるよ。(標準出力に出て来ると早とちりしたのは誰や?)
と、言う事で、オイラーの長年の謎、標準入出力とエラーは、どうやってセットアップされてるかが分かりました。よかった、よかった。
pty in kernel side
gdbと言う便利な乗り物があると、容易にカーネルの世界に入っていける。kern/tty_pty.c の中で、関係ありそうな関数にBPを置いて、ユーザー界のscriptコマンドを実行。
要求が来た。openpty()を実行する為の元ネタの要求。
(gdb) bt 5 #0 ptmioctl (dev=20736, cmd=1076392961, data=0xf371d2c4 "", flag=3, p=0xd1772c88) at /usr/src/sys/kern/tty_pty.c:1056 #1 0xd08f1061 in spec_ioctl (v=0xf371d19c) at /usr/src/sys/kern/spec_vnops.c:370 #2 0xd09d8df0 in VOP_IOCTL (vp=0xd167f8ec, command=1076392961, data=0xf371d2c4, fflag=3, cred=0xd178acc0, p=0xd1772c88) at /usr/src/sys/kern/vfs_vops.c:289 #3 0xd07898ba in vn_ioctl (fp=0xd1769d70, com=1076392961, data=0xf371d2c4 "", p=0xd1772c88) at /usr/src/sys/kern/vfs_vnops.c:488 #4 0xd03c8ffd in sys_ioctl (p=0xd1772c88, v=0xf371d400, retval=0xf371d420) at /usr/src/sys/kern/sys_generic.c:517 (More stack frames follow...)
少しステップ実行すると、
1069│ switch (cmd) { 1070│ case PTMGET: 1071│ fdplock(fdp); 1072│ /* Grab two filedescriptors. */ 1073├───────────────> if ((error = falloc(p, 0, &cfp, &cindx)) != 0) { 1074│ fdpunlock(fdp); 1075│ break; 1076│ } 1077│ if ((error = falloc(p, 0, &sfp, &sindx)) != 0) { 1078│ fdremove(fdp, cindx); 1079│ closef(cfp, p); 1080│ fdpunlock(fdp); 1081│ break;
こんな所にやってくる。ptyデバイス用に、マスターとスレーブの元を用意してる。ここを 通ると、ユーザー用のパーミションを設定したりしてる。
falloc(9)
These functions provide the interface for the UNIX file descriptors. File descriptors can be used to access vnodes (see vnode(9)), sockets (see socket(2)), pipes (see pipe(2)), kqueues (see kqueue(2)), and various special purpose communication endpoints. A new file and a file descriptor for it are allocated with the function falloc(). The flags argument can be used to set the UF_EXCLOSE flag on the new descriptor. The larval file and fd are returned via resultfp and resultfd, which must not be NULL. falloc() initializes the new file to have a reference count of two: one for the reference from the file descriptor table and one for the caller to release with FRELE() when it's done initializing it.
kern/kern_descrip.cもも見ておくといいよって言われた。開いてみたら、sys_dupなんてのが 出て来たぞ。fallocのコードも出て来た。芋づる式に見どころが出てくるなあ。
expect
openptyの用例として、expectなんてのも有るそうだ。ちょいと探りを入れてみる。 FreeBSDにちゃんとした、expectが有った。会話形式の手助けをやってくれるのね。
下記は、ssh でローカルホストに接続し、Passwordって質問されたら、自動応答。Last loginって返ってきたら、ログイン成功なんで、lsコマンドを実行。後はユーザーに任せるよってやつ。
#!/usr/local/bin/expect set timeout 5 spawn ssh localhost expect "Password" send "YOUR password\r" expect "Last login" send "ls\r" interact
確かに、ptyを使いたくなる場面だな。