i386 on vmm

Table of Contents

i386 on vmm

河豚板のHPを見ていたら、働くOpenBSDって事で、下記の事例が紹介されてた。

vmm/vmd: OpenBSDの仮想環境

オイラーも去年の今頃にやった記憶が有るけど、某雑誌の様に特集してみる。

環境の作成

クライアントはi386にする。だってqemuと違って、gdbでカーネル観光なんで 出来無いから使えるのはddbだけだ。ならば、アドレス表示は短かいのに限るからね。

doas rcctl -f start vmd
doas vmctl create -s 2G i386.img
doas vmctl start -c -i 1 -L -m 1G -B cdrom -r install78.iso -d i386.img i386
doas vmctl start -c -i 1 -L -m 1G -d i386.img i386

ダエモン君を手動で起動。2Gの仮想DISKを作成。そしてインストール。次は仮想DISK から起動。インストール時の設定がメールになってた。今迄気にする事はなかったな。

Subject: i386.my.domain install response file

System hostname = i386
Network interface to configure = vio0
IPv4 address for vio0 = autoconf
IPv6 address for vio0 = none
Network interface to configure = done
Start sshd(8) by default = no
Do you want the X Window System to be started by xenodm(1) = no
Change the default console to com0 = yes
Which speed should com0 use = 115200
Setup a user = sakae
Full name for user sakae = sakae
Which disk is the root disk = sd0
Encrypt the root disk with a (p)assphrase or (k)eydisk = no
Use (W)hole disk or (E)dit the MBR = whole
Use (A)uto layout, (E)dit auto layout, or create (C)ustom layout = c
Location of sets = cd0
Pathname to the sets = 7.8/i386
Set name(s) = -x*
Set name(s) = -g*
Set name(s) = done
Directory does not contain SHA256.sig. Continue without verification = yes
Location of sets = done
What timezone are you in = ?
What timezone are you in = Japan

下記はホスト側の状況

24234 ??  Ip       0:00.00 /usr/sbin/vmd
16247 ??  Ic       0:00.00 vmd: priv (vmd)
 7120 ??  Ipc      0:00.00 vmd: control (vmd)
69141 ??  IpUc     0:00.01 vmd: vmm (vmd)
 2213 ??  Ipc      0:00.00 vmd: agentx (vmd)
fu$ vmctl stat
   ID   PID VCPUS  MAXMEM  CURMEM     TTY        OWNER    STATE NAME
    1     -     1    1.0G       -       -         root  stopped i386

起動後の状況

fu$ vmctl stat
   ID   PID VCPUS  MAXMEM  CURMEM     TTY        OWNER    STATE NAME
    1  3620     1    1.0G    192M   ttyp4         root  running i386

上記では、起動時にオプションを色々と指定した。これをエイリアスにしとくって手 も有るけど、vm.confにパラメータを設定しておくと、vmdの起動時に、それを読み こんで、環境を用意してくれるぞ。

cat /etc/vm.conf
switch "uplink" {
        interface veb0
}

vm "i386" {
    disable
    memory 1G
    disk "/ram/home/sakae/MY/home/VMM/i386.img"
    interfaces 1
    interface { switch "uplink" }
}

後はダエモン君のリモコンを使って、起動/停止するだけだ。

doas vmctl start -cL i386
doas vmctl console i386
doas vmctl stop i386

ブリッジの設定

fu$ cat /etc/hostname.veb0
add em0
add vport0
up

仮想NICの設定

fu$ cat /etc/hostname.vport0
inet autoconf

クライアント側のネットワーク状況

i386$ ifconfig
vio0: flags=808b43<UP,BROADCAST,RUNNING,PROMISC,ALLMULTI,SIMPLEX,MULTICAST,AUTOCONF4> mtu 1500
        lladdr fe:e1:bb:d1:05:0a
        index 1 priority 0 llprio 3
        groups: egress
        media: Ethernet autoselect
        status: active
        inet 100.64.1.3 netmask 0xfffffffe

ホスト側のネットワーク状況

fu$ ifconfig
veb0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST>
        description: switch1-uplink
        index 5 llprio 3
        groups: veb
        em0 flags=3<LEARNING,DISCOVER>
                port 1 ifpriority 0 ifcost 0
        vport0 flags=3<LEARNING,DISCOVER>
                port 6 ifpriority 0 ifcost 0
