OpenBSD with qemu (2)

最近のTV番組はNHKに限らず、宣伝が多い。ってか、番組全体がコマーシャル。この間も、今コンビニの冷凍食品が熱いなんてタイトルで、各コンビニお勧めの冷凍品を紹介してた。

普段はコンビニなんて行かないから、事情に疎かったんだけど、何これ?高値安定のミニスーパーじゃん。

で、女房が目ざとく、 厚焼きスフレ パンケーキ なんてのをキャッチ。冷凍餃子とかピザは、スーパー品のお世話になるけど、このスフレはコンビニ限定?

早速、手に入れてこいと下命されましたよ。散歩のついでに偵察。1件目は冷凍品の棚が極小で無し。昔ながらの飲み物系が主体。2日目に行った所は、氷菓が充実してたけど、やはりお目当てな奴は無し。

女房に報告すると、有るのは東京だけかね。田舎には無いかもと諦めムード。3日目は、ちょいと遠出してみた。そしたら、置いてあったよ。売り切れ寸前で、残りが2個だった。買い占めてもよかったけど、試食と言う事で、1個だけ買った。この所の陽気で溶けるのが心配だったので、眼にも止まらぬ速さで帰還。

ご試食なさった女房曰く、驚く程旨いとは言えないと断。もう買わなくていいと購買中止宣言。鼻息荒く、これぐらいなら、自分で作れるわよとおっしゃる。

さて、どんな物を作るのやら、実験台になりますかな。

serial connection

前回は、ホストOSをOpenBSD(amd64)にして、ゲストOSをOpenBSD(i386)って環境を、qemuを使って構築した。色々やりたいんだけど、コンソール画面だとX Windowの為、切り貼りが出来ない。かと言って、ssh接続では、ちょっと心配な所が有るので、シリアル接続出来るようにしておく。

前回の起動スクリプトにちょっと、記述を加える。

qemu-system-i386 -m 128 -s  -serial pty  -net nic -net user,hostfwd=tcp::2022-:22 disk

何の事は無い、-serial pty を加えただけ。これで起動すると、

ob$ ./gdb-boot
char device redirected to /dev/ttyp4 (label serial0)

こんな風に、ホスト側は、/dev/ttyp4 を使って見と言ってくる。

次はゲスト側の設定。/etc/ttys ファイルを編集。

tty00   "/usr/libexec/getty std.9600"   vt100 on

元々は、offなってるので、それをonに変更する。ターミナルタイプは古風にvt100にした。ここがunknownだと支障をきたすので何かしら設定しておこう。そして、直ぐに使いたいなら、

doas kill -HUP 1

するだけ。

vm$ ps awx
  PID TT  STAT        TIME COMMAND
   :
14120 00  I+pU     0:00.26 /usr/libexec/getty std.9600 tty00

こんな風に待ち構えの体勢になってればOK。ホスト側から接続してみる。

ob$ cu -l /dev/ttyp4
Connected to /dev/ttyp4 (speed 9600) ;; 一度RETキーを叩くとバナーが出て来る

OpenBSD/i386 (vm.my.domain) (tty00)

login: sakae
Password:
Last login: Fri Feb 14 05:59:20 on ttyp0 from 10.0.2.2
OpenBSD 6.6 (DEB) #0: Sun Feb  9 14:02:01 JST 2020

Welcome to OpenBSD: The proactively secure Unix-like operating system.

vm$ exit

OpenBSD/i386 (vm.my.domain) (tty00)

login:  ;;  ~. 入力でcuが終了する。
[EOT]

disk

ふと思い立ったのでdiskのパラメータを確認。最初はカーネルが知ってる構造。

vm$ disklabel wd0
# /dev/rwd0c:
type: ESDI
disk: ESDI/IDE disk
label: QEMU HARDDISK
duid: c28637de21593793
flags:
bytes/sector: 512
sectors/track: 63
tracks/cylinder: 32
sectors/cylinder: 2016
cylinders: 1040
total sectors: 2097152
boundstart: 64
boundend: 2096640
drivedata: 0

16 partitions:
#                size           offset  fstype [fsize bsize   cpg]
  a:          2061440               64  4.2BSD   2048 16384 12958 # /
  b:            35136          2061504    swap                    # none
  c:          2097152                0  unused

