shutdown -p +5
もうすぐ10月、値上げの季節。
オイラーに取って痛いのは煙草の値上げ。オイラーの銘柄は一挙に40円も上がる。困ったものだ。
女房は、これを機に縁を切れと迫る。そんな女房への反論。
酒と煙草は神様がくれた適齢まで生きる為の薬
誰かの小説に出てきた言葉だ。これいいね。もう何十年も前に亡くなった、おじいちゃんを思い出す。昔かたぎの職人さん。酒は飲む分だけを近くの酒屋まで買いに行くのがオイラーのおつかいだった。行って来ると、必ず駄賃が貰えた。可愛い?孫に駄賃をあげたくて、おつかいをさせてた節があるな。
そして煙草は、煙管できざみ煙草をやってた。死ぬまでね。ってか、亡くなる寸前に、煙草をしなかったものだから、今日はしないの?って聞いたんだ。そしたら、もういいと。そして、数日後に亡くなった。薬が切れたんで、おさらばしたんだね。誰にも、迷惑かける事なく逝った。 オイラーもこうありたいものだ。ピンコロね。
2週間ぐらい前から、密かに煙草の買い占め運動を実施中。一挙に大量購入すると騒がれますからね。広く薄く購入です。
10個入れのカートンで買うんだけど、店によっては在庫切れだったりして、やる気あるのかね? 商機だのにね。ローソンは、どの店でも在庫豊富、だまっていても、ライターをバックしてくれる。優良店舗であった。
賞味期限が半年という事なんで、6カートンまで備蓄しようかと思ったけど、そこまで頑張らなくてもいいかなと思って、程ほどの所で止めた。
wallは続くよどこまでも
それって、万里の長城の事? それとも、長大な壁を作って後世まで名を残したい、どこかの大統領の事?
いいえ、前回からの続き、wallの解析。第一次調査隊は、荒い調査を完了してるんで、今回は細部を調査します。
まず、makemsgを見る。引数は、wall起動時に与えたメッセージファイル。
183│ snprintf(tmpname, sizeof(tmpname), "%s/wall.XXXXXXXXXX", _PATH_TMP) 184│ if ((fd = mkstemp(tmpname)) >= 0) { 185├───────────────> (void)unlink(tmpname); 186│ fp = fdopen(fd, "r+"); 187│ }
一時ファイルの名前を生成して、作って、消して、再度オープンして、ファイルディスクリプタを得ている。 出来上がったファイルの外観は
ob6$ ls -l /tmp/wall.ksky99AYqo -rw------- 1 sakae wheel 0 Sep 20 14:31 /tmp/wall.ksky99AYqo
直ぐに消されてしまうので、悪い人に付け入る隙を与えない。これが、OpenBSD流?
そして、ヘッダーを付けて、指定したファイルからのデータを読み込んで、表示すべきデータを mbufというメモリー上のバッファーに残す。
(gdb) p mbuf $16 = 0x39797f4e800 "\r", ' ' <repeats 79 times>, "\r\nBroadcast Message from sa kae@ob6.localdomain", ' ' <repeats 35 times>, "\a\a\r\n (/dev/ttyp5) at 1 4:40 ... "...
が、メモリー上のデータ。このデータがttymsgに最終的に引き継がれる。(iovと言う構造体)
次は、utmpの読み出し。最初の方は結構無効になったエントリーがあるので、有効なやつを取り出している。
(gdb) p utmp $21 = { ut_line = "ttyp0\000\000", ut_name = "sakae", '\000' <repeats 26 times>, ut_host = "xxx.xxx.xxx.xxx", '\000' <repeats 242 times>, ut_time = 1537159604 }
これらの有効なデータを集めておく。そして、いよいよttymsgで端末毎に出力するルーチンへと入っていく。
Breakpoint 2, ttymsg (iov=0x7f7ffffe57c0, iovcnt=1, line=0x75b22f6ea70 "ttyp0",tmout=300) at ttymsg.c:67 (gdb) c Continuing. Broadcast Message from sakae@ob6.localdomain 8 (/dev/ttyp5) at 16:18 ... HOGE FUGA Breakpoint 2, ttymsg (iov=0x7f7ffffe57c0, iovcnt=1, line=0x75b22f6de80 "ttyC0",tmout=300) at ttymsg.c:67
先に流れをみちゃったけど、デバイス事に、呼び出しが行われている。最初は、自分が使っている端末。2回目は、コンソールからloginしてるroot向け。堂々とrootにも物申す事が出来るって、いい事だ。今の日本みたいに、人の顔色を見て黙っていようなんてないからね。
SLIST_FOREACH(un, &utmphead, next) { if ((p = ttymsg(&iov, 1, un->tty, 60*5)) != NULL) warnx("%s", p); }
これが、線形リスト(デバイス名による)をforeachで回る部分だ。呼ばれた方で見ると、適切な名前が付いているので、分かり易い。
ttymsgの中では、lineにデバイス名が入っているので、それを元に、正式だデバイス名を組み立ててから、openする。そして、そのデバイスに対して出力って寸法。
113│ wret = writev(fd, iov, iovcnt);
実際の出力は、writevっている、write系のシステムコールで行われている。
(gdb) p iovcnt $6 = 1 (gdb) p iov $8 = { iov_base = 0x1c53f7348800, iov_len = 489 }
出力するデータの持ち方が、ベースアドレスからlenバイト分って仕掛けになってる。実体はmakemsgg作ったもの。
ob6$ echo ' Give me more money' > /dev/ttyp0 Give me more money ob6$ echo ' Give me more money' > /dev/ttyC0 ksh: cannot create /dev/ttyC0: Permission denied
wallがやってる事は、上の実験と同じ。但し、コンソール宛てとかだと、パーミションが無いので、 怒られる。そんな場合でも、wallを通せば、よしなに計らってくれるぞ。
shutdown -p +5
何時もマシンを落とす時、shutdown -p now なんだけど、今回はこの記事を書くために、+5を設定してみた。
なぜ shutdownかって? 前回wallに隠しオプションを見つけたからね。
なお、このshutdownコマンドは、opraterグループになってるので、自分もこのグループに参加してると、わざわざroot権限を得る必要は無い。
リナはどうなってるか、確認しておくか。
debian:~$ ls -l /sbin/shutdown lrwxrwxrwx 1 root root 14 Jun 14 05:20 /sbin/shutdown -> /bin/systemctl* debian:~$ ls -l /bin/systemctl -rwxr-xr-x 1 root root 181876 Jun 14 05:20 /bin/systemctl*
オイラーには、この思想、受け入れがたいな。こんなに機能を集中させちゃうから、いつまでたっても安定しないんだろう。unixの思想、シンプルであれを放棄してる。まあ、MSをお手本にしてたら、自然にそうなるわな。昔の人はいい事を言ってたな。
朱に染まれば、赤くなる。
ob6$ shutdown -p +5 : *** System shutdown message from sakae@ob6.localdomain System going down in 30 seconds *** FINAL System shutdown message from sakae@ob6.localdomain System going down IMMEDIATELY System shutdown time has arrived
この間、5分、2分、1分、30秒って具合にせわしなくメッセージが表示されてたよ。そして、-pのおかげで、ぷちっと電源が切れた。
禁断の -n
shutdown コマンドに取りつかれるきっかけとなった、wall -n を試してみるか。
ob6$ echo hoge fuga | wall -n Broadcast Message from sakae@ob6.localdomain 8 (/dev/ttyp1) at 15:58 ... hoge fuga
ヘッダーが出てこない触れ込みなんだけど、普通にwallした時の同じ挙動だなあ。 スーパーユーザーの特権が無いと、機能しないのかな。なにせ、隠しオプションって事だけで 舞い上がってしまって、詳細を見ていなかった。
ob6$ doas sh -c "echo hoge fuga | wall -n" doas (sakae@ob6.localdomain) password: hoge fuga
やっぱり、特権が必要だったのね。Broadcast からの2行が省略されてる。 もう一度、wallの該当部分を見ておくか。
case 'n': /* undoc option for shutdown: suppress banner */ pw = getpwnam("nobody"); if (geteuid() == 0 || (pw && getuid() == pw->pw_uid)) nobanner = 1; break;
後ろの方で、!nobanner なら、って断りを入れてメッセージを作ってた。(バナーが付く)
このcaseの小説(小節)の中に、オイラーの知らない単語が出て来た。知らない単語(関数)は、その場で辞書(man)を引くのが、言語力向上の常識。前後の文脈から推測するなんて、オイラーには、まだ無理。大阪なおみちゃんの、プチ怪しい日本語がかわゆいな。前後の文脈はおろか、空気を読むのもこれからか。いや、今のままの方がいいぞ。
emacsを使ってたら、辞書を引くのは簡単。
(define-key global-map (kbd "C-c m") 'man)
を登録しておけば、知らない単語に照準を合わせて、C-c m するだけ。調べ終わったら、q で、辞書が閉じられる。vimはよう知らんけど、どうやるんだろう? vimでマニュアルを引く
getueidって単語を知らなかったぞ。(自慢になるか)
The geteuid() function returns the effective user ID of the calling process.
実効ユーザーがrootかnobodyならば、ヘッダーを省略出来るよと読むんだな。なんだか、読解力のテストを受けているみたいだな。
wallの使われ方
shutdownにはwallが組み込まれているとの事なんで、どんな風に組み込んでいるか、確認してみる。
pathnames.h なんてのが有って、それを先に見てと、野生の勘が働いたぞ。
#define _PATH_FASTBOOT "/fastboot" #define _PATH_HALT "/sbin/halt" #define _PATH_REBOOT "/sbin/reboot" #define _PATH_WALL "/usr/bin/wall" #define _PATH_RC "/etc/rc"
前回だか学習した通りの名前付けになってた。これが分かれば、一発検出出来るな。
shutdown.c/timewarn(time_t timeleft)の中でforkした子供の部分
/* wall(1)'s undocumented '-n' flag suppresses its banner. */ execle(_PATH_WALL, _PATH_WALL, "-n", (char *)NULL, restricted_environ);
ここにも、-nは 秘密のやつだからねと、公然の秘密を謳ってる。 execleって、exec族って事までは分かるんだけど、その変化形か。そろそろ、変化形も覚える時期だな。そうしないと、過去、現在、未来が区別出来ない、不思議な英語を喋っちゃう恐れがあります。
あれ? NULLの前で、文字ポインターにキャストしてる。これが正しい使い方か。移植性を高める方策かな? NULLのキャスト とか、5章 ヌルポインター
でも、これとほぼ同じやつは、前回使った。あの時は、execlまでて、最後にeが付いていなかった。今回は付いてる。eは環境じゃなかろうかって想像出来る。じっと目を凝らすと、NULLの後ろに、更に引数 restricted_environ が付いているじゃん。窮屈な環境って読める。
もうマシンを落とすフェーズに入っているので、窮屈結構って事なんだろうな。さて、ここまで想像しといて、答え合わせに辞書を引く。推測当たった。
所で、
static char *restricted_environ[] = { "PATH=" _PATH_STDPATH, NULL };
このSTDPATHって、どんなもの?
ob6$ cc -E shutdown.c | grep PATH "PATH=" "/usr/bin:/bin:/usr/sbin:/sbin:/usr/X11R6/bin:/usr/local/bin",
ふーん、ユーザーが追加した(かもしれない)怪しげなPATHを除外してるんだな。それにしても、PATHの最後に、/usr/local/binが入ってる。ここは、一応親分の監視の目が光っていない所。それでも、この場所を含めてくれているって事は、親分のお情けか。まあ、一番優先度が無い場所ってのが、それを物語っている。
gdb call の復習
さて、前回やったgdbから任意の関数を呼べるっていう機能の復習です。この機能、オイラーに取っちゃ、rubyで多用してた、irb を彷彿させるんで、これからC語の実験台として多用してこう。C語はコンパイルっていう手間がかかるとお嘆きのスクリプターには、必見のはずですから。
実験として、上で出て来た、wall -n の小節部分を取り上げる。
さすがにC語はソースをコンパイルする必要が有るので、下記のような叩き台を作って、gdbにかけられるようにしとく。
ob6$ cat base.c #include <stdio.h> char *nobody= "nobody"; int main(){ puts("Like irb!"); __asm__ __volatile__("int3"); // drop gdb return (0); }
パスワード関係の動きなんで、元データのユーザー名だけを、自力で書いてみた。(別に書かなくてもいい。ただ、宣言の仕方を復習しただけです)関数の外で ポインター宣言してるので、文字列部分はROM状態になってる。
なお、パスワード関係の関数は、一切このソースには表れていない。libcを含んだバイナリーが 欲しいという事だけだ。
ob6$ cc -g base.c ob6$ gdb -q a.out Reading symbols from a.out...done. (gdb) r Starting program: /tmp/a.out Like irb! Program received signal SIGTRAP, Trace/breakpoint trap. 0x00001e13c8f0054c in main () at base.c:7 9 __asm__ __volatile__("int3"); // drop gdb (gdb)
ここまでで、irbもどきの実験環境が整った。 そんじゃ、小節を辿ってみる。
(gdb) call getpwnam(nobody) $1 = (struct passwd *) 0x1e16504d2eb0 <_pw_passwd> (gdb) p *$1 $2 = {pw_name = 0x1e16504d2f00 <_pw_string> "nobody", pw_passwd = 0x1e16504d2f07 <_pw_string+7> "*", pw_uid = 32767, pw_gid = 32767, pw_change = 0, pw_class = 0x1e16504d2f09 <_pw_string+9> "", pw_gecos = 0x1e16504d2f0a <_pw_string+10> "Unprivileged user", pw_dir = 0x1e16504d2f1c <_pw_string+28> "/nonexistent", pw_shell = 0x1e16504d2f29 <_pw_string+41> "/sbin/nologin", pw_expire = 0}
関数とかユーザー名(の変数名)は、TABキーでちゃんと補完されるよ。得られたデータは、ちゃんと型が付いている。でも、見にくくねぇ。
(gdb) set print pretty (gdb) p *$1 $5 = { pw_name = 0x1e16504d2f00 <_pw_string> "nobody", pw_passwd = 0x1e16504d2f07 <_pw_string+7> "*", pw_uid = 32767, pw_gid = 32767, pw_change = 0, pw_class = 0x1e16504d2f09 <_pw_string+9> "", pw_gecos = 0x1e16504d2f0a <_pw_string+10> "Unprivileged user", pw_dir = 0x1e16504d2f1c <_pw_string+28> "/nonexistent", pw_shell = 0x1e16504d2f29 <_pw_string+41> "/sbin/nologin", pw_expire = 0 }
set print prettyで、見やすくなれと、gdbに指示した。毎回これをやるのが面倒なら、.gdbinitに書いておけばよい。なそ、$1とかは、gdbの自動変数と呼ばれるもので、得た結果を後で使う事が出来る。
(gdb) call geteuid() $6 = 1000 (gdb) call getuid() $7 = 1000 (gdb) p $1->pw_uid $8 = 32767
後は、流れに沿って、実行してみた。getuidの1000ってのは、誰かしら? 調べてみるか。 つたない経験では、パスワード関係の何かを知りたい時は、getpwホニャラ で、よさそう なので、gdbさんに聞いてみる。
(gdb) call getpw getpwent getpwent.name getpwnam_r getpwuid_internal getpwent..yppbuf getpwnam getpwnam_shadow getpwuid_r getpwent.c getpwnam_internal getpwuid getpwuid_shadow
TABで補完候補を出してみた。getpwuidかな?
(gdb) call getpwuid(1000) $6 = (struct passwd *) 0x871fda64eb0 <_pw_passwd> (gdb) p *$6 $7 = { pw_name = 0x871fda64f00 <_pw_string> "sakae", pw_passwd = 0x871fda64f06 <_pw_string+6> "*", pw_uid = 1000, pw_gid = 1000, pw_change = 0, pw_class = 0x871fda64f08 <_pw_string+8> "staff", pw_gecos = 0x871fda64f0e <_pw_string+14> "sakae", pw_dir = 0x871fda64f14 <_pw_string+20> "/home/sakae", pw_shell = 0x871fda64f20 <_pw_string+32> "/bin/ksh", pw_expire = 0 }
ビンゴでした。そんじゃ、rootさんの登録情報は?
(gdb) p *(getpwnam("root")) $11 = { pw_name = 0x871fda64f00 <_pw_string> "root", pw_passwd = 0x871fda64f05 <_pw_string+5> "*", pw_uid = 0, pw_gid = 0, pw_change = 0, pw_class = 0x871fda64f07 <_pw_string+7> "daemon", pw_gecos = 0x871fda64f0e <_pw_string+14> "Charlie &", pw_dir = 0x871fda64f18 <_pw_string+24> "/root", pw_shell = 0x871fda64f1e <_pw_string+30> "/bin/ksh", pw_expire = 0 } (gdb) call getpwnam("hogefuga") $12 = (struct passwd *) 0x0
ここまで出来れば、gdbはC語のirbと言ってもいいでしょう。
以上、実習終了。
+5
shutdownする時刻を、+5 って、指定させられちゃったけど(-5としたらエラーにされた)何でかな? head -20 とかと違う趣だ。ってな事で、ソースに当たってみる。毎度、重箱の隅でスマソ。
shutdown.c の中の、getoffset関数の中。引数は、上位から渡ってくる、tmiearg。
if (!strcasecmp(timearg, "now")) { /* now */ offset = 0; return; } (void)time(&now); if (timearg[0] == '+') { /* +minutes */ minutes = strtonum(timearg, 0, INT_MAX, &errstr); if (errstr) errx(1, "relative offset is %s: %s", errstr, timearg); offset = minutes * 60; shuttime = now + offset; return; } /* handle hh:mm by getting rid of the colon */ : /* case [yy][mm][dd][hh][MM] */ :
こんな形で、shutdownする時刻を決めていた。now じゃ無かったら、気分的には、now + NN minutes の積りなのね。だから、+5 とかになるんか。
それ以外は、hh:mm での指定、コロンが含まれていないと、dateの設定を行う時に使う形式として処理してる。柔軟だな。
strtonumは、頑丈な作りの文字列から数値への変換器。上の設定だと、0以上(long long)INT_MAX以内に収まる数値を期待。エラーが見つかったら、errstrに内容を返すとな。
ob6$ shutdown -p +123min shutdown: relative offset is invalid: +123min
これだけでは、便利な関数strtonumの挙動が分かりずらいので、gdbで確認する。特に注意して見ておきたい所は、BUGの温床になりやすい off by one 境界値付近の挙動。
これの確認の為に、auto変数、char *foo なんてのを上で挙げたbase.cに追加しておく。なぜ必要かと言うと、strtonum のエラーが載ってくる引数に、ポインターのアドレスを要求されるから。(C語で書くと、&foo となるんだけど、gdbでは、この&をダイナミックに宣言したものでは、受け付けてくれなかったため)
(gdb) p (sizeof (char *)) $1 = 8 (gdb) call/x malloc(8) $1 = 0x8ec1c44a8b0 (gdb) call strtonum("100", (long long)-100, (long long)100, &$1) Attempt to take address of value not located in memory.
ひょっとして、使ってはいけないアドレスを返してきた? いや、gdbは、そんな使い方を する事、想定していないのだろう。深くは追及しないで、本来の目的を遂行する。
(gdb) call strtonum("100", (long long)-100, (long long)100, &foo) $3 = 100 (gdb) p foo $4 = 0x0 (gdb) call strtonum("101", (long long)-100, (long long)100, &foo) $5 = 0 (gdb) p foo $6 = 0x8ec716687f0 "too large" (gdb) call strtonum("-101", (long long)-100, (long long)100, &foo) $7 = 0 (gdb) p foo $8 = 0x8ec71663b99 "too small" (gdb) call strtonum("-100", (long long)-100, (long long)100, &foo) $9 = -100 (gdb) p foo $10 = 0x0
これで挙動は分かった。後は安心して使うだけ。
(gdb) p (sizeof (long)) $12 = 8 (gdb) p (sizeof (long long)) $13 = 8 (gdb) call strtonum("-88", -100L, 100L, &foo) $14 = -88
引数をキャストしたけど、あらかじめサイズを調べておけば、数値をそれなりのサイズに指定出来るよ。最初、キャストするのを忘れていて、不可解な挙動を示されて、悩んだのは秘密だ。ちゃんと、タイプを見極めろとな。
(gdb) p/x &foo $16 = 0x7f7ffffca028 (gdb) p/x &onbss $17 = 0x8db70c01058 (gdb) p &onbss $18 = (char **) 0x8db70c01058 <onbss>
このあたりにstackが有るのか。そしてbss領域に割り当てたポインター。これを見ると、普通に一般アプリが使えそうな所だな。
まとめ
wallを中心に見ちゃったけど、shutdownコマンドってのは、wallが表舞台に立つように設計された、rebootやhalt等へのラッパーなのね。知らんかったわい。(マシンを落とすコマンドは、shutdownだけと思っていたのは、秘密だ)
wallをバイパスしたいなら、halt -p とか、poweroff なんてコマンドを叩くのが良い。
最後に、wallで、しつこくメッセージを出すのを抑制する方法を見ておく。
static const struct interval { time_t timeleft, timetowait; } tlist[] = { { 10 H, 5 H }, { 5 H, 3 H }, { 2 H, 1 H }, { 1 H, 30 M }, { 30 M, 10 M }, { 20 M, 10 M }, { 10 M, 5 M }, { 5 M, 3 M }, { 2 M, 1 M }, { 1 M, 30 S }, { 30 S, 30 S }, { 0, 0 } };
この制御リストを使って、最終shutdown時間までの待ち時間によって、案内を出す時間間隔を決めている。時間に余裕が有る時は、ゆったりした時間間隔、まぎわになるとせわしなくって寸法だ。人間心理をついて、嫌味にならない配慮だな。
お彼岸なので、実家に線香をあげに行った。その時義母が嘆いていた。
近くのスーパーのレジが改悪?され、支払いは機械に向かって自らやるとの事。端数を払おうとして、がま口の中を吟味してると、早く金かカードを入れろと、喚くそうな。それも、こっそりならいざ知らず、大声で。とっても恥ずかしく、焦ってしまって、いつも恐々だったそうだ。
年寄りを虐めるんじゃないぞ。対抗策の秘伝を伝授した。がま口の中の小銭は、構わずにガバーと突っ込んじゃいなさい。機械がいそいそと金勘定してら、おたおたすんじゃねぇーって、大声で叱りつけておやんなさい。
この方法、昔々、方々の国を渡り歩く出張時に編み出した。国によって皆硬貨がまちまち。しかも劣化してるのも混じってる。そんな状況で、さっと硬貨を取り出せる訳が無い。
そこでスッチー(死語だな)ご愛用のワレットの登場。ワレットをさっとレジしてる人に広げて、必要な分だけ、ピックアップして貰う。こういう支払をする人は珍しがれて、にこにこしながら清算出来たよ。何、小銭だから、ちょろまかされても、知れたものと腹を決めての所業だけどね。
ついでに、5円とか50円とか、穴の開いたコインを見せると、めずらしがって興味津々だったよ。