vport0: flags=808943<UP,BROADCAST,RUNNING,PROMISC,SIMPLEX,MULTICAST,AUTOCONF4> mtu 1500
        lladdr fe:e1:ba:d0:e1:c1
        index 6 priority 0 llprio 3
        groups: vport
tap0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500
        lladdr fe:e1:ba:d1:1c:9b
        description: vm1-if0-i386
        index 8 priority 0 llprio 3
        groups: tap
        status: active
        inet 100.64.1.2 netmask 0xfffffffe

ddb

何も手を加えていないカーネルでddbできるか確認。

>> OpenBSD/i386 BOOT 3.67
\
com0: 115200 baud
switching console to com0
>> OpenBSD/i386 BOOT 3.67
boot>  -d
booting hd0a:/bsd: 10889747+2712580+208904+0+1146880 [796202+107+637824+699166]=0x104ef74
entry point at 0x201000

[ using 2133876 bytes of bsd ELF symbol table ]
Stopped at      db_enter+0x4:   popl    %ebp
ddb> c

起動時5秒以内に、-d して、カーネルが起動した早い段階でddbに入る様に指示。 ddbがシンボルテーブルを検出し、バナーが出る前に、ddbに突入してくれた。

i386# sysctl -a | grep ddb
ddb.radix=16
ddb.max_width=80
ddb.max_line=25
ddb.tab_stop_width=8
ddb.panic=1
ddb.console=1
ddb.log=1
ddb.trigger=0
ddb.suspend=0

ddb.console=1は、/etc/sysctl.confに記述しとくメインSWだな。

i386# sysctl ddb.trigger=1
Stopped at      db_enter+0x4:   popl    %ebp
ddb> bt
db_enter() at db_enter+0x4
ddb_sysctl(f6657cb8,1,cf7c3210,f6657cb0,cf7c1210,4,d348f69c) at ddb_sysctl+0xc2

sys_sysctl(d348f69c,f6657d50,f6657d48) at sys_sysctl+0x169
syscall(f6657d90) at syscall+0x3a3
Xsyscall_untramp() at Xsyscall_untramp+0xa9
end of kernel
ddb> c

メインSWがONになっていれば、任意のタイミングでddbをトリガーできるとな。 これは便利なんで、alias ddbon='sysctl ddb.trigger=1' と、登録しておこう。

huffman-morse.py

C言語とシェルスクリプトの「隙間」から生まれたのがPythonだそうだ。そして、 この前ひょんな事からnumpyに誘導されて、その威力を知った。もうpythonは避けて 通れないので、そこはかとなく触れてみる。そんな事なんで、楽しい課題を思い ついた。gzipの関連をチャッピーに指示した時の話題です。

Q: ハフマン復号なんて語句が出てきたので、モールス符号を思いだしました。 ハフマン・アルゴリズムを使って、pythonのソースコードを効率よく送信 する為のモールス符号を発明して下さい。 条件は、次の通りとします。