こちらは、ローダーが知ってる構造って事かな。

vm$ fdisk wd0
Disk: wd0       geometry: 520/64/63 [2097152 Sectors]
Offset: 0       Signature: 0xAA55
            Starting         Ending         LBA Info:
 #: id      C   H   S -      C   H   S [       start:        size ]
-------------------------------------------------------------------------------
 0: 00      0   0   0 -      0   0   0 [           0:           0 ] unused
 1: 00      0   0   0 -      0   0   0 [           0:           0 ] unused
 2: 00      0   0   0 -      0   0   0 [           0:           0 ] unused
*3: A6      0   1   2 -    519  63  63 [          64:     2096576 ] OpenBSD

MBRが管理する4区画。インストール時にdiskを全部使うって指定すると、最後の区画が使われるのね。

timecounter

前回、時間の元に何が使われてるか調べたけど、うっかりしててホスト側を見ていたんで、改めて、ゲスト側のやつを調べてみる。

vm$ sysctl -a | grep choice
kern.timecounter.choice=i8254(0) acpihpet0(1000) acpitimer0(1000)
vm$ sysctl -a | grep hardware
kern.timecounter.hardware=acpihpet0
vm$ dmesg | grep acpihpet0
acpihpet0 at acpi0: 100000000 Hz
vm$ dmesg | grep acpitimer
acpitimer0 at acpi0: 3579545 Hz, 24 bits

100MHzの高精度カウンターが使われているんだね。

ソースは何処? 前回みたいにi368の下で検索しても見つからず。ひょっとしてデバイスエリア? ビンゴでした。acpi/acpihpet.c

ACPI(4)                      Device Drivers Manual                     ACPI(4)

NAME
     acpi - Advanced Configuration and Power Interface

SYNOPSIS
     acpi0 at bios?

DESCRIPTION
     The acpi driver provides basic support for ACPI including loading ACPI
     tables from the firmware, parsing and interpreting AML code, event
     handling, suspending and powering off, and attaching device drivers.
     Userland may access acpi by using the apm(4) device.

     The following devices can attach to acpi:

           acpiac(4)           ACPI AC adapter
           acpials(4)          ACPI ambient light sensor
           acpiasus(4)         ASUS ACPI hotkeys
           acpibat(4)          ACPI control method battery
           acpibtn(4)          ACPI button
           acpicbkbd(4)        Chromebook keyboard backlight
           acpicpu(4)          ACPI processor power and performance state
           acpidock(4)         ACPI docking station
           acpiec(4)           ACPI embedded controller
           acpihpet(4)         ACPI high precision event timer
             :

仕様の策定はウィンテルです。昔お世話になったapmの拡張版。石はサウスブリッジに配置されてるとか。

このドライバーで、データを読みだしている所にBPを置いてみた。

(gdb) bt
#0  acpihpet_gettime (tc=0xd1103ce4 <hpet_timecounter>) at /usr/src/sys/dev/acpi/acpihpet.c:277
#1  0xd08e6618 in tc_delta (th=0xd1105db4 <th1>) at /usr/src/sys/kern/kern_tc.c:135
#2  0xd08e70a7 in tc_windup (new_boottime=0x0, new_offset=0x0, new_adjtimedelta=0x0) at /usr/src/sys/kern/kern_tc.c:493
#3  0xd08e7c64 in tc_ticktock () at /usr/src/sys/kern/kern_tc.c:673
#4  0xd0525027 in hardclock (frame=0xf1cba718) at /usr/src/sys/kern/kern_clock.c:184
#5  0xd0c66a4e in lapic_clockintr (arg=0xf1cba718) at /usr/src/sys/arch/i386/i386/lapic.c:259
#6  0xd09b241a in Xltimer_untramp ()
#7  0xf1cba718 in ?? ()
#8  0xd09b220d in Xsoftclock ()
#9  0x00000000 in ?? ()

このルーチンへ入ってきた時の引数。

