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を使いたくなる場面だな。

etc

Bourne Shell 自習テキスト

UNIXのバックグラウンド

UNIX の楽しみ

標準出力、標準エラー出力をファイル、画面それぞれに出力する方法