1. pythonに許される文字種
2. pythonで予約された語
3. pydoc用の """
4. ソースの検索対象は、標準添付モジュール

ソースをスキャン後、頻度を報告し、対応するハフマン符号を、モールス風に、 '.'と'-' で提示してください。

A: いいですね — 面白い課題です。 要求どおり、/標準添付モジュール(ローカルの Python 標準ライブラリ)を走査して頻度を集計し、その頻度に基づいてハフマン符号を生成(モールス風に .- で表現)するスクリプト/を作りました。

設計方針の要点(あなたの条件に合わせた扱い):

  • 1. pythonに許される文字種 → 実際のソースから現れたすべての文字(Unicode を含む)を「文字シンボル」として集計します(=CHAR:<c>=)。
  • 2. pythonで予約された語keyword.kwlist にある予約語は/単独のトークン/(=KW:<word>=)としてカウントします(キーワードを原子化して短い符号を割り当てられるように)。
  • 3. pydoc用の """ → モジュール・クラス・関数の docstringDOCSTRING トークン(開始・終了の """ を含む意味で)としてカウントし、さらに docstring 本文の文字は通常の文字として別集計します(つまり docstring 自体を明示的に区切れるように)。
  • 4. ソースの検索対象は、標準添付モジュール → デフォルトでは実行中の Python の標準ライブラリ(=sysconfig.getpaths()["stdlib"]= / =sys.baseprefix=)を再帰走査します。別パスを与えれば任意のディレクトリを走査できます。

スクリプトは次のことをします:

  1. 指定ディレクトリ内の *.py を再帰的に読み出す(デフォルトは実行中の Python の標準ライブラリ)。
  2. ast で AST を作り、モジュール/クラス/関数の docstring を検出してその位置の文字列を DOCSTRING として数える。
  3. tokenize でトークンを走査し、/予約語/(=KW:…=)を個別に数え、それ以外の文字は /1文字ずつ/(=CHAR:<c>=)でカウント。STRING トークンが docstring に相当する場合は DOCSTRING をインクリメント(実体の文字は別にカウント)。
  4. 得られたシンボル頻度でハフマン木を構築し、=0->'.'、=1->'-' のルールでモールス風コードを生成。
  5. 頻度テーブルと符号表を出力(上位 N を見やすく表示、全出力は CSV に保存可能)。

  1. スクリプトを保存して実行(標準ライブラリを走査):
python huffman_morse_for_python.py --top 60 --out stdlib_freq.csv

  • ハイブリッド設計(キーワードをトークン化) キーワードはソース中で非常に頻出し、かつ分離可能(単語境界がある)なので /個別トークンとして短いコード/を割り当てると効率が良くなります。モールス風に短い .- の列で表現できれば転送効率が上がります。
  • DOCSTRING トークン docstring の開始・終了を特別に扱えると、転送側で docstring ボディを別の圧縮手段(例えばそのまま gzip/deflate など)で送ることも可能になり、柔軟性が出ます。
  • 文字単位の符号化 残る部分(識別子、演算子、数値リテラル、文字列本文など)は文字単位でハフマン化します。これは変数名が多様でトークン化が困難なためです。
  • 符号の表現 ハフマン木の左枝を .=、右枝を =- にして「モールス風」にしています。ツリー形状により短い記号列が頻度の高いシンボルに割り当てられます。

code

csvの出力中に、不正なutf-8だと言われて数度追ちる事が当った。標準添付でも きちんとしていないのね。しっかりガードしたコードです。

import os
import argparse
import tokenize
import token
import csv
import ast
from collections import Counter
import heapq

# ============================================================
# 文字列を安全化(surrogate を完全排除)
# ============================================================
def safe_str(x):
    """Convert x to a UTF-8 safe printable string by replacing surrogates."""
    if not isinstance(x, str):
        x = str(x)
    return x.encode("utf-8", "replace").decode("utf-8", "replace")


# ============================================================
# Huffman Tree
# ============================================================
class HuffmanNode:
    def __init__(self, symbol=None, freq=0, left=None, right=None):
        self.symbol = symbol
        self.freq = freq
        self.left = left
        self.right = right

    def __lt__(self, other):
        return self.freq < other.freq


def build_huffman_tree(freqs):
    pq = [HuffmanNode(symbol=s, freq=f) for s, f in freqs.items()]
    heapq.heapify(pq)

    if len(pq) == 1:
        node = pq[0]
        return HuffmanNode(symbol=node.symbol, freq=node.freq)

    while len(pq) > 1:
        a = heapq.heappop(pq)
        b = heapq.heappop(pq)
        merged = HuffmanNode(symbol=None, freq=a.freq + b.freq, left=a, right=b)
        heapq.heappush(pq, merged)

    return pq[0]


def generate_codes(node, prefix="", code_map=None):
    if code_map is None:
        code_map = {}

    if node.symbol is not None:
        code_map[node.symbol] = prefix
        return code_map

    if node.left:
        generate_codes(node.left, prefix + ".", code_map)
    if node.right:
        generate_codes(node.right, prefix + "-", code_map)

    return code_map


# ============================================================
# Python .py ファイルスキャン
# ============================================================
def scan_python_sources(base_path):
    keywords = Counter()
    chars = Counter()
    doc_count = 0
    file_count = 0
    total_bytes = 0

    for root, _, files in os.walk(base_path):
        for fn in files:
            if fn.endswith(".py"):
                path = os.path.join(root, fn)
                file_count += 1
                try:
                    with open(path, "rb") as f:
                        data = f.read()
                        total_bytes += len(data)

                    text = data.decode("utf-8", "replace")
                    chars.update(list(text))

                    for tok in tokenize.generate_tokens(lambda L=iter(text.splitlines(keepends=True)): next(L)):
                        if tok.type == token.NAME and token.tok_name.get(tok.type) == "NAME":
                            if tok.string in python_keywords:
                                keywords[tok.string] += 1
                        elif tok.type == token.STRING:
                            if tok.string.startswith('"""') or tok.string.startswith("'''"):
                                doc_count += 1

                except Exception as e:
                    print(f"[WARN] Cannot read {path}: {e}")

    return keywords, chars, doc_count, file_count, total_bytes


python_keywords = {
    "def", "if", "for", "while", "return", "class", "try", "except",
    "in", "is", "not", "and", "or", "None", "True", "False", "with",
    "as", "elif", "else", "yield", "lambda", "from", "import", "global",
    "nonlocal", "assert", "async", "await", "raise", "del", "pass",
    "break", "continue"
}


# ============================================================
# CSV と標準出力のレポート
# ============================================================
def pretty_report(keywords, chars, doc_count, codes, out_csv):
    # print summary
    print("\n-- Keywords (top) --")
    for k, v in keywords.most_common(10):
        print(f"{k:<15} {v}")

    print("\nDOCSTRING occurrences:", doc_count)

    print("\n-- Characters (top) --")
    for c, v in chars.most_common(10):
        print(f"{safe_str(c)!r:<6} {v}")

    print("\n-- Top symbols with codes --")
    sym_freq = Counter()
    sym_freq.update(keywords)
    sym_freq.update(chars)
    for sym, cnt in sym_freq.most_common(20):
        print(f"{safe_str(sym):<18} {cnt:<8} {codes.get(sym, '')}")

    # CSV output (safe)
    if out_csv:
        with open(out_csv, "w", encoding="utf-8", errors="replace") as f:
            w = csv.writer(f)
            w.writerow(["symbol", "count", "code"])
            for sym, cnt in sym_freq.items():
                s = safe_str(sym)
                c = safe_str(codes.get(sym, ""))
                w.writerow([s, cnt, c])

        print(f"\nCSV written to: {out_csv}")


# ============================================================
# Main
# ============================================================
def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--path", default="/usr/lib/python3.12", help="scan base path")
    ap.add_argument("--out", help="output CSV file")
    ap.add_argument("--top", type=int, default=10)
    args = ap.parse_args()

    print("Scanning path:", args.path)

    keywords, chars, doc_count, file_count, total_bytes = scan_python_sources(args.path)

    print(f"Scanned {file_count} .py files, {total_bytes} bytes")
    total_syms = sum(keywords.values()) + sum(chars.values())
    print(f"Total symbol occurrences: {total_syms:,}")

    # merge for Huffman input
    combined = Counter()
    combined.update(keywords)
    combined.update(chars)

    # build Huffman codes
    print("\nBuilding Huffman tree and generating codes...")
    tree = build_huffman_tree(combined)
    codes = generate_codes(tree)

    pretty_report(keywords, chars, doc_count, codes, args.out)


if __name__ == "__main__":
    main()

more code

おまけでpureなASCIIファイル用のコード。

import sys
import heapq
from collections import Counter

def build_huffman_tree(frequencies):
    """
    文字の頻度辞書からハフマン木を構築し、ルートノードを返します。
    """
    # ヒープキューは (頻度, 文字または内部ノードのタプル) を格納する
    # heapqは最初の要素(頻度)を優先して最小ヒープを構築する
    heap = [[freq, char] for char, freq in frequencies.items()]
    heapq.heapify(heap)

    while len(heap) > 1:
        # 頻度が最も低い2つのノードを取り出す
        low1 = heapq.heappop(heap)
        low2 = heapq.heappop(heap)
        
        # 新しい内部ノード(親)を作成する
        # ノード構造: [合計頻度, [左の子], [右の子]]
        # 識別子としてタプルを使用し、ノードの比較でエラーが出ないようにする
        parent = [low1[0] + low2[0], low1, low2]
        
        # 親ノードをヒープに戻す
        heapq.heappush(heap, parent)
        
    return heap[0] # 最後に残ったルートノードを返す

def generate_huffman_codes(node, prefix="", codes={}):
    """
    ハフマン木をトラバースして、各文字のハフマン符号を生成します。
    """
    if len(node) == 2 and isinstance(node[1], str):
        # リーフノード(文字)に到達したら、符号を辞書に格納する
        # このスクリプトでは、出力形式に合わせて0を'.'、1を'-'に変換する
        codes[node[1]] = prefix.replace('0', '.').replace('1', '-')
    else:
        # 内部ノードの場合、左右の子を再帰的に探索する
        # 左の子には '0' を、右の子には '1' をプレフィックスに追加する
        generate_huffman_codes(node[1], prefix + "0", codes)
        generate_huffman_codes(node[2], prefix + "1", codes)
    return codes

def main():
    # 標準入力からテキスト全体を読み込む
    text = sys.stdin.read()
    
    if not text:
        return

    # 文字の出現頻度を計算する
    # collections.Counterを使用すると効率的
    frequencies = Counter(text)
    
    # ハフマン木を構築する
    root_node = build_huffman_tree(frequencies)
    
    # ハフマン符号を生成する
    huffman_codes = generate_huffman_codes(root_node)
    
    # 結果を指定された形式で出力する
    # 文字コード順(ASCII/Unicode順)でソートして見やすくする
    for char in sorted(huffman_codes.keys()):
        freq = frequencies[char]
        code = huffman_codes[char]
        # タブ区切りで出力: 文字 頻度 ハフマンBit
        print(f"{char}\t{freq}\t{code}")

if __name__ == "__main__":
    main()

下記は英単語辞書の変換例。最後の234937は単語数(改行の個数)。

sakae@lu:t$ cat /usr/share/dict/words | tr "A-Z" "a-z" | python3 gen.py | sort -k 2 -nr
e       234805  ...
i       200614  ---.
a       198939  --.-
o       170392  -.--
:
x       6926    -.-.-----
q       3729    -.-.----.-
j       3075    -.-.----..
        234937  ..-

モールス符号ってハフマン符号そのものだと、今まで思っていたけど、違うのね。そりゃ そうだわさ。モールス符号が発明された時期には、コンピュータは出現していなかったから ね。新聞だとか本から地道に文字頻度を抽出して、コードを発明するって凄いな。 和文のモールス符号って、そういう形跡が全くないんだよな(どなたか、ご存知なら教え てください)。

python heapq

heapqって何だ? って、調べていたら、こんなのに出会った。

Python heapqモジュール完全マスターガイド

array/tuple/set/queueの魅力と使い分けフローチャート

競プロで使える!Python標準ライブラリ

Pythonの基本的な組み込み型とその一般論まとめ

チャッピーなり grok なり gemini にコードを作成させると、有用なモジュールを 利用してくれる。これを丁寧に見ていけば、ためになるぞ。

README

今、自分の蔵書を開いている。2008年にオライリーから出版された、 ビューティフルコードって本だ。どうだ、俺のコード凄いだろうって 自慢のエッセイを33人から寄稿してもらったもの。出版時の講演だかで 訳者の久野先生からサインを頂いた希少な本だ。

この中に、python凄いだろってのが2編有ったので読み返した次第。 1編は、辞書の実装について。もう一編は、Numpyでの配列イテレータの実装。 昔は、pythonに興味が無かったから、ほとんで読み飛ばし状態だったけど、 本腰を入れて読むと面白いな。

難しかったのは、配列のスライスが有る事。スライスすると新らしい配列が 論理的に誕生するんだけど、内部実装ではコピーを避けて共有する構造に なってるそうだ。それもイテレータで扱かえないと話にならない。 何とか、マクロを使って実現できたとか。人知れず苦労が有るんですなあ。

この他、matzさんのruby楽だ編とか、haskellの関数透明性を利用した機能とか 改めて読み直したい編が多数出てきたぞ。思わぬクリスマス・プレゼントだな。 そう言えば、恒例のrubyからのクリスマス・リリースも近いな。


This year's Index

Home