(gdb) p *tc
$2 = {
  tc_get_timecount = 0xd079cb40 <acpihpet_gettime>,
  tc_poll_pps = 0x0,
  tc_counter_mask = 4294967295,
  tc_frequency = 100000000,
  tc_name = 0xd17fe414 "acpihpet0",
  tc_quality = 1000,
  tc_priv = 0xd17fe400,
  tc_next = {
    sle_next = 0xd10fc014 <acpi_timecounter>
  },
  tc_freq_adj = 0
}

3個有ると言うカウンターの最初のやつを時計の元として使っているんだな。

inittodr

と、ここまではいいんだけど、OSがbootした時に、初期時刻をどうやって設定してるのだろう? いわゆるboottimeね。このままでは埒があかないので、man(9)を漁ってみる。 チャプター9は、kernel内部の有名な関数を解説したものだ。リナでは、ここに全く説明が無い。カーネルなんて見るものじゃありませんと、最初から拒否してますよ。

o32$ cd /usr/share/man/man9
o32$ ls | wc -l
     167
o32$ ls *time*
bintimeadd.9    microtime.9     rt_timer_add.9  time_second.9   timeout.9

man9のエリアに移動して、文書の数を確認。沢山有り過ぎて迷ってしまうので、キーワードで絞り込み。おお、知ってる関数 microtime が出てきたぞ。手始めに、それを見る。

o32$ man microtime
  :
SEE ALSO
     settimeofday(2), hardclock(9), hz(9), inittodr(9), time_second(9)

関連の関数が列挙されたので、ざっと目を通す。

INITTODR(9)                Kernel Developer's Manual               INITTODR(9)

NAME
     inittodr - initialize system time

DESCRIPTION
     The inittodr() function determines the time and sets the system clock.
     It tries to pick the correct time using a set of heuristics that examine
     the system's battery-backed clock and the time reported by the file
     system, as given in base.  Those heuristics include:

     o   If the battery-backed clock has a valid time, it is used.

     o   If the battery-backed clock does not have a valid time, and the time
         provided in base is within reason, base is used as the current time.

     o   If the battery-backed clock appears invalid, and base appears
         nonsensical or was not provided (was given as zero), an arbitrary
         base (typically some time in the late 1970s) will be used.

     Once a system time has been determined, it is passed to the tc_setclock()
     function.

面白い物を発見である。timeを初期化ですって。初期化は、OS起動時に一度だけ実行されるんだよな。と言う事は、今まで避けていた、OS起動を追跡しろって事になる。

今までのqemuをキックするスクリプトに、-S(大文字)を追加して、電源ON時にgdbに制御を渡すようにする。そして、起動すると待ちに入るので、gdbを起動。

Reading symbols from bsd.gdb...done.
0x0000fff0 in ?? ()
(gdb) b inittodr
Breakpoint 1 at 0xd05ef360: file /usr/src/sys/arch/i386/isa/clock.c, line 584.
(gdb) c
Continuing.

Breakpoint 1, inittodr (base=-3379844726758732800) at /usr/src/sys/arch/i386/isa/clock.c:584
584     {
(gdb) bt
#0  inittodr (base=-3379844726758732800) at /usr/src/sys/arch/i386/isa/clock.c:584
#1  0xd0747f61 in ffs_mountroot () at /usr/src/sys/ufs/ffs/ffs_vfsops.c:193
#2  0xd07a2c1e in dk_mountroot () at /usr/src/sys/kern/subr_disk.c:1349
#3  0xd038ce81 in main (framep=0x0) at /usr/src/sys/kern/init_main.c:492

お目当てのinittodrにBPを置いて、継続。コンソールの方では、起動プロセスが実行されて行き、やっとこさBPに到達したよ。呼び出された所は、なんとファイルシステムのマウントルーチン内からだ。思ってもみない場所だな。

いや、終了時に正しくunmountされたかどうかとかの確認に、現在時刻が必要なんだろうね。そAれで、やむなく正しそうな時刻を設定したとな(多分ですが)。RTCから呼び出した時刻データ。

(gdb) p rtclk
$1 = {88, 0, 1, 0, 34, 0, 7, 21, 2, 32}

このルーチンの中で、MC146818ってRTCな石から時刻を取り出している。mc146818reg.hに石の諸元が解説されてた。

 * Definitions for the Motorola MC146818A Real Time Clock.
 * They also apply for the (compatible) Dallas Semiconductor DS1287A RTC.

モトローラは知ってるけど、ダラスセミコンダクターって聞いた事ないぞ。ひょっとしてダラスから昇格してテキサスを名乗るあの会社の事(子会社って線もあるな)。 Dallas Semiconductorによると、TIとは全く関係無いっぽい。マキシマに買収されちゃったみたいだね。

        dt.dt_sec = hexdectodec(rtclk[MC_SEC]);
        dt.dt_min = hexdectodec(rtclk[MC_MIN]);
        dt.dt_hour = hexdectodec(rtclk[MC_HOUR]);
        dt.dt_day = hexdectodec(rtclk[MC_DOM]);
        dt.dt_mon = hexdectodec(rtclk[MC_MONTH]);
        dt.dt_year = clock_expandyear(hexdectodec(rtclk[MC_YEAR]));

        ts.tv_sec = clock_ymdhms_to_secs(&dt) - utc_offset;

        if (base < ts.tv_sec - 5*SECYR)
                printf("WARNING: file system time much less than clock time\n");

チップから読みだしたデータをclock_ymdhms_to_secs(struct clock_ymdhms *dt)を使って、epoc timeに変換してる。

        days = 0;
        for (i = POSIX_BASE_YEAR; i < year; i++)
                days += days_in_year(i);
        if (leapyear(year) && dt->dt_mon > FEBRUARY)
                days++;

        /* Months */
        for (i = 1; i < dt->dt_mon; i++)
                days += days_in_month(i);
        days += (dt->dt_day - 1);

        /* Add hours, minutes, seconds. */
        secs = (time_t)((days
            * 24 + dt->dt_hour)
            * 60 + dt->dt_min)
            * 60 + dt->dt_sec;

        return (secs);

うるう年は考慮してるけど、うるう秒には無頓着ってのが仕様なんだな。この関数は、clock_subr.cに置いてあった。

それから、もう一つ興味深いのは、RTCのyearから、正確な年を割り出すルーチン。RTCなチップが保持する年は、西暦の下2桁のみだ。これをどうにかして、世紀に置き換えたい。 clock_expandyear(int clockyear)では、涙ぐましい処理が行われている。

        clockcentury = (clockyear < 70) ? 20 : 19;
        clockyear += 100 * clockcentury;

チップから読みだしたデータが70未満なら、20xx年、それ以上なら19xx年と言う扱い。 この流れを汲んで、dateコマンドでエィヤァと日時をセットする時、下記の解釈が行われる。

           ccyy    Year.  If yy is specified, but cc is not, a value for yy
                   between 69 and 99 results in a cc value of 19.  Otherwise,
                   a cc value of 20 is used.

このルーチンの中で、CMOSのレイアウトチェックが行われている。標準品かそれ以外かだ。 これに引っかかると、

#ifdef DIAGNOSTIC
                printf("clock: unknown CMOS layout\n");
#endif

こんなメッセージをコンソールに表示する。qemuがサポートするやつは、チェックに引っかかっていた。

それから、gdbで追っていて、長い事放置してると、時刻がずれてくる。当然と言えば当然なんだけど、rebootするとこのずれが解消される。ntpdを動かしているわけでは無いので、時刻の元は、このチップ頼み。と言う事は、正しい時刻をqemuがエミュレートするチップから受け取っているって事になるな。

今さら、qemuのソースを紐解く元気は無いので、何方か検証宜しく!!

今回もgdbで流れをざっと追って、後でそれを頼りにソース内を飛び回る事をやってる。 そこで、TAGファイルが欲しくなったよ。

o32$ cd /sys
o32$ find . -name '*.[ch]' | xargs etags

サイズが、80Mになったけど、気にしない。尚、archの下には、amd64とかsolarisとかのマシン独立なやつが置いてある。同名な関数が定義されてて参照する時に邪魔になるので、i386以外は、バッサリ削除しておくと良い。

ntpd

OpenBSDで時刻を合わせる方法は、唯一ntpdだけと思われる。他のOSだとntpdateなんてのが普通に有るんだけどね。そこで、ntpdを試してみた。起動時に -d を付けるとフォーグラウンドにずっと居座って、記録を表示してくれるんで、後でログを眺める必要無し。

ob$ doas ntpd -d
/var/db/ntpd.drift is empty
ntp engine ready
constraint reply from 172.217.161.36: offset 157.468158
peer 162.159.200.123 now valid
peer 162.159.200.1 now valid
peer 129.250.35.250 now valid
peer 162.159.200.123 now valid
peer 129.250.35.251 now valid
adjusting local clock by 186.814016s
adjusting local clock by 186.315737s
adjusting local clock by 185.985736s
 :
adjusting local clock by 170.953536s
adjusting local clock by 170.626519s
adjusting local clock by 169.503546s

約1時間のランニング結果。ゆっくりと時間調整値が動いている事が確認出来る。動作が安定した時点で、/var/db/ntpd.driftに変動の癖が記憶され、監視間隔の決定とかに使われるとな。

ob$ ntpctl -s all
5/5 peers valid, constraint offset 157s, clock unsynced, clock offset is 167011.028ms

peer
   wt tl st  next  poll          offset       delay      jitter
162.159.200.123 time.cloudflare.com
    1 10  3   25s   30s         0.073ms    27.964ms     2.479ms
162.159.200.1 from pool pool.ntp.org
    1 10  3   18s   33s         1.362ms    29.330ms    10.831ms
129.250.35.250 from pool pool.ntp.org
    1 10  2   23s   33s         2.910ms    26.681ms     2.693ms
162.159.200.123 from pool pool.ntp.org
    1 10  3   31s   34s        -0.549ms    27.126ms     3.460ms
129.250.35.251 from pool pool.ntp.org
    1 10  2    5s   34s         3.998ms    25.934ms     2.148ms

そして、こちらは、5台の時間サーバーの状況。ジッター(バラツキ)が少なく、stが小さいやつが安心。stは、n次標準の値だ。超高精度の1次標準から時刻を頂いてきて2次標準、そこから頂いてきて3次標準と言う具合になってて、サーバーが混むのを緩和してる。福岡大学だかのサーバーが全世界から参照されて、大問題になってたのは記憶に新しい。

こんな風に緩やかに時刻を合わせていくのは、時間飛びによるファイルの不整合を防ぐ目的が有るからだ。

でも、時と場合によっては、速やかに時刻合わせをしたい事がある。そうntpdateを使えば出来るよ。でもOpenBSDには、そんなの無い。いや、ちゃんと用意されてるよ。以下はその実験。

ob$ date
Mon Feb 17 06:21:26 JST 2020
ob$ doas date 0300
doas (sakae@ob.my.domain) password:
Mon Feb 17 03:00:00 JST 2020
ob$ doas ntpd -s
doas (sakae@ob.my.domain) password:
/var/db/ntpd.drift is empty
ob$ date
Mon Feb 17 06:25:25 JST 2020

最初に時刻を確認、次に時刻を朝の3時に設定。次にntpdate相当の ntpd -s を実行。これで、時刻合わせが強制的に実施されてから、ntpdが裏側に回って、調整を続ける。再度時刻を確認。正しい時刻になった。

ob$ doas ntpd -d
 :
clock is now synced
constraint reply from 172.217.175.68: offset -0.535891
adjusting clock frequency by 0.253648 to 0.253648ppm
peer 129.128.12.20 now invalid
peer 51.159.34.173 now valid
adjusting clock frequency by -4.889372 to -4.635724ppm

時刻が合った所で、再びモニター。今度は容易に同期した。

ob$ cat /var/db/ntpd.drift
-4.636

ドリフトファイルにも、値が書き込まれていた。

ob$ ntpctl -s all
peer
   wt tl st  next  poll          offset       delay      jitter
   :
51.159.34.173 from pool pool.ntp.org
 *  1 10  2   28s   31s         1.791ms   247.719ms     2.648ms

ステータスを見ると、主に使われるサーバーに★印がついた。ジッターが一番小さいものが選ばれている風だ。

etc

Pocket Science Lab ベアボーンボード

オープンソースな多機能測定器Pocket Science LabをUbuntuで使う